@tokamak-private-dapps/private-state-cli 0.1.9 → 1.0.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.
@@ -390,13 +390,13 @@
390
390
  </li>
391
391
  <li>
392
392
  Even if you lose the channel wallet file, only your Ethereum private key, together with the correct
393
- channel context, can recover your channel wallet as long as you still know the wallet password.
393
+ channel context, can recover your channel wallet as long as you still know the wallet secret.
394
394
  </li>
395
395
  <li>
396
- If your channel wallet file and wallet password are both stolen, your channel funds can be stolen.
396
+ If your channel wallet file and wallet secret are both stolen, your channel funds can be stolen.
397
397
  </li>
398
398
  <li>
399
- If you lose your wallet password, you lose the ability to derive your channel L2 private key. In that
399
+ If you lose your wallet secret, you lose the ability to derive your channel L2 private key. In that
400
400
  case, you lose ownership of all notes because you can no longer use them, and that ownership cannot be
401
401
  recovered.
402
402
  </li>
@@ -416,29 +416,29 @@
416
416
  <li>
417
417
  <span class="guide-label">Join a channel:</span> <code>join-channel</code>
418
418
  <div class="guide-example">
419
- <code>join-channel --channel-name 'my-private-channel' --password '__WALLET_PASSWORD__' --network 'sepolia' --private-key '__L1_PRIVATE_KEY__' --alchemy-api-key '__ALCHEMY_API_KEY__'</code>
419
+ <code>join-channel --channel-name 'my-private-channel' --network 'sepolia' --account 'my-account' --wallet-secret-path '/path/to/wallet-secret' --rpc-url '__RPC_URL__'</code>
420
420
  </div>
421
421
  </li>
422
422
  <li>
423
423
  <span class="guide-label">Create notes:</span> <code>deposit-bridge</code> -&gt; <code>deposit-channel</code> -&gt; <code>mint-notes</code>
424
424
  <div class="guide-example">
425
- <code>deposit-bridge --amount '100' --network 'sepolia' --private-key '__L1_PRIVATE_KEY__' --alchemy-api-key '__ALCHEMY_API_KEY__'</code><br />
426
- <code>deposit-channel --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --amount '100'</code><br />
427
- <code>mint-notes --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --amounts '[&quot;50&quot;,&quot;50&quot;,&quot;0&quot;]'</code>
425
+ <code>deposit-bridge --amount '100' --network 'sepolia' --account 'my-account' --rpc-url '__RPC_URL__'</code><br />
426
+ <code>deposit-channel --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --amount '100'</code><br />
427
+ <code>mint-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --amounts '[&quot;50&quot;,&quot;50&quot;,&quot;0&quot;]'</code>
428
428
  </div>
429
429
  </li>
430
430
  <li>
431
431
  <span class="guide-label">Split, merge, or transfer note ownership:</span> <code>transfer-notes</code>
432
432
  <div class="guide-example">
433
- <code>transfer-notes --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --note-ids '[&quot;0xNoteIdA&quot;]' --recipients '[&quot;0xRecipientL2AddressA&quot;,&quot;0xRecipientL2AddressB&quot;]' --amounts '[&quot;25&quot;,&quot;25&quot;]'</code>
433
+ <code>transfer-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --note-ids '[&quot;0xNoteIdA&quot;]' --recipients '[&quot;0xRecipientL2AddressA&quot;,&quot;0xRecipientL2AddressB&quot;]' --amounts '[&quot;25&quot;,&quot;25&quot;]'</code>
434
434
  </div>
435
435
  </li>
436
436
  <li>
437
437
  <span class="guide-label">Redeem notes back to L1:</span> <code>redeem-notes</code> -&gt; <code>withdraw-channel</code> -&gt; <code>withdraw-bridge</code>
438
438
  <div class="guide-example">
439
- <code>redeem-notes --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --note-ids '[&quot;0xNoteIdA&quot;]'</code><br />
440
- <code>withdraw-channel --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --amount '25'</code><br />
441
- <code>withdraw-bridge --amount '25' --network 'sepolia' --private-key '__L1_PRIVATE_KEY__' --alchemy-api-key '__ALCHEMY_API_KEY__'</code>
439
+ <code>redeem-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --note-ids '[&quot;0xNoteIdA&quot;]'</code><br />
440
+ <code>withdraw-channel --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --amount '25'</code><br />
441
+ <code>withdraw-bridge --amount '25' --network 'sepolia' --account 'my-account' --rpc-url '__RPC_URL__'</code>
442
442
  </div>
443
443
  </li>
444
444
  </ol>
@@ -448,111 +448,32 @@
448
448
  </main>
449
449
 
450
450
  <script src="../../../node_modules/ethers/dist/ethers.umd.js"></script>
451
- <script>
451
+ <script type="module">
452
+ import {
453
+ PRIVATE_STATE_CLI_COMMANDS,
454
+ PRIVATE_STATE_CLI_FIELD_CATALOG,
455
+ } from "./lib/private-state-cli-command-registry.mjs";
456
+
452
457
  const storageKey = "private-state-cli-assistant:v2";
453
- const windowNameKey = "private-state-cli-assistant-state:";
454
458
  const cliEntry = "node packages/apps/private-state/cli/private-state-bridge-cli.mjs";
455
459
  const defaultWorkspaceRootLabel = navigator.userAgent.includes("Windows")
456
460
  ? "%USERPROFILE%\\\\tokamak-private-channels\\\\workspace"
457
461
  : "~/tokamak-private-channels/workspace";
458
- const walletEncryptionVersion = 1;
459
- const walletEncryptionAlgorithm = "aes-256-gcm";
460
- const textEncoder = new TextEncoder();
461
- const textDecoder = new TextDecoder();
462
-
463
- const fieldCatalog = {
464
- channelName: {
465
- label: "Channel Name",
466
- type: "text",
467
- placeholder: "demo-channel",
468
- },
469
- network: {
470
- label: "Network",
471
- type: "select",
472
- options: ["sepolia", "mainnet", "anvil"],
473
- },
474
- alchemyApiKey: {
475
- label: "Alchemy API Key",
476
- type: "password",
477
- placeholder: "alchemy-key",
478
- },
479
- privateKey: {
480
- label: "L1 Private Key",
481
- type: "password",
482
- placeholder: "0x...",
483
- },
484
- password: {
485
- label: "Wallet Password",
486
- type: "password",
487
- placeholder: "channel-password",
488
- },
489
- wallet: {
490
- label: "Wallet Name",
491
- type: "text",
492
- placeholder: "channel-0xYourL1Address",
493
- },
494
- amount: {
495
- label: "Amount",
496
- type: "text",
497
- placeholder: "3",
498
- },
499
- amounts: {
500
- label: "Amounts",
501
- type: "textarea",
502
- placeholder: "[1,2,3]",
503
- },
504
- noteIds: {
505
- label: "Note IDs",
506
- type: "textarea",
507
- placeholder: "[\"0x...\"]",
508
- },
509
- recipients: {
510
- label: "Recipients JSON",
511
- type: "textarea",
512
- placeholder: "[\"0xRecipientL2Address\"]",
513
- },
514
- force: {
515
- label: "Force Exit",
516
- type: "checkbox",
517
- hint: "Bypass the CLI zero-balance guard for exit-channel.",
518
- },
519
- docker: {
520
- label: "Docker Install Mode",
521
- type: "checkbox",
522
- hint: "Forward --docker to install-zk-evm. This mode is supported only on Linux hosts by the Tokamak CLI.",
523
- },
524
- };
525
-
526
- const commands = [
527
- { id: "install-zk-evm", description: "Install the local Tokamak zk-EVM toolchain. Optionally forward --docker for Linux-host Docker installs.", fields: ["docker"] },
528
- { id: "uninstall-zk-evm", description: "Remove the Tokamak zk-EVM CLI runtime workspace.", fields: [] },
529
- { id: "create-channel", description: "Create a channel with an immutable operating policy and initialize its workspace.", fields: ["channelName", "network", "privateKey", "alchemyApiKey"] },
530
- { id: "recover-workspace", description: "Rebuild the saved channel workspace from bridge state.", fields: ["channelName", "network", "alchemyApiKey"] },
531
- { id: "deposit-bridge", description: "Deposit canonical tokens into the shared bridge vault.", fields: ["amount", "network", "privateKey", "alchemyApiKey"] },
532
- { id: "withdraw-bridge", description: "Withdraw shared bridge-vault funds back to the L1 wallet.", fields: ["amount", "network", "privateKey", "alchemyApiKey"] },
533
- { id: "get-my-bridge-fund", description: "Read the current bridge-vault balance.", fields: ["network", "privateKey", "alchemyApiKey"] },
534
- { id: "join-channel", description: "Accept the channel policy and bind the caller to a channel-specific L2 identity.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
535
- { id: "recover-wallet", description: "Rebuild the recoverable portion of a wallet.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
536
- { id: "get-my-wallet-meta", description: "Check whether a saved wallet matches on-chain registration.", fields: ["wallet", "password", "network"] },
537
- { id: "get-my-l1-address", description: "Derive the L1 address for a private key.", fields: ["privateKey"] },
538
- { id: "list-local-wallets", description: "List saved local wallet names that can be reused with --wallet.", fields: ["network", "channelName"] },
539
- { id: "deposit-channel", description: "Move bridged funds into the channel L2 accounting balance.", fields: ["wallet", "password", "network", "amount"] },
540
- { id: "withdraw-channel", description: "Move channel L2 balance back into the shared bridge vault.", fields: ["wallet", "password", "network", "amount"] },
541
- { id: "get-my-channel-fund", description: "Read the current channel L2 accounting balance.", fields: ["wallet", "password", "network"] },
542
- { id: "exit-channel", description: "Exit a channel. The assistant keeps the CLI zero-balance guard unless Force Exit is enabled.", fields: ["wallet", "password", "network", "force"] },
543
- { id: "mint-notes", description: "Mint one or two private-state notes from the wallet channel balance.", fields: ["wallet", "password", "network", "amounts"] },
544
- { id: "transfer-notes", description: "Spend tracked notes into the registered 1->1, 1->2, or 2->1 encrypted transfer shapes.", fields: ["wallet", "password", "network", "noteIds", "recipients", "amounts"] },
545
- { id: "redeem-notes", description: "Redeem exactly one tracked note back into liquid accounting balance.", fields: ["wallet", "password", "network", "noteIds"] },
546
- { id: "get-my-notes", description: "Refresh encrypted note-log recovery and show current wallet notes.", fields: ["wallet", "password", "network"] },
547
- ];
462
+ const fieldCatalog = PRIVATE_STATE_CLI_FIELD_CATALOG;
463
+ const commands = PRIVATE_STATE_CLI_COMMANDS.map((command) => ({
464
+ ...command,
465
+ fields: [...new Set([...command.fields, "json"])],
466
+ }));
548
467
 
549
468
  const defaultState = {
550
469
  commandId: "join-channel",
551
470
  channelName: "",
471
+ joinToll: "1",
552
472
  network: "sepolia",
553
- alchemyApiKey: "",
554
- privateKey: "",
555
- password: "",
473
+ rpcUrl: "",
474
+ account: "",
475
+ privateKeyFile: "",
476
+ walletSecretPath: "",
556
477
  wallet: "",
557
478
  amount: "",
558
479
  amounts: "",
@@ -564,14 +485,19 @@
564
485
  transferAmount1: "",
565
486
  transferRecipient2: "",
566
487
  transferAmount2: "",
567
- force: false,
568
488
  docker: false,
489
+ includeLocalArtifacts: false,
490
+ groth16CliVersion: "",
491
+ tokamakZkEvmCliVersion: "",
492
+ fromGenesis: false,
493
+ gpu: false,
494
+ json: false,
569
495
  };
570
496
 
571
497
  const sharedFieldKeys = [
572
498
  "network",
573
- "alchemyApiKey",
574
- "privateKey",
499
+ "rpcUrl",
500
+ "account",
575
501
  ];
576
502
 
577
503
  const state = loadState();
@@ -589,14 +515,6 @@
589
515
  statusMessage: "",
590
516
  hasLoaded: false,
591
517
  };
592
- const privateKeyAddressState = {
593
- inputValue: "",
594
- derivedAddress: "",
595
- statusMessage: "",
596
- };
597
- let scryptModulePromise = null;
598
- let ethersModulePromise = null;
599
- let privateKeyLookupGeneration = 0;
600
518
  let walletNoteRefreshTimer = null;
601
519
 
602
520
  const workspacePickerEl = document.getElementById("workspace-picker");
@@ -611,9 +529,8 @@
611
529
  const workspacePathLabelEl = document.getElementById("workspace-path-label");
612
530
  let workspaceStatusEl = null;
613
531
  const maskedSecretValues = {
614
- alchemyApiKey: "__ALCHEMY_API_KEY__",
615
- privateKey: "__L1_PRIVATE_KEY__",
616
- password: "__WALLET_PASSWORD__",
532
+ rpcUrl: "__RPC_URL__",
533
+ privateKeyFile: "__PRIVATE_KEY_FILE__",
617
534
  };
618
535
 
619
536
  document.getElementById("copy-command").addEventListener("click", async () => {
@@ -635,17 +552,6 @@
635
552
  if (parsed) {
636
553
  return parsed;
637
554
  }
638
- } catch {
639
- // Fallback to window.name below.
640
- }
641
-
642
- try {
643
- if (typeof window.name === "string" && window.name.startsWith(windowNameKey)) {
644
- const parsed = parseSerializedState(window.name.slice(windowNameKey.length));
645
- if (parsed) {
646
- return parsed;
647
- }
648
- }
649
555
  } catch {
650
556
  // Fall through to defaults.
651
557
  }
@@ -658,12 +564,7 @@
658
564
  try {
659
565
  window.localStorage.setItem(storageKey, serialized);
660
566
  } catch {
661
- // window.name fallback below still preserves state across refreshes in the current tab.
662
- }
663
- try {
664
- window.name = `${windowNameKey}${serialized}`;
665
- } catch {
666
- // Ignore window.name failures.
567
+ // Ignore storage failures; the generated command remains available in the preview.
667
568
  }
668
569
  }
669
570
 
@@ -734,19 +635,7 @@
734
635
 
735
636
  function walletOptionsForCommand(command = currentCommand()) {
736
637
  const options = currentWorkspaceOptions();
737
- if (!commandNeedsWallet(command)) {
738
- return options.walletOptions;
739
- }
740
- const derivedAddress = privateKeyAddressState.derivedAddress.trim().toLowerCase();
741
- if (!derivedAddress) {
742
- return [];
743
- }
744
- return options.walletOptions.filter((walletName) => {
745
- const walletEntry = options.walletEntriesByName?.[walletName];
746
- return walletEntry?.l1Address
747
- && derivedAddress
748
- && ethers.toBigInt(walletEntry.l1Address) === ethers.toBigInt(derivedAddress);
749
- });
638
+ return options.walletOptions;
750
639
  }
751
640
 
752
641
  function buildWorkspaceStatusMessage() {
@@ -933,175 +822,15 @@
933
822
  }, 250);
934
823
  }
935
824
 
936
- function addHexPrefix(value) {
937
- const normalized = String(value ?? "");
938
- return normalized.startsWith("0x") || normalized.startsWith("0X") ? normalized : `0x${normalized}`;
939
- }
940
-
941
- function hexToBytes(value) {
942
- const normalizedWithPrefix = addHexPrefix(value);
943
- const normalized = normalizedWithPrefix.slice(2);
944
- if (normalized.length === 0) {
945
- return new Uint8Array();
946
- }
947
- return Uint8Array.from(
948
- normalized.match(/.{1,2}/g).map((pair) => Number.parseInt(pair, 16)),
949
- );
950
- }
951
-
952
- async function deriveScryptKeyBytes(password, saltHex) {
953
- const browserEthers = window.ethers?.ethers ?? window.ethers;
954
- if (browserEthers?.scryptSync) {
955
- return browserEthers.getBytes(
956
- browserEthers.scryptSync(textEncoder.encode(String(password)), saltHex, 16384, 8, 1, 32),
957
- );
958
- }
959
- if (!scryptModulePromise) {
960
- scryptModulePromise = import("../../../node_modules/@noble/hashes/esm/scrypt.js");
961
- }
962
- const { scrypt } = await scryptModulePromise;
963
- return scrypt(textEncoder.encode(String(password)), hexToBytes(saltHex), {
964
- N: 16384,
965
- r: 8,
966
- p: 1,
967
- dkLen: 32,
968
- });
969
- }
970
-
971
- async function deriveAddressFromPrivateKey(privateKey) {
972
- const browserWalletClass = window.ethers?.Wallet ?? window.ethers?.ethers?.Wallet;
973
- if (browserWalletClass) {
974
- return new browserWalletClass(String(privateKey)).address;
975
- }
976
- if (!ethersModulePromise) {
977
- ethersModulePromise = import("../../../node_modules/ethers/lib.esm/index.js");
978
- }
979
- const { Wallet } = await ethersModulePromise;
980
- return new Wallet(String(privateKey)).address;
981
- }
982
-
983
- async function refreshPrivateKeyAddressState(privateKeyValue) {
984
- const normalizedValue = String(privateKeyValue ?? "").trim();
985
- privateKeyAddressState.inputValue = normalizedValue;
986
-
987
- if (normalizedValue.length === 0) {
988
- privateKeyAddressState.derivedAddress = "";
989
- privateKeyAddressState.statusMessage = "";
990
- return;
991
- }
992
-
993
- const currentGeneration = ++privateKeyLookupGeneration;
994
- try {
995
- const derivedAddress = await deriveAddressFromPrivateKey(normalizedValue);
996
- if (currentGeneration !== privateKeyLookupGeneration) {
997
- return;
998
- }
999
- privateKeyAddressState.derivedAddress = derivedAddress;
1000
- privateKeyAddressState.statusMessage = `L1 address: ${derivedAddress}`;
1001
- } catch {
1002
- if (currentGeneration !== privateKeyLookupGeneration) {
1003
- return;
1004
- }
1005
- privateKeyAddressState.derivedAddress = "";
1006
- privateKeyAddressState.statusMessage = "Invalid private key.";
1007
- }
1008
- }
1009
-
1010
- async function decryptWalletJson(envelope, walletPassword) {
1011
- if (
1012
- envelope?.version !== walletEncryptionVersion
1013
- || envelope?.algorithm !== walletEncryptionAlgorithm
1014
- || envelope?.kdf !== "scrypt"
1015
- ) {
1016
- throw new Error("Unsupported wallet encryption envelope.");
1017
- }
1018
-
1019
- const encryptionKeyBytes = await deriveScryptKeyBytes(walletPassword, envelope.salt);
1020
- const cryptoKey = await crypto.subtle.importKey("raw", encryptionKeyBytes, "AES-GCM", false, ["decrypt"]);
1021
- const ciphertext = hexToBytes(envelope.ciphertext);
1022
- const tag = hexToBytes(envelope.tag);
1023
- const combinedCiphertext = new Uint8Array(ciphertext.length + tag.length);
1024
- combinedCiphertext.set(ciphertext, 0);
1025
- combinedCiphertext.set(tag, ciphertext.length);
1026
- const plaintextBuffer = await crypto.subtle.decrypt(
1027
- {
1028
- name: "AES-GCM",
1029
- iv: hexToBytes(envelope.iv),
1030
- tagLength: 128,
1031
- },
1032
- cryptoKey,
1033
- combinedCiphertext,
1034
- );
1035
- return JSON.parse(textDecoder.decode(new Uint8Array(plaintextBuffer)));
1036
- }
1037
-
1038
- function extractUnusedNoteOptions(walletJson) {
1039
- const notes = walletJson?.notes ?? {};
1040
- const unusedNotes = notes.unused ?? {};
1041
- const unusedOrder = Array.isArray(notes.unusedOrder) ? notes.unusedOrder : Object.keys(unusedNotes);
1042
- return unusedOrder
1043
- .map((commitment) => unusedNotes[commitment])
1044
- .filter((note) => note && typeof note.commitment === "string")
1045
- .map((note) => ({
1046
- value: note.commitment,
1047
- amountBaseUnits: String(note.value ?? "0"),
1048
- label: typeof note.value === "string"
1049
- ? `${note.commitment} (${note.value})`
1050
- : note.commitment,
1051
- }));
1052
- }
1053
-
1054
825
  async function refreshWalletNoteOptions() {
1055
826
  if (!commandNeedsWalletNotes()) {
1056
827
  return;
1057
828
  }
1058
- if (!workspaceDirectoryState.loaded) {
1059
- clearWalletNoteState("Select a workspace directory to load wallet note IDs.");
1060
- return;
1061
- }
1062
- if (!state.wallet) {
1063
- clearWalletNoteState("Choose a wallet to load note IDs.");
1064
- return;
1065
- }
1066
- if (!state.password) {
1067
- clearWalletNoteState("Enter the wallet password to load note IDs.");
1068
- return;
1069
- }
1070
-
1071
- const walletEntry = currentWalletEntry();
1072
- if (!walletEntry) {
1073
- clearWalletNoteState("Selected wallet is unavailable for the current network.");
1074
- return;
1075
- }
1076
-
1077
- const cacheKey = `${state.network}::${state.wallet}::${state.password}`;
1078
- if (walletNoteState.cacheKey === cacheKey && walletNoteState.hasLoaded) {
1079
- return;
1080
- }
1081
-
1082
- walletNoteState.cacheKey = cacheKey;
1083
- walletNoteState.statusMessage = "Loading wallet note IDs...";
1084
- walletNoteState.hasLoaded = false;
1085
-
1086
- try {
1087
- const walletFileHandle = await walletEntry.handle.getFileHandle("wallet.json");
1088
- const walletEnvelope = JSON.parse(await (await walletFileHandle.getFile()).text());
1089
- const walletJson = await decryptWalletJson(walletEnvelope, state.password);
1090
- walletNoteState.options = extractUnusedNoteOptions(walletJson);
1091
- walletNoteState.canonicalAssetDecimals = Number(walletJson?.canonicalAssetDecimals ?? 18);
1092
- walletNoteState.walletL2Address = typeof walletJson?.l2Address === "string" ? walletJson.l2Address : "";
1093
- walletNoteState.statusMessage = walletNoteState.options.length === 0
1094
- ? "This wallet has no unused note IDs."
1095
- : `Loaded ${walletNoteState.options.length} unused note ID${walletNoteState.options.length === 1 ? "" : "s"}.`;
1096
- walletNoteState.hasLoaded = true;
1097
- syncSelectedNoteIdsToAvailableOptions();
1098
- } catch {
1099
- clearWalletNoteState("Unable to decrypt the selected wallet. Check the wallet and password.");
1100
- }
829
+ clearWalletNoteState("Enter note IDs manually. The CLI uses the wallet-local default secret file and the assistant does not request wallet secrets.");
1101
830
  }
1102
831
 
1103
- function needsAlchemy(command) {
1104
- return command.fields.includes("alchemyApiKey") && state.network !== "anvil";
832
+ function acceptsRpcUrl(command) {
833
+ return command.fields.includes("rpcUrl");
1105
834
  }
1106
835
 
1107
836
  function currentCommand() {
@@ -1112,8 +841,9 @@
1112
841
  const transferOutputs = command.id === "transfer-notes"
1113
842
  ? currentTransferOutputs()
1114
843
  : null;
844
+ const optionalFields = new Set(command.optionalFields ?? []);
1115
845
  return command.fields.filter((fieldKey) => {
1116
- if (fieldKey === "alchemyApiKey" && !needsAlchemy(command)) {
846
+ if (fieldCatalog[fieldKey]?.optional || optionalFields.has(fieldKey)) {
1117
847
  return false;
1118
848
  }
1119
849
  if (fieldKey === "amounts" && command.id === "mint-notes") {
@@ -1137,7 +867,7 @@
1137
867
  : null;
1138
868
 
1139
869
  for (const fieldKey of command.fields) {
1140
- if (fieldKey === "alchemyApiKey" && !needsAlchemy(command)) {
870
+ if (fieldKey === "rpcUrl" && !String(state.rpcUrl ?? "").trim()) {
1141
871
  continue;
1142
872
  }
1143
873
 
@@ -1159,45 +889,11 @@
1159
889
  ? maskedSecretValues[fieldKey]
1160
890
  : value;
1161
891
 
1162
- switch (fieldKey) {
1163
- case "channelName":
1164
- appendOption(parts, "--channel-name", renderedValue);
1165
- break;
1166
- case "network":
1167
- appendOption(parts, "--network", renderedValue);
1168
- break;
1169
- case "alchemyApiKey":
1170
- appendOption(parts, "--alchemy-api-key", renderedValue);
1171
- break;
1172
- case "privateKey":
1173
- appendOption(parts, "--private-key", renderedValue);
1174
- break;
1175
- case "password":
1176
- appendOption(parts, "--password", renderedValue);
1177
- break;
1178
- case "wallet":
1179
- appendOption(parts, "--wallet", renderedValue);
1180
- break;
1181
- case "amount":
1182
- appendOption(parts, "--amount", renderedValue);
1183
- break;
1184
- case "amounts":
1185
- appendOption(parts, "--amounts", renderedValue);
1186
- break;
1187
- case "noteIds":
1188
- appendOption(parts, "--note-ids", renderedValue);
1189
- break;
1190
- case "recipients":
1191
- appendOption(parts, "--recipients", renderedValue);
1192
- break;
1193
- case "force":
1194
- parts.push("--force");
1195
- break;
1196
- case "docker":
1197
- parts.push("--docker");
1198
- break;
1199
- default:
1200
- break;
892
+ const fieldConfig = fieldCatalog[fieldKey];
893
+ if (fieldConfig?.type === "checkbox") {
894
+ parts.push(fieldConfig.option);
895
+ } else if (fieldConfig?.option) {
896
+ appendOption(parts, fieldConfig.option, renderedValue);
1201
897
  }
1202
898
  }
1203
899
 
@@ -1380,11 +1076,9 @@
1380
1076
  placeholder.value = "";
1381
1077
  placeholder.textContent = !workspaceDirectoryState.loaded
1382
1078
  ? "Select a workspace directory for this network first"
1383
- : commandNeedsWallet(currentCommand()) && !privateKeyAddressState.derivedAddress
1384
- ? "Enter a valid L1 private key first"
1385
- : walletOptions.length === 0
1386
- ? "No wallet matches the current L1 private key"
1387
- : "Choose wallet";
1079
+ : walletOptions.length === 0
1080
+ ? "No wallet exists for this network"
1081
+ : "Choose wallet";
1388
1082
  select.appendChild(placeholder);
1389
1083
  for (const option of walletOptions) {
1390
1084
  const optionEl = document.createElement("option");
@@ -1489,7 +1183,7 @@
1489
1183
 
1490
1184
  const hint = document.createElement("p");
1491
1185
  hint.className = "hint";
1492
- hint.textContent = walletNoteState.statusMessage || `Choose a wallet and enter its password to load up to ${noteSelectionLimit()} note IDs.`;
1186
+ hint.textContent = walletNoteState.statusMessage || "Enter note IDs manually. Wallet secrets are not requested by this assistant.";
1493
1187
  wrapper.appendChild(hint);
1494
1188
 
1495
1189
  const totalHint = document.createElement("p");
@@ -1691,19 +1385,6 @@
1691
1385
  workspaceDirectoryState.statusMessage = buildWorkspaceStatusMessage();
1692
1386
  clearWalletNoteState();
1693
1387
  }
1694
- if (fieldKey === "privateKey") {
1695
- await refreshPrivateKeyAddressState(event.target.value);
1696
- if (memoryStatusEl) {
1697
- memoryStatusEl.textContent = privateKeyAddressState.statusMessage;
1698
- }
1699
- syncWorkspaceSelectionsForCurrentNetwork();
1700
- }
1701
- if (fieldKey === "password") {
1702
- clearWalletNoteState();
1703
- if (commandNeedsWalletNotes()) {
1704
- walletNoteState.statusMessage = "Loading wallet note IDs...";
1705
- }
1706
- }
1707
1388
  persistState();
1708
1389
  if (fieldKey === "network") {
1709
1390
  await refreshWalletNoteOptions();
@@ -1712,34 +1393,9 @@
1712
1393
  if (fieldKey === "network") {
1713
1394
  renderWorkspacePicker();
1714
1395
  renderCommandFields();
1715
- } else if (fieldKey === "privateKey" && commandNeedsWallet()) {
1716
- renderCommandFields();
1717
- } else if (fieldKey === "password" && commandNeedsWalletNotes()) {
1718
- rerenderCommandField("noteIds");
1719
- if (currentCommand().id === "transfer-notes") {
1720
- rerenderCommandField("recipients");
1721
- }
1722
- scheduleWalletNoteRefresh();
1723
1396
  }
1724
1397
  });
1725
1398
 
1726
- if (fieldKey === "password") {
1727
- input.addEventListener("blur", async () => {
1728
- if (walletNoteRefreshTimer) {
1729
- window.clearTimeout(walletNoteRefreshTimer);
1730
- walletNoteRefreshTimer = null;
1731
- }
1732
- await refreshWalletNoteOptions();
1733
- if (commandNeedsWalletNotes()) {
1734
- rerenderCommandField("noteIds");
1735
- if (currentCommand().id === "transfer-notes") {
1736
- rerenderCommandField("recipients");
1737
- }
1738
- }
1739
- renderPreview();
1740
- });
1741
- }
1742
-
1743
1399
  label.appendChild(input);
1744
1400
  if (config.type === "checkbox" && config.hint) {
1745
1401
  const hint = document.createElement("p");
@@ -1754,20 +1410,12 @@
1754
1410
  memoryFieldsEl.innerHTML = "";
1755
1411
  [
1756
1412
  "network",
1757
- "alchemyApiKey",
1758
- "privateKey",
1413
+ "rpcUrl",
1414
+ "account",
1759
1415
  ].forEach((fieldKey) => {
1760
1416
  memoryFieldsEl.appendChild(createField(fieldKey));
1761
1417
  });
1762
- memoryStatusEl.textContent = privateKeyAddressState.statusMessage;
1763
- void refreshPrivateKeyAddressState(state.privateKey).then(() => {
1764
- memoryStatusEl.textContent = privateKeyAddressState.statusMessage;
1765
- syncWorkspaceSelectionsForCurrentNetwork();
1766
- if (currentCommand().fields.includes("wallet")) {
1767
- renderCommandFields();
1768
- renderPreview();
1769
- }
1770
- });
1418
+ memoryStatusEl.textContent = "Use account import --private-key-file before signing commands. Optional --rpc-url is saved as the network RPC_URL for later commands.";
1771
1419
  }
1772
1420
 
1773
1421
  function renderCommandSelect() {
@@ -1799,7 +1447,7 @@
1799
1447
  }
1800
1448
 
1801
1449
  for (const fieldKey of commandOnlyFields) {
1802
- if (fieldKey === "alchemyApiKey" && !needsAlchemy(command)) {
1450
+ if (fieldKey === "rpcUrl" && !acceptsRpcUrl(command)) {
1803
1451
  continue;
1804
1452
  }
1805
1453
  if (fieldKey === "amounts" && command.id === "transfer-notes") {
@@ -1827,9 +1475,7 @@
1827
1475
  : "",
1828
1476
  ].join("");
1829
1477
  warningEl.textContent = missing.length === 0
1830
- ? state.network === "anvil"
1831
- ? `Ready. --alchemy-api-key is omitted automatically on anvil.${workspaceWarnings}`
1832
- : `Ready.${workspaceWarnings}`
1478
+ ? `Ready. If --rpc-url is omitted, the CLI uses the saved network RPC_URL.${workspaceWarnings}`
1833
1479
  : `Missing inputs: ${missing.map((fieldKey) => fieldCatalog[fieldKey].label).join(", ")}.${workspaceWarnings}`;
1834
1480
  }
1835
1481