@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.
- package/CHANGELOG.md +101 -0
- package/README.md +138 -25
- package/assets/tx-fees.json +170 -0
- package/cli-assistant.html +59 -413
- package/lib/private-state-cli-command-registry.mjs +376 -0
- package/lib/private-state-cli-shared.mjs +7 -7
- package/lib/private-state-note-delivery.mjs +415 -0
- package/lib/private-state-runtime-management.mjs +1311 -0
- package/lib/private-state-tokamak-helpers.mjs +184 -0
- package/package.json +2 -1
- package/private-state-bridge-cli.mjs +2675 -2253
package/cli-assistant.html
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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' --
|
|
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> -> <code>deposit-channel</code> -> <code>mint-notes</code>
|
|
424
424
|
<div class="guide-example">
|
|
425
|
-
<code>deposit-bridge --amount '100' --network 'sepolia' --
|
|
426
|
-
<code>deposit-channel --wallet 'my-private-channel-0xYourL1Address' --
|
|
427
|
-
<code>mint-notes --wallet 'my-private-channel-0xYourL1Address' --
|
|
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 '["50","50","0"]'</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' --
|
|
433
|
+
<code>transfer-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --note-ids '["0xNoteIdA"]' --recipients '["0xRecipientL2AddressA","0xRecipientL2AddressB"]' --amounts '["25","25"]'</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> -> <code>withdraw-channel</code> -> <code>withdraw-bridge</code>
|
|
438
438
|
<div class="guide-example">
|
|
439
|
-
<code>redeem-notes --wallet 'my-private-channel-0xYourL1Address' --
|
|
440
|
-
<code>withdraw-channel --wallet 'my-private-channel-0xYourL1Address' --
|
|
441
|
-
<code>withdraw-bridge --amount '25' --network 'sepolia' --
|
|
439
|
+
<code>redeem-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --note-ids '["0xNoteIdA"]'</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
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
"
|
|
574
|
-
"
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1104
|
-
return command.fields.includes("
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
:
|
|
1384
|
-
? "
|
|
1385
|
-
:
|
|
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 ||
|
|
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
|
-
"
|
|
1758
|
-
"
|
|
1413
|
+
"rpcUrl",
|
|
1414
|
+
"account",
|
|
1759
1415
|
].forEach((fieldKey) => {
|
|
1760
1416
|
memoryFieldsEl.appendChild(createField(fieldKey));
|
|
1761
1417
|
});
|
|
1762
|
-
memoryStatusEl.textContent =
|
|
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 === "
|
|
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
|
-
?
|
|
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
|
|