@tokamak-private-dapps/private-state-cli 0.1.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -390,16 +390,21 @@
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>
403
+ <li>
404
+ Channel policy is immutable after creation. Joining a channel means accepting its verifier bindings,
405
+ DApp metadata, function layout, managed storage vector, and refund policy; later policy-level fixes
406
+ require a new channel or migration.
407
+ </li>
403
408
  </ul>
404
409
  </div>
405
410
  </section>
@@ -411,29 +416,29 @@
411
416
  <li>
412
417
  <span class="guide-label">Join a channel:</span> <code>join-channel</code>
413
418
  <div class="guide-example">
414
- <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>
415
420
  </div>
416
421
  </li>
417
422
  <li>
418
423
  <span class="guide-label">Create notes:</span> <code>deposit-bridge</code> -&gt; <code>deposit-channel</code> -&gt; <code>mint-notes</code>
419
424
  <div class="guide-example">
420
- <code>deposit-bridge --amount '100' --network 'sepolia' --private-key '__L1_PRIVATE_KEY__' --alchemy-api-key '__ALCHEMY_API_KEY__'</code><br />
421
- <code>deposit-channel --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --amount '100'</code><br />
422
- <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>
423
428
  </div>
424
429
  </li>
425
430
  <li>
426
431
  <span class="guide-label">Split, merge, or transfer note ownership:</span> <code>transfer-notes</code>
427
432
  <div class="guide-example">
428
- <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>
429
434
  </div>
430
435
  </li>
431
436
  <li>
432
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>
433
438
  <div class="guide-example">
434
- <code>redeem-notes --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --note-ids '[&quot;0xNoteIdA&quot;]'</code><br />
435
- <code>withdraw-channel --wallet 'my-private-channel-0xYourL1Address' --password '__WALLET_PASSWORD__' --network 'sepolia' --amount '25'</code><br />
436
- <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>
437
442
  </div>
438
443
  </li>
439
444
  </ol>
@@ -443,111 +448,32 @@
443
448
  </main>
444
449
 
445
450
  <script src="../../../node_modules/ethers/dist/ethers.umd.js"></script>
446
- <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
+
447
457
  const storageKey = "private-state-cli-assistant:v2";
448
- const windowNameKey = "private-state-cli-assistant-state:";
449
458
  const cliEntry = "node packages/apps/private-state/cli/private-state-bridge-cli.mjs";
450
459
  const defaultWorkspaceRootLabel = navigator.userAgent.includes("Windows")
451
460
  ? "%USERPROFILE%\\\\tokamak-private-channels\\\\workspace"
452
461
  : "~/tokamak-private-channels/workspace";
453
- const walletEncryptionVersion = 1;
454
- const walletEncryptionAlgorithm = "aes-256-gcm";
455
- const textEncoder = new TextEncoder();
456
- const textDecoder = new TextDecoder();
457
-
458
- const fieldCatalog = {
459
- channelName: {
460
- label: "Channel Name",
461
- type: "text",
462
- placeholder: "demo-channel",
463
- },
464
- network: {
465
- label: "Network",
466
- type: "select",
467
- options: ["sepolia", "mainnet", "anvil"],
468
- },
469
- alchemyApiKey: {
470
- label: "Alchemy API Key",
471
- type: "password",
472
- placeholder: "alchemy-key",
473
- },
474
- privateKey: {
475
- label: "L1 Private Key",
476
- type: "password",
477
- placeholder: "0x...",
478
- },
479
- password: {
480
- label: "Wallet Password",
481
- type: "password",
482
- placeholder: "channel-password",
483
- },
484
- wallet: {
485
- label: "Wallet Name",
486
- type: "text",
487
- placeholder: "channel-0xYourL1Address",
488
- },
489
- amount: {
490
- label: "Amount",
491
- type: "text",
492
- placeholder: "3",
493
- },
494
- amounts: {
495
- label: "Amounts",
496
- type: "textarea",
497
- placeholder: "[1,2,3]",
498
- },
499
- noteIds: {
500
- label: "Note IDs",
501
- type: "textarea",
502
- placeholder: "[\"0x...\"]",
503
- },
504
- recipients: {
505
- label: "Recipients JSON",
506
- type: "textarea",
507
- placeholder: "[\"0xRecipientL2Address\"]",
508
- },
509
- force: {
510
- label: "Force Exit",
511
- type: "checkbox",
512
- hint: "Bypass the CLI zero-balance guard for exit-channel.",
513
- },
514
- docker: {
515
- label: "Docker Install Mode",
516
- type: "checkbox",
517
- hint: "Forward --docker to install-zk-evm. This mode is supported only on Linux hosts by the Tokamak CLI.",
518
- },
519
- };
520
-
521
- const commands = [
522
- { id: "install-zk-evm", description: "Install the local Tokamak zk-EVM toolchain. Optionally forward --docker for Linux-host Docker installs.", fields: ["docker"] },
523
- { id: "uninstall-zk-evm", description: "Remove the Tokamak zk-EVM CLI runtime workspace.", fields: [] },
524
- { id: "create-channel", description: "Create the bridge channel and initialize its workspace.", fields: ["channelName", "network", "privateKey", "alchemyApiKey"] },
525
- { id: "recover-workspace", description: "Rebuild the saved channel workspace from bridge state.", fields: ["channelName", "network", "alchemyApiKey"] },
526
- { id: "deposit-bridge", description: "Deposit canonical tokens into the shared bridge vault.", fields: ["amount", "network", "privateKey", "alchemyApiKey"] },
527
- { id: "withdraw-bridge", description: "Withdraw shared bridge-vault funds back to the L1 wallet.", fields: ["amount", "network", "privateKey", "alchemyApiKey"] },
528
- { id: "get-my-bridge-fund", description: "Read the current bridge-vault balance.", fields: ["network", "privateKey", "alchemyApiKey"] },
529
- { id: "join-channel", description: "Bind the caller to a channel-specific L2 identity.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
530
- { id: "recover-wallet", description: "Rebuild the recoverable portion of a wallet.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
531
- { id: "get-my-wallet-meta", description: "Check whether a saved wallet matches on-chain registration.", fields: ["wallet", "password", "network"] },
532
- { id: "get-my-l1-address", description: "Derive the L1 address for a private key.", fields: ["privateKey"] },
533
- { id: "list-local-wallets", description: "List saved local wallet names that can be reused with --wallet.", fields: ["network", "channelName"] },
534
- { id: "deposit-channel", description: "Move bridged funds into the channel L2 accounting balance.", fields: ["wallet", "password", "network", "amount"] },
535
- { id: "withdraw-channel", description: "Move channel L2 balance back into the shared bridge vault.", fields: ["wallet", "password", "network", "amount"] },
536
- { id: "get-my-channel-fund", description: "Read the current channel L2 accounting balance.", fields: ["wallet", "password", "network"] },
537
- { 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"] },
538
- { id: "mint-notes", description: "Mint one or two private-state notes from the wallet channel balance.", fields: ["wallet", "password", "network", "amounts"] },
539
- { 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"] },
540
- { id: "redeem-notes", description: "Redeem exactly one tracked note back into liquid accounting balance.", fields: ["wallet", "password", "network", "noteIds"] },
541
- { id: "get-my-notes", description: "Refresh encrypted note-log recovery and show current wallet notes.", fields: ["wallet", "password", "network"] },
542
- ];
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
+ }));
543
467
 
544
468
  const defaultState = {
545
469
  commandId: "join-channel",
546
470
  channelName: "",
471
+ joinToll: "1",
547
472
  network: "sepolia",
548
- alchemyApiKey: "",
549
- privateKey: "",
550
- password: "",
473
+ rpcUrl: "",
474
+ account: "",
475
+ privateKeyFile: "",
476
+ walletSecretPath: "",
551
477
  wallet: "",
552
478
  amount: "",
553
479
  amounts: "",
@@ -559,14 +485,19 @@
559
485
  transferAmount1: "",
560
486
  transferRecipient2: "",
561
487
  transferAmount2: "",
562
- force: false,
563
488
  docker: false,
489
+ includeLocalArtifacts: false,
490
+ groth16CliVersion: "",
491
+ tokamakZkEvmCliVersion: "",
492
+ fromGenesis: false,
493
+ gpu: false,
494
+ json: false,
564
495
  };
565
496
 
566
497
  const sharedFieldKeys = [
567
498
  "network",
568
- "alchemyApiKey",
569
- "privateKey",
499
+ "rpcUrl",
500
+ "account",
570
501
  ];
571
502
 
572
503
  const state = loadState();
@@ -584,14 +515,6 @@
584
515
  statusMessage: "",
585
516
  hasLoaded: false,
586
517
  };
587
- const privateKeyAddressState = {
588
- inputValue: "",
589
- derivedAddress: "",
590
- statusMessage: "",
591
- };
592
- let scryptModulePromise = null;
593
- let ethersModulePromise = null;
594
- let privateKeyLookupGeneration = 0;
595
518
  let walletNoteRefreshTimer = null;
596
519
 
597
520
  const workspacePickerEl = document.getElementById("workspace-picker");
@@ -606,9 +529,8 @@
606
529
  const workspacePathLabelEl = document.getElementById("workspace-path-label");
607
530
  let workspaceStatusEl = null;
608
531
  const maskedSecretValues = {
609
- alchemyApiKey: "__ALCHEMY_API_KEY__",
610
- privateKey: "__L1_PRIVATE_KEY__",
611
- password: "__WALLET_PASSWORD__",
532
+ rpcUrl: "__RPC_URL__",
533
+ privateKeyFile: "__PRIVATE_KEY_FILE__",
612
534
  };
613
535
 
614
536
  document.getElementById("copy-command").addEventListener("click", async () => {
@@ -630,17 +552,6 @@
630
552
  if (parsed) {
631
553
  return parsed;
632
554
  }
633
- } catch {
634
- // Fallback to window.name below.
635
- }
636
-
637
- try {
638
- if (typeof window.name === "string" && window.name.startsWith(windowNameKey)) {
639
- const parsed = parseSerializedState(window.name.slice(windowNameKey.length));
640
- if (parsed) {
641
- return parsed;
642
- }
643
- }
644
555
  } catch {
645
556
  // Fall through to defaults.
646
557
  }
@@ -653,12 +564,7 @@
653
564
  try {
654
565
  window.localStorage.setItem(storageKey, serialized);
655
566
  } catch {
656
- // window.name fallback below still preserves state across refreshes in the current tab.
657
- }
658
- try {
659
- window.name = `${windowNameKey}${serialized}`;
660
- } catch {
661
- // Ignore window.name failures.
567
+ // Ignore storage failures; the generated command remains available in the preview.
662
568
  }
663
569
  }
664
570
 
@@ -729,19 +635,7 @@
729
635
 
730
636
  function walletOptionsForCommand(command = currentCommand()) {
731
637
  const options = currentWorkspaceOptions();
732
- if (!commandNeedsWallet(command)) {
733
- return options.walletOptions;
734
- }
735
- const derivedAddress = privateKeyAddressState.derivedAddress.trim().toLowerCase();
736
- if (!derivedAddress) {
737
- return [];
738
- }
739
- return options.walletOptions.filter((walletName) => {
740
- const walletEntry = options.walletEntriesByName?.[walletName];
741
- return walletEntry?.l1Address
742
- && derivedAddress
743
- && ethers.toBigInt(walletEntry.l1Address) === ethers.toBigInt(derivedAddress);
744
- });
638
+ return options.walletOptions;
745
639
  }
746
640
 
747
641
  function buildWorkspaceStatusMessage() {
@@ -928,175 +822,15 @@
928
822
  }, 250);
929
823
  }
930
824
 
931
- function addHexPrefix(value) {
932
- const normalized = String(value ?? "");
933
- return normalized.startsWith("0x") || normalized.startsWith("0X") ? normalized : `0x${normalized}`;
934
- }
935
-
936
- function hexToBytes(value) {
937
- const normalizedWithPrefix = addHexPrefix(value);
938
- const normalized = normalizedWithPrefix.slice(2);
939
- if (normalized.length === 0) {
940
- return new Uint8Array();
941
- }
942
- return Uint8Array.from(
943
- normalized.match(/.{1,2}/g).map((pair) => Number.parseInt(pair, 16)),
944
- );
945
- }
946
-
947
- async function deriveScryptKeyBytes(password, saltHex) {
948
- const browserEthers = window.ethers?.ethers ?? window.ethers;
949
- if (browserEthers?.scryptSync) {
950
- return browserEthers.getBytes(
951
- browserEthers.scryptSync(textEncoder.encode(String(password)), saltHex, 16384, 8, 1, 32),
952
- );
953
- }
954
- if (!scryptModulePromise) {
955
- scryptModulePromise = import("../../../node_modules/@noble/hashes/esm/scrypt.js");
956
- }
957
- const { scrypt } = await scryptModulePromise;
958
- return scrypt(textEncoder.encode(String(password)), hexToBytes(saltHex), {
959
- N: 16384,
960
- r: 8,
961
- p: 1,
962
- dkLen: 32,
963
- });
964
- }
965
-
966
- async function deriveAddressFromPrivateKey(privateKey) {
967
- const browserWalletClass = window.ethers?.Wallet ?? window.ethers?.ethers?.Wallet;
968
- if (browserWalletClass) {
969
- return new browserWalletClass(String(privateKey)).address;
970
- }
971
- if (!ethersModulePromise) {
972
- ethersModulePromise = import("../../../node_modules/ethers/lib.esm/index.js");
973
- }
974
- const { Wallet } = await ethersModulePromise;
975
- return new Wallet(String(privateKey)).address;
976
- }
977
-
978
- async function refreshPrivateKeyAddressState(privateKeyValue) {
979
- const normalizedValue = String(privateKeyValue ?? "").trim();
980
- privateKeyAddressState.inputValue = normalizedValue;
981
-
982
- if (normalizedValue.length === 0) {
983
- privateKeyAddressState.derivedAddress = "";
984
- privateKeyAddressState.statusMessage = "";
985
- return;
986
- }
987
-
988
- const currentGeneration = ++privateKeyLookupGeneration;
989
- try {
990
- const derivedAddress = await deriveAddressFromPrivateKey(normalizedValue);
991
- if (currentGeneration !== privateKeyLookupGeneration) {
992
- return;
993
- }
994
- privateKeyAddressState.derivedAddress = derivedAddress;
995
- privateKeyAddressState.statusMessage = `L1 address: ${derivedAddress}`;
996
- } catch {
997
- if (currentGeneration !== privateKeyLookupGeneration) {
998
- return;
999
- }
1000
- privateKeyAddressState.derivedAddress = "";
1001
- privateKeyAddressState.statusMessage = "Invalid private key.";
1002
- }
1003
- }
1004
-
1005
- async function decryptWalletJson(envelope, walletPassword) {
1006
- if (
1007
- envelope?.version !== walletEncryptionVersion
1008
- || envelope?.algorithm !== walletEncryptionAlgorithm
1009
- || envelope?.kdf !== "scrypt"
1010
- ) {
1011
- throw new Error("Unsupported wallet encryption envelope.");
1012
- }
1013
-
1014
- const encryptionKeyBytes = await deriveScryptKeyBytes(walletPassword, envelope.salt);
1015
- const cryptoKey = await crypto.subtle.importKey("raw", encryptionKeyBytes, "AES-GCM", false, ["decrypt"]);
1016
- const ciphertext = hexToBytes(envelope.ciphertext);
1017
- const tag = hexToBytes(envelope.tag);
1018
- const combinedCiphertext = new Uint8Array(ciphertext.length + tag.length);
1019
- combinedCiphertext.set(ciphertext, 0);
1020
- combinedCiphertext.set(tag, ciphertext.length);
1021
- const plaintextBuffer = await crypto.subtle.decrypt(
1022
- {
1023
- name: "AES-GCM",
1024
- iv: hexToBytes(envelope.iv),
1025
- tagLength: 128,
1026
- },
1027
- cryptoKey,
1028
- combinedCiphertext,
1029
- );
1030
- return JSON.parse(textDecoder.decode(new Uint8Array(plaintextBuffer)));
1031
- }
1032
-
1033
- function extractUnusedNoteOptions(walletJson) {
1034
- const notes = walletJson?.notes ?? {};
1035
- const unusedNotes = notes.unused ?? {};
1036
- const unusedOrder = Array.isArray(notes.unusedOrder) ? notes.unusedOrder : Object.keys(unusedNotes);
1037
- return unusedOrder
1038
- .map((commitment) => unusedNotes[commitment])
1039
- .filter((note) => note && typeof note.commitment === "string")
1040
- .map((note) => ({
1041
- value: note.commitment,
1042
- amountBaseUnits: String(note.value ?? "0"),
1043
- label: typeof note.value === "string"
1044
- ? `${note.commitment} (${note.value})`
1045
- : note.commitment,
1046
- }));
1047
- }
1048
-
1049
825
  async function refreshWalletNoteOptions() {
1050
826
  if (!commandNeedsWalletNotes()) {
1051
827
  return;
1052
828
  }
1053
- if (!workspaceDirectoryState.loaded) {
1054
- clearWalletNoteState("Select a workspace directory to load wallet note IDs.");
1055
- return;
1056
- }
1057
- if (!state.wallet) {
1058
- clearWalletNoteState("Choose a wallet to load note IDs.");
1059
- return;
1060
- }
1061
- if (!state.password) {
1062
- clearWalletNoteState("Enter the wallet password to load note IDs.");
1063
- return;
1064
- }
1065
-
1066
- const walletEntry = currentWalletEntry();
1067
- if (!walletEntry) {
1068
- clearWalletNoteState("Selected wallet is unavailable for the current network.");
1069
- return;
1070
- }
1071
-
1072
- const cacheKey = `${state.network}::${state.wallet}::${state.password}`;
1073
- if (walletNoteState.cacheKey === cacheKey && walletNoteState.hasLoaded) {
1074
- return;
1075
- }
1076
-
1077
- walletNoteState.cacheKey = cacheKey;
1078
- walletNoteState.statusMessage = "Loading wallet note IDs...";
1079
- walletNoteState.hasLoaded = false;
1080
-
1081
- try {
1082
- const walletFileHandle = await walletEntry.handle.getFileHandle("wallet.json");
1083
- const walletEnvelope = JSON.parse(await (await walletFileHandle.getFile()).text());
1084
- const walletJson = await decryptWalletJson(walletEnvelope, state.password);
1085
- walletNoteState.options = extractUnusedNoteOptions(walletJson);
1086
- walletNoteState.canonicalAssetDecimals = Number(walletJson?.canonicalAssetDecimals ?? 18);
1087
- walletNoteState.walletL2Address = typeof walletJson?.l2Address === "string" ? walletJson.l2Address : "";
1088
- walletNoteState.statusMessage = walletNoteState.options.length === 0
1089
- ? "This wallet has no unused note IDs."
1090
- : `Loaded ${walletNoteState.options.length} unused note ID${walletNoteState.options.length === 1 ? "" : "s"}.`;
1091
- walletNoteState.hasLoaded = true;
1092
- syncSelectedNoteIdsToAvailableOptions();
1093
- } catch {
1094
- clearWalletNoteState("Unable to decrypt the selected wallet. Check the wallet and password.");
1095
- }
829
+ clearWalletNoteState("Enter note IDs manually. The CLI uses the wallet-local default secret file and the assistant does not request wallet secrets.");
1096
830
  }
1097
831
 
1098
- function needsAlchemy(command) {
1099
- return command.fields.includes("alchemyApiKey") && state.network !== "anvil";
832
+ function acceptsRpcUrl(command) {
833
+ return command.fields.includes("rpcUrl");
1100
834
  }
1101
835
 
1102
836
  function currentCommand() {
@@ -1107,8 +841,9 @@
1107
841
  const transferOutputs = command.id === "transfer-notes"
1108
842
  ? currentTransferOutputs()
1109
843
  : null;
844
+ const optionalFields = new Set(command.optionalFields ?? []);
1110
845
  return command.fields.filter((fieldKey) => {
1111
- if (fieldKey === "alchemyApiKey" && !needsAlchemy(command)) {
846
+ if (fieldCatalog[fieldKey]?.optional || optionalFields.has(fieldKey)) {
1112
847
  return false;
1113
848
  }
1114
849
  if (fieldKey === "amounts" && command.id === "mint-notes") {
@@ -1132,7 +867,7 @@
1132
867
  : null;
1133
868
 
1134
869
  for (const fieldKey of command.fields) {
1135
- if (fieldKey === "alchemyApiKey" && !needsAlchemy(command)) {
870
+ if (fieldKey === "rpcUrl" && !String(state.rpcUrl ?? "").trim()) {
1136
871
  continue;
1137
872
  }
1138
873
 
@@ -1154,45 +889,11 @@
1154
889
  ? maskedSecretValues[fieldKey]
1155
890
  : value;
1156
891
 
1157
- switch (fieldKey) {
1158
- case "channelName":
1159
- appendOption(parts, "--channel-name", renderedValue);
1160
- break;
1161
- case "network":
1162
- appendOption(parts, "--network", renderedValue);
1163
- break;
1164
- case "alchemyApiKey":
1165
- appendOption(parts, "--alchemy-api-key", renderedValue);
1166
- break;
1167
- case "privateKey":
1168
- appendOption(parts, "--private-key", renderedValue);
1169
- break;
1170
- case "password":
1171
- appendOption(parts, "--password", renderedValue);
1172
- break;
1173
- case "wallet":
1174
- appendOption(parts, "--wallet", renderedValue);
1175
- break;
1176
- case "amount":
1177
- appendOption(parts, "--amount", renderedValue);
1178
- break;
1179
- case "amounts":
1180
- appendOption(parts, "--amounts", renderedValue);
1181
- break;
1182
- case "noteIds":
1183
- appendOption(parts, "--note-ids", renderedValue);
1184
- break;
1185
- case "recipients":
1186
- appendOption(parts, "--recipients", renderedValue);
1187
- break;
1188
- case "force":
1189
- parts.push("--force");
1190
- break;
1191
- case "docker":
1192
- parts.push("--docker");
1193
- break;
1194
- default:
1195
- 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);
1196
897
  }
1197
898
  }
1198
899
 
@@ -1375,11 +1076,9 @@
1375
1076
  placeholder.value = "";
1376
1077
  placeholder.textContent = !workspaceDirectoryState.loaded
1377
1078
  ? "Select a workspace directory for this network first"
1378
- : commandNeedsWallet(currentCommand()) && !privateKeyAddressState.derivedAddress
1379
- ? "Enter a valid L1 private key first"
1380
- : walletOptions.length === 0
1381
- ? "No wallet matches the current L1 private key"
1382
- : "Choose wallet";
1079
+ : walletOptions.length === 0
1080
+ ? "No wallet exists for this network"
1081
+ : "Choose wallet";
1383
1082
  select.appendChild(placeholder);
1384
1083
  for (const option of walletOptions) {
1385
1084
  const optionEl = document.createElement("option");
@@ -1484,7 +1183,7 @@
1484
1183
 
1485
1184
  const hint = document.createElement("p");
1486
1185
  hint.className = "hint";
1487
- 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.";
1488
1187
  wrapper.appendChild(hint);
1489
1188
 
1490
1189
  const totalHint = document.createElement("p");
@@ -1686,19 +1385,6 @@
1686
1385
  workspaceDirectoryState.statusMessage = buildWorkspaceStatusMessage();
1687
1386
  clearWalletNoteState();
1688
1387
  }
1689
- if (fieldKey === "privateKey") {
1690
- await refreshPrivateKeyAddressState(event.target.value);
1691
- if (memoryStatusEl) {
1692
- memoryStatusEl.textContent = privateKeyAddressState.statusMessage;
1693
- }
1694
- syncWorkspaceSelectionsForCurrentNetwork();
1695
- }
1696
- if (fieldKey === "password") {
1697
- clearWalletNoteState();
1698
- if (commandNeedsWalletNotes()) {
1699
- walletNoteState.statusMessage = "Loading wallet note IDs...";
1700
- }
1701
- }
1702
1388
  persistState();
1703
1389
  if (fieldKey === "network") {
1704
1390
  await refreshWalletNoteOptions();
@@ -1707,34 +1393,9 @@
1707
1393
  if (fieldKey === "network") {
1708
1394
  renderWorkspacePicker();
1709
1395
  renderCommandFields();
1710
- } else if (fieldKey === "privateKey" && commandNeedsWallet()) {
1711
- renderCommandFields();
1712
- } else if (fieldKey === "password" && commandNeedsWalletNotes()) {
1713
- rerenderCommandField("noteIds");
1714
- if (currentCommand().id === "transfer-notes") {
1715
- rerenderCommandField("recipients");
1716
- }
1717
- scheduleWalletNoteRefresh();
1718
1396
  }
1719
1397
  });
1720
1398
 
1721
- if (fieldKey === "password") {
1722
- input.addEventListener("blur", async () => {
1723
- if (walletNoteRefreshTimer) {
1724
- window.clearTimeout(walletNoteRefreshTimer);
1725
- walletNoteRefreshTimer = null;
1726
- }
1727
- await refreshWalletNoteOptions();
1728
- if (commandNeedsWalletNotes()) {
1729
- rerenderCommandField("noteIds");
1730
- if (currentCommand().id === "transfer-notes") {
1731
- rerenderCommandField("recipients");
1732
- }
1733
- }
1734
- renderPreview();
1735
- });
1736
- }
1737
-
1738
1399
  label.appendChild(input);
1739
1400
  if (config.type === "checkbox" && config.hint) {
1740
1401
  const hint = document.createElement("p");
@@ -1749,20 +1410,12 @@
1749
1410
  memoryFieldsEl.innerHTML = "";
1750
1411
  [
1751
1412
  "network",
1752
- "alchemyApiKey",
1753
- "privateKey",
1413
+ "rpcUrl",
1414
+ "account",
1754
1415
  ].forEach((fieldKey) => {
1755
1416
  memoryFieldsEl.appendChild(createField(fieldKey));
1756
1417
  });
1757
- memoryStatusEl.textContent = privateKeyAddressState.statusMessage;
1758
- void refreshPrivateKeyAddressState(state.privateKey).then(() => {
1759
- memoryStatusEl.textContent = privateKeyAddressState.statusMessage;
1760
- syncWorkspaceSelectionsForCurrentNetwork();
1761
- if (currentCommand().fields.includes("wallet")) {
1762
- renderCommandFields();
1763
- renderPreview();
1764
- }
1765
- });
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.";
1766
1419
  }
1767
1420
 
1768
1421
  function renderCommandSelect() {
@@ -1794,7 +1447,7 @@
1794
1447
  }
1795
1448
 
1796
1449
  for (const fieldKey of commandOnlyFields) {
1797
- if (fieldKey === "alchemyApiKey" && !needsAlchemy(command)) {
1450
+ if (fieldKey === "rpcUrl" && !acceptsRpcUrl(command)) {
1798
1451
  continue;
1799
1452
  }
1800
1453
  if (fieldKey === "amounts" && command.id === "transfer-notes") {
@@ -1822,9 +1475,7 @@
1822
1475
  : "",
1823
1476
  ].join("");
1824
1477
  warningEl.textContent = missing.length === 0
1825
- ? state.network === "anvil"
1826
- ? `Ready. --alchemy-api-key is omitted automatically on anvil.${workspaceWarnings}`
1827
- : `Ready.${workspaceWarnings}`
1478
+ ? `Ready. If --rpc-url is omitted, the CLI uses the saved network RPC_URL.${workspaceWarnings}`
1828
1479
  : `Missing inputs: ${missing.map((fieldKey) => fieldCatalog[fieldKey].label).join(", ")}.${workspaceWarnings}`;
1829
1480
  }
1830
1481