@tokamak-private-dapps/private-state-cli 1.0.2 → 1.1.1

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,
@@ -132,6 +133,8 @@ const PRIVATE_STATE_UNINSTALL_CONFIRMATION =
132
133
  const PRIVATE_STATE_CLI_PACKAGE_NAME = privateStateCliPackageJson.name;
133
134
  const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
134
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;
135
138
  let jsonOutputRequested = false;
136
139
  let activeCliArgs = {};
137
140
 
@@ -329,6 +332,12 @@ async function main() {
329
332
  return;
330
333
  }
331
334
 
335
+ if (args.command === "help-commands") {
336
+ assertHelpCommandsArgs(args);
337
+ printHelp();
338
+ return;
339
+ }
340
+
332
341
  if (args.command === "install") {
333
342
  assertInstallZkEvmArgs(args);
334
343
  await handleInstallZkEvm({ args });
@@ -341,34 +350,34 @@ async function main() {
341
350
  return;
342
351
  }
343
352
 
344
- if (args.command === "update") {
353
+ if (args.command === "help-update") {
345
354
  assertUpdateArgs(args);
346
355
  await handleUpdate();
347
356
  return;
348
357
  }
349
358
 
350
- if (args.command === "doctor") {
359
+ if (args.command === "help-doctor") {
351
360
  assertDoctorArgs(args);
352
361
  await handleDoctor({ args });
353
362
  return;
354
363
  }
355
364
 
356
- if (args.command === "guide") {
365
+ if (args.command === "help-guide") {
357
366
  assertGuideArgs(args);
358
367
  await handleGuide({ args });
359
368
  return;
360
369
  }
361
370
 
362
- if (args.command === "transaction-fees") {
371
+ if (args.command === "help-transaction-fees") {
363
372
  assertTransactionFeesArgs(args);
364
373
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
365
374
  await handleTransactionFees({ network, provider, rpcUrl });
366
375
  return;
367
376
  }
368
377
 
369
- if (args.command === "get-my-l1-address") {
370
- assertGetMyL1AddressArgs(args);
371
- handleGetMyL1Address({ args });
378
+ if (args.command === "account-get-l1-address") {
379
+ assertAccountGetL1AddressArgs(args);
380
+ handleAccountGetL1Address({ args });
372
381
  return;
373
382
  }
374
383
 
@@ -378,46 +387,58 @@ async function main() {
378
387
  return;
379
388
  }
380
389
 
381
- if (args.command === "list-local-wallets") {
390
+ if (args.command === "wallet-list") {
382
391
  assertListLocalWalletsArgs(args);
383
392
  handleListLocalWallets({ args });
384
393
  return;
385
394
  }
386
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
+
387
408
  const walletCommandHandlers = {
388
- "mint-notes": {
409
+ "wallet-mint-notes": {
389
410
  assert: assertMintNotesArgs,
390
411
  run: ({ provider }) => handleMintNotes({ args, provider }),
391
412
  },
392
- "redeem-notes": {
413
+ "wallet-redeem-notes": {
393
414
  assert: assertRedeemNotesArgs,
394
415
  run: ({ provider }) => handleRedeemNotes({ args, provider }),
395
416
  },
396
- "get-my-notes": {
397
- assert: assertGetMyNotesArgs,
398
- run: ({ provider }) => handleGetMyNotes({ args, provider }),
417
+ "wallet-get-notes": {
418
+ assert: assertWalletGetNotesArgs,
419
+ run: ({ provider }) => handleWalletGetNotes({ args, provider }),
399
420
  },
400
- "transfer-notes": {
421
+ "wallet-transfer-notes": {
401
422
  assert: assertTransferNotesArgs,
402
423
  run: ({ provider }) => handleTransferNotes({ args, provider }),
403
424
  },
404
- "deposit-channel": {
405
- assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "deposit-channel"),
425
+ "wallet-deposit-channel": {
426
+ assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "wallet-deposit-channel"),
406
427
  run: ({ provider }) => handleGrothVaultMove({ args, provider, direction: "deposit" }),
407
428
  },
408
- "withdraw-channel": {
409
- assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "withdraw-channel"),
429
+ "wallet-withdraw-channel": {
430
+ assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "wallet-withdraw-channel"),
410
431
  run: ({ provider }) => handleGrothVaultMove({ args, provider, direction: "withdraw" }),
411
432
  },
412
- "get-my-wallet-meta": {
413
- assert: assertGetMyWalletMetaArgs,
414
- run: ({ provider }) => handleGetMyWalletMeta({ args, provider }),
433
+ "wallet-get-meta": {
434
+ assert: assertWalletGetMetaArgs,
435
+ run: ({ provider }) => handleWalletGetMeta({ args, provider }),
415
436
  },
416
- "get-my-channel-fund": {
417
- assert: assertGetMyChannelFundArgs,
418
- run: ({ provider }) => handleGetMyChannelFund({ args, provider }),
437
+ "wallet-get-channel-fund": {
438
+ assert: assertWalletGetChannelFundArgs,
439
+ run: ({ provider }) => handleWalletGetChannelFund({ args, provider }),
419
440
  },
420
- "exit-channel": {
441
+ "channel-exit": {
421
442
  assert: assertExitChannelArgs,
422
443
  run: ({ provider }) => handleExitChannel({ args, provider }),
423
444
  },
@@ -431,56 +452,56 @@ async function main() {
431
452
  }
432
453
 
433
454
  switch (args.command) {
434
- case "create-channel": {
455
+ case "channel-create": {
435
456
  assertCreateChannelArgs(args);
436
457
  const { network, provider } = loadExplicitCommandRuntime(args);
437
458
  await prepareDeploymentArtifacts(network.chainId);
438
459
  await handleChannelCreate({ args, network, provider });
439
460
  return;
440
461
  }
441
- case "recover-workspace": {
462
+ case "channel-recover-workspace": {
442
463
  assertRecoverWorkspaceArgs(args);
443
464
  const { network, provider } = loadExplicitCommandRuntime(args);
444
465
  await prepareDeploymentArtifacts(network.chainId);
445
466
  await handleWorkspaceInit({ args, network, provider });
446
467
  return;
447
468
  }
448
- case "get-channel": {
469
+ case "channel-get-meta": {
449
470
  assertGetChannelArgs(args);
450
471
  const { network, provider } = loadExplicitCommandRuntime(args);
451
472
  await prepareDeploymentArtifacts(network.chainId);
452
473
  await handleGetChannel({ args, network, provider });
453
474
  return;
454
475
  }
455
- case "deposit-bridge": {
476
+ case "account-deposit-bridge": {
456
477
  assertDepositBridgeArgs(args);
457
478
  const { network, provider } = loadExplicitCommandRuntime(args);
458
479
  await prepareDeploymentArtifacts(network.chainId);
459
480
  await handleDepositBridge({ args, network, provider });
460
481
  return;
461
482
  }
462
- case "withdraw-bridge": {
483
+ case "account-withdraw-bridge": {
463
484
  assertWithdrawBridgeArgs(args);
464
485
  const { network, provider } = loadExplicitCommandRuntime(args);
465
486
  await prepareDeploymentArtifacts(network.chainId);
466
487
  await handleWithdrawBridge({ args, network, provider });
467
488
  return;
468
489
  }
469
- case "get-my-bridge-fund": {
470
- assertGetMyBridgeFundArgs(args);
490
+ case "account-get-bridge-fund": {
491
+ assertAccountGetBridgeFundArgs(args);
471
492
  const { network, provider } = loadExplicitCommandRuntime(args);
472
493
  await prepareDeploymentArtifacts(network.chainId);
473
- await handleGetMyBridgeFund({ args, provider });
494
+ await handleAccountGetBridgeFund({ args, provider });
474
495
  return;
475
496
  }
476
- case "recover-wallet": {
497
+ case "wallet-recover-workspace": {
477
498
  assertRecoverWalletArgs(args);
478
499
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
479
500
  await prepareDeploymentArtifacts(network.chainId);
480
501
  await handleRecoverWallet({ args, network, provider, rpcUrl });
481
502
  return;
482
503
  }
483
- case "join-channel": {
504
+ case "channel-join": {
484
505
  assertJoinChannelArgs(args);
485
506
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
486
507
  await prepareDeploymentArtifacts(network.chainId);
@@ -518,7 +539,7 @@ async function handleChannelCreate({ args, network, provider }) {
518
539
  const policySnapshot = dapp.policySnapshot;
519
540
 
520
541
  printImmutableChannelPolicyWarning({
521
- action: "create-channel",
542
+ action: "channel create",
522
543
  channelName,
523
544
  channelId,
524
545
  policySnapshot,
@@ -527,7 +548,7 @@ async function handleChannelCreate({ args, network, provider }) {
527
548
  await waitForReceipt(await bridgeCore.createChannel(channelId, dappId, joinToll, dapp.metadataDigest));
528
549
  const channelInfo = await bridgeCore.getChannel(channelId);
529
550
 
530
- const workspaceResult = await initializeChannelWorkspace({
551
+ const workspaceResult = await syncChannelWorkspace({
531
552
  workspaceName,
532
553
  channelName,
533
554
  network,
@@ -535,11 +556,12 @@ async function handleChannelCreate({ args, network, provider }) {
535
556
  bridgeResources,
536
557
  persist: true,
537
558
  fromGenesis: true,
538
- progressAction: "create-channel",
559
+ minimumToBlock: receipt.blockNumber,
560
+ progressAction: "channel create",
539
561
  });
540
562
 
541
563
  printJson({
542
- action: "create-channel",
564
+ action: "channel create",
543
565
  channelName,
544
566
  channelId: channelId.toString(),
545
567
  dappId,
@@ -637,7 +659,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
637
659
  const workspaceName = channelName;
638
660
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
639
661
 
640
- const { workspaceDir, workspace, currentSnapshot } = await initializeChannelWorkspace({
662
+ const { workspaceDir, workspace, currentSnapshot } = await syncChannelWorkspace({
641
663
  workspaceName,
642
664
  channelName,
643
665
  network,
@@ -647,11 +669,11 @@ async function handleWorkspaceInit({ args, network, provider }) {
647
669
  allowExistingWorkspaceSync: true,
648
670
  useWorkspaceRecoveryIndex: true,
649
671
  fromGenesis: args.fromGenesis === true,
650
- progressAction: "recover-workspace",
672
+ progressAction: "channel recover-workspace",
651
673
  });
652
674
 
653
675
  printJson({
654
- action: "recover-workspace",
676
+ action: "channel recover-workspace",
655
677
  workspace: workspaceName,
656
678
  workspaceDir,
657
679
  channelName,
@@ -683,7 +705,7 @@ async function handleGetChannel({ args, network, provider }) {
683
705
  } catch (error) {
684
706
  if (isContractError(error, bridgeCore.interface, "UnknownChannel")) {
685
707
  printJson({
686
- action: "get-channel",
708
+ action: "channel get-meta",
687
709
  channelName,
688
710
  channelId: channelId.toString(),
689
711
  exists: false,
@@ -722,7 +744,7 @@ async function handleGetChannel({ args, network, provider }) {
722
744
  ]);
723
745
 
724
746
  printJson({
725
- action: "get-channel",
747
+ action: "channel get-meta",
726
748
  channelName,
727
749
  channelId: channelId.toString(),
728
750
  exists: true,
@@ -745,7 +767,7 @@ async function handleGetChannel({ args, network, provider }) {
745
767
  });
746
768
  }
747
769
 
748
- async function initializeChannelWorkspace({
770
+ async function syncChannelWorkspace({
749
771
  workspaceName,
750
772
  channelName,
751
773
  network,
@@ -755,6 +777,7 @@ async function initializeChannelWorkspace({
755
777
  allowExistingWorkspaceSync = false,
756
778
  useWorkspaceRecoveryIndex = false,
757
779
  fromGenesis = false,
780
+ minimumToBlock = null,
758
781
  progressAction = null,
759
782
  }) {
760
783
  const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), workspaceName);
@@ -788,7 +811,10 @@ async function initializeChannelWorkspace({
788
811
  const canonicalAssetDecimals = await fetchTokenDecimals(provider, canonicalAsset);
789
812
  const currentRootVectorHash = normalizeBytes32Hex(await channelManager.currentRootVectorHash());
790
813
  const genesisBlockNumber = Number(await channelManager.genesisBlockNumber());
791
- const latestBlock = await provider.getBlockNumber();
814
+ const observedLatestBlock = await provider.getBlockNumber();
815
+ const latestBlock = minimumToBlock === null
816
+ ? observedLatestBlock
817
+ : Math.max(observedLatestBlock, Number(minimumToBlock));
792
818
  const managedStorageAddresses = normalizedAddressVector(await channelManager.getManagedStorageAddresses());
793
819
  const policySnapshot = await readChannelPolicySnapshot({
794
820
  channelManager,
@@ -820,11 +846,6 @@ async function initializeChannelWorkspace({
820
846
  ethers.toBigInt(derivedAPubBlockHash) === ethers.toBigInt(normalizeBytes32Hex(channelInfo.aPubBlockHash)),
821
847
  `Derived channel block-info hash ${derivedAPubBlockHash} does not match onchain ${channelInfo.aPubBlockHash}.`,
822
848
  );
823
- const localSnapshotReusable = !fromGenesis && canReuseLocalWorkspaceSnapshot({
824
- existingArtifacts,
825
- currentRootVectorHash,
826
- managedStorageAddresses,
827
- });
828
849
  const recoveryIndex = useWorkspaceRecoveryIndex && !fromGenesis
829
850
  ? getUsableWorkspaceRecoveryIndex({
830
851
  existingArtifacts,
@@ -833,11 +854,17 @@ async function initializeChannelWorkspace({
833
854
  managedStorageAddresses,
834
855
  })
835
856
  : null;
857
+ const localSnapshotReusable = !fromGenesis && (!useWorkspaceRecoveryIndex || recoveryIndex)
858
+ && canReuseLocalWorkspaceSnapshot({
859
+ existingArtifacts,
860
+ currentRootVectorHash,
861
+ managedStorageAddresses,
862
+ });
836
863
  if (useWorkspaceRecoveryIndex && !fromGenesis && !localSnapshotReusable && !recoveryIndex) {
837
864
  throw new Error([
838
865
  `Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
839
866
  "The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
840
- "Run recover-workspace --from-genesis or recover-wallet --from-genesis to rebuild from channel genesis.",
867
+ "Run channel recover-workspace --from-genesis or wallet recover-workspace --from-genesis to rebuild from channel genesis.",
841
868
  ].join(" "));
842
869
  }
843
870
  const reconstruction = localSnapshotReusable
@@ -928,7 +955,7 @@ async function initializeChannelWorkspace({
928
955
  async function handleDepositBridge({ args, network, provider }) {
929
956
  if (args.wallet !== undefined) {
930
957
  throw new Error(
931
- "--wallet is not supported by deposit-bridge. Channel wallet keys are set up only by join-channel.",
958
+ "--wallet is not supported by account deposit-bridge. Channel wallet keys are set up only by channel join.",
932
959
  );
933
960
  }
934
961
  const signer = requireL1Signer(args, provider);
@@ -952,7 +979,7 @@ async function handleDepositBridge({ args, network, provider }) {
952
979
  const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
953
980
 
954
981
  printJson({
955
- action: "deposit-bridge",
982
+ action: "account deposit-bridge",
956
983
  amountInput,
957
984
  amountBaseUnits: amount.toString(),
958
985
  l1Address: signer.address,
@@ -968,7 +995,7 @@ async function handleDepositBridge({ args, network, provider }) {
968
995
  });
969
996
  }
970
997
 
971
- async function handleGetMyBridgeFund({ args, provider }) {
998
+ async function handleAccountGetBridgeFund({ args, provider }) {
972
999
  const signer = requireL1Signer(args, provider);
973
1000
  const chainId = Number((await provider.getNetwork()).chainId);
974
1001
  const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId });
@@ -980,7 +1007,7 @@ async function handleGetMyBridgeFund({ args, provider }) {
980
1007
  const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
981
1008
 
982
1009
  printJson({
983
- action: "get-my-bridge-fund",
1010
+ action: "account get-bridge-fund",
984
1011
  l1Address: signer.address,
985
1012
  bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
986
1013
  canonicalAsset: bridgeVaultContext.canonicalAsset,
@@ -1002,7 +1029,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1002
1029
  walletName,
1003
1030
  });
1004
1031
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
1005
- const initialized = await initializeChannelWorkspace({
1032
+ const initialized = await syncChannelWorkspace({
1006
1033
  workspaceName: channelName,
1007
1034
  channelName,
1008
1035
  network,
@@ -1012,7 +1039,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1012
1039
  allowExistingWorkspaceSync: true,
1013
1040
  useWorkspaceRecoveryIndex: true,
1014
1041
  fromGenesis: args.fromGenesis === true,
1015
- progressAction: "recover-wallet",
1042
+ progressAction: "wallet recover-workspace",
1016
1043
  });
1017
1044
  const context = {
1018
1045
  workspaceName: channelName,
@@ -1050,13 +1077,41 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1050
1077
  const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
1051
1078
  const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
1052
1079
 
1053
- expect(
1054
- registration.exists,
1055
- cliError(
1056
- CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
1057
- `No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
1058
- ),
1059
- );
1080
+ if (!registration.exists) {
1081
+ const cleanup = removeLocalWalletArtifacts(walletName, context.workspace.network);
1082
+ if (cleanup.removed) {
1083
+ printJson({
1084
+ action: "wallet recover-workspace",
1085
+ status: "stale-wallet-removed",
1086
+ wallet: walletName,
1087
+ removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
1088
+ removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
1089
+ walletSecretSource: resolvedWalletSecretSource(args),
1090
+ walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
1091
+ workspace: context.workspaceName,
1092
+ channelName: context.workspace.channelName,
1093
+ channelId: context.workspace.channelId,
1094
+ l1Address: signer.address,
1095
+ l2Address: l2Identity.l2Address,
1096
+ l2StorageKey: storageKey,
1097
+ leafIndex: leafIndex.toString(),
1098
+ reason: "The local wallet existed, but the L1 address is no longer registered in the channel.",
1099
+ nextAction: buildRecoverWalletRemovedNextAction({
1100
+ channelName,
1101
+ networkName: network.name,
1102
+ accountName: args.account,
1103
+ }),
1104
+ });
1105
+ return;
1106
+ }
1107
+ expect(
1108
+ false,
1109
+ cliError(
1110
+ CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
1111
+ `No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
1112
+ ),
1113
+ );
1114
+ }
1060
1115
  expect(
1061
1116
  ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
1062
1117
  "The existing channel registration L2 address does not match the derived L2 address.",
@@ -1100,10 +1155,11 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1100
1155
  provider,
1101
1156
  signer,
1102
1157
  noteReceiveKeyMaterial,
1103
- progressAction: "recover-wallet",
1158
+ progressAction: "wallet recover-workspace",
1159
+ fromGenesis: args.fromGenesis === true,
1104
1160
  });
1105
1161
  printJson({
1106
- action: "recover-wallet",
1162
+ action: "wallet recover-workspace",
1107
1163
  status: "already-recovered",
1108
1164
  wallet: walletName,
1109
1165
  walletDir: existingWallet.walletDir,
@@ -1125,7 +1181,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1125
1181
  return;
1126
1182
  }
1127
1183
 
1128
- clearWalletRecoveryArtifacts(walletPath(walletName, context.workspace.network));
1184
+ fs.rmSync(walletPath(walletName, context.workspace.network), { recursive: true, force: true });
1129
1185
 
1130
1186
  const walletContext = ensureWallet({
1131
1187
  channelContext: context,
@@ -1147,11 +1203,12 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
1147
1203
  provider,
1148
1204
  signer,
1149
1205
  noteReceiveKeyMaterial,
1150
- progressAction: "recover-wallet",
1206
+ progressAction: "wallet recover-workspace",
1207
+ fromGenesis: args.fromGenesis === true,
1151
1208
  });
1152
1209
 
1153
1210
  printJson({
1154
- action: "recover-wallet",
1211
+ action: "wallet recover-workspace",
1155
1212
  status: "recovered",
1156
1213
  wallet: walletName,
1157
1214
  walletDir: walletContext.walletDir,
@@ -1301,8 +1358,30 @@ function assertExistingRecoverableWallet({
1301
1358
  );
1302
1359
  }
1303
1360
 
1304
- function clearWalletRecoveryArtifacts(walletDir) {
1305
- fs.rmSync(walletDir, { recursive: true, force: true });
1361
+ function removeLocalWalletArtifacts(walletName, networkName) {
1362
+ const walletDir = walletPath(walletName, networkName);
1363
+ const walletSecretFile = walletSecretPath(networkName, walletName);
1364
+ const walletSecretDir = path.dirname(walletSecretFile);
1365
+ const removedWalletDir = fs.existsSync(walletDir);
1366
+ const removedWalletSecret = fs.existsSync(walletSecretFile) || fs.existsSync(walletSecretDir);
1367
+ if (removedWalletDir) {
1368
+ fs.rmSync(walletDir, { recursive: true, force: true });
1369
+ }
1370
+ if (removedWalletSecret) {
1371
+ fs.rmSync(walletSecretDir, { recursive: true, force: true });
1372
+ }
1373
+ return {
1374
+ walletDir,
1375
+ walletSecretFile,
1376
+ removed: removedWalletDir || removedWalletSecret,
1377
+ removedWalletDir,
1378
+ removedWalletSecret,
1379
+ };
1380
+ }
1381
+
1382
+ function buildRecoverWalletRemovedNextAction({ channelName, networkName, accountName }) {
1383
+ const account = accountName ? String(accountName) : "<ACCOUNT>";
1384
+ return `channel join --channel-name ${channelName} --network ${networkName} --account ${account} --wallet-secret-path <PATH>`;
1306
1385
  }
1307
1386
 
1308
1387
  async function handleInstallZkEvm({ args }) {
@@ -1740,10 +1819,10 @@ function trimFixedNumber(value, maxDecimals) {
1740
1819
  return trimmed ? `${integer}.${trimmed}` : integer;
1741
1820
  }
1742
1821
 
1743
- function handleGetMyL1Address({ args }) {
1822
+ function handleAccountGetL1Address({ args }) {
1744
1823
  const signer = requireL1Signer(args);
1745
1824
  printJson({
1746
- action: "get-my-l1-address",
1825
+ action: "account get-l1-address",
1747
1826
  l1Address: signer.address,
1748
1827
  account: args.account ?? null,
1749
1828
  });
@@ -1770,7 +1849,7 @@ function handleAccountImport({ args }) {
1770
1849
  privateKeyPath,
1771
1850
  }, 0o600);
1772
1851
  printJson({
1773
- action: "account-import",
1852
+ action: "account import",
1774
1853
  account,
1775
1854
  network: networkName,
1776
1855
  l1Address: getAddress(signer.address),
@@ -1791,7 +1870,7 @@ function handleListLocalWallets({ args }) {
1791
1870
  });
1792
1871
 
1793
1872
  printJson({
1794
- action: "list-local-wallets",
1873
+ action: "wallet list",
1795
1874
  workspaceRoot,
1796
1875
  filters: {
1797
1876
  network: networkFilter,
@@ -1801,6 +1880,175 @@ function handleListLocalWallets({ args }) {
1801
1880
  });
1802
1881
  }
1803
1882
 
1883
+ function handleWalletExport({ args }) {
1884
+ const outputPath = path.resolve(String(requireArg(args.output, "--output")));
1885
+ expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
1886
+ ensureDir(path.dirname(outputPath));
1887
+
1888
+ const includeNotes = args.includeNotes === true;
1889
+ const wallets = args.all === true
1890
+ ? listLocalWallets({ networkFilter: "mainnet" }).filter((wallet) => wallet.hasEncryptedWallet)
1891
+ : [resolveExportWalletInfo({
1892
+ networkName: requireNetworkName(args),
1893
+ walletName: requireWalletName(args),
1894
+ })];
1895
+
1896
+ expect(
1897
+ wallets.length > 0,
1898
+ args.all === true
1899
+ ? "No local mainnet wallets are available to export."
1900
+ : "No local wallet is available to export.",
1901
+ );
1902
+
1903
+ const archive = new AdmZip();
1904
+ const files = new Map();
1905
+ const exportedWallets = [];
1906
+ for (const wallet of wallets) {
1907
+ const normalized = normalizeExportWalletInfo(wallet);
1908
+ exportedWallets.push({
1909
+ network: normalized.network,
1910
+ channelName: normalized.channelName,
1911
+ wallet: normalized.wallet,
1912
+ });
1913
+ for (const filePath of walletExportFilePaths(normalized, { includeNotes })) {
1914
+ const archivePath = archivePathForLocalCliFile(filePath);
1915
+ if (!files.has(archivePath)) {
1916
+ files.set(archivePath, filePath);
1917
+ }
1918
+ }
1919
+ }
1920
+
1921
+ const manifest = {
1922
+ format: WALLET_EXPORT_FORMAT,
1923
+ formatVersion: WALLET_EXPORT_FORMAT_VERSION,
1924
+ createdAt: new Date().toISOString(),
1925
+ cliPackage: PRIVATE_STATE_CLI_PACKAGE_NAME,
1926
+ cliVersion: privateStateCliPackageJson.version,
1927
+ exportMode: args.all === true ? "all-mainnet" : "single-wallet",
1928
+ includeNotes,
1929
+ notes: includeNotes
1930
+ ? [
1931
+ "Includes the channel workspace cache required for immediate wallet command use when the cache is still chain-aligned.",
1932
+ ]
1933
+ : [
1934
+ "Includes wallet identity, encrypted wallet state, metadata, and wallet-local secret only.",
1935
+ "Run channel recover-workspace after import before wallet commands need channel state.",
1936
+ ],
1937
+ wallets: exportedWallets,
1938
+ files: [...files.keys()].sort(),
1939
+ };
1940
+
1941
+ archive.addFile("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
1942
+ for (const archivePath of manifest.files) {
1943
+ archive.addFile(archivePath, fs.readFileSync(files.get(archivePath)));
1944
+ }
1945
+ archive.writeZip(outputPath);
1946
+ protectSecretFile(outputPath, "wallet export ZIP");
1947
+
1948
+ printJson({
1949
+ action: "wallet export",
1950
+ output: outputPath,
1951
+ exportMode: manifest.exportMode,
1952
+ includeNotes,
1953
+ walletCount: exportedWallets.length,
1954
+ fileCount: manifest.files.length,
1955
+ wallets: exportedWallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
1956
+ });
1957
+ }
1958
+
1959
+ function handleWalletImport({ args }) {
1960
+ const inputPath = path.resolve(String(requireArg(args.input, "--input")));
1961
+ expect(fs.existsSync(inputPath), `Import ZIP does not exist: ${inputPath}.`);
1962
+
1963
+ const { archive, manifest } = readWalletImportArchive(inputPath);
1964
+
1965
+ const archiveFiles = new Set(manifest.files);
1966
+ for (const entry of archive.getEntries()) {
1967
+ if (entry.isDirectory) {
1968
+ continue;
1969
+ }
1970
+ expect(
1971
+ entry.entryName === "manifest.json" || archiveFiles.has(entry.entryName),
1972
+ `Unexpected file in wallet import ZIP: ${entry.entryName}.`,
1973
+ );
1974
+ }
1975
+
1976
+ const targetRoot = privateStateCliDataRoot();
1977
+ ensureDir(targetRoot);
1978
+ const plannedWrites = manifest.files.map((archivePath) => {
1979
+ validateWalletArchivePath(archivePath);
1980
+ const entry = archive.getEntry(archivePath);
1981
+ expect(entry && !entry.isDirectory, `Wallet import ZIP is missing ${archivePath}.`);
1982
+ const targetPath = path.resolve(targetRoot, archivePath);
1983
+ expectPathWithinRoot(targetPath, targetRoot, `Unsafe import target for ${archivePath}.`);
1984
+ expect(!fs.existsSync(targetPath), `Refusing to overwrite existing file: ${targetPath}.`);
1985
+ return {
1986
+ archivePath,
1987
+ targetPath,
1988
+ data: entry.getData(),
1989
+ };
1990
+ });
1991
+
1992
+ commitWalletImportFiles({ targetRoot, plannedWrites });
1993
+
1994
+ printJson({
1995
+ action: "wallet import",
1996
+ input: inputPath,
1997
+ exportMode: manifest.exportMode,
1998
+ includeNotes: Boolean(manifest.includeNotes),
1999
+ walletCount: manifest.wallets.length,
2000
+ fileCount: plannedWrites.length,
2001
+ wallets: manifest.wallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
2002
+ nextStep: manifest.includeNotes
2003
+ ? "Wallet commands can run immediately if the imported channel workspace cache is still chain-aligned."
2004
+ : "Run channel recover-workspace before wallet commands need channel state.",
2005
+ });
2006
+ }
2007
+
2008
+ function readWalletImportArchive(inputPath) {
2009
+ try {
2010
+ const archive = new AdmZip(inputPath);
2011
+ const manifestEntry = archive.getEntry("manifest.json");
2012
+ expect(manifestEntry, "Wallet import ZIP is missing manifest.json.");
2013
+ const manifest = JSON.parse(manifestEntry.getData().toString("utf8"));
2014
+ validateWalletExportManifest(manifest);
2015
+ return { archive, manifest };
2016
+ } catch (error) {
2017
+ throw new Error(`Failed to read wallet import ZIP ${inputPath}: ${error.message}`);
2018
+ }
2019
+ }
2020
+
2021
+ function commitWalletImportFiles({ targetRoot, plannedWrites }) {
2022
+ const stagingRoot = fs.mkdtempSync(path.join(targetRoot, ".wallet-import-"));
2023
+ const committedPaths = [];
2024
+ try {
2025
+ for (const write of plannedWrites) {
2026
+ write.stagingPath = path.resolve(stagingRoot, write.archivePath);
2027
+ expectPathWithinRoot(write.stagingPath, stagingRoot, `Unsafe staging target for ${write.archivePath}.`);
2028
+ ensureDir(path.dirname(write.stagingPath));
2029
+ fs.writeFileSync(write.stagingPath, write.data);
2030
+ applyImportedWalletFileMode(write.archivePath, write.stagingPath);
2031
+ }
2032
+
2033
+ for (const write of plannedWrites) {
2034
+ expect(!fs.existsSync(write.targetPath), `Refusing to overwrite existing file: ${write.targetPath}.`);
2035
+ }
2036
+
2037
+ for (const write of plannedWrites) {
2038
+ ensureDir(path.dirname(write.targetPath));
2039
+ fs.renameSync(write.stagingPath, write.targetPath);
2040
+ committedPaths.push(write.targetPath);
2041
+ }
2042
+ } catch (error) {
2043
+ for (const committedPath of committedPaths.reverse()) {
2044
+ fs.rmSync(committedPath, { force: true });
2045
+ }
2046
+ throw error;
2047
+ } finally {
2048
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
2049
+ }
2050
+ }
2051
+
1804
2052
  async function handleGuide({ args }) {
1805
2053
  const guide = {
1806
2054
  action: "guide",
@@ -1816,7 +2064,7 @@ async function handleGuide({ args }) {
1816
2064
  nextSafeAction: null,
1817
2065
  why: null,
1818
2066
  candidateCommands: [],
1819
- 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.",
2067
+ 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.",
1820
2068
  };
1821
2069
 
1822
2070
  guide.state.local = inspectGuideLocalState(args);
@@ -1839,12 +2087,12 @@ async function handleGuide({ args }) {
1839
2087
 
1840
2088
  if (!args.network) {
1841
2089
  setGuideNextAction(guide, {
1842
- command: "guide --network <NAME>",
2090
+ command: "help guide --network <NAME>",
1843
2091
  why: "Select a network before the guide can inspect RPC, deployment artifacts, channels, accounts, or wallets.",
1844
2092
  candidates: [
1845
- "guide --network mainnet",
1846
- "guide --network sepolia",
1847
- "guide --network anvil",
2093
+ "help guide --network mainnet",
2094
+ "help guide --network sepolia",
2095
+ "help guide --network anvil",
1848
2096
  ],
1849
2097
  });
1850
2098
  printJson(guide);
@@ -1857,7 +2105,7 @@ async function handleGuide({ args }) {
1857
2105
  guide.checks.push(networkRuntime.check);
1858
2106
  if (!networkRuntime.network) {
1859
2107
  setGuideNextAction(guide, {
1860
- command: "guide --network <NAME>",
2108
+ command: "help guide --network <NAME>",
1861
2109
  why: `The requested network ${networkName} is not supported by the CLI network config.`,
1862
2110
  });
1863
2111
  printJson(guide);
@@ -2225,8 +2473,8 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
2225
2473
  function applyGuideNextAction(guide) {
2226
2474
  if (guide.state.local?.walletSelectorError && guide.selectors.network) {
2227
2475
  setGuideNextAction(guide, {
2228
- command: `list-local-wallets --network ${guide.selectors.network}`,
2229
- why: "The selected wallet name is malformed. List local wallets and retry guide with an existing deterministic wallet name.",
2476
+ command: `wallet list --network ${guide.selectors.network}`,
2477
+ why: "The selected wallet name is malformed. List local wallets and retry help guide with an existing deterministic wallet name.",
2230
2478
  });
2231
2479
  return;
2232
2480
  }
@@ -2254,14 +2502,14 @@ function applyGuideNextAction(guide) {
2254
2502
  if (guide.selectors.channelName && guide.state.channel?.onchain?.exists === false) {
2255
2503
  const account = guide.selectors.account ?? "<ACCOUNT>";
2256
2504
  setGuideNextAction(guide, {
2257
- command: `create-channel --channel-name ${guide.selectors.channelName} --join-toll <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2505
+ command: `channel create --channel-name ${guide.selectors.channelName} --join-toll <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2258
2506
  why: "The selected channel name is not registered on-chain yet.",
2259
2507
  });
2260
2508
  return;
2261
2509
  }
2262
2510
  if (guide.selectors.channelName && guide.state.channel?.onchain?.exists && !guide.state.channel?.local?.workspaceExists) {
2263
2511
  setGuideNextAction(guide, {
2264
- command: `recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --from-genesis`,
2512
+ command: `channel recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --from-genesis`,
2265
2513
  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.",
2266
2514
  });
2267
2515
  return;
@@ -2270,7 +2518,7 @@ function applyGuideNextAction(guide) {
2270
2518
  const channelName = guide.selectors.channelName ?? guide.state.channel?.channelName ?? "<CHANNEL>";
2271
2519
  const account = guide.selectors.account ?? "<ACCOUNT>";
2272
2520
  setGuideNextAction(guide, {
2273
- command: `join-channel --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2521
+ command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2274
2522
  why: "The selected local wallet does not exist. Join the channel to create the wallet and register the channel L2 identity.",
2275
2523
  });
2276
2524
  return;
@@ -2279,7 +2527,7 @@ function applyGuideNextAction(guide) {
2279
2527
  const channelName = guide.state.wallet.channelName ?? guide.selectors.channelName ?? "<CHANNEL>";
2280
2528
  const account = guide.selectors.account ?? "<ACCOUNT>";
2281
2529
  setGuideNextAction(guide, {
2282
- command: `join-channel --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2530
+ command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH>`,
2283
2531
  why: "The local wallet exists, but the corresponding L1 address is not registered in the channel.",
2284
2532
  });
2285
2533
  return;
@@ -2296,46 +2544,46 @@ function applyGuideNextAction(guide) {
2296
2544
  if (guide.state.wallet?.exists && bridgeBalance === 0n && (channelBalance === null || channelBalance === 0n) && unusedNotes === 0) {
2297
2545
  const account = guide.selectors.account ?? "<ACCOUNT>";
2298
2546
  setGuideNextAction(guide, {
2299
- command: `deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2547
+ command: `account deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account}`,
2300
2548
  why: "The wallet is joined, but there is no bridge balance, channel balance, or local unused note to spend.",
2301
2549
  });
2302
2550
  return;
2303
2551
  }
2304
2552
  if (guide.state.wallet?.exists && bridgeBalance !== null && bridgeBalance > 0n && channelBalance === 0n) {
2305
2553
  setGuideNextAction(guide, {
2306
- command: `deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS>`,
2554
+ command: `wallet deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS>`,
2307
2555
  why: "The account has funds in the shared bridge vault, but the wallet has no channel L2 accounting balance.",
2308
2556
  });
2309
2557
  return;
2310
2558
  }
2311
2559
  if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
2312
2560
  setGuideNextAction(guide, {
2313
- command: `mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2561
+ command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2314
2562
  why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
2315
2563
  });
2316
2564
  return;
2317
2565
  }
2318
2566
  if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
2319
2567
  setGuideNextAction(guide, {
2320
- command: `transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
2568
+ 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>]`,
2321
2569
  why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
2322
2570
  candidates: [
2323
- `get-my-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2324
- `redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
2571
+ `wallet get-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2572
+ `wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
2325
2573
  ],
2326
2574
  });
2327
2575
  return;
2328
2576
  }
2329
2577
  if (guide.state.wallet?.exists && channelBalance === 0n) {
2330
2578
  setGuideNextAction(guide, {
2331
- command: `exit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2579
+ command: `channel exit --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
2332
2580
  why: "The wallet has zero channel balance, so channel exit is allowed by both the CLI and bridge contract.",
2333
2581
  });
2334
2582
  return;
2335
2583
  }
2336
2584
 
2337
2585
  setGuideNextAction(guide, {
2338
- command: "guide --network <NAME> --channel-name <CHANNEL> --account <ACCOUNT> --wallet <WALLET>",
2586
+ command: "help guide --network <NAME> --channel-name <CHANNEL> --account <ACCOUNT> --wallet <WALLET>",
2339
2587
  why: "Provide more selectors so the guide can choose a single next safe action.",
2340
2588
  });
2341
2589
  }
@@ -2399,12 +2647,12 @@ function redactRpcUrl(rpcUrl) {
2399
2647
  }
2400
2648
  }
2401
2649
 
2402
- async function handleGetMyWalletMeta({ args, provider }) {
2650
+ async function handleWalletGetMeta({ args, provider }) {
2403
2651
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
2404
2652
  const contextResult = await loadPreferredWalletChannelContext({
2405
2653
  walletContext: wallet,
2406
2654
  provider,
2407
- progressAction: "get-my-wallet-meta",
2655
+ progressAction: "wallet get-meta",
2408
2656
  });
2409
2657
  const context = contextResult.context;
2410
2658
  const {
@@ -2420,7 +2668,7 @@ async function handleGetMyWalletMeta({ args, provider }) {
2420
2668
  });
2421
2669
 
2422
2670
  printJson({
2423
- action: "get-my-wallet-meta",
2671
+ action: "wallet get-meta",
2424
2672
  wallet: wallet.walletName,
2425
2673
  network: walletMetadata.network,
2426
2674
  channelName: walletMetadata.channelName,
@@ -2490,7 +2738,7 @@ async function loadWalletChannelRegistrationState({
2490
2738
  registration.exists,
2491
2739
  cliError(
2492
2740
  CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
2493
- `No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
2741
+ `No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
2494
2742
  ),
2495
2743
  );
2496
2744
  expect(
@@ -2508,7 +2756,7 @@ async function loadWalletChannelRegistrationState({
2508
2756
  };
2509
2757
  }
2510
2758
 
2511
- async function handleGetMyChannelFund({ args, provider }) {
2759
+ async function handleWalletGetChannelFund({ args, provider }) {
2512
2760
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
2513
2761
  const {
2514
2762
  signer,
@@ -2520,11 +2768,11 @@ async function handleGetMyChannelFund({ args, provider }) {
2520
2768
  } = await loadWalletChannelFundState({
2521
2769
  walletContext: wallet,
2522
2770
  provider,
2523
- progressAction: "get-my-channel-fund",
2771
+ progressAction: "wallet get-channel-fund",
2524
2772
  });
2525
2773
 
2526
2774
  printJson({
2527
- action: "get-my-channel-fund",
2775
+ action: "wallet get-channel-fund",
2528
2776
  wallet: wallet.walletName,
2529
2777
  network: walletMetadata.network,
2530
2778
  channelName: walletMetadata.channelName,
@@ -2556,7 +2804,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2556
2804
  !existingRegistration.exists,
2557
2805
  [
2558
2806
  `L1 address ${signer.address} is already registered in channel ${context.workspace.channelName}.`,
2559
- "Use recover-wallet or normal wallet commands for an existing channel registration.",
2807
+ "Use wallet recover-workspace or normal wallet commands for an existing channel registration.",
2560
2808
  ].join(" "),
2561
2809
  );
2562
2810
  const walletSecret = prepareJoinWalletSecretForName({
@@ -2593,7 +2841,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2593
2841
  );
2594
2842
  let nextNonce = await provider.getTransactionCount(signer.address, "pending");
2595
2843
  printImmutableChannelPolicyWarning({
2596
- action: "join-channel",
2844
+ action: "channel join",
2597
2845
  channelName: context.workspace.channelName,
2598
2846
  channelId: ethers.toBigInt(context.workspace.channelId),
2599
2847
  channelManager: context.workspace.channelManager,
@@ -2616,6 +2864,13 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2616
2864
  );
2617
2865
  status = "joined";
2618
2866
 
2867
+ await refreshPersistedWorkspaceAfterLocalTransaction({
2868
+ context,
2869
+ provider,
2870
+ receipt,
2871
+ progressAction: "channel join",
2872
+ });
2873
+
2619
2874
  const walletContext = ensureWallet({
2620
2875
  channelContext: context,
2621
2876
  signerAddress: signer.address,
@@ -2629,7 +2884,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
2629
2884
  });
2630
2885
 
2631
2886
  printJson({
2632
- action: "join-channel",
2887
+ action: "channel join",
2633
2888
  workspace: context.workspaceName,
2634
2889
  wallet: walletContext.walletName,
2635
2890
  walletSecretSource: resolvedWalletSecretSource(args),
@@ -2665,17 +2920,18 @@ async function handleExitChannel({ args, provider }) {
2665
2920
  channelFund === 0n,
2666
2921
  [
2667
2922
  `The current channel fund for ${signer.address} is ${channelFund.toString()}.`,
2668
- "exit-channel requires a zero channel balance.",
2669
- "Run withdraw-channel first, then retry exit-channel.",
2923
+ "channel exit requires a zero channel balance.",
2924
+ "Run wallet withdraw-channel first, then retry channel exit.",
2670
2925
  ].join(" "),
2671
2926
  );
2672
2927
  const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(signer.address);
2673
2928
  const receipt = await waitForReceipt(
2674
2929
  await context.bridgeTokenVault.connect(signer).exitChannel(ethers.toBigInt(context.workspace.channelId)),
2675
2930
  );
2931
+ const cleanup = removeLocalWalletArtifacts(walletContext.walletName, walletMetadata.network);
2676
2932
 
2677
2933
  printJson({
2678
- action: "exit-channel",
2934
+ action: "channel exit",
2679
2935
  wallet: walletContext.walletName,
2680
2936
  network: walletMetadata.network,
2681
2937
  channelName: walletMetadata.channelName,
@@ -2690,15 +2946,17 @@ async function handleExitChannel({ args, provider }) {
2690
2946
  gasUsed: receiptGasUsed(receipt),
2691
2947
  txUrl: explorerTxUrl(network, receipt.hash),
2692
2948
  receipt: sanitizeReceipt(receipt),
2949
+ removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
2950
+ removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
2693
2951
  });
2694
2952
  }
2695
2953
 
2696
2954
  async function handleGrothVaultMove({ args, provider, direction }) {
2697
- const operationName = args.command === "withdraw-channel"
2698
- ? "withdraw-channel"
2955
+ const operationName = args.command === "wallet-withdraw-channel"
2956
+ ? "wallet withdraw-channel"
2699
2957
  : direction === "deposit"
2700
- ? "deposit-channel"
2701
- : "withdraw";
2958
+ ? "wallet deposit-channel"
2959
+ : "wallet withdraw-channel";
2702
2960
  emitProgress(operationName, "loading");
2703
2961
  const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
2704
2962
  const contextResult = await loadPreferredWalletChannelContext({
@@ -2729,7 +2987,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2729
2987
  availableBalance >= amount,
2730
2988
  [
2731
2989
  `Deposit amount ${amount.toString()} exceeds the shared bridge-vault balance`,
2732
- `${availableBalance.toString()} for ${signer.address}. Run deposit-bridge first.`,
2990
+ `${availableBalance.toString()} for ${signer.address}. Run account deposit-bridge first.`,
2733
2991
  ].join(" "),
2734
2992
  );
2735
2993
  }
@@ -2738,7 +2996,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2738
2996
  registration.exists,
2739
2997
  cliError(
2740
2998
  CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
2741
- `No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
2999
+ `No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
2742
3000
  ),
2743
3001
  );
2744
3002
  expect(
@@ -2802,7 +3060,12 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2802
3060
  sealWalletOperationDir(operationDir, walletContext.walletSecret);
2803
3061
 
2804
3062
  context.currentSnapshot = transition.nextSnapshot;
2805
- persistCurrentState(context);
3063
+ await refreshPersistedWorkspaceAfterLocalTransaction({
3064
+ context,
3065
+ provider,
3066
+ receipt,
3067
+ progressAction: operationName,
3068
+ });
2806
3069
 
2807
3070
  emitProgress(operationName, "done");
2808
3071
  printJson({
@@ -2818,6 +3081,8 @@ async function handleGrothVaultMove({ args, provider, direction }) {
2818
3081
  updatedRoot: transition.update.updatedRoot,
2819
3082
  gasUsed: receiptGasUsed(receipt),
2820
3083
  txUrl: explorerTxUrl(network, receipt.hash),
3084
+ usedWorkspaceCache: contextResult.usingWorkspaceCache,
3085
+ recoveredWorkspace: contextResult.recoveredWorkspace,
2821
3086
  });
2822
3087
  }
2823
3088
 
@@ -2835,7 +3100,7 @@ async function handleWithdrawBridge({ args, network, provider }) {
2835
3100
  const receipt = await waitForReceipt(await bridgeTokenVault.claimToWallet(amount));
2836
3101
 
2837
3102
  printJson({
2838
- action: "withdraw-bridge",
3103
+ action: "account withdraw-bridge",
2839
3104
  l1Address: signer.address,
2840
3105
  amountInput,
2841
3106
  amountBaseUnits: amount.toString(),
@@ -2927,13 +3192,13 @@ async function handleMintNotes({ args, provider }) {
2927
3192
  const { channelFund } = await loadWalletChannelFundState({
2928
3193
  walletContext: wallet,
2929
3194
  provider,
2930
- progressAction: "mint-notes",
3195
+ progressAction: "wallet mint-notes",
2931
3196
  });
2932
3197
  expect(
2933
3198
  totalMintAmount <= channelFund,
2934
3199
  [
2935
3200
  `Mint amount total ${totalMintAmount.toString()} exceeds the current channel fund`,
2936
- `${channelFund.toString()}. Run get-my-channel-fund to inspect the available balance.`,
3201
+ `${channelFund.toString()}. Run wallet get-channel-fund to inspect the available balance.`,
2937
3202
  ].join(" "),
2938
3203
  );
2939
3204
  const templatePayload = buildMintNotesTemplatePayload({
@@ -2944,12 +3209,12 @@ async function handleMintNotes({ args, provider }) {
2944
3209
  args,
2945
3210
  wallet,
2946
3211
  provider,
2947
- operationName: "mint-notes",
3212
+ operationName: "wallet mint-notes",
2948
3213
  templatePayload,
2949
3214
  });
2950
3215
 
2951
3216
  printJson({
2952
- action: "mint-notes",
3217
+ action: "wallet mint-notes",
2953
3218
  wallet: wallet.walletName,
2954
3219
  workspace: execution.context.workspaceName,
2955
3220
  operationDir: execution.operationDir,
@@ -2988,12 +3253,12 @@ async function handleRedeemNotes({ args, provider }) {
2988
3253
  args,
2989
3254
  wallet,
2990
3255
  provider,
2991
- operationName: "redeem-notes",
3256
+ operationName: "wallet redeem-notes",
2992
3257
  templatePayload,
2993
3258
  });
2994
3259
 
2995
3260
  printJson({
2996
- action: "redeem-notes",
3261
+ action: "wallet redeem-notes",
2997
3262
  wallet: wallet.walletName,
2998
3263
  workspace: execution.context.workspaceName,
2999
3264
  operationDir: execution.operationDir,
@@ -3020,7 +3285,7 @@ async function handleRedeemNotes({ args, provider }) {
3020
3285
  });
3021
3286
  }
3022
3287
 
3023
- async function handleGetMyNotes({ args, provider }) {
3288
+ async function handleWalletGetNotes({ args, provider }) {
3024
3289
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
3025
3290
  expect(
3026
3291
  typeof wallet.wallet.controller === "string" && wallet.wallet.controller.length > 0,
@@ -3030,7 +3295,7 @@ async function handleGetMyNotes({ args, provider }) {
3030
3295
  const { context } = await loadPreferredWalletChannelContext({
3031
3296
  walletContext: wallet,
3032
3297
  provider,
3033
- progressAction: "get-my-notes",
3298
+ progressAction: "wallet get-notes",
3034
3299
  });
3035
3300
  const signer = restoreWalletSigner(wallet, provider);
3036
3301
  const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
@@ -3038,7 +3303,7 @@ async function handleGetMyNotes({ args, provider }) {
3038
3303
  context,
3039
3304
  provider,
3040
3305
  signer,
3041
- progressAction: "get-my-notes",
3306
+ progressAction: "wallet get-notes",
3042
3307
  });
3043
3308
 
3044
3309
  const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
@@ -3063,7 +3328,7 @@ async function handleGetMyNotes({ args, provider }) {
3063
3328
  const spentTotal = spentTrackedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n);
3064
3329
 
3065
3330
  printJson({
3066
- action: "get-my-notes",
3331
+ action: "wallet get-notes",
3067
3332
  wallet: wallet.walletName,
3068
3333
  network: walletMetadata.network,
3069
3334
  channelName: walletMetadata.channelName,
@@ -3087,7 +3352,7 @@ async function handleTransferNotes({ args, provider }) {
3087
3352
  const preparedContextResult = await loadPreferredWalletChannelContext({
3088
3353
  walletContext: wallet,
3089
3354
  provider,
3090
- progressAction: "transfer-notes",
3355
+ progressAction: "wallet transfer-notes",
3091
3356
  });
3092
3357
  const context = preparedContextResult.context;
3093
3358
  const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
@@ -3123,7 +3388,7 @@ async function handleTransferNotes({ args, provider }) {
3123
3388
  args,
3124
3389
  wallet,
3125
3390
  provider,
3126
- operationName: "transfer-notes",
3391
+ operationName: "wallet transfer-notes",
3127
3392
  templatePayload,
3128
3393
  });
3129
3394
  const outputNotes = buildLifecycleTrackedOutputs({
@@ -3134,7 +3399,7 @@ async function handleTransferNotes({ args, provider }) {
3134
3399
  });
3135
3400
 
3136
3401
  printJson({
3137
- action: "transfer-notes",
3402
+ action: "wallet transfer-notes",
3138
3403
  wallet: wallet.walletName,
3139
3404
  workspace: execution.context.workspaceName,
3140
3405
  operationDir: execution.operationDir,
@@ -3463,6 +3728,7 @@ async function recoverWalletReceivedNotes({
3463
3728
  signer,
3464
3729
  noteReceiveKeyMaterial = null,
3465
3730
  progressAction = null,
3731
+ fromGenesis = false,
3466
3732
  }) {
3467
3733
  const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ?? await deriveNoteReceiveKeyMaterial({
3468
3734
  signer,
@@ -3477,6 +3743,7 @@ async function recoverWalletReceivedNotes({
3477
3743
  provider,
3478
3744
  noteReceivePrivateKey: resolvedNoteReceiveKeyMaterial.privateKey,
3479
3745
  progressAction,
3746
+ fromGenesis,
3480
3747
  });
3481
3748
  return {
3482
3749
  noteReceiveKeyMaterial: resolvedNoteReceiveKeyMaterial,
@@ -3490,13 +3757,16 @@ async function recoverDeliveredNotesFromEventLogs({
3490
3757
  provider,
3491
3758
  noteReceivePrivateKey,
3492
3759
  progressAction = null,
3760
+ fromGenesis = false,
3493
3761
  }) {
3494
3762
  const latestBlock = await provider.getBlockNumber();
3495
- const scanStartBlock = requireUsableWalletNoteReceiveRecoveryIndex({
3496
- walletContext,
3497
- context,
3498
- latestBlock,
3499
- });
3763
+ const scanStartBlock = fromGenesis
3764
+ ? Number(context.workspace.genesisBlockNumber)
3765
+ : requireUsableWalletNoteReceiveRecoveryIndex({
3766
+ walletContext,
3767
+ context,
3768
+ latestBlock,
3769
+ });
3500
3770
  const scanRange = {
3501
3771
  fromBlock: scanStartBlock,
3502
3772
  toBlock: latestBlock,
@@ -3611,7 +3881,7 @@ function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, l
3611
3881
  throw new Error([
3612
3882
  `Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
3613
3883
  `Expected noteReceiveLastScannedBlock to be an integer between ${genesisBlockNumber} and ${Number(latestBlock) + 1}.`,
3614
- "Run recover-wallet --from-genesis to rebuild wallet note state from channel genesis.",
3884
+ "Run wallet recover-workspace --from-genesis to rebuild wallet note state from channel genesis.",
3615
3885
  ].join(" "));
3616
3886
  }
3617
3887
  return nextBlock;
@@ -3851,16 +4121,16 @@ function buildRedeemNotesTemplatePayload({ wallet, inputNotes }) {
3851
4121
  }
3852
4122
 
3853
4123
  function selectMintNotesMethod(noteCount) {
3854
- expect(noteCount >= 1, "mint-notes requires at least one output amount.");
4124
+ expect(noteCount >= 1, "wallet mint-notes requires at least one output amount.");
3855
4125
  expect(
3856
4126
  noteCount <= 2,
3857
- "mint-notes supports only one or two output amounts with the currently registered DApp.",
4127
+ "wallet mint-notes supports only one or two output amounts with the currently registered DApp.",
3858
4128
  );
3859
4129
  return `mintNotes${noteCount}`;
3860
4130
  }
3861
4131
 
3862
4132
  function selectRedeemNotesMethod(noteCount) {
3863
- expect(noteCount === 1, "redeem-notes supports exactly one input note with the currently registered DApp.");
4133
+ expect(noteCount === 1, "wallet redeem-notes supports exactly one input note with the currently registered DApp.");
3864
4134
  return `redeemNotes${noteCount}`;
3865
4135
  }
3866
4136
 
@@ -3979,7 +4249,7 @@ function selectTransferNotesMethod(inputCount, outputCount) {
3979
4249
  if (inputCount === 2 && outputCount === 1) {
3980
4250
  return "transferNotes2To1";
3981
4251
  }
3982
- throw new Error("transfer-notes supports only 1->1, 1->2, and 2->1 note transfers.");
4252
+ throw new Error("wallet transfer-notes supports only 1->1, 1->2, and 2->1 note transfers.");
3983
4253
  }
3984
4254
 
3985
4255
  function loadWalletUnusedInputNotes(walletContext, noteIds) {
@@ -4115,7 +4385,7 @@ async function recoverWalletChannelWorkspace({ walletContext, provider, progress
4115
4385
  const networkName = walletContext.wallet.network ?? networkNameFromChainId(Number(walletContext.wallet.chainId));
4116
4386
  const network = resolveCliNetwork(networkName);
4117
4387
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
4118
- await initializeChannelWorkspace({
4388
+ await syncChannelWorkspace({
4119
4389
  workspaceName: walletContext.wallet.channelName,
4120
4390
  channelName: walletContext.wallet.channelName,
4121
4391
  network,
@@ -4128,6 +4398,49 @@ async function recoverWalletChannelWorkspace({ walletContext, provider, progress
4128
4398
  });
4129
4399
  }
4130
4400
 
4401
+ async function refreshPersistedWorkspaceAfterLocalTransaction({
4402
+ context,
4403
+ provider,
4404
+ receipt,
4405
+ progressAction = null,
4406
+ }) {
4407
+ if (!context.persistChannelWorkspace || !context.workspaceDir) {
4408
+ return context;
4409
+ }
4410
+ const network = resolveCliNetwork(context.workspace.network);
4411
+ const bridgeResources = loadBridgeResources({ chainId: Number(context.workspace.chainId) });
4412
+ const refreshed = await syncChannelWorkspace({
4413
+ workspaceName: context.workspaceName,
4414
+ channelName: context.workspace.channelName,
4415
+ network,
4416
+ provider,
4417
+ bridgeResources,
4418
+ persist: true,
4419
+ allowExistingWorkspaceSync: true,
4420
+ useWorkspaceRecoveryIndex: true,
4421
+ minimumToBlock: receipt?.blockNumber ?? null,
4422
+ progressAction,
4423
+ });
4424
+
4425
+ context.workspaceDir = refreshed.workspaceDir;
4426
+ context.workspace = refreshed.workspace;
4427
+ context.currentSnapshot = refreshed.currentSnapshot;
4428
+ context.blockInfo = refreshed.blockInfo;
4429
+ context.contractCodes = refreshed.contractCodes;
4430
+ context.bridgeAbiManifest = bridgeResources.bridgeAbiManifest;
4431
+ context.channelManager = new Contract(
4432
+ refreshed.workspace.channelManager,
4433
+ bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
4434
+ provider,
4435
+ );
4436
+ context.bridgeTokenVault = new Contract(
4437
+ refreshed.workspace.bridgeTokenVault,
4438
+ bridgeResources.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
4439
+ provider,
4440
+ );
4441
+ return context;
4442
+ }
4443
+
4131
4444
  function isRecoverableWalletWorkspaceFailure(error) {
4132
4445
  const message = String(error?.message ?? error);
4133
4446
  return (message.includes("--verify") && message.includes("failed with exit code"))
@@ -4179,6 +4492,7 @@ async function executeWalletDirectTemplateCommand({
4179
4492
  txSubmitterAccount,
4180
4493
  l2Identity,
4181
4494
  context: contextResult.context,
4495
+ provider,
4182
4496
  operationName,
4183
4497
  functionName: templatePayload.method,
4184
4498
  templatePayload,
@@ -4210,6 +4524,7 @@ async function executeWalletDirectTemplateCommand({
4210
4524
  txSubmitterAccount,
4211
4525
  l2Identity,
4212
4526
  context: contextResult.context,
4527
+ provider,
4213
4528
  operationName,
4214
4529
  functionName: templatePayload.method,
4215
4530
  templatePayload,
@@ -4230,6 +4545,7 @@ async function executeWalletTemplateSend({
4230
4545
  txSubmitterAccount,
4231
4546
  l2Identity,
4232
4547
  context,
4548
+ provider,
4233
4549
  operationName,
4234
4550
  functionName,
4235
4551
  templatePayload,
@@ -4323,7 +4639,12 @@ async function executeWalletTemplateSend({
4323
4639
  applyNoteLifecycleToWallet(wallet, noteLifecycle, functionName, receipt.hash);
4324
4640
  context.currentSnapshot = nextSnapshot;
4325
4641
  persistWallet(wallet);
4326
- persistCurrentState(context);
4642
+ await refreshPersistedWorkspaceAfterLocalTransaction({
4643
+ context,
4644
+ provider,
4645
+ receipt,
4646
+ progressAction: operationName,
4647
+ });
4327
4648
  sealWalletOperationDir(operationDir, wallet.walletSecret);
4328
4649
 
4329
4650
  return {
@@ -4383,19 +4704,22 @@ async function loadJoinChannelContext({ args, network, provider }) {
4383
4704
  const channelName = requireArg(args.channelName, "--channel-name");
4384
4705
 
4385
4706
  const bridgeResources = loadBridgeResources({ chainId });
4386
- const initialized = await initializeChannelWorkspace({
4707
+ const initialized = await syncChannelWorkspace({
4387
4708
  workspaceName: channelName,
4388
4709
  channelName,
4389
4710
  network: { chainId, name: resolvedNetworkName },
4390
4711
  provider,
4391
4712
  bridgeResources,
4392
- persist: false,
4713
+ persist: true,
4714
+ allowExistingWorkspaceSync: true,
4715
+ useWorkspaceRecoveryIndex: true,
4716
+ progressAction: "channel join",
4393
4717
  });
4394
4718
 
4395
4719
  return {
4396
4720
  workspaceName: channelName,
4397
- workspaceDir: null,
4398
- persistChannelWorkspace: false,
4721
+ workspaceDir: initialized.workspaceDir,
4722
+ persistChannelWorkspace: true,
4399
4723
  workspace: initialized.workspace,
4400
4724
  bridgeAbiManifest: bridgeResources.bridgeAbiManifest,
4401
4725
  currentSnapshot: initialized.currentSnapshot,
@@ -4477,7 +4801,7 @@ function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
4477
4801
  wallet.l2DerivationMode === CHANNEL_BOUND_L2_DERIVATION_MODE,
4478
4802
  [
4479
4803
  `Wallet ${walletName} was not created with the current channel-bound L2 derivation rule.`,
4480
- "Create a fresh wallet with join-channel.",
4804
+ "Create a fresh wallet with channel join.",
4481
4805
  ].join(" "),
4482
4806
  );
4483
4807
  expect(
@@ -5710,7 +6034,13 @@ function parseArgs(argv) {
5710
6034
  }
5711
6035
 
5712
6036
  parsed.command = parsed.positional[0];
5713
- if ((parsed.command === "account" || parsed.command === "wallet") && parsed.positional[1]) {
6037
+ if (
6038
+ (parsed.command === "account"
6039
+ || parsed.command === "channel"
6040
+ || parsed.command === "wallet"
6041
+ || parsed.command === "help")
6042
+ && parsed.positional[1]
6043
+ ) {
5714
6044
  parsed.command = `${parsed.command}-${parsed.positional[1]}`;
5715
6045
  parsed.positional = [parsed.command];
5716
6046
  }
@@ -5871,7 +6201,7 @@ function resolveWalletDefaultSecret(networkName, walletName) {
5871
6201
  CLI_ERROR_CODES.MISSING_WALLET_SECRET,
5872
6202
  [
5873
6203
  `Missing wallet default secret file: ${secretPath}.`,
5874
- "Run join-channel with --wallet-secret-path before wallet commands.",
6204
+ "Run channel join with --wallet-secret-path before wallet commands.",
5875
6205
  ].join(" "),
5876
6206
  );
5877
6207
  }
@@ -5884,12 +6214,17 @@ function prepareJoinWalletSecretForName({
5884
6214
  walletName,
5885
6215
  }) {
5886
6216
  const secretPath = walletSecretPath(networkName, walletName);
6217
+ const { channelName } = parseWalletName(walletName);
6218
+ const walletDir = walletPath(walletName, networkName);
5887
6219
  expect(
5888
- !walletConfigExists(walletPath(walletName, networkName)),
6220
+ !walletConfigExists(walletDir),
5889
6221
  [
5890
6222
  `Wallet ${walletName} already exists on ${networkName}.`,
5891
- "join-channel always creates a new local wallet.",
5892
- "Use recover-wallet or normal wallet commands for an existing local wallet.",
6223
+ "channel join always creates a new local wallet.",
6224
+ "If this wallet was previously exited on-chain, run",
6225
+ `wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}`,
6226
+ "once to remove the stale local wallet, then retry channel join.",
6227
+ "Use normal wallet commands for an existing active local wallet.",
5893
6228
  ].join(" "),
5894
6229
  );
5895
6230
  const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
@@ -5898,13 +6233,6 @@ function prepareJoinWalletSecretForName({
5898
6233
  ? readSecretFile(sourcePath, "--wallet-secret-path")
5899
6234
  : readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
5900
6235
  if (sourcePath !== canonicalPath) {
5901
- expect(
5902
- !fs.existsSync(canonicalPath),
5903
- [
5904
- `Wallet default secret file already exists: ${canonicalPath}.`,
5905
- "Remove it before joining with a different --wallet-secret-path.",
5906
- ].join(" "),
5907
- );
5908
6236
  writeSecretFile(canonicalPath, walletSecret);
5909
6237
  }
5910
6238
  return walletSecret;
@@ -6027,6 +6355,148 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
6027
6355
  );
6028
6356
  }
6029
6357
 
6358
+ function privateStateCliDataRoot() {
6359
+ const root = path.dirname(workspaceRoot);
6360
+ expect(
6361
+ path.dirname(secretRoot) === root,
6362
+ `Unexpected CLI data root layout: ${workspaceRoot} and ${secretRoot} are not siblings.`,
6363
+ );
6364
+ return root;
6365
+ }
6366
+
6367
+ function resolveExportWalletInfo({ networkName, walletName }) {
6368
+ resolveCliNetwork(networkName);
6369
+ const walletDir = walletPath(walletName, networkName);
6370
+ return {
6371
+ wallet: walletName,
6372
+ network: networkName,
6373
+ channelName: parseWalletName(walletName).channelName,
6374
+ walletDir,
6375
+ metadataPath: walletMetadataPath(walletDir),
6376
+ hasMetadata: fs.existsSync(walletMetadataPath(walletDir)),
6377
+ hasEncryptedWallet: walletConfigExists(walletDir),
6378
+ };
6379
+ }
6380
+
6381
+ function normalizeExportWalletInfo(walletInfo) {
6382
+ const wallet = requireWalletName({ wallet: walletInfo.wallet });
6383
+ const network = requireNetworkName({ network: walletInfo.network });
6384
+ const walletDir = walletInfo.walletDir ?? walletPath(wallet, network);
6385
+ const metadataPath = walletMetadataPath(walletDir);
6386
+ const encryptedWalletPath = walletConfigPath(walletDir);
6387
+ const metadata = readJsonIfExists(metadataPath);
6388
+ const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
6389
+ const walletSecret = walletSecretPath(network, wallet);
6390
+
6391
+ expect(fs.existsSync(encryptedWalletPath), `Wallet export cannot find encrypted wallet file: ${encryptedWalletPath}.`);
6392
+ expect(fs.existsSync(metadataPath), `Wallet export cannot find wallet metadata file: ${metadataPath}.`);
6393
+ expect(fs.existsSync(walletSecret), `Wallet export cannot find wallet-local secret file: ${walletSecret}.`);
6394
+ expect(
6395
+ metadata.network === network,
6396
+ `Wallet export metadata network ${metadata.network} does not match ${network}.`,
6397
+ );
6398
+ expect(
6399
+ metadata.channelName === channelName,
6400
+ `Wallet export metadata channel ${metadata.channelName} does not match ${channelName}.`,
6401
+ );
6402
+
6403
+ return {
6404
+ network,
6405
+ channelName,
6406
+ wallet,
6407
+ walletDir,
6408
+ walletSecretPath: walletSecret,
6409
+ };
6410
+ }
6411
+
6412
+ function walletExportFilePaths(walletInfo, { includeNotes }) {
6413
+ const walletFiles = [
6414
+ walletInfo.walletSecretPath,
6415
+ walletConfigPath(walletInfo.walletDir),
6416
+ walletMetadataPath(walletInfo.walletDir),
6417
+ ];
6418
+ if (!includeNotes) {
6419
+ return walletFiles;
6420
+ }
6421
+
6422
+ const workspaceDir = channelWorkspacePath(walletInfo.network, walletInfo.channelName);
6423
+ const currentDir = channelWorkspaceCurrentPath(workspaceDir);
6424
+ const workspaceFiles = [
6425
+ channelWorkspaceConfigPath(workspaceDir),
6426
+ path.join(currentDir, "state_snapshot.json"),
6427
+ path.join(currentDir, "state_snapshot.normalized.json"),
6428
+ path.join(currentDir, "block_info.json"),
6429
+ path.join(currentDir, "contract_codes.json"),
6430
+ ];
6431
+ for (const filePath of workspaceFiles) {
6432
+ expect(
6433
+ fs.existsSync(filePath),
6434
+ [
6435
+ `wallet export --include-notes requires channel workspace cache file: ${filePath}.`,
6436
+ "Run channel recover-workspace first, or export without --include-notes.",
6437
+ ].join(" "),
6438
+ );
6439
+ }
6440
+ return [...walletFiles, ...workspaceFiles];
6441
+ }
6442
+
6443
+ function archivePathForLocalCliFile(filePath) {
6444
+ const root = privateStateCliDataRoot();
6445
+ const absolutePath = path.resolve(filePath);
6446
+ expectPathWithinRoot(absolutePath, root, `Cannot export file outside CLI data root: ${absolutePath}.`);
6447
+ return path.relative(root, absolutePath).split(path.sep).join("/");
6448
+ }
6449
+
6450
+ function validateWalletExportManifest(manifest) {
6451
+ expect(manifest?.format === WALLET_EXPORT_FORMAT, "Wallet import ZIP has an unsupported format.");
6452
+ expect(
6453
+ Number(manifest.formatVersion) === WALLET_EXPORT_FORMAT_VERSION,
6454
+ `Wallet import ZIP format version ${manifest?.formatVersion} is not supported.`,
6455
+ );
6456
+ expect(Array.isArray(manifest.files), "Wallet import ZIP manifest is missing files[].");
6457
+ expect(Array.isArray(manifest.wallets), "Wallet import ZIP manifest is missing wallets[].");
6458
+ expect(typeof manifest.includeNotes === "boolean", "Wallet import ZIP manifest is missing includeNotes.");
6459
+ expect(manifest.wallets.length > 0, "Wallet import ZIP manifest does not list any wallets.");
6460
+ const uniqueFiles = new Set(manifest.files);
6461
+ expect(uniqueFiles.size === manifest.files.length, "Wallet import ZIP manifest contains duplicate file paths.");
6462
+ expect(manifest.files.length > 0, "Wallet import ZIP manifest does not list any files.");
6463
+ for (const filePath of manifest.files) {
6464
+ validateWalletArchivePath(filePath);
6465
+ }
6466
+ for (const wallet of manifest.wallets) {
6467
+ requireNetworkName({ network: wallet.network });
6468
+ requireWalletName({ wallet: wallet.wallet });
6469
+ requireArg(wallet.channelName, "wallets[].channelName");
6470
+ }
6471
+ }
6472
+
6473
+ function validateWalletArchivePath(archivePath) {
6474
+ expect(typeof archivePath === "string" && archivePath.length > 0, "Wallet import ZIP contains an empty path.");
6475
+ expect(!archivePath.includes("\0"), `Wallet import ZIP contains an invalid path: ${archivePath}.`);
6476
+ expect(!archivePath.includes("\\"), `Wallet import ZIP path must use forward slashes: ${archivePath}.`);
6477
+ expect(!path.posix.isAbsolute(archivePath), `Wallet import ZIP path must be relative: ${archivePath}.`);
6478
+ expect(path.posix.normalize(archivePath) === archivePath, `Wallet import ZIP path is not normalized: ${archivePath}.`);
6479
+ expect(
6480
+ archivePath.startsWith("secrets/") || archivePath.startsWith("workspace/"),
6481
+ `Wallet import ZIP path must start with secrets/ or workspace/: ${archivePath}.`,
6482
+ );
6483
+ }
6484
+
6485
+ function expectPathWithinRoot(targetPath, rootPath, message) {
6486
+ const relative = path.relative(path.resolve(rootPath), path.resolve(targetPath));
6487
+ expect(relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative), message);
6488
+ }
6489
+
6490
+ function applyImportedWalletFileMode(archivePath, targetPath) {
6491
+ if (
6492
+ archivePath.startsWith("secrets/")
6493
+ || archivePath.endsWith("/wallet.json")
6494
+ || archivePath.endsWith("/wallet.metadata.json")
6495
+ ) {
6496
+ protectSecretFile(targetPath, `imported wallet file ${archivePath}`);
6497
+ }
6498
+ }
6499
+
6030
6500
  function channelDataPath(workspaceDir) {
6031
6501
  return workspaceChannelDir(workspaceDir);
6032
6502
  }
@@ -6125,14 +6595,18 @@ function assertUninstallArgs(args) {
6125
6595
  assertAllowedCommandSchema(args, "uninstall");
6126
6596
  }
6127
6597
 
6598
+ function assertHelpCommandsArgs(args) {
6599
+ assertAllowedCommandSchema(args, "help-commands");
6600
+ }
6601
+
6128
6602
  function assertUpdateArgs(args) {
6129
- assertAllowedCommandSchema(args, "update");
6603
+ assertAllowedCommandSchema(args, "help-update");
6130
6604
  }
6131
6605
 
6132
6606
  function assertDoctorArgs(args) {
6133
- assertAllowedCommandSchema(args, "doctor");
6607
+ assertAllowedCommandSchema(args, "help-doctor");
6134
6608
  if (args.gpu !== undefined && args.gpu !== true) {
6135
- throw new Error("doctor option --gpu does not accept a value.");
6609
+ throw new Error("help doctor option --gpu does not accept a value.");
6136
6610
  }
6137
6611
  }
6138
6612
 
@@ -6149,11 +6623,11 @@ function assertGuideArgs(args) {
6149
6623
  if (args.wallet !== undefined) {
6150
6624
  requireWalletName(args);
6151
6625
  }
6152
- assertAllowedCommandSchema(args, "guide");
6626
+ assertAllowedCommandSchema(args, "help-guide");
6153
6627
  }
6154
6628
 
6155
6629
  function assertTransactionFeesArgs(args) {
6156
- assertAllowedCommandSchema(args, "transaction-fees");
6630
+ assertAllowedCommandSchema(args, "help-transaction-fees");
6157
6631
  }
6158
6632
 
6159
6633
  function assertAccountImportArgs(args) {
@@ -6161,7 +6635,7 @@ function assertAccountImportArgs(args) {
6161
6635
  }
6162
6636
 
6163
6637
  function assertMintNotesArgs(args) {
6164
- assertAllowedCommandSchema(args, "mint-notes");
6638
+ assertAllowedCommandSchema(args, "wallet-mint-notes");
6165
6639
  assertTxSubmitterArg(args);
6166
6640
  parseAmountVector(args.amounts, {
6167
6641
  allowZeroEntries: true,
@@ -6170,13 +6644,13 @@ function assertMintNotesArgs(args) {
6170
6644
  }
6171
6645
 
6172
6646
  function assertRedeemNotesArgs(args) {
6173
- assertAllowedCommandSchema(args, "redeem-notes");
6647
+ assertAllowedCommandSchema(args, "wallet-redeem-notes");
6174
6648
  assertTxSubmitterArg(args);
6175
6649
  selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
6176
6650
  }
6177
6651
 
6178
6652
  function assertTransferNotesArgs(args) {
6179
- assertAllowedCommandSchema(args, "transfer-notes");
6653
+ assertAllowedCommandSchema(args, "wallet-transfer-notes");
6180
6654
  assertTxSubmitterArg(args);
6181
6655
  const noteIds = parseNoteIdVector(args.noteIds);
6182
6656
  const recipients = parseRecipientVector(args.recipients);
@@ -6197,28 +6671,28 @@ function assertTxSubmitterArg(args) {
6197
6671
  }
6198
6672
  }
6199
6673
 
6200
- function assertGetMyNotesArgs(args) {
6201
- assertWalletSecretArgs(args, "get-my-notes");
6674
+ function assertWalletGetNotesArgs(args) {
6675
+ assertWalletSecretArgs(args, "wallet-get-notes");
6202
6676
  }
6203
6677
 
6204
6678
  function assertCreateChannelArgs(args) {
6205
- assertAllowedCommandSchema(args, "create-channel");
6679
+ assertAllowedCommandSchema(args, "channel-create");
6206
6680
  }
6207
6681
 
6208
6682
  function assertRecoverWorkspaceArgs(args) {
6209
- assertAllowedCommandSchema(args, "recover-workspace");
6683
+ assertAllowedCommandSchema(args, "channel-recover-workspace");
6210
6684
  }
6211
6685
 
6212
6686
  function assertGetChannelArgs(args) {
6213
- assertAllowedCommandSchema(args, "get-channel");
6687
+ assertAllowedCommandSchema(args, "channel-get-meta");
6214
6688
  }
6215
6689
 
6216
6690
  function assertDepositBridgeArgs(args) {
6217
- assertAllowedCommandSchema(args, "deposit-bridge");
6691
+ assertAllowedCommandSchema(args, "account-deposit-bridge");
6218
6692
  }
6219
6693
 
6220
- function assertGetMyBridgeFundArgs(args) {
6221
- assertAllowedCommandSchema(args, "get-my-bridge-fund");
6694
+ function assertAccountGetBridgeFundArgs(args) {
6695
+ assertAllowedCommandSchema(args, "account-get-bridge-fund");
6222
6696
  }
6223
6697
 
6224
6698
  function assertExplicitSignerCommandArgs(args, commandName) {
@@ -6226,22 +6700,22 @@ function assertExplicitSignerCommandArgs(args, commandName) {
6226
6700
  }
6227
6701
 
6228
6702
  function assertRecoverWalletArgs(args) {
6229
- assertExplicitSignerCommandArgs(args, "recover-wallet");
6703
+ assertExplicitSignerCommandArgs(args, "wallet-recover-workspace");
6230
6704
  if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
6231
- throw new Error("recover-wallet option --from-genesis does not accept a value.");
6705
+ throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
6232
6706
  }
6233
6707
  }
6234
6708
 
6235
6709
  function assertJoinChannelArgs(args) {
6236
- assertAllowedCommandSchema(args, "join-channel");
6710
+ assertAllowedCommandSchema(args, "channel-join");
6237
6711
  }
6238
6712
 
6239
- function assertGetMyWalletMetaArgs(args) {
6240
- assertWalletSecretArgs(args, "get-my-wallet-meta");
6713
+ function assertWalletGetMetaArgs(args) {
6714
+ assertWalletSecretArgs(args, "wallet-get-meta");
6241
6715
  }
6242
6716
 
6243
- function assertGetMyL1AddressArgs(args) {
6244
- assertAllowedCommandSchema(args, "get-my-l1-address");
6717
+ function assertAccountGetL1AddressArgs(args) {
6718
+ assertAllowedCommandSchema(args, "account-get-l1-address");
6245
6719
  }
6246
6720
 
6247
6721
  function assertListLocalWalletsArgs(args) {
@@ -6251,19 +6725,45 @@ function assertListLocalWalletsArgs(args) {
6251
6725
  if (args.channelName !== undefined) {
6252
6726
  requireArg(args.channelName, "--channel-name");
6253
6727
  }
6254
- assertAllowedCommandSchema(args, "list-local-wallets");
6728
+ assertAllowedCommandSchema(args, "wallet-list");
6729
+ }
6730
+
6731
+ function assertWalletExportArgs(args) {
6732
+ assertAllowedCommandSchema(args, "wallet-export");
6733
+ assertFlagOption(args, "all", "wallet export");
6734
+ assertFlagOption(args, "includeNotes", "wallet export");
6735
+ requireArg(args.output, "--output");
6736
+ if (args.all === true) {
6737
+ expect(
6738
+ args.network === undefined && args.wallet === undefined,
6739
+ "wallet export --all exports every local mainnet wallet and does not accept --network or --wallet.",
6740
+ );
6741
+ return;
6742
+ }
6743
+ requireNetworkName(args);
6744
+ requireWalletName(args);
6745
+ }
6746
+
6747
+ function assertWalletImportArgs(args) {
6748
+ assertAllowedCommandSchema(args, "wallet-import");
6749
+ }
6750
+
6751
+ function assertFlagOption(args, key, commandName) {
6752
+ if (args[key] !== undefined && args[key] !== true) {
6753
+ throw new Error(`${commandName} option --${toKebabCase(key)} does not accept a value.`);
6754
+ }
6255
6755
  }
6256
6756
 
6257
6757
  function assertWithdrawBridgeArgs(args) {
6258
- assertAllowedCommandSchema(args, "withdraw-bridge");
6758
+ assertAllowedCommandSchema(args, "account-withdraw-bridge");
6259
6759
  }
6260
6760
 
6261
- function assertGetMyChannelFundArgs(args) {
6262
- assertWalletSecretArgs(args, "get-my-channel-fund");
6761
+ function assertWalletGetChannelFundArgs(args) {
6762
+ assertWalletSecretArgs(args, "wallet-get-channel-fund");
6263
6763
  }
6264
6764
 
6265
6765
  function assertExitChannelArgs(args) {
6266
- assertWalletSecretArgs(args, "exit-channel");
6766
+ assertWalletSecretArgs(args, "channel-exit");
6267
6767
  }
6268
6768
 
6269
6769
  function createWalletOperationDir(walletName, networkName, suffix) {
@@ -6289,17 +6789,6 @@ function persistWalletMetadata(context) {
6289
6789
  });
6290
6790
  }
6291
6791
 
6292
- function persistCurrentState(context) {
6293
- if (!context.persistChannelWorkspace || !context.workspaceDir) {
6294
- return;
6295
- }
6296
- writeJson(path.join(channelWorkspaceCurrentPath(context.workspaceDir), "state_snapshot.json"), context.currentSnapshot);
6297
- writeJson(
6298
- path.join(channelWorkspaceCurrentPath(context.workspaceDir), "state_snapshot.normalized.json"),
6299
- context.currentSnapshot,
6300
- );
6301
- }
6302
-
6303
6792
  function printHelp() {
6304
6793
  const commandHelp = PRIVATE_STATE_CLI_COMMANDS.map((command) => [
6305
6794
  ` ${privateStateCliCommandSynopsis(command)}`,
@@ -6313,10 +6802,10 @@ ${commandHelp}
6313
6802
  Secret source options:
6314
6803
  Use account import --private-key-file once to create a protected local account secret.
6315
6804
  L1 signing commands use --account only.
6316
- A wallet secret source file is arbitrary high-entropy secret text read once by join-channel.
6805
+ A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
6317
6806
  Create one before joining a channel, for example:
6318
6807
  openssl rand -hex 32 > ./wallet-secret.txt
6319
- private-state-cli join-channel --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt
6808
+ private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt
6320
6809
  Bridge-facing commands accept optional --rpc-url. When provided, it is saved to
6321
6810
  ~/tokamak-private-channels/secrets/<network>/.env as RPC_URL. When omitted, the CLI reads RPC_URL from that file.
6322
6811
  Wallet commands use wallet-local default secret files only.
@@ -6331,7 +6820,7 @@ Options:
6331
6820
  Print the command result as JSON. Without --json, commands print human-readable output.
6332
6821
 
6333
6822
  --help
6334
- Show this help
6823
+ Show this help. Equivalent to help commands.
6335
6824
  `);
6336
6825
  }
6337
6826
 
@@ -7056,18 +7545,18 @@ function buildRecoveryHints(error, args = {}) {
7056
7545
  || message.includes("does not match the wallet channel")
7057
7546
  || message.includes("The provided wallet does not belong to the selected channel")
7058
7547
  ) {
7059
- hints.push(`private-state-cli list-local-wallets --network ${networkName}`);
7060
- hints.push(`private-state-cli guide --network ${networkName} --wallet ${walletName}`);
7548
+ hints.push(`private-state-cli wallet list --network ${networkName}`);
7549
+ hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
7061
7550
  }
7062
7551
 
7063
7552
  if (error?.code === CLI_ERROR_CODES.MISSING_WALLET_SECRET) {
7064
7553
  hints.push("restore the wallet-local default secret file from backup before running wallet commands.");
7065
- hints.push(`private-state-cli guide --network ${networkName} --wallet ${walletName}`);
7554
+ hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
7066
7555
  }
7067
7556
 
7068
7557
  if (error?.code === CLI_ERROR_CODES.WALLET_DECRYPT_FAILED) {
7069
7558
  hints.push("verify that the wallet-local default secret file is the same secret used when the wallet was created.");
7070
- hints.push("if the encrypted wallet file is corrupted but the wallet secret and L1 account secret still exist, rerun recover-wallet.");
7559
+ hints.push("if the encrypted wallet file is corrupted but the wallet secret and L1 account secret still exist, rerun wallet recover-workspace.");
7071
7560
  hints.push("if the wallet secret was lost, the local L2 key cannot be recovered from the encrypted wallet file.");
7072
7561
  }
7073
7562
 
@@ -7076,7 +7565,7 @@ function buildRecoveryHints(error, args = {}) {
7076
7565
  || message.includes("Missing --account.")
7077
7566
  ) {
7078
7567
  hints.push(`private-state-cli account import --account ${accountName} --network ${networkName} --private-key-file <PATH>`);
7079
- hints.push(`private-state-cli guide --network ${networkName} --account ${accountName}`);
7568
+ hints.push(`private-state-cli help guide --network ${networkName} --account ${accountName}`);
7080
7569
  }
7081
7570
 
7082
7571
  if (
@@ -7084,31 +7573,31 @@ function buildRecoveryHints(error, args = {}) {
7084
7573
  || message.includes("DApp deployment artifact")
7085
7574
  ) {
7086
7575
  hints.push("private-state-cli install");
7087
- hints.push("private-state-cli doctor --json");
7576
+ hints.push("private-state-cli help doctor --json");
7088
7577
  }
7089
7578
 
7090
7579
  if (error?.code === CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION) {
7091
- hints.push(`private-state-cli join-channel --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH>`);
7092
- hints.push(`private-state-cli guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
7580
+ hints.push(`private-state-cli channel join --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH>`);
7581
+ hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
7093
7582
  }
7094
7583
 
7095
7584
  if (error?.code === CLI_ERROR_CODES.STALE_WORKSPACE) {
7096
- hints.push(`private-state-cli recover-workspace --channel-name ${channelName} --network ${networkName}`);
7097
- hints.push(`private-state-cli guide --network ${networkName} --channel-name ${channelName}`);
7585
+ hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
7586
+ hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName}`);
7098
7587
  }
7099
7588
 
7100
7589
  if (message.includes("Workspace recovery index is missing or unusable")) {
7101
- hints.push(`private-state-cli recover-workspace --channel-name ${channelName} --network ${networkName} --from-genesis`);
7102
- hints.push(`private-state-cli recover-wallet --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
7590
+ hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --from-genesis`);
7591
+ hints.push(`private-state-cli wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
7103
7592
  }
7104
7593
 
7105
7594
  if (message.includes("Wallet note recovery index is missing or unusable")) {
7106
- hints.push(`private-state-cli recover-wallet --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
7595
+ hints.push(`private-state-cli wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
7107
7596
  }
7108
7597
 
7109
7598
  if (message.includes("Missing channel selector")) {
7110
- hints.push(`private-state-cli list-local-wallets --network ${networkName}`);
7111
- hints.push(`private-state-cli guide --network ${networkName} --channel-name <CHANNEL> --wallet <WALLET>`);
7599
+ hints.push(`private-state-cli wallet list --network ${networkName}`);
7600
+ hints.push(`private-state-cli help guide --network ${networkName} --channel-name <CHANNEL> --wallet <WALLET>`);
7112
7601
  }
7113
7602
 
7114
7603
  return [...new Set(hints)];