@tokamak-private-dapps/private-state-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import process from "node:process";
7
7
  import readline from "node:readline/promises";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { createRequire } from "node:module";
10
+ import AdmZip from "adm-zip";
10
11
  import {
11
12
  createCipheriv,
12
13
  createDecipheriv,
@@ -68,6 +69,7 @@ import {
68
69
  installPrivateStateCliArtifacts,
69
70
  installTokamakCliRuntimeForPrivateState,
70
71
  inspectGroth16Runtime,
72
+ parseJsonReport,
71
73
  printDoctorHumanReport,
72
74
  privateStateCliArtifactPaths,
73
75
  readTokamakCliPackageReport,
@@ -77,6 +79,7 @@ import {
77
79
  resolveArtifactCacheBaseRoot,
78
80
  resolvePrivateStateInstallRuntimeVersions,
79
81
  resolveTokamakCliResourceDirForRuntimeRoot,
82
+ stripAnsi,
80
83
  writePrivateStateCliInstallManifest,
81
84
  } from "./lib/private-state-runtime-management.mjs";
82
85
  import {
@@ -127,8 +130,11 @@ const secretRoot = path.resolve(os.homedir(), "tokamak-private-channels", "secre
127
130
  const flatDeploymentArtifactPathsByChainId = new Map();
128
131
  const PRIVATE_STATE_UNINSTALL_CONFIRMATION =
129
132
  "I understand that the wallet secrets deleted due to this decision cannot be recovered";
133
+ const PRIVATE_STATE_CLI_PACKAGE_NAME = privateStateCliPackageJson.name;
130
134
  const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
131
135
  const TOKAMAK_ZKEVM_CLI_PACKAGE_NAME = "@tokamak-zk-evm/cli";
136
+ const WALLET_EXPORT_FORMAT = "tokamak-private-state-wallet-export";
137
+ const WALLET_EXPORT_FORMAT_VERSION = 1;
132
138
  let jsonOutputRequested = false;
133
139
  let activeCliArgs = {};
134
140
 
@@ -326,6 +332,12 @@ async function main() {
326
332
  return;
327
333
  }
328
334
 
335
+ if (args.command === "help-commands") {
336
+ assertHelpCommandsArgs(args);
337
+ printHelp();
338
+ return;
339
+ }
340
+
329
341
  if (args.command === "install") {
330
342
  assertInstallZkEvmArgs(args);
331
343
  await handleInstallZkEvm({ args });
@@ -338,28 +350,34 @@ async function main() {
338
350
  return;
339
351
  }
340
352
 
341
- if (args.command === "doctor") {
353
+ if (args.command === "help-update") {
354
+ assertUpdateArgs(args);
355
+ await handleUpdate();
356
+ return;
357
+ }
358
+
359
+ if (args.command === "help-doctor") {
342
360
  assertDoctorArgs(args);
343
361
  await handleDoctor({ args });
344
362
  return;
345
363
  }
346
364
 
347
- if (args.command === "guide") {
365
+ if (args.command === "help-guide") {
348
366
  assertGuideArgs(args);
349
367
  await handleGuide({ args });
350
368
  return;
351
369
  }
352
370
 
353
- if (args.command === "transaction-fees") {
371
+ if (args.command === "help-transaction-fees") {
354
372
  assertTransactionFeesArgs(args);
355
373
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
356
374
  await handleTransactionFees({ network, provider, rpcUrl });
357
375
  return;
358
376
  }
359
377
 
360
- if (args.command === "get-my-l1-address") {
361
- assertGetMyL1AddressArgs(args);
362
- handleGetMyL1Address({ args });
378
+ if (args.command === "account-get-l1-address") {
379
+ assertAccountGetL1AddressArgs(args);
380
+ handleAccountGetL1Address({ args });
363
381
  return;
364
382
  }
365
383
 
@@ -369,46 +387,58 @@ async function main() {
369
387
  return;
370
388
  }
371
389
 
372
- if (args.command === "list-local-wallets") {
390
+ if (args.command === "wallet-list") {
373
391
  assertListLocalWalletsArgs(args);
374
392
  handleListLocalWallets({ args });
375
393
  return;
376
394
  }
377
395
 
396
+ if (args.command === "wallet-export") {
397
+ assertWalletExportArgs(args);
398
+ handleWalletExport({ args });
399
+ return;
400
+ }
401
+
402
+ if (args.command === "wallet-import") {
403
+ assertWalletImportArgs(args);
404
+ handleWalletImport({ args });
405
+ return;
406
+ }
407
+
378
408
  const walletCommandHandlers = {
379
- "mint-notes": {
409
+ "wallet-mint-notes": {
380
410
  assert: assertMintNotesArgs,
381
411
  run: ({ provider }) => handleMintNotes({ args, provider }),
382
412
  },
383
- "redeem-notes": {
413
+ "wallet-redeem-notes": {
384
414
  assert: assertRedeemNotesArgs,
385
415
  run: ({ provider }) => handleRedeemNotes({ args, provider }),
386
416
  },
387
- "get-my-notes": {
388
- assert: assertGetMyNotesArgs,
389
- run: ({ provider }) => handleGetMyNotes({ args, provider }),
417
+ "wallet-get-notes": {
418
+ assert: assertWalletGetNotesArgs,
419
+ run: ({ provider }) => handleWalletGetNotes({ args, provider }),
390
420
  },
391
- "transfer-notes": {
421
+ "wallet-transfer-notes": {
392
422
  assert: assertTransferNotesArgs,
393
423
  run: ({ provider }) => handleTransferNotes({ args, provider }),
394
424
  },
395
- "deposit-channel": {
396
- assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "deposit-channel"),
425
+ "wallet-deposit-channel": {
426
+ assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "wallet-deposit-channel"),
397
427
  run: ({ provider }) => handleGrothVaultMove({ args, provider, direction: "deposit" }),
398
428
  },
399
- "withdraw-channel": {
400
- assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "withdraw-channel"),
429
+ "wallet-withdraw-channel": {
430
+ assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "wallet-withdraw-channel"),
401
431
  run: ({ provider }) => handleGrothVaultMove({ args, provider, direction: "withdraw" }),
402
432
  },
403
- "get-my-wallet-meta": {
404
- assert: assertGetMyWalletMetaArgs,
405
- run: ({ provider }) => handleGetMyWalletMeta({ args, provider }),
433
+ "wallet-get-meta": {
434
+ assert: assertWalletGetMetaArgs,
435
+ run: ({ provider }) => handleWalletGetMeta({ args, provider }),
406
436
  },
407
- "get-my-channel-fund": {
408
- assert: assertGetMyChannelFundArgs,
409
- run: ({ provider }) => handleGetMyChannelFund({ args, provider }),
437
+ "wallet-get-channel-fund": {
438
+ assert: assertWalletGetChannelFundArgs,
439
+ run: ({ provider }) => handleWalletGetChannelFund({ args, provider }),
410
440
  },
411
- "exit-channel": {
441
+ "channel-exit": {
412
442
  assert: assertExitChannelArgs,
413
443
  run: ({ provider }) => handleExitChannel({ args, provider }),
414
444
  },
@@ -422,56 +452,56 @@ async function main() {
422
452
  }
423
453
 
424
454
  switch (args.command) {
425
- case "create-channel": {
455
+ case "channel-create": {
426
456
  assertCreateChannelArgs(args);
427
457
  const { network, provider } = loadExplicitCommandRuntime(args);
428
458
  await prepareDeploymentArtifacts(network.chainId);
429
459
  await handleChannelCreate({ args, network, provider });
430
460
  return;
431
461
  }
432
- case "recover-workspace": {
462
+ case "channel-recover-workspace": {
433
463
  assertRecoverWorkspaceArgs(args);
434
464
  const { network, provider } = loadExplicitCommandRuntime(args);
435
465
  await prepareDeploymentArtifacts(network.chainId);
436
466
  await handleWorkspaceInit({ args, network, provider });
437
467
  return;
438
468
  }
439
- case "get-channel": {
469
+ case "channel-get-meta": {
440
470
  assertGetChannelArgs(args);
441
471
  const { network, provider } = loadExplicitCommandRuntime(args);
442
472
  await prepareDeploymentArtifacts(network.chainId);
443
473
  await handleGetChannel({ args, network, provider });
444
474
  return;
445
475
  }
446
- case "deposit-bridge": {
476
+ case "account-deposit-bridge": {
447
477
  assertDepositBridgeArgs(args);
448
478
  const { network, provider } = loadExplicitCommandRuntime(args);
449
479
  await prepareDeploymentArtifacts(network.chainId);
450
480
  await handleDepositBridge({ args, network, provider });
451
481
  return;
452
482
  }
453
- case "withdraw-bridge": {
483
+ case "account-withdraw-bridge": {
454
484
  assertWithdrawBridgeArgs(args);
455
485
  const { network, provider } = loadExplicitCommandRuntime(args);
456
486
  await prepareDeploymentArtifacts(network.chainId);
457
487
  await handleWithdrawBridge({ args, network, provider });
458
488
  return;
459
489
  }
460
- case "get-my-bridge-fund": {
461
- assertGetMyBridgeFundArgs(args);
490
+ case "account-get-bridge-fund": {
491
+ assertAccountGetBridgeFundArgs(args);
462
492
  const { network, provider } = loadExplicitCommandRuntime(args);
463
493
  await prepareDeploymentArtifacts(network.chainId);
464
- await handleGetMyBridgeFund({ args, provider });
494
+ await handleAccountGetBridgeFund({ args, provider });
465
495
  return;
466
496
  }
467
- case "recover-wallet": {
497
+ case "wallet-recover-workspace": {
468
498
  assertRecoverWalletArgs(args);
469
499
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
470
500
  await prepareDeploymentArtifacts(network.chainId);
471
501
  await handleRecoverWallet({ args, network, provider, rpcUrl });
472
502
  return;
473
503
  }
474
- case "join-channel": {
504
+ case "channel-join": {
475
505
  assertJoinChannelArgs(args);
476
506
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
477
507
  await prepareDeploymentArtifacts(network.chainId);
@@ -509,7 +539,7 @@ async function handleChannelCreate({ args, network, provider }) {
509
539
  const policySnapshot = dapp.policySnapshot;
510
540
 
511
541
  printImmutableChannelPolicyWarning({
512
- action: "create-channel",
542
+ action: "channel create",
513
543
  channelName,
514
544
  channelId,
515
545
  policySnapshot,
@@ -518,17 +548,20 @@ async function handleChannelCreate({ args, network, provider }) {
518
548
  await waitForReceipt(await bridgeCore.createChannel(channelId, dappId, joinToll, dapp.metadataDigest));
519
549
  const channelInfo = await bridgeCore.getChannel(channelId);
520
550
 
521
- const workspaceResult = await initializeChannelWorkspace({
551
+ const workspaceResult = await syncChannelWorkspace({
522
552
  workspaceName,
523
553
  channelName,
524
554
  network,
525
555
  provider,
526
556
  bridgeResources,
527
557
  persist: true,
558
+ fromGenesis: true,
559
+ minimumToBlock: receipt.blockNumber,
560
+ progressAction: "channel create",
528
561
  });
529
562
 
530
563
  printJson({
531
- action: "create-channel",
564
+ action: "channel create",
532
565
  channelName,
533
566
  channelId: channelId.toString(),
534
567
  dappId,
@@ -626,7 +659,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
626
659
  const workspaceName = channelName;
627
660
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
628
661
 
629
- const { workspaceDir, workspace, currentSnapshot } = await initializeChannelWorkspace({
662
+ const { workspaceDir, workspace, currentSnapshot } = await syncChannelWorkspace({
630
663
  workspaceName,
631
664
  channelName,
632
665
  network,
@@ -636,11 +669,11 @@ async function handleWorkspaceInit({ args, network, provider }) {
636
669
  allowExistingWorkspaceSync: true,
637
670
  useWorkspaceRecoveryIndex: true,
638
671
  fromGenesis: args.fromGenesis === true,
639
- progressAction: "recover-workspace",
672
+ progressAction: "channel recover-workspace",
640
673
  });
641
674
 
642
675
  printJson({
643
- action: "recover-workspace",
676
+ action: "channel recover-workspace",
644
677
  workspace: workspaceName,
645
678
  workspaceDir,
646
679
  channelName,
@@ -672,7 +705,7 @@ async function handleGetChannel({ args, network, provider }) {
672
705
  } catch (error) {
673
706
  if (isContractError(error, bridgeCore.interface, "UnknownChannel")) {
674
707
  printJson({
675
- action: "get-channel",
708
+ action: "channel get-meta",
676
709
  channelName,
677
710
  channelId: channelId.toString(),
678
711
  exists: false,
@@ -711,7 +744,7 @@ async function handleGetChannel({ args, network, provider }) {
711
744
  ]);
712
745
 
713
746
  printJson({
714
- action: "get-channel",
747
+ action: "channel get-meta",
715
748
  channelName,
716
749
  channelId: channelId.toString(),
717
750
  exists: true,
@@ -734,7 +767,7 @@ async function handleGetChannel({ args, network, provider }) {
734
767
  });
735
768
  }
736
769
 
737
- async function initializeChannelWorkspace({
770
+ async function syncChannelWorkspace({
738
771
  workspaceName,
739
772
  channelName,
740
773
  network,
@@ -744,6 +777,7 @@ async function initializeChannelWorkspace({
744
777
  allowExistingWorkspaceSync = false,
745
778
  useWorkspaceRecoveryIndex = false,
746
779
  fromGenesis = false,
780
+ minimumToBlock = null,
747
781
  progressAction = null,
748
782
  }) {
749
783
  const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), workspaceName);
@@ -777,7 +811,10 @@ async function initializeChannelWorkspace({
777
811
  const canonicalAssetDecimals = await fetchTokenDecimals(provider, canonicalAsset);
778
812
  const currentRootVectorHash = normalizeBytes32Hex(await channelManager.currentRootVectorHash());
779
813
  const genesisBlockNumber = Number(await channelManager.genesisBlockNumber());
780
- const latestBlock = await provider.getBlockNumber();
814
+ const observedLatestBlock = await provider.getBlockNumber();
815
+ const latestBlock = minimumToBlock === null
816
+ ? observedLatestBlock
817
+ : Math.max(observedLatestBlock, Number(minimumToBlock));
781
818
  const managedStorageAddresses = normalizedAddressVector(await channelManager.getManagedStorageAddresses());
782
819
  const policySnapshot = await readChannelPolicySnapshot({
783
820
  channelManager,
@@ -822,6 +859,13 @@ async function initializeChannelWorkspace({
822
859
  managedStorageAddresses,
823
860
  })
824
861
  : null;
862
+ if (useWorkspaceRecoveryIndex && !fromGenesis && !localSnapshotReusable && !recoveryIndex) {
863
+ throw new Error([
864
+ `Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
865
+ "The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
866
+ "Run channel recover-workspace --from-genesis or wallet recover-workspace --from-genesis to rebuild from channel genesis.",
867
+ ].join(" "));
868
+ }
825
869
  const reconstruction = localSnapshotReusable
826
870
  ? {
827
871
  currentSnapshot: existingArtifacts.stateSnapshot,
@@ -910,7 +954,7 @@ async function initializeChannelWorkspace({
910
954
  async function handleDepositBridge({ args, network, provider }) {
911
955
  if (args.wallet !== undefined) {
912
956
  throw new Error(
913
- "--wallet is not supported by deposit-bridge. Channel wallet keys are set up only by join-channel.",
957
+ "--wallet is not supported by account deposit-bridge. Channel wallet keys are set up only by channel join.",
914
958
  );
915
959
  }
916
960
  const signer = requireL1Signer(args, provider);
@@ -934,7 +978,7 @@ async function handleDepositBridge({ args, network, provider }) {
934
978
  const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
935
979
 
936
980
  printJson({
937
- action: "deposit-bridge",
981
+ action: "account deposit-bridge",
938
982
  amountInput,
939
983
  amountBaseUnits: amount.toString(),
940
984
  l1Address: signer.address,
@@ -950,7 +994,7 @@ async function handleDepositBridge({ args, network, provider }) {
950
994
  });
951
995
  }
952
996
 
953
- async function handleGetMyBridgeFund({ args, provider }) {
997
+ async function handleAccountGetBridgeFund({ args, provider }) {
954
998
  const signer = requireL1Signer(args, provider);
955
999
  const chainId = Number((await provider.getNetwork()).chainId);
956
1000
  const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId });
@@ -962,7 +1006,7 @@ async function handleGetMyBridgeFund({ args, provider }) {
962
1006
  const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
963
1007
 
964
1008
  printJson({
965
- action: "get-my-bridge-fund",
1009
+ action: "account get-bridge-fund",
966
1010
  l1Address: signer.address,
967
1011
  bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
968
1012
  canonicalAsset: bridgeVaultContext.canonicalAsset,
@@ -984,7 +1028,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
984
1028
  walletName,
985
1029
  });
986
1030
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
987
- const initialized = await initializeChannelWorkspace({
1031
+ const initialized = await syncChannelWorkspace({
988
1032
  workspaceName: channelName,
989
1033
  channelName,
990
1034
  network,
@@ -993,7 +1037,8 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
993
1037
  persist: true,
994
1038
  allowExistingWorkspaceSync: true,
995
1039
  useWorkspaceRecoveryIndex: true,
996
- progressAction: "recover-wallet",
1040
+ fromGenesis: args.fromGenesis === true,
1041
+ progressAction: "wallet recover-workspace",
997
1042
  });
998
1043
  const context = {
999
1044
  workspaceName: channelName,
@@ -1031,13 +1076,41 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1031
1076
  const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
1032
1077
  const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
1033
1078
 
1034
- expect(
1035
- registration.exists,
1036
- cliError(
1037
- CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
1038
- `No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
1039
- ),
1040
- );
1079
+ if (!registration.exists) {
1080
+ const cleanup = removeLocalWalletArtifacts(walletName, context.workspace.network);
1081
+ if (cleanup.removed) {
1082
+ printJson({
1083
+ action: "wallet recover-workspace",
1084
+ status: "stale-wallet-removed",
1085
+ wallet: walletName,
1086
+ removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
1087
+ removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
1088
+ walletSecretSource: resolvedWalletSecretSource(args),
1089
+ walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
1090
+ workspace: context.workspaceName,
1091
+ channelName: context.workspace.channelName,
1092
+ channelId: context.workspace.channelId,
1093
+ l1Address: signer.address,
1094
+ l2Address: l2Identity.l2Address,
1095
+ l2StorageKey: storageKey,
1096
+ leafIndex: leafIndex.toString(),
1097
+ reason: "The local wallet existed, but the L1 address is no longer registered in the channel.",
1098
+ nextAction: buildRecoverWalletRemovedNextAction({
1099
+ channelName,
1100
+ networkName: network.name,
1101
+ accountName: args.account,
1102
+ }),
1103
+ });
1104
+ return;
1105
+ }
1106
+ expect(
1107
+ false,
1108
+ cliError(
1109
+ CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
1110
+ `No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
1111
+ ),
1112
+ );
1113
+ }
1041
1114
  expect(
1042
1115
  ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
1043
1116
  "The existing channel registration L2 address does not match the derived L2 address.",
@@ -1081,10 +1154,10 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1081
1154
  provider,
1082
1155
  signer,
1083
1156
  noteReceiveKeyMaterial,
1084
- progressAction: "recover-wallet",
1157
+ progressAction: "wallet recover-workspace",
1085
1158
  });
1086
1159
  printJson({
1087
- action: "recover-wallet",
1160
+ action: "wallet recover-workspace",
1088
1161
  status: "already-recovered",
1089
1162
  wallet: walletName,
1090
1163
  walletDir: existingWallet.walletDir,
@@ -1106,7 +1179,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1106
1179
  return;
1107
1180
  }
1108
1181
 
1109
- clearWalletRecoveryArtifacts(walletPath(walletName, context.workspace.network));
1182
+ fs.rmSync(walletPath(walletName, context.workspace.network), { recursive: true, force: true });
1110
1183
 
1111
1184
  const walletContext = ensureWallet({
1112
1185
  channelContext: context,
@@ -1128,11 +1201,11 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1128
1201
  provider,
1129
1202
  signer,
1130
1203
  noteReceiveKeyMaterial,
1131
- progressAction: "recover-wallet",
1204
+ progressAction: "wallet recover-workspace",
1132
1205
  });
1133
1206
 
1134
1207
  printJson({
1135
- action: "recover-wallet",
1208
+ action: "wallet recover-workspace",
1136
1209
  status: "recovered",
1137
1210
  wallet: walletName,
1138
1211
  walletDir: walletContext.walletDir,
@@ -1282,8 +1355,30 @@ function assertExistingRecoverableWallet({
1282
1355
  );
1283
1356
  }
1284
1357
 
1285
- function clearWalletRecoveryArtifacts(walletDir) {
1286
- fs.rmSync(walletDir, { recursive: true, force: true });
1358
+ function removeLocalWalletArtifacts(walletName, networkName) {
1359
+ const walletDir = walletPath(walletName, networkName);
1360
+ const walletSecretFile = walletSecretPath(networkName, walletName);
1361
+ const walletSecretDir = path.dirname(walletSecretFile);
1362
+ const removedWalletDir = fs.existsSync(walletDir);
1363
+ const removedWalletSecret = fs.existsSync(walletSecretFile) || fs.existsSync(walletSecretDir);
1364
+ if (removedWalletDir) {
1365
+ fs.rmSync(walletDir, { recursive: true, force: true });
1366
+ }
1367
+ if (removedWalletSecret) {
1368
+ fs.rmSync(walletSecretDir, { recursive: true, force: true });
1369
+ }
1370
+ return {
1371
+ walletDir,
1372
+ walletSecretFile,
1373
+ removed: removedWalletDir || removedWalletSecret,
1374
+ removedWalletDir,
1375
+ removedWalletSecret,
1376
+ };
1377
+ }
1378
+
1379
+ function buildRecoverWalletRemovedNextAction({ channelName, networkName, accountName }) {
1380
+ const account = accountName ? String(accountName) : "<ACCOUNT>";
1381
+ return `channel join --channel-name ${channelName} --network ${networkName} --account ${account} --wallet-secret-path <PATH>`;
1287
1382
  }
1288
1383
 
1289
1384
  async function handleInstallZkEvm({ args }) {
@@ -1423,31 +1518,44 @@ function assertSafeManagedRoot(rootPath, label) {
1423
1518
  }
1424
1519
  }
1425
1520
 
1426
- function uninstallGlobalPrivateStateCliPackage() {
1427
- const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
1428
- const list = runCaptured(npmCommand, ["ls", "-g", "@tokamak-private-dapps/private-state-cli", "--depth=0", "--json"]);
1429
- if (list.status !== 0) {
1430
- const listReport = parseJsonReport(list.stdout);
1521
+ function npmCommandName() {
1522
+ return process.platform === "win32" ? "npm.cmd" : "npm";
1523
+ }
1524
+
1525
+ function inspectGlobalPrivateStateCliPackage() {
1526
+ const list = runCaptured(npmCommandName(), ["ls", "-g", PRIVATE_STATE_CLI_PACKAGE_NAME, "--depth=0", "--json"]);
1527
+ const report = parseJsonReport(list.stdout);
1528
+ const packageReport = report?.dependencies?.[PRIVATE_STATE_CLI_PACKAGE_NAME] ?? null;
1529
+ const installed = Boolean(packageReport);
1530
+ if (!installed) {
1431
1531
  const missing = /empty|missing|not found|not installed/iu.test(`${list.stdout}\n${list.stderr}`);
1432
1532
  return {
1433
- attempted: false,
1434
1533
  installed: false,
1435
- reason: missing || listReport ? "global package is not installed" : "unable to inspect global npm package",
1534
+ version: null,
1436
1535
  status: list.status,
1536
+ reason: missing || report ? "global package is not installed" : "unable to inspect global npm package",
1437
1537
  stderr: stripAnsi(list.stderr).trim(),
1438
1538
  };
1439
1539
  }
1440
- const report = parseJsonReport(list.stdout);
1441
- const installed = Boolean(report?.dependencies?.["@tokamak-private-dapps/private-state-cli"]);
1442
- if (!installed) {
1540
+ return {
1541
+ installed: true,
1542
+ version: packageReport.version ?? null,
1543
+ status: list.status,
1544
+ };
1545
+ }
1546
+
1547
+ function uninstallGlobalPrivateStateCliPackage() {
1548
+ const inspection = inspectGlobalPrivateStateCliPackage();
1549
+ if (!inspection.installed) {
1443
1550
  return {
1444
1551
  attempted: false,
1445
1552
  installed: false,
1446
- reason: "global package is not installed",
1447
- status: list.status,
1553
+ reason: inspection.reason,
1554
+ status: inspection.status,
1555
+ stderr: inspection.stderr,
1448
1556
  };
1449
1557
  }
1450
- const uninstall = runCaptured(npmCommand, ["uninstall", "-g", "@tokamak-private-dapps/private-state-cli"]);
1558
+ const uninstall = runCaptured(npmCommandName(), ["uninstall", "-g", PRIVATE_STATE_CLI_PACKAGE_NAME]);
1451
1559
  return {
1452
1560
  attempted: true,
1453
1561
  installed: true,
@@ -1458,6 +1566,106 @@ function uninstallGlobalPrivateStateCliPackage() {
1458
1566
  };
1459
1567
  }
1460
1568
 
1569
+ async function handleUpdate() {
1570
+ const currentVersion = privateStateCliPackageJson.version;
1571
+ const latestVersion = await fetchLatestPrivateStateCliVersion();
1572
+ const registryComparison = compareSemver(currentVersion, latestVersion);
1573
+ const globalPackage = inspectGlobalPrivateStateCliPackage();
1574
+ const runningFromRepositoryCheckout = isRepositoryCliPackageRoot(privateStateCliPackageRoot);
1575
+ const updateAvailable = registryComparison < 0;
1576
+
1577
+ const result = {
1578
+ action: "update",
1579
+ packageName: PRIVATE_STATE_CLI_PACKAGE_NAME,
1580
+ currentVersion,
1581
+ latestVersion,
1582
+ updateAvailable,
1583
+ registryState: registryComparison > 0 ? "local-version-ahead-of-registry" : "normal",
1584
+ runningFromRepositoryCheckout,
1585
+ globalPackage,
1586
+ attempted: false,
1587
+ updated: false,
1588
+ command: `npm install -g ${PRIVATE_STATE_CLI_PACKAGE_NAME}@latest`,
1589
+ };
1590
+
1591
+ if (!updateAvailable) {
1592
+ printJson(result);
1593
+ return;
1594
+ }
1595
+
1596
+ if (runningFromRepositoryCheckout) {
1597
+ printJson({
1598
+ ...result,
1599
+ reason: "running from a repository checkout; update the checkout with git/npm instead of mutating source files",
1600
+ });
1601
+ return;
1602
+ }
1603
+
1604
+ if (!globalPackage.installed) {
1605
+ printJson({
1606
+ ...result,
1607
+ reason: "global npm package is not installed; install or update the CLI with the printed command",
1608
+ });
1609
+ return;
1610
+ }
1611
+
1612
+ const install = runCaptured(npmCommandName(), ["install", "-g", `${PRIVATE_STATE_CLI_PACKAGE_NAME}@latest`]);
1613
+ if (install.status !== 0) {
1614
+ throw new Error([
1615
+ `Unable to update ${PRIVATE_STATE_CLI_PACKAGE_NAME} to ${latestVersion}.`,
1616
+ stripAnsi(install.stderr || install.stdout).trim(),
1617
+ ].filter(Boolean).join(" "));
1618
+ }
1619
+ printJson({
1620
+ ...result,
1621
+ attempted: true,
1622
+ updated: true,
1623
+ installedVersion: latestVersion,
1624
+ stdout: stripAnsi(install.stdout).trim(),
1625
+ stderr: stripAnsi(install.stderr).trim(),
1626
+ });
1627
+ }
1628
+
1629
+ async function fetchLatestPrivateStateCliVersion() {
1630
+ const url = `https://registry.npmjs.org/${encodeURIComponent(PRIVATE_STATE_CLI_PACKAGE_NAME)}/latest`;
1631
+ const response = await fetch(url, {
1632
+ redirect: "follow",
1633
+ signal: typeof globalThis.AbortSignal?.timeout === "function" ? globalThis.AbortSignal.timeout(10_000) : undefined,
1634
+ });
1635
+ if (!response.ok) {
1636
+ throw new Error(`Unable to fetch ${PRIVATE_STATE_CLI_PACKAGE_NAME} latest version from npm registry: HTTP ${response.status}.`);
1637
+ }
1638
+ const metadata = await response.json();
1639
+ const version = metadata?.version;
1640
+ if (typeof version !== "string" || version.trim() === "") {
1641
+ throw new Error(`npm registry response for ${PRIVATE_STATE_CLI_PACKAGE_NAME} did not include a version.`);
1642
+ }
1643
+ return version;
1644
+ }
1645
+
1646
+ function compareSemver(left, right) {
1647
+ const parse = (value) => String(value).split("-", 1)[0].split(".").map((part) => {
1648
+ const parsed = Number.parseInt(part, 10);
1649
+ return Number.isFinite(parsed) ? parsed : 0;
1650
+ });
1651
+ const leftParts = parse(left);
1652
+ const rightParts = parse(right);
1653
+ const length = Math.max(leftParts.length, rightParts.length);
1654
+ for (let index = 0; index < length; index += 1) {
1655
+ const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
1656
+ if (delta !== 0) {
1657
+ return delta < 0 ? -1 : 1;
1658
+ }
1659
+ }
1660
+ return 0;
1661
+ }
1662
+
1663
+ function isRepositoryCliPackageRoot(packageRoot) {
1664
+ const segments = path.resolve(packageRoot).split(path.sep);
1665
+ const suffix = ["packages", "apps", "private-state", "cli"];
1666
+ return suffix.every((segment, index) => segments[segments.length - suffix.length + index] === segment);
1667
+ }
1668
+
1461
1669
  async function handleDoctor({ args }) {
1462
1670
  const report = buildDoctorReport({ probeGpu: args.gpu === true });
1463
1671
  if (isJsonOutputRequested()) {
@@ -1608,10 +1816,10 @@ function trimFixedNumber(value, maxDecimals) {
1608
1816
  return trimmed ? `${integer}.${trimmed}` : integer;
1609
1817
  }
1610
1818
 
1611
- function handleGetMyL1Address({ args }) {
1819
+ function handleAccountGetL1Address({ args }) {
1612
1820
  const signer = requireL1Signer(args);
1613
1821
  printJson({
1614
- action: "get-my-l1-address",
1822
+ action: "account get-l1-address",
1615
1823
  l1Address: signer.address,
1616
1824
  account: args.account ?? null,
1617
1825
  });
@@ -1638,7 +1846,7 @@ function handleAccountImport({ args }) {
1638
1846
  privateKeyPath,
1639
1847
  }, 0o600);
1640
1848
  printJson({
1641
- action: "account-import",
1849
+ action: "account import",
1642
1850
  account,
1643
1851
  network: networkName,
1644
1852
  l1Address: getAddress(signer.address),
@@ -1659,7 +1867,7 @@ function handleListLocalWallets({ args }) {
1659
1867
  });
1660
1868
 
1661
1869
  printJson({
1662
- action: "list-local-wallets",
1870
+ action: "wallet list",
1663
1871
  workspaceRoot,
1664
1872
  filters: {
1665
1873
  network: networkFilter,
@@ -1669,6 +1877,175 @@ function handleListLocalWallets({ args }) {
1669
1877
  });
1670
1878
  }
1671
1879
 
1880
+ function handleWalletExport({ args }) {
1881
+ const outputPath = path.resolve(String(requireArg(args.output, "--output")));
1882
+ expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
1883
+ ensureDir(path.dirname(outputPath));
1884
+
1885
+ const includeNotes = args.includeNotes === true;
1886
+ const wallets = args.all === true
1887
+ ? listLocalWallets({ networkFilter: "mainnet" }).filter((wallet) => wallet.hasEncryptedWallet)
1888
+ : [resolveExportWalletInfo({
1889
+ networkName: requireNetworkName(args),
1890
+ walletName: requireWalletName(args),
1891
+ })];
1892
+
1893
+ expect(
1894
+ wallets.length > 0,
1895
+ args.all === true
1896
+ ? "No local mainnet wallets are available to export."
1897
+ : "No local wallet is available to export.",
1898
+ );
1899
+
1900
+ const archive = new AdmZip();
1901
+ const files = new Map();
1902
+ const exportedWallets = [];
1903
+ for (const wallet of wallets) {
1904
+ const normalized = normalizeExportWalletInfo(wallet);
1905
+ exportedWallets.push({
1906
+ network: normalized.network,
1907
+ channelName: normalized.channelName,
1908
+ wallet: normalized.wallet,
1909
+ });
1910
+ for (const filePath of walletExportFilePaths(normalized, { includeNotes })) {
1911
+ const archivePath = archivePathForLocalCliFile(filePath);
1912
+ if (!files.has(archivePath)) {
1913
+ files.set(archivePath, filePath);
1914
+ }
1915
+ }
1916
+ }
1917
+
1918
+ const manifest = {
1919
+ format: WALLET_EXPORT_FORMAT,
1920
+ formatVersion: WALLET_EXPORT_FORMAT_VERSION,
1921
+ createdAt: new Date().toISOString(),
1922
+ cliPackage: PRIVATE_STATE_CLI_PACKAGE_NAME,
1923
+ cliVersion: privateStateCliPackageJson.version,
1924
+ exportMode: args.all === true ? "all-mainnet" : "single-wallet",
1925
+ includeNotes,
1926
+ notes: includeNotes
1927
+ ? [
1928
+ "Includes the channel workspace cache required for immediate wallet command use when the cache is still chain-aligned.",
1929
+ ]
1930
+ : [
1931
+ "Includes wallet identity, encrypted wallet state, metadata, and wallet-local secret only.",
1932
+ "Run channel recover-workspace after import before wallet commands need channel state.",
1933
+ ],
1934
+ wallets: exportedWallets,
1935
+ files: [...files.keys()].sort(),
1936
+ };
1937
+
1938
+ archive.addFile("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
1939
+ for (const archivePath of manifest.files) {
1940
+ archive.addFile(archivePath, fs.readFileSync(files.get(archivePath)));
1941
+ }
1942
+ archive.writeZip(outputPath);
1943
+ protectSecretFile(outputPath, "wallet export ZIP");
1944
+
1945
+ printJson({
1946
+ action: "wallet export",
1947
+ output: outputPath,
1948
+ exportMode: manifest.exportMode,
1949
+ includeNotes,
1950
+ walletCount: exportedWallets.length,
1951
+ fileCount: manifest.files.length,
1952
+ wallets: exportedWallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
1953
+ });
1954
+ }
1955
+
1956
+ function handleWalletImport({ args }) {
1957
+ const inputPath = path.resolve(String(requireArg(args.input, "--input")));
1958
+ expect(fs.existsSync(inputPath), `Import ZIP does not exist: ${inputPath}.`);
1959
+
1960
+ const { archive, manifest } = readWalletImportArchive(inputPath);
1961
+
1962
+ const archiveFiles = new Set(manifest.files);
1963
+ for (const entry of archive.getEntries()) {
1964
+ if (entry.isDirectory) {
1965
+ continue;
1966
+ }
1967
+ expect(
1968
+ entry.entryName === "manifest.json" || archiveFiles.has(entry.entryName),
1969
+ `Unexpected file in wallet import ZIP: ${entry.entryName}.`,
1970
+ );
1971
+ }
1972
+
1973
+ const targetRoot = privateStateCliDataRoot();
1974
+ ensureDir(targetRoot);
1975
+ const plannedWrites = manifest.files.map((archivePath) => {
1976
+ validateWalletArchivePath(archivePath);
1977
+ const entry = archive.getEntry(archivePath);
1978
+ expect(entry && !entry.isDirectory, `Wallet import ZIP is missing ${archivePath}.`);
1979
+ const targetPath = path.resolve(targetRoot, archivePath);
1980
+ expectPathWithinRoot(targetPath, targetRoot, `Unsafe import target for ${archivePath}.`);
1981
+ expect(!fs.existsSync(targetPath), `Refusing to overwrite existing file: ${targetPath}.`);
1982
+ return {
1983
+ archivePath,
1984
+ targetPath,
1985
+ data: entry.getData(),
1986
+ };
1987
+ });
1988
+
1989
+ commitWalletImportFiles({ targetRoot, plannedWrites });
1990
+
1991
+ printJson({
1992
+ action: "wallet import",
1993
+ input: inputPath,
1994
+ exportMode: manifest.exportMode,
1995
+ includeNotes: Boolean(manifest.includeNotes),
1996
+ walletCount: manifest.wallets.length,
1997
+ fileCount: plannedWrites.length,
1998
+ wallets: manifest.wallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
1999
+ nextStep: manifest.includeNotes
2000
+ ? "Wallet commands can run immediately if the imported channel workspace cache is still chain-aligned."
2001
+ : "Run channel recover-workspace before wallet commands need channel state.",
2002
+ });
2003
+ }
2004
+
2005
+ function readWalletImportArchive(inputPath) {
2006
+ try {
2007
+ const archive = new AdmZip(inputPath);
2008
+ const manifestEntry = archive.getEntry("manifest.json");
2009
+ expect(manifestEntry, "Wallet import ZIP is missing manifest.json.");
2010
+ const manifest = JSON.parse(manifestEntry.getData().toString("utf8"));
2011
+ validateWalletExportManifest(manifest);
2012
+ return { archive, manifest };
2013
+ } catch (error) {
2014
+ throw new Error(`Failed to read wallet import ZIP ${inputPath}: ${error.message}`);
2015
+ }
2016
+ }
2017
+
2018
+ function commitWalletImportFiles({ targetRoot, plannedWrites }) {
2019
+ const stagingRoot = fs.mkdtempSync(path.join(targetRoot, ".wallet-import-"));
2020
+ const committedPaths = [];
2021
+ try {
2022
+ for (const write of plannedWrites) {
2023
+ write.stagingPath = path.resolve(stagingRoot, write.archivePath);
2024
+ expectPathWithinRoot(write.stagingPath, stagingRoot, `Unsafe staging target for ${write.archivePath}.`);
2025
+ ensureDir(path.dirname(write.stagingPath));
2026
+ fs.writeFileSync(write.stagingPath, write.data);
2027
+ applyImportedWalletFileMode(write.archivePath, write.stagingPath);
2028
+ }
2029
+
2030
+ for (const write of plannedWrites) {
2031
+ expect(!fs.existsSync(write.targetPath), `Refusing to overwrite existing file: ${write.targetPath}.`);
2032
+ }
2033
+
2034
+ for (const write of plannedWrites) {
2035
+ ensureDir(path.dirname(write.targetPath));
2036
+ fs.renameSync(write.stagingPath, write.targetPath);
2037
+ committedPaths.push(write.targetPath);
2038
+ }
2039
+ } catch (error) {
2040
+ for (const committedPath of committedPaths.reverse()) {
2041
+ fs.rmSync(committedPath, { force: true });
2042
+ }
2043
+ throw error;
2044
+ } finally {
2045
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
2046
+ }
2047
+ }
2048
+
1672
2049
  async function handleGuide({ args }) {
1673
2050
  const guide = {
1674
2051
  action: "guide",
@@ -1684,7 +2061,7 @@ async function handleGuide({ args }) {
1684
2061
  nextSafeAction: null,
1685
2062
  why: null,
1686
2063
  candidateCommands: [],
1687
- privacyTip: "For mint-notes, transfer-notes, and redeem-notes, add --tx-submitter <ACCOUNT> to let a separate local L1 account submit executeChannelTransaction and pay gas.",
2064
+ privacyTip: "For wallet mint-notes, wallet transfer-notes, and wallet redeem-notes, add --tx-submitter <ACCOUNT> to let a separate local L1 account submit executeChannelTransaction and pay gas.",
1688
2065
  };
1689
2066
 
1690
2067
  guide.state.local = inspectGuideLocalState(args);
@@ -1707,12 +2084,12 @@ async function handleGuide({ args }) {
1707
2084
 
1708
2085
  if (!args.network) {
1709
2086
  setGuideNextAction(guide, {
1710
- command: "guide --network <NAME>",
2087
+ command: "help guide --network <NAME>",
1711
2088
  why: "Select a network before the guide can inspect RPC, deployment artifacts, channels, accounts, or wallets.",
1712
2089
  candidates: [
1713
- "guide --network mainnet",
1714
- "guide --network sepolia",
1715
- "guide --network anvil",
2090
+ "help guide --network mainnet",
2091
+ "help guide --network sepolia",
2092
+ "help guide --network anvil",
1716
2093
  ],
1717
2094
  });
1718
2095
  printJson(guide);
@@ -1725,7 +2102,7 @@ async function handleGuide({ args }) {
1725
2102
  guide.checks.push(networkRuntime.check);
1726
2103
  if (!networkRuntime.network) {
1727
2104
  setGuideNextAction(guide, {
1728
- command: "guide --network <NAME>",
2105
+ command: "help guide --network <NAME>",
1729
2106
  why: `The requested network ${networkName} is not supported by the CLI network config.`,
1730
2107
  });
1731
2108
  printJson(guide);
@@ -2093,8 +2470,8 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
2093
2470
  function applyGuideNextAction(guide) {
2094
2471
  if (guide.state.local?.walletSelectorError && guide.selectors.network) {
2095
2472
  setGuideNextAction(guide, {
2096
- command: `list-local-wallets --network ${guide.selectors.network}`,
2097
- why: "The selected wallet name is malformed. List local wallets and retry guide with an existing deterministic wallet name.",
2473
+ command: `wallet list --network ${guide.selectors.network}`,
2474
+ why: "The selected wallet name is malformed. List local wallets and retry help guide with an existing deterministic wallet name.",
2098
2475
  });
2099
2476
  return;
2100
2477
  }
@@ -2122,15 +2499,15 @@ function applyGuideNextAction(guide) {
2122
2499
  if (guide.selectors.channelName && guide.state.channel?.onchain?.exists === false) {
2123
2500
  const account = guide.selectors.account ?? "<ACCOUNT>";
2124
2501
  setGuideNextAction(guide, {
2125
- command: `create-channel --channel-name ${guide.selectors.channelName} --join-toll <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2502
+ command: `channel create --channel-name ${guide.selectors.channelName} --join-toll <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2126
2503
  why: "The selected channel name is not registered on-chain yet.",
2127
2504
  });
2128
2505
  return;
2129
2506
  }
2130
2507
  if (guide.selectors.channelName && guide.state.channel?.onchain?.exists && !guide.state.channel?.local?.workspaceExists) {
2131
2508
  setGuideNextAction(guide, {
2132
- command: `recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network}`,
2133
- why: "The channel exists on-chain, but the local channel workspace has not been recovered yet.",
2509
+ command: `channel recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --from-genesis`,
2510
+ why: "The channel exists on-chain, but the local channel workspace has not been recovered yet, so there is no local recovery index to resume from.",
2134
2511
  });
2135
2512
  return;
2136
2513
  }
@@ -2138,7 +2515,7 @@ function applyGuideNextAction(guide) {
2138
2515
  const channelName = guide.selectors.channelName ?? guide.state.channel?.channelName ?? "<CHANNEL>";
2139
2516
  const account = guide.selectors.account ?? "<ACCOUNT>";
2140
2517
  setGuideNextAction(guide, {
2141
- command: `join-channel --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2518
+ command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2142
2519
  why: "The selected local wallet does not exist. Join the channel to create the wallet and register the channel L2 identity.",
2143
2520
  });
2144
2521
  return;
@@ -2147,7 +2524,7 @@ function applyGuideNextAction(guide) {
2147
2524
  const channelName = guide.state.wallet.channelName ?? guide.selectors.channelName ?? "<CHANNEL>";
2148
2525
  const account = guide.selectors.account ?? "<ACCOUNT>";
2149
2526
  setGuideNextAction(guide, {
2150
- command: `join-channel --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2527
+ command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2151
2528
  why: "The local wallet exists, but the corresponding L1 address is not registered in the channel.",
2152
2529
  });
2153
2530
  return;
@@ -2164,46 +2541,46 @@ function applyGuideNextAction(guide) {
2164
2541
  if (guide.state.wallet?.exists && bridgeBalance === 0n && (channelBalance === null || channelBalance === 0n) && unusedNotes === 0) {
2165
2542
  const account = guide.selectors.account ?? "<ACCOUNT>";
2166
2543
  setGuideNextAction(guide, {
2167
- command: `deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2544
+ command: `account deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2168
2545
  why: "The wallet is joined, but there is no bridge balance, channel balance, or local unused note to spend.",
2169
2546
  });
2170
2547
  return;
2171
2548
  }
2172
2549
  if (guide.state.wallet?.exists && bridgeBalance !== null && bridgeBalance > 0n && channelBalance === 0n) {
2173
2550
  setGuideNextAction(guide, {
2174
- command: `deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS>`,
2551
+ command: `wallet deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS>`,
2175
2552
  why: "The account has funds in the shared bridge vault, but the wallet has no channel L2 accounting balance.",
2176
2553
  });
2177
2554
  return;
2178
2555
  }
2179
2556
  if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
2180
2557
  setGuideNextAction(guide, {
2181
- command: `mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2558
+ command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2182
2559
  why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
2183
2560
  });
2184
2561
  return;
2185
2562
  }
2186
2563
  if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
2187
2564
  setGuideNextAction(guide, {
2188
- command: `transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2565
+ command: `wallet transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2189
2566
  why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
2190
2567
  candidates: [
2191
- `get-my-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2192
- `redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
2568
+ `wallet get-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2569
+ `wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
2193
2570
  ],
2194
2571
  });
2195
2572
  return;
2196
2573
  }
2197
2574
  if (guide.state.wallet?.exists && channelBalance === 0n) {
2198
2575
  setGuideNextAction(guide, {
2199
- command: `exit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2576
+ command: `channel exit --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2200
2577
  why: "The wallet has zero channel balance, so channel exit is allowed by both the CLI and bridge contract.",
2201
2578
  });
2202
2579
  return;
2203
2580
  }
2204
2581
 
2205
2582
  setGuideNextAction(guide, {
2206
- command: "guide --network <NAME> --channel-name <CHANNEL> --account <ACCOUNT> --wallet <WALLET>",
2583
+ command: "help guide --network <NAME> --channel-name <CHANNEL> --account <ACCOUNT> --wallet <WALLET>",
2207
2584
  why: "Provide more selectors so the guide can choose a single next safe action.",
2208
2585
  });
2209
2586
  }
@@ -2267,25 +2644,28 @@ function redactRpcUrl(rpcUrl) {
2267
2644
  }
2268
2645
  }
2269
2646
 
2270
- async function handleGetMyWalletMeta({ args, provider }) {
2647
+ async function handleWalletGetMeta({ args, provider }) {
2271
2648
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
2272
- const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
2273
- const context = await loadChannelContext({
2274
- args,
2275
- networkName: walletMetadata.network,
2649
+ const contextResult = await loadPreferredWalletChannelContext({
2650
+ walletContext: wallet,
2276
2651
  provider,
2652
+ progressAction: "wallet get-meta",
2653
+ });
2654
+ const context = contextResult.context;
2655
+ const {
2656
+ signer,
2657
+ l2Identity,
2658
+ registration,
2659
+ expectedStorageKey,
2660
+ matchesWallet,
2661
+ } = await loadWalletChannelRegistrationState({
2277
2662
  walletContext: wallet,
2663
+ context,
2664
+ provider,
2278
2665
  });
2279
2666
 
2280
- const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
2281
- const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
2282
- const matchesWallet = registration.exists
2283
- && ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
2284
- && ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
2285
- === ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
2286
-
2287
2667
  printJson({
2288
- action: "get-my-wallet-meta",
2668
+ action: "wallet get-meta",
2289
2669
  wallet: wallet.walletName,
2290
2670
  network: walletMetadata.network,
2291
2671
  channelName: walletMetadata.channelName,
@@ -2301,31 +2681,23 @@ async function handleGetMyWalletMeta({ args, provider }) {
2301
2681
  }
2302
2682
 
2303
2683
  async function loadWalletChannelFundState({ walletContext, provider, progressAction = null }) {
2304
- const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
2305
2684
  const contextResult = await loadPreferredWalletChannelContext({
2306
2685
  walletContext,
2307
2686
  provider,
2308
2687
  progressAction,
2309
2688
  });
2310
2689
  const context = contextResult.context;
2311
- const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
2312
- expect(
2313
- registration.exists,
2314
- cliError(
2315
- CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
2316
- `No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
2317
- ),
2318
- );
2319
- const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
2320
- expect(
2321
- ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
2322
- "The local wallet L2 address does not match the registered channel L2 address.",
2323
- );
2324
- expect(
2325
- ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
2326
- === ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey)),
2327
- "The local wallet L2 storage key does not match the registered channelTokenVault key.",
2328
- );
2690
+ const {
2691
+ signer,
2692
+ l2Identity,
2693
+ registration,
2694
+ expectedStorageKey,
2695
+ } = await loadWalletChannelRegistrationState({
2696
+ walletContext,
2697
+ context,
2698
+ provider,
2699
+ requireRegistration: true,
2700
+ });
2329
2701
 
2330
2702
  const stateManager = await buildStateManager(context.currentSnapshot, context.contractCodes);
2331
2703
  const channelDeposit = await currentStorageBigInt(
@@ -2344,7 +2716,44 @@ async function loadWalletChannelFundState({ walletContext, provider, progressAct
2344
2716
  };
2345
2717
  }
2346
2718
 
2347
- async function handleGetMyChannelFund({ args, provider }) {
2719
+ async function loadWalletChannelRegistrationState({
2720
+ walletContext,
2721
+ context,
2722
+ provider,
2723
+ requireRegistration = false,
2724
+ }) {
2725
+ const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
2726
+ const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
2727
+ const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
2728
+ const matchesWallet = registration.exists
2729
+ && ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
2730
+ && ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
2731
+ === ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
2732
+
2733
+ if (requireRegistration) {
2734
+ expect(
2735
+ registration.exists,
2736
+ cliError(
2737
+ CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
2738
+ `No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
2739
+ ),
2740
+ );
2741
+ expect(
2742
+ matchesWallet,
2743
+ "The local wallet L2 address or storage key does not match the registered channelTokenVault state.",
2744
+ );
2745
+ }
2746
+
2747
+ return {
2748
+ signer,
2749
+ l2Identity,
2750
+ registration,
2751
+ expectedStorageKey,
2752
+ matchesWallet,
2753
+ };
2754
+ }
2755
+
2756
+ async function handleWalletGetChannelFund({ args, provider }) {
2348
2757
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
2349
2758
  const {
2350
2759
  signer,
@@ -2353,10 +2762,14 @@ async function handleGetMyChannelFund({ args, provider }) {
2353
2762
  registration,
2354
2763
  expectedStorageKey,
2355
2764
  channelFund,
2356
- } = await loadWalletChannelFundState({ walletContext: wallet, provider });
2765
+ } = await loadWalletChannelFundState({
2766
+ walletContext: wallet,
2767
+ provider,
2768
+ progressAction: "wallet get-channel-fund",
2769
+ });
2357
2770
 
2358
2771
  printJson({
2359
- action: "get-my-channel-fund",
2772
+ action: "wallet get-channel-fund",
2360
2773
  wallet: wallet.walletName,
2361
2774
  network: walletMetadata.network,
2362
2775
  channelName: walletMetadata.channelName,
@@ -2376,9 +2789,9 @@ async function handleGetMyChannelFund({ args, provider }) {
2376
2789
  }
2377
2790
 
2378
2791
  async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2379
- const context = await loadChannelContext({
2792
+ const context = await loadJoinChannelContext({
2380
2793
  args,
2381
- networkName: network.name,
2794
+ network,
2382
2795
  provider,
2383
2796
  });
2384
2797
  const signer = requireL1Signer(args, provider);
@@ -2388,7 +2801,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2388
2801
  !existingRegistration.exists,
2389
2802
  [
2390
2803
  `L1 address ${signer.address} is already registered in channel ${context.workspace.channelName}.`,
2391
- "Use recover-wallet or normal wallet commands for an existing channel registration.",
2804
+ "Use wallet recover-workspace or normal wallet commands for an existing channel registration.",
2392
2805
  ].join(" "),
2393
2806
  );
2394
2807
  const walletSecret = prepareJoinWalletSecretForName({
@@ -2425,7 +2838,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2425
2838
  );
2426
2839
  let nextNonce = await provider.getTransactionCount(signer.address, "pending");
2427
2840
  printImmutableChannelPolicyWarning({
2428
- action: "join-channel",
2841
+ action: "channel join",
2429
2842
  channelName: context.workspace.channelName,
2430
2843
  channelId: ethers.toBigInt(context.workspace.channelId),
2431
2844
  channelManager: context.workspace.channelManager,
@@ -2461,7 +2874,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2461
2874
  });
2462
2875
 
2463
2876
  printJson({
2464
- action: "join-channel",
2877
+ action: "channel join",
2465
2878
  workspace: context.workspaceName,
2466
2879
  wallet: walletContext.walletName,
2467
2880
  walletSecretSource: resolvedWalletSecretSource(args),
@@ -2497,17 +2910,18 @@ async function handleExitChannel({ args, provider }) {
2497
2910
  channelFund === 0n,
2498
2911
  [
2499
2912
  `The current channel fund for ${signer.address} is ${channelFund.toString()}.`,
2500
- "exit-channel requires a zero channel balance.",
2501
- "Run withdraw-channel first, then retry exit-channel.",
2913
+ "channel exit requires a zero channel balance.",
2914
+ "Run wallet withdraw-channel first, then retry channel exit.",
2502
2915
  ].join(" "),
2503
2916
  );
2504
2917
  const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(signer.address);
2505
2918
  const receipt = await waitForReceipt(
2506
2919
  await context.bridgeTokenVault.connect(signer).exitChannel(ethers.toBigInt(context.workspace.channelId)),
2507
2920
  );
2921
+ const cleanup = removeLocalWalletArtifacts(walletContext.walletName, walletMetadata.network);
2508
2922
 
2509
2923
  printJson({
2510
- action: "exit-channel",
2924
+ action: "channel exit",
2511
2925
  wallet: walletContext.walletName,
2512
2926
  network: walletMetadata.network,
2513
2927
  channelName: walletMetadata.channelName,
@@ -2522,15 +2936,17 @@ async function handleExitChannel({ args, provider }) {
2522
2936
  gasUsed: receiptGasUsed(receipt),
2523
2937
  txUrl: explorerTxUrl(network, receipt.hash),
2524
2938
  receipt: sanitizeReceipt(receipt),
2939
+ removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
2940
+ removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
2525
2941
  });
2526
2942
  }
2527
2943
 
2528
2944
  async function handleGrothVaultMove({ args, provider, direction }) {
2529
- const operationName = args.command === "withdraw-channel"
2530
- ? "withdraw-channel"
2945
+ const operationName = args.command === "wallet-withdraw-channel"
2946
+ ? "wallet withdraw-channel"
2531
2947
  : direction === "deposit"
2532
- ? "deposit-channel"
2533
- : "withdraw";
2948
+ ? "wallet deposit-channel"
2949
+ : "wallet withdraw-channel";
2534
2950
  emitProgress(operationName, "loading");
2535
2951
  const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
2536
2952
  const contextResult = await loadPreferredWalletChannelContext({
@@ -2561,7 +2977,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2561
2977
  availableBalance >= amount,
2562
2978
  [
2563
2979
  `Deposit amount ${amount.toString()} exceeds the shared bridge-vault balance`,
2564
- `${availableBalance.toString()} for ${signer.address}. Run deposit-bridge first.`,
2980
+ `${availableBalance.toString()} for ${signer.address}. Run account deposit-bridge first.`,
2565
2981
  ].join(" "),
2566
2982
  );
2567
2983
  }
@@ -2570,7 +2986,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2570
2986
  registration.exists,
2571
2987
  cliError(
2572
2988
  CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
2573
- `No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
2989
+ `No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
2574
2990
  ),
2575
2991
  );
2576
2992
  expect(
@@ -2634,7 +3050,12 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2634
3050
  sealWalletOperationDir(operationDir, walletContext.walletSecret);
2635
3051
 
2636
3052
  context.currentSnapshot = transition.nextSnapshot;
2637
- persistCurrentState(context);
3053
+ await refreshPersistedWorkspaceAfterLocalTransaction({
3054
+ context,
3055
+ provider,
3056
+ receipt,
3057
+ progressAction: operationName,
3058
+ });
2638
3059
 
2639
3060
  emitProgress(operationName, "done");
2640
3061
  printJson({
@@ -2650,6 +3071,8 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2650
3071
  updatedRoot: transition.update.updatedRoot,
2651
3072
  gasUsed: receiptGasUsed(receipt),
2652
3073
  txUrl: explorerTxUrl(network, receipt.hash),
3074
+ usedWorkspaceCache: contextResult.usingWorkspaceCache,
3075
+ recoveredWorkspace: contextResult.recoveredWorkspace,
2653
3076
  });
2654
3077
  }
2655
3078
 
@@ -2667,7 +3090,7 @@ async function handleWithdrawBridge({ args, network, provider }) {
2667
3090
  const receipt = await waitForReceipt(await bridgeTokenVault.claimToWallet(amount));
2668
3091
 
2669
3092
  printJson({
2670
- action: "withdraw-bridge",
3093
+ action: "account withdraw-bridge",
2671
3094
  l1Address: signer.address,
2672
3095
  amountInput,
2673
3096
  amountBaseUnits: amount.toString(),
@@ -2759,13 +3182,13 @@ async function handleMintNotes({ args, provider }) {
2759
3182
  const { channelFund } = await loadWalletChannelFundState({
2760
3183
  walletContext: wallet,
2761
3184
  provider,
2762
- progressAction: "mint-notes",
3185
+ progressAction: "wallet mint-notes",
2763
3186
  });
2764
3187
  expect(
2765
3188
  totalMintAmount <= channelFund,
2766
3189
  [
2767
3190
  `Mint amount total ${totalMintAmount.toString()} exceeds the current channel fund`,
2768
- `${channelFund.toString()}. Run get-my-channel-fund to inspect the available balance.`,
3191
+ `${channelFund.toString()}. Run wallet get-channel-fund to inspect the available balance.`,
2769
3192
  ].join(" "),
2770
3193
  );
2771
3194
  const templatePayload = buildMintNotesTemplatePayload({
@@ -2776,12 +3199,12 @@ async function handleMintNotes({ args, provider }) {
2776
3199
  args,
2777
3200
  wallet,
2778
3201
  provider,
2779
- operationName: "mint-notes",
3202
+ operationName: "wallet mint-notes",
2780
3203
  templatePayload,
2781
3204
  });
2782
3205
 
2783
3206
  printJson({
2784
- action: "mint-notes",
3207
+ action: "wallet mint-notes",
2785
3208
  wallet: wallet.walletName,
2786
3209
  workspace: execution.context.workspaceName,
2787
3210
  operationDir: execution.operationDir,
@@ -2820,12 +3243,12 @@ async function handleRedeemNotes({ args, provider }) {
2820
3243
  args,
2821
3244
  wallet,
2822
3245
  provider,
2823
- operationName: "redeem-notes",
3246
+ operationName: "wallet redeem-notes",
2824
3247
  templatePayload,
2825
3248
  });
2826
3249
 
2827
3250
  printJson({
2828
- action: "redeem-notes",
3251
+ action: "wallet redeem-notes",
2829
3252
  wallet: wallet.walletName,
2830
3253
  workspace: execution.context.workspaceName,
2831
3254
  operationDir: execution.operationDir,
@@ -2852,7 +3275,7 @@ async function handleRedeemNotes({ args, provider }) {
2852
3275
  });
2853
3276
  }
2854
3277
 
2855
- async function handleGetMyNotes({ args, provider }) {
3278
+ async function handleWalletGetNotes({ args, provider }) {
2856
3279
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
2857
3280
  expect(
2858
3281
  typeof wallet.wallet.controller === "string" && wallet.wallet.controller.length > 0,
@@ -2862,7 +3285,7 @@ async function handleGetMyNotes({ args, provider }) {
2862
3285
  const { context } = await loadPreferredWalletChannelContext({
2863
3286
  walletContext: wallet,
2864
3287
  provider,
2865
- progressAction: "get-my-notes",
3288
+ progressAction: "wallet get-notes",
2866
3289
  });
2867
3290
  const signer = restoreWalletSigner(wallet, provider);
2868
3291
  const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
@@ -2870,7 +3293,7 @@ async function handleGetMyNotes({ args, provider }) {
2870
3293
  context,
2871
3294
  provider,
2872
3295
  signer,
2873
- progressAction: "get-my-notes",
3296
+ progressAction: "wallet get-notes",
2874
3297
  });
2875
3298
 
2876
3299
  const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
@@ -2895,7 +3318,7 @@ async function handleGetMyNotes({ args, provider }) {
2895
3318
  const spentTotal = spentTrackedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n);
2896
3319
 
2897
3320
  printJson({
2898
- action: "get-my-notes",
3321
+ action: "wallet get-notes",
2899
3322
  wallet: wallet.walletName,
2900
3323
  network: walletMetadata.network,
2901
3324
  channelName: walletMetadata.channelName,
@@ -2919,7 +3342,7 @@ async function handleTransferNotes({ args, provider }) {
2919
3342
  const preparedContextResult = await loadPreferredWalletChannelContext({
2920
3343
  walletContext: wallet,
2921
3344
  provider,
2922
- progressAction: "transfer-notes",
3345
+ progressAction: "wallet transfer-notes",
2923
3346
  });
2924
3347
  const context = preparedContextResult.context;
2925
3348
  const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
@@ -2955,7 +3378,7 @@ async function handleTransferNotes({ args, provider }) {
2955
3378
  args,
2956
3379
  wallet,
2957
3380
  provider,
2958
- operationName: "transfer-notes",
3381
+ operationName: "wallet transfer-notes",
2959
3382
  templatePayload,
2960
3383
  });
2961
3384
  const outputNotes = buildLifecycleTrackedOutputs({
@@ -2966,7 +3389,7 @@ async function handleTransferNotes({ args, provider }) {
2966
3389
  });
2967
3390
 
2968
3391
  printJson({
2969
- action: "transfer-notes",
3392
+ action: "wallet transfer-notes",
2970
3393
  wallet: wallet.walletName,
2971
3394
  workspace: execution.context.workspaceName,
2972
3395
  operationDir: execution.operationDir,
@@ -3323,11 +3746,12 @@ async function recoverDeliveredNotesFromEventLogs({
3323
3746
  noteReceivePrivateKey,
3324
3747
  progressAction = null,
3325
3748
  }) {
3326
- const scanStartBlock = Math.max(
3327
- Number(walletContext.wallet.noteReceiveLastScannedBlock),
3328
- Number(context.workspace.genesisBlockNumber),
3329
- );
3330
3749
  const latestBlock = await provider.getBlockNumber();
3750
+ const scanStartBlock = requireUsableWalletNoteReceiveRecoveryIndex({
3751
+ walletContext,
3752
+ context,
3753
+ latestBlock,
3754
+ });
3331
3755
  const scanRange = {
3332
3756
  fromBlock: scanStartBlock,
3333
3757
  toBlock: latestBlock,
@@ -3431,6 +3855,23 @@ async function recoverDeliveredNotesFromEventLogs({
3431
3855
  };
3432
3856
  }
3433
3857
 
3858
+ function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, latestBlock }) {
3859
+ const nextBlock = Number(walletContext.wallet.noteReceiveLastScannedBlock);
3860
+ const genesisBlockNumber = Number(context.workspace.genesisBlockNumber);
3861
+ if (
3862
+ !Number.isInteger(nextBlock)
3863
+ || nextBlock < genesisBlockNumber
3864
+ || nextBlock > Number(latestBlock) + 1
3865
+ ) {
3866
+ throw new Error([
3867
+ `Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
3868
+ `Expected noteReceiveLastScannedBlock to be an integer between ${genesisBlockNumber} and ${Number(latestBlock) + 1}.`,
3869
+ "Run wallet recover-workspace --from-genesis to rebuild wallet note state from channel genesis.",
3870
+ ].join(" "));
3871
+ }
3872
+ return nextBlock;
3873
+ }
3874
+
3434
3875
  function extractEncryptedNoteValueFromBridgeLog(log) {
3435
3876
  if (!Array.isArray(log?.topics) || log.topics.length !== 1) {
3436
3877
  return null;
@@ -3665,16 +4106,16 @@ function buildRedeemNotesTemplatePayload({ wallet, inputNotes }) {
3665
4106
  }
3666
4107
 
3667
4108
  function selectMintNotesMethod(noteCount) {
3668
- expect(noteCount >= 1, "mint-notes requires at least one output amount.");
4109
+ expect(noteCount >= 1, "wallet mint-notes requires at least one output amount.");
3669
4110
  expect(
3670
4111
  noteCount <= 2,
3671
- "mint-notes supports only one or two output amounts with the currently registered DApp.",
4112
+ "wallet mint-notes supports only one or two output amounts with the currently registered DApp.",
3672
4113
  );
3673
4114
  return `mintNotes${noteCount}`;
3674
4115
  }
3675
4116
 
3676
4117
  function selectRedeemNotesMethod(noteCount) {
3677
- expect(noteCount === 1, "redeem-notes supports exactly one input note with the currently registered DApp.");
4118
+ expect(noteCount === 1, "wallet redeem-notes supports exactly one input note with the currently registered DApp.");
3678
4119
  return `redeemNotes${noteCount}`;
3679
4120
  }
3680
4121
 
@@ -3793,7 +4234,7 @@ function selectTransferNotesMethod(inputCount, outputCount) {
3793
4234
  if (inputCount === 2 && outputCount === 1) {
3794
4235
  return "transferNotes2To1";
3795
4236
  }
3796
- throw new Error("transfer-notes supports only 1->1, 1->2, and 2->1 note transfers.");
4237
+ throw new Error("wallet transfer-notes supports only 1->1, 1->2, and 2->1 note transfers.");
3797
4238
  }
3798
4239
 
3799
4240
  function loadWalletUnusedInputNotes(walletContext, noteIds) {
@@ -3929,7 +4370,7 @@ async function recoverWalletChannelWorkspace({ walletContext, provider, progress
3929
4370
  const networkName = walletContext.wallet.network ?? networkNameFromChainId(Number(walletContext.wallet.chainId));
3930
4371
  const network = resolveCliNetwork(networkName);
3931
4372
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
3932
- await initializeChannelWorkspace({
4373
+ await syncChannelWorkspace({
3933
4374
  workspaceName: walletContext.wallet.channelName,
3934
4375
  channelName: walletContext.wallet.channelName,
3935
4376
  network,
@@ -3942,6 +4383,49 @@ async function recoverWalletChannelWorkspace({ walletContext, provider, progress
3942
4383
  });
3943
4384
  }
3944
4385
 
4386
+ async function refreshPersistedWorkspaceAfterLocalTransaction({
4387
+ context,
4388
+ provider,
4389
+ receipt,
4390
+ progressAction = null,
4391
+ }) {
4392
+ if (!context.persistChannelWorkspace || !context.workspaceDir) {
4393
+ return context;
4394
+ }
4395
+ const network = resolveCliNetwork(context.workspace.network);
4396
+ const bridgeResources = loadBridgeResources({ chainId: Number(context.workspace.chainId) });
4397
+ const refreshed = await syncChannelWorkspace({
4398
+ workspaceName: context.workspaceName,
4399
+ channelName: context.workspace.channelName,
4400
+ network,
4401
+ provider,
4402
+ bridgeResources,
4403
+ persist: true,
4404
+ allowExistingWorkspaceSync: true,
4405
+ useWorkspaceRecoveryIndex: true,
4406
+ minimumToBlock: receipt?.blockNumber ?? null,
4407
+ progressAction,
4408
+ });
4409
+
4410
+ context.workspaceDir = refreshed.workspaceDir;
4411
+ context.workspace = refreshed.workspace;
4412
+ context.currentSnapshot = refreshed.currentSnapshot;
4413
+ context.blockInfo = refreshed.blockInfo;
4414
+ context.contractCodes = refreshed.contractCodes;
4415
+ context.bridgeAbiManifest = bridgeResources.bridgeAbiManifest;
4416
+ context.channelManager = new Contract(
4417
+ refreshed.workspace.channelManager,
4418
+ bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
4419
+ provider,
4420
+ );
4421
+ context.bridgeTokenVault = new Contract(
4422
+ refreshed.workspace.bridgeTokenVault,
4423
+ bridgeResources.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
4424
+ provider,
4425
+ );
4426
+ return context;
4427
+ }
4428
+
3945
4429
  function isRecoverableWalletWorkspaceFailure(error) {
3946
4430
  const message = String(error?.message ?? error);
3947
4431
  return (message.includes("--verify") && message.includes("failed with exit code"))
@@ -3993,6 +4477,7 @@ async function executeWalletDirectTemplateCommand({
3993
4477
  txSubmitterAccount,
3994
4478
  l2Identity,
3995
4479
  context: contextResult.context,
4480
+ provider,
3996
4481
  operationName,
3997
4482
  functionName: templatePayload.method,
3998
4483
  templatePayload,
@@ -4024,6 +4509,7 @@ async function executeWalletDirectTemplateCommand({
4024
4509
  txSubmitterAccount,
4025
4510
  l2Identity,
4026
4511
  context: contextResult.context,
4512
+ provider,
4027
4513
  operationName,
4028
4514
  functionName: templatePayload.method,
4029
4515
  templatePayload,
@@ -4044,6 +4530,7 @@ async function executeWalletTemplateSend({
4044
4530
  txSubmitterAccount,
4045
4531
  l2Identity,
4046
4532
  context,
4533
+ provider,
4047
4534
  operationName,
4048
4535
  functionName,
4049
4536
  templatePayload,
@@ -4137,7 +4624,12 @@ async function executeWalletTemplateSend({
4137
4624
  applyNoteLifecycleToWallet(wallet, noteLifecycle, functionName, receipt.hash);
4138
4625
  context.currentSnapshot = nextSnapshot;
4139
4626
  persistWallet(wallet);
4140
- persistCurrentState(context);
4627
+ await refreshPersistedWorkspaceAfterLocalTransaction({
4628
+ context,
4629
+ provider,
4630
+ receipt,
4631
+ progressAction: operationName,
4632
+ });
4141
4633
  sealWalletOperationDir(operationDir, wallet.walletSecret);
4142
4634
 
4143
4635
  return {
@@ -4191,27 +4683,13 @@ async function loadWorkspaceContext(workspaceName, networkName, provider) {
4191
4683
  };
4192
4684
  }
4193
4685
 
4194
- async function loadChannelContext({ args, networkName, provider, walletContext = null }) {
4686
+ async function loadJoinChannelContext({ args, network, provider }) {
4195
4687
  const chainId = Number((await provider.getNetwork()).chainId);
4196
- const resolvedNetworkName = networkName ?? networkNameFromChainId(chainId);
4197
- const channelName = args.channelName ?? walletContext?.wallet.channelName;
4198
- if (args.channelName && walletContext) {
4199
- expect(
4200
- args.channelName === walletContext.wallet.channelName,
4201
- [
4202
- `The provided --channel-name (${args.channelName}) does not match the wallet channel`,
4203
- `(${walletContext.wallet.channelName}).`,
4204
- ].join(" "),
4205
- );
4206
- }
4207
- if (!channelName) {
4208
- throw new Error(
4209
- "Missing channel selector. Provide either --channel-name or --wallet bound to a channel.",
4210
- );
4211
- }
4688
+ const resolvedNetworkName = network?.name ?? networkNameFromChainId(chainId);
4689
+ const channelName = requireArg(args.channelName, "--channel-name");
4212
4690
 
4213
4691
  const bridgeResources = loadBridgeResources({ chainId });
4214
- const initialized = await initializeChannelWorkspace({
4692
+ const initialized = await syncChannelWorkspace({
4215
4693
  workspaceName: channelName,
4216
4694
  channelName,
4217
4695
  network: { chainId, name: resolvedNetworkName },
@@ -4305,7 +4783,7 @@ function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
4305
4783
  wallet.l2DerivationMode === CHANNEL_BOUND_L2_DERIVATION_MODE,
4306
4784
  [
4307
4785
  `Wallet ${walletName} was not created with the current channel-bound L2 derivation rule.`,
4308
- "Create a fresh wallet with join-channel.",
4786
+ "Create a fresh wallet with channel join.",
4309
4787
  ].join(" "),
4310
4788
  );
4311
4789
  expect(
@@ -5538,7 +6016,13 @@ function parseArgs(argv) {
5538
6016
  }
5539
6017
 
5540
6018
  parsed.command = parsed.positional[0];
5541
- if ((parsed.command === "account" || parsed.command === "wallet") && parsed.positional[1]) {
6019
+ if (
6020
+ (parsed.command === "account"
6021
+ || parsed.command === "channel"
6022
+ || parsed.command === "wallet"
6023
+ || parsed.command === "help")
6024
+ && parsed.positional[1]
6025
+ ) {
5542
6026
  parsed.command = `${parsed.command}-${parsed.positional[1]}`;
5543
6027
  parsed.positional = [parsed.command];
5544
6028
  }
@@ -5699,7 +6183,7 @@ function resolveWalletDefaultSecret(networkName, walletName) {
5699
6183
  CLI_ERROR_CODES.MISSING_WALLET_SECRET,
5700
6184
  [
5701
6185
  `Missing wallet default secret file: ${secretPath}.`,
5702
- "Run join-channel with --wallet-secret-path before wallet commands.",
6186
+ "Run channel join with --wallet-secret-path before wallet commands.",
5703
6187
  ].join(" "),
5704
6188
  );
5705
6189
  }
@@ -5712,12 +6196,17 @@ function prepareJoinWalletSecretForName({
5712
6196
  walletName,
5713
6197
  }) {
5714
6198
  const secretPath = walletSecretPath(networkName, walletName);
6199
+ const { channelName } = parseWalletName(walletName);
6200
+ const walletDir = walletPath(walletName, networkName);
5715
6201
  expect(
5716
- !walletConfigExists(walletPath(walletName, networkName)),
6202
+ !walletConfigExists(walletDir),
5717
6203
  [
5718
6204
  `Wallet ${walletName} already exists on ${networkName}.`,
5719
- "join-channel always creates a new local wallet.",
5720
- "Use recover-wallet or normal wallet commands for an existing local wallet.",
6205
+ "channel join always creates a new local wallet.",
6206
+ "If this wallet was previously exited on-chain, run",
6207
+ `wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}`,
6208
+ "once to remove the stale local wallet, then retry channel join.",
6209
+ "Use normal wallet commands for an existing active local wallet.",
5721
6210
  ].join(" "),
5722
6211
  );
5723
6212
  const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
@@ -5726,13 +6215,6 @@ function prepareJoinWalletSecretForName({
5726
6215
  ? readSecretFile(sourcePath, "--wallet-secret-path")
5727
6216
  : readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
5728
6217
  if (sourcePath !== canonicalPath) {
5729
- expect(
5730
- !fs.existsSync(canonicalPath),
5731
- [
5732
- `Wallet default secret file already exists: ${canonicalPath}.`,
5733
- "Remove it before joining with a different --wallet-secret-path.",
5734
- ].join(" "),
5735
- );
5736
6218
  writeSecretFile(canonicalPath, walletSecret);
5737
6219
  }
5738
6220
  return walletSecret;
@@ -5855,6 +6337,148 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
5855
6337
  );
5856
6338
  }
5857
6339
 
6340
+ function privateStateCliDataRoot() {
6341
+ const root = path.dirname(workspaceRoot);
6342
+ expect(
6343
+ path.dirname(secretRoot) === root,
6344
+ `Unexpected CLI data root layout: ${workspaceRoot} and ${secretRoot} are not siblings.`,
6345
+ );
6346
+ return root;
6347
+ }
6348
+
6349
+ function resolveExportWalletInfo({ networkName, walletName }) {
6350
+ resolveCliNetwork(networkName);
6351
+ const walletDir = walletPath(walletName, networkName);
6352
+ return {
6353
+ wallet: walletName,
6354
+ network: networkName,
6355
+ channelName: parseWalletName(walletName).channelName,
6356
+ walletDir,
6357
+ metadataPath: walletMetadataPath(walletDir),
6358
+ hasMetadata: fs.existsSync(walletMetadataPath(walletDir)),
6359
+ hasEncryptedWallet: walletConfigExists(walletDir),
6360
+ };
6361
+ }
6362
+
6363
+ function normalizeExportWalletInfo(walletInfo) {
6364
+ const wallet = requireWalletName({ wallet: walletInfo.wallet });
6365
+ const network = requireNetworkName({ network: walletInfo.network });
6366
+ const walletDir = walletInfo.walletDir ?? walletPath(wallet, network);
6367
+ const metadataPath = walletMetadataPath(walletDir);
6368
+ const encryptedWalletPath = walletConfigPath(walletDir);
6369
+ const metadata = readJsonIfExists(metadataPath);
6370
+ const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
6371
+ const walletSecret = walletSecretPath(network, wallet);
6372
+
6373
+ expect(fs.existsSync(encryptedWalletPath), `Wallet export cannot find encrypted wallet file: ${encryptedWalletPath}.`);
6374
+ expect(fs.existsSync(metadataPath), `Wallet export cannot find wallet metadata file: ${metadataPath}.`);
6375
+ expect(fs.existsSync(walletSecret), `Wallet export cannot find wallet-local secret file: ${walletSecret}.`);
6376
+ expect(
6377
+ metadata.network === network,
6378
+ `Wallet export metadata network ${metadata.network} does not match ${network}.`,
6379
+ );
6380
+ expect(
6381
+ metadata.channelName === channelName,
6382
+ `Wallet export metadata channel ${metadata.channelName} does not match ${channelName}.`,
6383
+ );
6384
+
6385
+ return {
6386
+ network,
6387
+ channelName,
6388
+ wallet,
6389
+ walletDir,
6390
+ walletSecretPath: walletSecret,
6391
+ };
6392
+ }
6393
+
6394
+ function walletExportFilePaths(walletInfo, { includeNotes }) {
6395
+ const walletFiles = [
6396
+ walletInfo.walletSecretPath,
6397
+ walletConfigPath(walletInfo.walletDir),
6398
+ walletMetadataPath(walletInfo.walletDir),
6399
+ ];
6400
+ if (!includeNotes) {
6401
+ return walletFiles;
6402
+ }
6403
+
6404
+ const workspaceDir = channelWorkspacePath(walletInfo.network, walletInfo.channelName);
6405
+ const currentDir = channelWorkspaceCurrentPath(workspaceDir);
6406
+ const workspaceFiles = [
6407
+ channelWorkspaceConfigPath(workspaceDir),
6408
+ path.join(currentDir, "state_snapshot.json"),
6409
+ path.join(currentDir, "state_snapshot.normalized.json"),
6410
+ path.join(currentDir, "block_info.json"),
6411
+ path.join(currentDir, "contract_codes.json"),
6412
+ ];
6413
+ for (const filePath of workspaceFiles) {
6414
+ expect(
6415
+ fs.existsSync(filePath),
6416
+ [
6417
+ `wallet export --include-notes requires channel workspace cache file: ${filePath}.`,
6418
+ "Run channel recover-workspace first, or export without --include-notes.",
6419
+ ].join(" "),
6420
+ );
6421
+ }
6422
+ return [...walletFiles, ...workspaceFiles];
6423
+ }
6424
+
6425
+ function archivePathForLocalCliFile(filePath) {
6426
+ const root = privateStateCliDataRoot();
6427
+ const absolutePath = path.resolve(filePath);
6428
+ expectPathWithinRoot(absolutePath, root, `Cannot export file outside CLI data root: ${absolutePath}.`);
6429
+ return path.relative(root, absolutePath).split(path.sep).join("/");
6430
+ }
6431
+
6432
+ function validateWalletExportManifest(manifest) {
6433
+ expect(manifest?.format === WALLET_EXPORT_FORMAT, "Wallet import ZIP has an unsupported format.");
6434
+ expect(
6435
+ Number(manifest.formatVersion) === WALLET_EXPORT_FORMAT_VERSION,
6436
+ `Wallet import ZIP format version ${manifest?.formatVersion} is not supported.`,
6437
+ );
6438
+ expect(Array.isArray(manifest.files), "Wallet import ZIP manifest is missing files[].");
6439
+ expect(Array.isArray(manifest.wallets), "Wallet import ZIP manifest is missing wallets[].");
6440
+ expect(typeof manifest.includeNotes === "boolean", "Wallet import ZIP manifest is missing includeNotes.");
6441
+ expect(manifest.wallets.length > 0, "Wallet import ZIP manifest does not list any wallets.");
6442
+ const uniqueFiles = new Set(manifest.files);
6443
+ expect(uniqueFiles.size === manifest.files.length, "Wallet import ZIP manifest contains duplicate file paths.");
6444
+ expect(manifest.files.length > 0, "Wallet import ZIP manifest does not list any files.");
6445
+ for (const filePath of manifest.files) {
6446
+ validateWalletArchivePath(filePath);
6447
+ }
6448
+ for (const wallet of manifest.wallets) {
6449
+ requireNetworkName({ network: wallet.network });
6450
+ requireWalletName({ wallet: wallet.wallet });
6451
+ requireArg(wallet.channelName, "wallets[].channelName");
6452
+ }
6453
+ }
6454
+
6455
+ function validateWalletArchivePath(archivePath) {
6456
+ expect(typeof archivePath === "string" && archivePath.length > 0, "Wallet import ZIP contains an empty path.");
6457
+ expect(!archivePath.includes("\0"), `Wallet import ZIP contains an invalid path: ${archivePath}.`);
6458
+ expect(!archivePath.includes("\\"), `Wallet import ZIP path must use forward slashes: ${archivePath}.`);
6459
+ expect(!path.posix.isAbsolute(archivePath), `Wallet import ZIP path must be relative: ${archivePath}.`);
6460
+ expect(path.posix.normalize(archivePath) === archivePath, `Wallet import ZIP path is not normalized: ${archivePath}.`);
6461
+ expect(
6462
+ archivePath.startsWith("secrets/") || archivePath.startsWith("workspace/"),
6463
+ `Wallet import ZIP path must start with secrets/ or workspace/: ${archivePath}.`,
6464
+ );
6465
+ }
6466
+
6467
+ function expectPathWithinRoot(targetPath, rootPath, message) {
6468
+ const relative = path.relative(path.resolve(rootPath), path.resolve(targetPath));
6469
+ expect(relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative), message);
6470
+ }
6471
+
6472
+ function applyImportedWalletFileMode(archivePath, targetPath) {
6473
+ if (
6474
+ archivePath.startsWith("secrets/")
6475
+ || archivePath.endsWith("/wallet.json")
6476
+ || archivePath.endsWith("/wallet.metadata.json")
6477
+ ) {
6478
+ protectSecretFile(targetPath, `imported wallet file ${archivePath}`);
6479
+ }
6480
+ }
6481
+
5858
6482
  function channelDataPath(workspaceDir) {
5859
6483
  return workspaceChannelDir(workspaceDir);
5860
6484
  }
@@ -5953,10 +6577,18 @@ function assertUninstallArgs(args) {
5953
6577
  assertAllowedCommandSchema(args, "uninstall");
5954
6578
  }
5955
6579
 
6580
+ function assertHelpCommandsArgs(args) {
6581
+ assertAllowedCommandSchema(args, "help-commands");
6582
+ }
6583
+
6584
+ function assertUpdateArgs(args) {
6585
+ assertAllowedCommandSchema(args, "help-update");
6586
+ }
6587
+
5956
6588
  function assertDoctorArgs(args) {
5957
- assertAllowedCommandSchema(args, "doctor");
6589
+ assertAllowedCommandSchema(args, "help-doctor");
5958
6590
  if (args.gpu !== undefined && args.gpu !== true) {
5959
- throw new Error("doctor option --gpu does not accept a value.");
6591
+ throw new Error("help doctor option --gpu does not accept a value.");
5960
6592
  }
5961
6593
  }
5962
6594
 
@@ -5973,11 +6605,11 @@ function assertGuideArgs(args) {
5973
6605
  if (args.wallet !== undefined) {
5974
6606
  requireWalletName(args);
5975
6607
  }
5976
- assertAllowedCommandSchema(args, "guide");
6608
+ assertAllowedCommandSchema(args, "help-guide");
5977
6609
  }
5978
6610
 
5979
6611
  function assertTransactionFeesArgs(args) {
5980
- assertAllowedCommandSchema(args, "transaction-fees");
6612
+ assertAllowedCommandSchema(args, "help-transaction-fees");
5981
6613
  }
5982
6614
 
5983
6615
  function assertAccountImportArgs(args) {
@@ -5985,7 +6617,7 @@ function assertAccountImportArgs(args) {
5985
6617
  }
5986
6618
 
5987
6619
  function assertMintNotesArgs(args) {
5988
- assertAllowedCommandSchema(args, "mint-notes");
6620
+ assertAllowedCommandSchema(args, "wallet-mint-notes");
5989
6621
  assertTxSubmitterArg(args);
5990
6622
  parseAmountVector(args.amounts, {
5991
6623
  allowZeroEntries: true,
@@ -5994,13 +6626,13 @@ function assertMintNotesArgs(args) {
5994
6626
  }
5995
6627
 
5996
6628
  function assertRedeemNotesArgs(args) {
5997
- assertAllowedCommandSchema(args, "redeem-notes");
6629
+ assertAllowedCommandSchema(args, "wallet-redeem-notes");
5998
6630
  assertTxSubmitterArg(args);
5999
6631
  selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
6000
6632
  }
6001
6633
 
6002
6634
  function assertTransferNotesArgs(args) {
6003
- assertAllowedCommandSchema(args, "transfer-notes");
6635
+ assertAllowedCommandSchema(args, "wallet-transfer-notes");
6004
6636
  assertTxSubmitterArg(args);
6005
6637
  const noteIds = parseNoteIdVector(args.noteIds);
6006
6638
  const recipients = parseRecipientVector(args.recipients);
@@ -6021,28 +6653,28 @@ function assertTxSubmitterArg(args) {
6021
6653
  }
6022
6654
  }
6023
6655
 
6024
- function assertGetMyNotesArgs(args) {
6025
- assertWalletSecretArgs(args, "get-my-notes");
6656
+ function assertWalletGetNotesArgs(args) {
6657
+ assertWalletSecretArgs(args, "wallet-get-notes");
6026
6658
  }
6027
6659
 
6028
6660
  function assertCreateChannelArgs(args) {
6029
- assertAllowedCommandSchema(args, "create-channel");
6661
+ assertAllowedCommandSchema(args, "channel-create");
6030
6662
  }
6031
6663
 
6032
6664
  function assertRecoverWorkspaceArgs(args) {
6033
- assertAllowedCommandSchema(args, "recover-workspace");
6665
+ assertAllowedCommandSchema(args, "channel-recover-workspace");
6034
6666
  }
6035
6667
 
6036
6668
  function assertGetChannelArgs(args) {
6037
- assertAllowedCommandSchema(args, "get-channel");
6669
+ assertAllowedCommandSchema(args, "channel-get-meta");
6038
6670
  }
6039
6671
 
6040
6672
  function assertDepositBridgeArgs(args) {
6041
- assertAllowedCommandSchema(args, "deposit-bridge");
6673
+ assertAllowedCommandSchema(args, "account-deposit-bridge");
6042
6674
  }
6043
6675
 
6044
- function assertGetMyBridgeFundArgs(args) {
6045
- assertAllowedCommandSchema(args, "get-my-bridge-fund");
6676
+ function assertAccountGetBridgeFundArgs(args) {
6677
+ assertAllowedCommandSchema(args, "account-get-bridge-fund");
6046
6678
  }
6047
6679
 
6048
6680
  function assertExplicitSignerCommandArgs(args, commandName) {
@@ -6050,19 +6682,22 @@ function assertExplicitSignerCommandArgs(args, commandName) {
6050
6682
  }
6051
6683
 
6052
6684
  function assertRecoverWalletArgs(args) {
6053
- assertExplicitSignerCommandArgs(args, "recover-wallet");
6685
+ assertExplicitSignerCommandArgs(args, "wallet-recover-workspace");
6686
+ if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
6687
+ throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
6688
+ }
6054
6689
  }
6055
6690
 
6056
6691
  function assertJoinChannelArgs(args) {
6057
- assertAllowedCommandSchema(args, "join-channel");
6692
+ assertAllowedCommandSchema(args, "channel-join");
6058
6693
  }
6059
6694
 
6060
- function assertGetMyWalletMetaArgs(args) {
6061
- assertWalletSecretArgs(args, "get-my-wallet-meta");
6695
+ function assertWalletGetMetaArgs(args) {
6696
+ assertWalletSecretArgs(args, "wallet-get-meta");
6062
6697
  }
6063
6698
 
6064
- function assertGetMyL1AddressArgs(args) {
6065
- assertAllowedCommandSchema(args, "get-my-l1-address");
6699
+ function assertAccountGetL1AddressArgs(args) {
6700
+ assertAllowedCommandSchema(args, "account-get-l1-address");
6066
6701
  }
6067
6702
 
6068
6703
  function assertListLocalWalletsArgs(args) {
@@ -6072,19 +6707,45 @@ function assertListLocalWalletsArgs(args) {
6072
6707
  if (args.channelName !== undefined) {
6073
6708
  requireArg(args.channelName, "--channel-name");
6074
6709
  }
6075
- assertAllowedCommandSchema(args, "list-local-wallets");
6710
+ assertAllowedCommandSchema(args, "wallet-list");
6711
+ }
6712
+
6713
+ function assertWalletExportArgs(args) {
6714
+ assertAllowedCommandSchema(args, "wallet-export");
6715
+ assertFlagOption(args, "all", "wallet export");
6716
+ assertFlagOption(args, "includeNotes", "wallet export");
6717
+ requireArg(args.output, "--output");
6718
+ if (args.all === true) {
6719
+ expect(
6720
+ args.network === undefined && args.wallet === undefined,
6721
+ "wallet export --all exports every local mainnet wallet and does not accept --network or --wallet.",
6722
+ );
6723
+ return;
6724
+ }
6725
+ requireNetworkName(args);
6726
+ requireWalletName(args);
6727
+ }
6728
+
6729
+ function assertWalletImportArgs(args) {
6730
+ assertAllowedCommandSchema(args, "wallet-import");
6731
+ }
6732
+
6733
+ function assertFlagOption(args, key, commandName) {
6734
+ if (args[key] !== undefined && args[key] !== true) {
6735
+ throw new Error(`${commandName} option --${toKebabCase(key)} does not accept a value.`);
6736
+ }
6076
6737
  }
6077
6738
 
6078
6739
  function assertWithdrawBridgeArgs(args) {
6079
- assertAllowedCommandSchema(args, "withdraw-bridge");
6740
+ assertAllowedCommandSchema(args, "account-withdraw-bridge");
6080
6741
  }
6081
6742
 
6082
- function assertGetMyChannelFundArgs(args) {
6083
- assertWalletSecretArgs(args, "get-my-channel-fund");
6743
+ function assertWalletGetChannelFundArgs(args) {
6744
+ assertWalletSecretArgs(args, "wallet-get-channel-fund");
6084
6745
  }
6085
6746
 
6086
6747
  function assertExitChannelArgs(args) {
6087
- assertWalletSecretArgs(args, "exit-channel");
6748
+ assertWalletSecretArgs(args, "channel-exit");
6088
6749
  }
6089
6750
 
6090
6751
  function createWalletOperationDir(walletName, networkName, suffix) {
@@ -6110,17 +6771,6 @@ function persistWalletMetadata(context) {
6110
6771
  });
6111
6772
  }
6112
6773
 
6113
- function persistCurrentState(context) {
6114
- if (!context.persistChannelWorkspace || !context.workspaceDir) {
6115
- return;
6116
- }
6117
- writeJson(path.join(channelWorkspaceCurrentPath(context.workspaceDir), "state_snapshot.json"), context.currentSnapshot);
6118
- writeJson(
6119
- path.join(channelWorkspaceCurrentPath(context.workspaceDir), "state_snapshot.normalized.json"),
6120
- context.currentSnapshot,
6121
- );
6122
- }
6123
-
6124
6774
  function printHelp() {
6125
6775
  const commandHelp = PRIVATE_STATE_CLI_COMMANDS.map((command) => [
6126
6776
  ` ${privateStateCliCommandSynopsis(command)}`,
@@ -6134,10 +6784,10 @@ ${commandHelp}
6134
6784
  Secret source options:
6135
6785
  Use account import --private-key-file once to create a protected local account secret.
6136
6786
  L1 signing commands use --account only.
6137
- A wallet secret source file is arbitrary high-entropy secret text read once by join-channel.
6787
+ A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
6138
6788
  Create one before joining a channel, for example:
6139
6789
  openssl rand -hex 32 > ./wallet-secret.txt
6140
- private-state-cli join-channel --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt
6790
+ private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt
6141
6791
  Bridge-facing commands accept optional --rpc-url. When provided, it is saved to
6142
6792
  ~/tokamak-private-channels/secrets/<network>/.env as RPC_URL. When omitted, the CLI reads RPC_URL from that file.
6143
6793
  Wallet commands use wallet-local default secret files only.
@@ -6152,7 +6802,7 @@ Options:
6152
6802
  Print the command result as JSON. Without --json, commands print human-readable output.
6153
6803
 
6154
6804
  --help
6155
- Show this help
6805
+ Show this help. Equivalent to help commands.
6156
6806
  `);
6157
6807
  }
6158
6808
 
@@ -6577,6 +7227,7 @@ function loadWalletCommandRuntime(args) {
6577
7227
  const HUMAN_RESULT_RENDERERS = Object.freeze({
6578
7228
  guide: printGuideHumanResult,
6579
7229
  "transaction-fees": printTransactionFeesHumanResult,
7230
+ update: printUpdateHumanResult,
6580
7231
  });
6581
7232
 
6582
7233
  function normalizePrivateKey(value) {
@@ -6668,6 +7319,35 @@ function printTransactionFeesHumanResult(report) {
6668
7319
  console.log(lines.join("\n"));
6669
7320
  }
6670
7321
 
7322
+ function printUpdateHumanResult(report) {
7323
+ const lines = [
7324
+ "Private-State CLI Update",
7325
+ `Package: ${formatHumanValue(report.packageName)}`,
7326
+ `Current version: ${formatHumanValue(report.currentVersion)}`,
7327
+ `Latest registry version: ${formatHumanValue(report.latestVersion)}`,
7328
+ ];
7329
+ if (report.registryState === "local-version-ahead-of-registry") {
7330
+ lines.push("Status: local version is newer than the npm registry latest tag.");
7331
+ } else if (!report.updateAvailable) {
7332
+ lines.push("Status: up to date.");
7333
+ } else if (report.updated) {
7334
+ lines.push("Status: updated global npm install.");
7335
+ } else {
7336
+ lines.push(
7337
+ "Status: update available.",
7338
+ `Reason: ${formatHumanValue(report.reason)}`,
7339
+ `Command: ${formatHumanValue(report.command)}`,
7340
+ );
7341
+ }
7342
+ lines.push(
7343
+ `Global install: ${report.globalPackage?.installed ? `yes (${formatHumanValue(report.globalPackage.version)})` : "no"}`,
7344
+ `Repository checkout: ${report.runningFromRepositoryCheckout ? "yes" : "no"}`,
7345
+ "",
7346
+ "Run with --json to inspect the full update report.",
7347
+ );
7348
+ console.log(lines.join("\n"));
7349
+ }
7350
+
6671
7351
  function formatHumanTable(headers, rows) {
6672
7352
  const values = [headers, ...rows].map((row) => row.map((value) => String(value ?? "")));
6673
7353
  const widths = headers.map((_header, columnIndex) =>
@@ -6847,18 +7527,18 @@ function buildRecoveryHints(error, args = {}) {
6847
7527
  || message.includes("does not match the wallet channel")
6848
7528
  || message.includes("The provided wallet does not belong to the selected channel")
6849
7529
  ) {
6850
- hints.push(`private-state-cli list-local-wallets --network ${networkName}`);
6851
- hints.push(`private-state-cli guide --network ${networkName} --wallet ${walletName}`);
7530
+ hints.push(`private-state-cli wallet list --network ${networkName}`);
7531
+ hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
6852
7532
  }
6853
7533
 
6854
7534
  if (error?.code === CLI_ERROR_CODES.MISSING_WALLET_SECRET) {
6855
7535
  hints.push("restore the wallet-local default secret file from backup before running wallet commands.");
6856
- hints.push(`private-state-cli guide --network ${networkName} --wallet ${walletName}`);
7536
+ hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
6857
7537
  }
6858
7538
 
6859
7539
  if (error?.code === CLI_ERROR_CODES.WALLET_DECRYPT_FAILED) {
6860
7540
  hints.push("verify that the wallet-local default secret file is the same secret used when the wallet was created.");
6861
- hints.push("if the encrypted wallet file is corrupted but the wallet secret and L1 account secret still exist, rerun recover-wallet.");
7541
+ hints.push("if the encrypted wallet file is corrupted but the wallet secret and L1 account secret still exist, rerun wallet recover-workspace.");
6862
7542
  hints.push("if the wallet secret was lost, the local L2 key cannot be recovered from the encrypted wallet file.");
6863
7543
  }
6864
7544
 
@@ -6867,7 +7547,7 @@ function buildRecoveryHints(error, args = {}) {
6867
7547
  || message.includes("Missing --account.")
6868
7548
  ) {
6869
7549
  hints.push(`private-state-cli account import --account ${accountName} --network ${networkName} --private-key-file <PATH>`);
6870
- hints.push(`private-state-cli guide --network ${networkName} --account ${accountName}`);
7550
+ hints.push(`private-state-cli help guide --network ${networkName} --account ${accountName}`);
6871
7551
  }
6872
7552
 
6873
7553
  if (
@@ -6875,22 +7555,31 @@ function buildRecoveryHints(error, args = {}) {
6875
7555
  || message.includes("DApp deployment artifact")
6876
7556
  ) {
6877
7557
  hints.push("private-state-cli install");
6878
- hints.push("private-state-cli doctor --json");
7558
+ hints.push("private-state-cli help doctor --json");
6879
7559
  }
6880
7560
 
6881
7561
  if (error?.code === CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION) {
6882
- hints.push(`private-state-cli join-channel --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH>`);
6883
- hints.push(`private-state-cli guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
7562
+ hints.push(`private-state-cli channel join --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH>`);
7563
+ hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
6884
7564
  }
6885
7565
 
6886
7566
  if (error?.code === CLI_ERROR_CODES.STALE_WORKSPACE) {
6887
- hints.push(`private-state-cli recover-workspace --channel-name ${channelName} --network ${networkName}`);
6888
- hints.push(`private-state-cli guide --network ${networkName} --channel-name ${channelName}`);
7567
+ hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
7568
+ hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName}`);
7569
+ }
7570
+
7571
+ if (message.includes("Workspace recovery index is missing or unusable")) {
7572
+ hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --from-genesis`);
7573
+ hints.push(`private-state-cli wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
7574
+ }
7575
+
7576
+ if (message.includes("Wallet note recovery index is missing or unusable")) {
7577
+ hints.push(`private-state-cli wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
6889
7578
  }
6890
7579
 
6891
7580
  if (message.includes("Missing channel selector")) {
6892
- hints.push(`private-state-cli list-local-wallets --network ${networkName}`);
6893
- hints.push(`private-state-cli guide --network ${networkName} --channel-name <CHANNEL> --wallet <WALLET>`);
7581
+ hints.push(`private-state-cli wallet list --network ${networkName}`);
7582
+ hints.push(`private-state-cli help guide --network ${networkName} --channel-name <CHANNEL> --wallet <WALLET>`);
6894
7583
  }
6895
7584
 
6896
7585
  return [...new Set(hints)];