@tokamak-private-dapps/private-state-cli 2.4.3 → 3.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.
@@ -1,1534 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta
7
- name="description"
8
- content="Local assistant for constructing Tokamak private-state bridge CLI commands."
9
- />
10
- <meta property="og:title" content="Private-State CLI Assistant" />
11
- <meta
12
- property="og:description"
13
- content="Local assistant for constructing Tokamak private-state bridge CLI commands."
14
- />
15
- <meta property="og:type" content="website" />
16
- <meta name="twitter:card" content="summary" />
17
- <title>Private-State CLI Assistant</title>
18
- <script type="application/ld+json">
19
- {
20
- "@context": "https://schema.org",
21
- "@type": "SoftwareApplication",
22
- "name": "Private-State CLI Assistant",
23
- "applicationCategory": "DeveloperApplication",
24
- "operatingSystem": "Any",
25
- "description": "Local assistant for constructing Tokamak private-state bridge CLI commands."
26
- }
27
- </script>
28
- <style>
29
- body {
30
- margin: 0;
31
- padding: 12px;
32
- font-family: sans-serif;
33
- font-size: 14px;
34
- line-height: 1.3;
35
- background: #f5f5f2;
36
- color: #111;
37
- }
38
-
39
- main {
40
- max-width: 1480px;
41
- margin: 0 auto;
42
- }
43
-
44
- section {
45
- margin-bottom: 0;
46
- }
47
-
48
- form,
49
- .box {
50
- border: 1px solid #d7d7d0;
51
- background: #fff;
52
- border-radius: 8px;
53
- padding: 10px;
54
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
55
- }
56
-
57
- .fields {
58
- display: grid;
59
- gap: 10px;
60
- grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
61
- align-items: start;
62
- }
63
-
64
- #memory-fields {
65
- grid-template-columns: repeat(3, minmax(0, 1fr));
66
- align-items: start;
67
- }
68
-
69
- .fields > *,
70
- .field-stack {
71
- min-width: 0;
72
- align-self: start;
73
- }
74
-
75
- label {
76
- display: grid;
77
- gap: 6px;
78
- font-size: 13px;
79
- min-width: 0;
80
- font-weight: 600;
81
- }
82
-
83
- input,
84
- select,
85
- textarea,
86
- button {
87
- font: inherit;
88
- }
89
-
90
- input,
91
- select,
92
- textarea {
93
- box-sizing: border-box;
94
- min-width: 0;
95
- width: 100%;
96
- padding: 8px 10px;
97
- border: 1px solid #cfd1c7;
98
- border-radius: 8px;
99
- background: #fff;
100
- font-size: 13px;
101
- }
102
-
103
- .field-stack button {
104
- box-sizing: border-box;
105
- min-width: 0;
106
- width: 100%;
107
- }
108
-
109
- button {
110
- padding: 7px 11px;
111
- border: 1px solid #bfc2b6;
112
- border-radius: 8px;
113
- background: #fafaf7;
114
- cursor: pointer;
115
- font-size: 13px;
116
- }
117
-
118
- button:hover {
119
- background: #f1f2ec;
120
- }
121
-
122
- textarea {
123
- min-height: 96px;
124
- resize: vertical;
125
- }
126
-
127
- .actions {
128
- display: flex;
129
- flex-wrap: wrap;
130
- gap: 8px;
131
- margin-top: 6px;
132
- }
133
-
134
- .inline-actions {
135
- display: flex;
136
- flex-wrap: wrap;
137
- gap: 6px;
138
- }
139
-
140
- .inline-actions button {
141
- width: auto;
142
- }
143
-
144
- .recipient-input-row {
145
- display: grid;
146
- grid-template-columns: minmax(0, 1fr) auto;
147
- gap: 6px;
148
- align-items: center;
149
- }
150
-
151
- .recipient-input-row button {
152
- width: auto;
153
- white-space: nowrap;
154
- }
155
-
156
- .transfer-pair-grid {
157
- display: grid;
158
- gap: 12px;
159
- grid-template-columns: 1fr;
160
- align-items: start;
161
- }
162
-
163
- .transfer-pair {
164
- display: grid;
165
- gap: 8px;
166
- align-content: start;
167
- }
168
-
169
- .field-stack {
170
- display: grid;
171
- gap: 6px;
172
- align-content: start;
173
- }
174
-
175
- .field-span-full {
176
- grid-column: 1 / -1;
177
- }
178
-
179
- .note-option-list {
180
- display: grid;
181
- gap: 6px;
182
- padding: 8px;
183
- border: 1px solid #cfd1c7;
184
- border-radius: 8px;
185
- background: #fff;
186
- }
187
-
188
- .note-option-item {
189
- display: grid;
190
- grid-template-columns: auto minmax(0, 1fr);
191
- gap: 10px;
192
- align-items: start;
193
- }
194
-
195
- .note-option-item input[type="checkbox"] {
196
- margin-top: 3px;
197
- }
198
-
199
- .note-option-copy {
200
- min-width: 0;
201
- overflow-wrap: anywhere;
202
- line-height: 1.35;
203
- }
204
-
205
- .note-option-list.is-disabled {
206
- color: #666;
207
- background: #f6f6f3;
208
- }
209
-
210
- pre {
211
- margin: 0;
212
- white-space: pre-wrap;
213
- word-break: break-word;
214
- max-height: 240px;
215
- overflow: auto;
216
- font-size: 13px;
217
- line-height: 1.35;
218
- }
219
-
220
- .hint,
221
- .status {
222
- color: #444;
223
- font-size: 12px;
224
- overflow-wrap: anywhere;
225
- line-height: 1.3;
226
- }
227
-
228
- h1,
229
- h2 {
230
- margin: 0 0 8px;
231
- line-height: 1.1;
232
- }
233
-
234
- h1 {
235
- font-size: 26px;
236
- }
237
-
238
- h2 {
239
- font-size: 20px;
240
- }
241
-
242
- p {
243
- margin: 0 0 6px;
244
- }
245
-
246
- .page-grid {
247
- display: grid;
248
- grid-template-columns: minmax(0, 1.7fr) minmax(360px, 1fr);
249
- gap: 12px;
250
- align-items: start;
251
- }
252
-
253
- .page-column {
254
- display: grid;
255
- gap: 12px;
256
- align-content: start;
257
- }
258
-
259
- .compact-select {
260
- max-width: none;
261
- }
262
-
263
- #workspace-picker {
264
- margin-bottom: 10px;
265
- }
266
-
267
- #memory-status {
268
- margin-top: 6px;
269
- min-height: 1.3em;
270
- }
271
-
272
- #command-preview {
273
- min-height: 150px;
274
- padding: 8px 10px;
275
- border: 1px solid #e0e1d9;
276
- border-radius: 8px;
277
- background: #fbfbf8;
278
- }
279
-
280
- .intro {
281
- margin-bottom: 10px;
282
- }
283
-
284
- .footer-stack {
285
- display: grid;
286
- gap: 12px;
287
- margin-top: 12px;
288
- }
289
-
290
- .guide-list {
291
- margin: 0;
292
- padding-left: 20px;
293
- display: grid;
294
- gap: 8px;
295
- }
296
-
297
- .guide-list li {
298
- line-height: 1.4;
299
- }
300
-
301
- .guide-label {
302
- font-weight: 600;
303
- }
304
-
305
- .guide-example {
306
- margin-top: 6px;
307
- padding: 8px 10px;
308
- border: 1px solid #e0e1d9;
309
- border-radius: 8px;
310
- background: #fbfbf8;
311
- font-size: 13px;
312
- line-height: 1.35;
313
- overflow-wrap: anywhere;
314
- }
315
-
316
- @media (max-width: 900px) {
317
- .page-grid {
318
- grid-template-columns: 1fr;
319
- }
320
-
321
- #memory-fields {
322
- grid-template-columns: 1fr;
323
- }
324
- }
325
- </style>
326
- </head>
327
- <body>
328
- <main>
329
- <h1>Private-State CLI Assistant</h1>
330
- <p class="hint intro">
331
- This page stores your common inputs locally and assembles the current
332
- <code>private-state-bridge-cli.mjs</code> command. A plain browser page cannot execute your terminal directly.
333
- </p>
334
-
335
- <div class="page-grid">
336
- <div class="page-column">
337
- <section>
338
- <h2>Shared Inputs</h2>
339
- <form class="box">
340
- <div id="workspace-picker" class="field-stack"></div>
341
- <div id="memory-fields" class="fields"></div>
342
- <p id="memory-status" class="hint"></p>
343
- </form>
344
- </section>
345
-
346
- <section>
347
- <h2>Command Inputs</h2>
348
- <form class="box">
349
- <div id="command-fields" class="fields"></div>
350
- </form>
351
- </section>
352
- </div>
353
-
354
- <div class="page-column">
355
- <section>
356
- <h2>Command</h2>
357
- <div class="box">
358
- <div class="fields">
359
- <label class="compact-select">
360
- Command
361
- <select id="command-select"></select>
362
- </label>
363
- </div>
364
- <p id="command-description" class="hint"></p>
365
- </div>
366
- </section>
367
-
368
- <section>
369
- <h2>Command Output</h2>
370
- <div class="box">
371
- <p id="warning" class="hint"></p>
372
- <pre id="command-preview"></pre>
373
- <div class="actions">
374
- <button id="copy-command" type="button">Copy command</button>
375
- </div>
376
- <p id="status" class="status" aria-live="polite"></p>
377
- </div>
378
- </section>
379
- </div>
380
- </div>
381
-
382
- <div class="footer-stack">
383
- <section>
384
- <h2>Security Notes</h2>
385
- <div class="box">
386
- <ul class="guide-list">
387
- <li>
388
- Your wallet backup metadata is stored in your local workspace at
389
- <code id="workspace-path-label"></code>.
390
- </li>
391
- <li>
392
- A wallet backup does not include viewing keys, spending keys, key derivation material, or plaintext
393
- note owner, value, and salt fields.
394
- </li>
395
- <li>
396
- The viewing key decrypts encrypted note-delivery events. The spending key authorizes note use. Export
397
- and import those keys only when that authority should move to the target machine.
398
- </li>
399
- <li>
400
- If you lose the spending key and cannot recreate it from the original account, channel context, and
401
- wallet secret source, you lose the ability to spend, transfer, or redeem notes.
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>
408
- </ul>
409
- </div>
410
- </section>
411
-
412
- <section>
413
- <h2>Command Guide</h2>
414
- <div class="box">
415
- <ol class="guide-list">
416
- <li>
417
- <span class="guide-label">Configure RPC:</span> <code>set rpc</code>
418
- <div class="guide-example">
419
- <code>set rpc --network 'sepolia' --rpc-url '__RPC_URL__' --provider 'alchemy'</code>
420
- </div>
421
- </li>
422
- <li>
423
- <span class="guide-label">Join a channel:</span> <code>channel join</code>
424
- <div class="guide-example">
425
- <code>channel join --channel-name 'my-private-channel' --network 'sepolia' --account 'my-account' --wallet-secret-path '/path/to/wallet-secret'</code><br />
426
- Pays the join toll directly from the L1 wallet, not from bridge-deposited balance.
427
- </div>
428
- </li>
429
- <li>
430
- <span class="guide-label">Fund note balance:</span> <code>account deposit-bridge</code> -&gt; <code>wallet deposit-channel</code> -&gt; <code>wallet mint-notes</code>
431
- <div class="guide-example">
432
- <code>account deposit-bridge --amount '100' --network 'sepolia' --account 'my-account'</code><br />
433
- <code>wallet deposit-channel --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --amount '100'</code><br />
434
- <code>wallet mint-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --amounts '[&quot;50&quot;,&quot;50&quot;,&quot;0&quot;]'</code>
435
- </div>
436
- </li>
437
- <li>
438
- <span class="guide-label">Split, merge, or transfer note ownership:</span> <code>wallet transfer-notes</code>
439
- <div class="guide-example">
440
- <code>wallet 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>
441
- </div>
442
- </li>
443
- <li>
444
- <span class="guide-label">Redeem notes back to L1:</span> <code>wallet redeem-notes</code> -&gt; <code>wallet withdraw-channel</code> -&gt; <code>account withdraw-bridge</code>
445
- <div class="guide-example">
446
- <code>wallet redeem-notes --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --note-ids '[&quot;0xNoteIdA&quot;]'</code><br />
447
- <code>wallet withdraw-channel --wallet 'my-private-channel-0xYourL1Address' --network 'sepolia' --amount '25'</code><br />
448
- <code>account withdraw-bridge --amount '25' --network 'sepolia' --account 'my-account'</code>
449
- </div>
450
- </li>
451
- </ol>
452
- </div>
453
- </section>
454
- </div>
455
- </main>
456
-
457
- <script src="../../../node_modules/ethers/dist/ethers.umd.js"></script>
458
- <script type="module">
459
- import {
460
- PRIVATE_STATE_CLI_COMMANDS,
461
- PRIVATE_STATE_CLI_FIELD_CATALOG,
462
- } from "./lib/private-state-cli-command-registry.mjs";
463
-
464
- const storageKey = "private-state-cli-assistant:v2";
465
- const cliEntry = "node packages/apps/private-state/cli/private-state-bridge-cli.mjs";
466
- const defaultWorkspaceRootLabel = navigator.userAgent.includes("Windows")
467
- ? "%USERPROFILE%\\\\tokamak-private-channels\\\\workspace"
468
- : "~/tokamak-private-channels/workspace";
469
- const fieldCatalog = PRIVATE_STATE_CLI_FIELD_CATALOG;
470
- const commands = PRIVATE_STATE_CLI_COMMANDS.map((command) => ({
471
- ...command,
472
- fields: [...new Set([...command.fields, "json"])],
473
- }));
474
-
475
- const defaultState = {
476
- commandId: "channel-join",
477
- channelName: "",
478
- joinToll: "1",
479
- network: "sepolia",
480
- rpcUrl: "",
481
- account: "",
482
- privateKeyFile: "",
483
- walletSecretPath: "",
484
- wallet: "",
485
- amount: "",
486
- amounts: "",
487
- mintAmount1: "3",
488
- mintAmount2: "",
489
- noteIds: "[\"0x...\"]",
490
- recipients: "",
491
- transferRecipient1: "",
492
- transferAmount1: "",
493
- transferRecipient2: "",
494
- transferAmount2: "",
495
- acknowledgeActionImpact: false,
496
- docker: false,
497
- includeLocalArtifacts: false,
498
- groth16CliVersion: "",
499
- tokamakZkEvmCliVersion: "",
500
- fromGenesis: false,
501
- gpu: false,
502
- json: false,
503
- };
504
-
505
- const sharedFieldKeys = [
506
- "network",
507
- "rpcUrl",
508
- "account",
509
- ];
510
-
511
- const state = loadState();
512
- const workspaceDirectoryState = {
513
- handle: null,
514
- byNetwork: {},
515
- loaded: false,
516
- statusMessage: "",
517
- };
518
- const walletNoteState = {
519
- cacheKey: "",
520
- options: [],
521
- canonicalAssetDecimals: 18,
522
- walletL2Address: "",
523
- statusMessage: "",
524
- hasLoaded: false,
525
- };
526
- let walletNoteRefreshTimer = null;
527
-
528
- const workspacePickerEl = document.getElementById("workspace-picker");
529
- const memoryFieldsEl = document.getElementById("memory-fields");
530
- const memoryStatusEl = document.getElementById("memory-status");
531
- const commandSelectEl = document.getElementById("command-select");
532
- const commandDescriptionEl = document.getElementById("command-description");
533
- const commandFieldsEl = document.getElementById("command-fields");
534
- const warningEl = document.getElementById("warning");
535
- const previewEl = document.getElementById("command-preview");
536
- const statusEl = document.getElementById("status");
537
- const workspacePathLabelEl = document.getElementById("workspace-path-label");
538
- let workspaceStatusEl = null;
539
- const maskedSecretValues = {
540
- rpcUrl: "__RPC_URL__",
541
- privateKeyFile: "__PRIVATE_KEY_FILE__",
542
- };
543
-
544
- document.getElementById("copy-command").addEventListener("click", async () => {
545
- await copyText(buildCommand({ maskSecrets: false }));
546
- setStatus("Command copied.");
547
- });
548
-
549
- function loadState() {
550
- const parseSerializedState = (raw) => {
551
- if (!raw) {
552
- return null;
553
- }
554
- return { ...structuredClone(defaultState), ...JSON.parse(raw) };
555
- };
556
-
557
- try {
558
- const raw = window.localStorage.getItem(storageKey);
559
- const parsed = parseSerializedState(raw);
560
- if (parsed) {
561
- return parsed;
562
- }
563
- } catch {
564
- // Fall through to defaults.
565
- }
566
-
567
- return structuredClone(defaultState);
568
- }
569
-
570
- function persistState() {
571
- const serialized = JSON.stringify(state);
572
- try {
573
- window.localStorage.setItem(storageKey, serialized);
574
- } catch {
575
- // Ignore storage failures; the generated command remains available in the preview.
576
- }
577
- }
578
-
579
- function setStatus(message) {
580
- statusEl.textContent = message;
581
- }
582
-
583
- function setWorkspaceStatus(message) {
584
- workspaceDirectoryState.statusMessage = message;
585
- if (workspaceStatusEl) {
586
- workspaceStatusEl.textContent = message;
587
- }
588
- renderWorkspacePathLabel();
589
- }
590
-
591
- function showWorkspaceSelectionError(message) {
592
- workspaceDirectoryState.handle = null;
593
- workspaceDirectoryState.byNetwork = {};
594
- workspaceDirectoryState.loaded = false;
595
- setWorkspaceStatus(message);
596
- warningEl.textContent = message;
597
- }
598
-
599
- function shellEscape(value) {
600
- return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
601
- }
602
-
603
- function currentWorkspacePathLabel() {
604
- if (workspaceDirectoryState.loaded && workspaceDirectoryState.handle) {
605
- return `${defaultWorkspaceRootLabel} (selected directory: ${workspaceDirectoryState.handle.name})`;
606
- }
607
- return defaultWorkspaceRootLabel;
608
- }
609
-
610
- function renderWorkspacePathLabel() {
611
- workspacePathLabelEl.textContent = currentWorkspacePathLabel();
612
- }
613
-
614
- function appendOption(parts, optionName, value) {
615
- parts.push(optionName, shellEscape(value));
616
- }
617
-
618
- function commandNeedsWallet(command = currentCommand()) {
619
- return command.fields.includes("wallet");
620
- }
621
-
622
- function commandNeedsWalletNotes(command = currentCommand()) {
623
- return command.id === "wallet-transfer-notes" || command.id === "wallet-redeem-notes";
624
- }
625
-
626
- function commandNeedsExistingChannel(command = currentCommand()) {
627
- return command.fields.includes("channelName")
628
- && command.id !== "channel-create"
629
- && command.id !== "channel-recover-workspace";
630
- }
631
-
632
- function currentWorkspaceOptions() {
633
- return workspaceDirectoryState.byNetwork[state.network] ?? {
634
- channelOptions: [],
635
- walletOptions: [],
636
- walletEntriesByName: {},
637
- };
638
- }
639
-
640
- function currentWalletEntry() {
641
- return currentWorkspaceOptions().walletEntriesByName?.[state.wallet] ?? null;
642
- }
643
-
644
- function walletOptionsForCommand(command = currentCommand()) {
645
- const options = currentWorkspaceOptions();
646
- return options.walletOptions;
647
- }
648
-
649
- function buildWorkspaceStatusMessage() {
650
- if (!workspaceDirectoryState.loaded) {
651
- return `Default CLI workspace root: ${defaultWorkspaceRootLabel}`;
652
- }
653
-
654
- const loadedNetworkCount = Object.keys(workspaceDirectoryState.byNetwork).length;
655
- const currentOptions = currentWorkspaceOptions();
656
- if (currentOptions.channelOptions.length === 0) {
657
- return `Loaded ${loadedNetworkCount} network folder${loadedNetworkCount === 1 ? "" : "s"}, but none contains channel folders for ${state.network}.`;
658
- }
659
- return `Loaded ${loadedNetworkCount} network folder${loadedNetworkCount === 1 ? "" : "s"}, ${currentOptions.channelOptions.length} channel folder${currentOptions.channelOptions.length === 1 ? "" : "s"}, and ${currentOptions.walletOptions.length} wallet folder${currentOptions.walletOptions.length === 1 ? "" : "s"} for ${state.network}.`;
660
- }
661
-
662
- function syncWorkspaceSelectionsForCurrentNetwork() {
663
- const options = currentWorkspaceOptions();
664
- if (commandNeedsExistingChannel() && !options.channelOptions.includes(state.channelName)) {
665
- state.channelName = options.channelOptions[0] ?? "";
666
- }
667
- const walletOptions = walletOptionsForCommand();
668
- if (commandNeedsWallet() && !walletOptions.includes(state.wallet)) {
669
- state.wallet = walletOptions[0] ?? "";
670
- }
671
- }
672
-
673
- function clearWalletNoteState(message = "") {
674
- walletNoteState.cacheKey = "";
675
- walletNoteState.options = [];
676
- walletNoteState.canonicalAssetDecimals = 18;
677
- walletNoteState.walletL2Address = "";
678
- walletNoteState.statusMessage = message;
679
- walletNoteState.hasLoaded = false;
680
- if (commandNeedsWalletNotes()) {
681
- state.noteIds = "";
682
- }
683
- }
684
-
685
- function parseSelectedNoteIds() {
686
- if (!state.noteIds) {
687
- return [];
688
- }
689
- try {
690
- const parsed = JSON.parse(state.noteIds);
691
- return Array.isArray(parsed)
692
- ? parsed.filter((entry) => typeof entry === "string" && entry.length > 0)
693
- : [];
694
- } catch {
695
- return [];
696
- }
697
- }
698
-
699
- function currentMintAmounts() {
700
- return [state.mintAmount1, state.mintAmount2]
701
- .map((value) => String(value ?? "").trim())
702
- .filter((value) => value.length > 0);
703
- }
704
-
705
- function noteSelectionLimit() {
706
- return currentCommand().id === "wallet-redeem-notes" ? 1 : 2;
707
- }
708
-
709
- function mintAmountsTotalLabel() {
710
- const amounts = currentMintAmounts();
711
- if (amounts.length === 0) {
712
- return "Total amount: 0";
713
- }
714
- if (!amounts.every((value) => /^\d+$/.test(value))) {
715
- return "Total amount unavailable. Enter whole-number amounts only.";
716
- }
717
- const ethersLib = (window.ethers?.ethers ?? window.ethers);
718
- const total = amounts.reduce((sum, value) => sum + ethersLib.toBigInt(value), 0n);
719
- return `Total amount: ${total.toString()}`;
720
- }
721
-
722
- function syncSelectedNoteIdsToAvailableOptions() {
723
- const available = new Set(walletNoteState.options.map((option) => option.value));
724
- const selected = parseSelectedNoteIds().filter((noteId) => available.has(noteId));
725
- const normalized = selected.slice(0, noteSelectionLimit());
726
- state.noteIds = normalized.length === 0 ? "" : JSON.stringify(normalized);
727
- }
728
-
729
- function selectedNoteTotalLabel() {
730
- const selectedIds = parseSelectedNoteIds();
731
- if (selectedIds.length === 0) {
732
- return "Selected total: 0";
733
- }
734
- const optionMap = new Map(walletNoteState.options.map((option) => [option.value, option]));
735
- const ethersLib = (window.ethers?.ethers ?? window.ethers);
736
- const totalBaseUnits = selectedIds.reduce((sum, noteId) => {
737
- const option = optionMap.get(noteId);
738
- return sum + ethersLib.toBigInt(option?.amountBaseUnits ?? 0n);
739
- }, 0n);
740
- const totalTokens = ethersLib.formatUnits(
741
- totalBaseUnits,
742
- walletNoteState.canonicalAssetDecimals,
743
- );
744
- return `Selected total: ${totalTokens} (${totalBaseUnits.toString()} base units)`;
745
- }
746
-
747
- function selectedTransferInputNoteCount() {
748
- return parseSelectedNoteIds().length;
749
- }
750
-
751
- function transferPairLimit() {
752
- return selectedTransferInputNoteCount() === 1 ? 2 : 1;
753
- }
754
-
755
- function transferPairRows() {
756
- return [
757
- {
758
- recipient: String(state.transferRecipient1 ?? "").trim(),
759
- amount: String(state.transferAmount1 ?? "").trim(),
760
- },
761
- {
762
- recipient: String(state.transferRecipient2 ?? "").trim(),
763
- amount: String(state.transferAmount2 ?? "").trim(),
764
- },
765
- ];
766
- }
767
-
768
- function currentTransferOutputs() {
769
- const limit = transferPairLimit();
770
- const rows = transferPairRows().slice(0, limit);
771
- const recipients = [];
772
- const amounts = [];
773
- let hasPartialPair = false;
774
-
775
- for (const row of rows) {
776
- if (!row.recipient && !row.amount) {
777
- continue;
778
- }
779
- if (!row.recipient || !row.amount) {
780
- hasPartialPair = true;
781
- continue;
782
- }
783
- recipients.push(row.recipient);
784
- amounts.push(row.amount);
785
- }
786
-
787
- return {
788
- limit,
789
- recipients,
790
- amounts,
791
- hasPartialPair,
792
- };
793
- }
794
-
795
- function transferPairHintLabel() {
796
- const inputNoteCount = selectedTransferInputNoteCount();
797
- if (inputNoteCount === 0) {
798
- return "Select 1 input note to unlock up to 2 recipient/amount pairs. Select 2 input notes to use 1 pair.";
799
- }
800
- if (inputNoteCount === 1) {
801
- return "With 1 selected input note, you may enter up to 2 recipient/amount pairs.";
802
- }
803
- return "With 2 selected input notes, exactly 1 recipient/amount pair can be used.";
804
- }
805
-
806
- function rerenderCommandField(fieldKey) {
807
- const existing = commandFieldsEl.querySelector(`[data-field-key="${fieldKey}"]`);
808
- if (!existing) {
809
- return;
810
- }
811
- const replacement = createField(fieldKey);
812
- replacement.dataset.fieldKey = fieldKey;
813
- existing.replaceWith(replacement);
814
- }
815
-
816
- function scheduleWalletNoteRefresh() {
817
- if (walletNoteRefreshTimer) {
818
- window.clearTimeout(walletNoteRefreshTimer);
819
- }
820
- walletNoteRefreshTimer = window.setTimeout(async () => {
821
- walletNoteRefreshTimer = null;
822
- await refreshWalletNoteOptions();
823
- if (commandNeedsWalletNotes()) {
824
- rerenderCommandField("noteIds");
825
- if (currentCommand().id === "wallet-transfer-notes") {
826
- rerenderCommandField("recipients");
827
- }
828
- }
829
- renderPreview();
830
- }, 250);
831
- }
832
-
833
- async function refreshWalletNoteOptions() {
834
- if (!commandNeedsWalletNotes()) {
835
- return;
836
- }
837
- clearWalletNoteState("Enter note IDs manually. The CLI uses local wallet metadata plus protected viewing/spending key files when commands require them.");
838
- }
839
-
840
- function acceptsRpcUrl(command) {
841
- return command.fields.includes("rpcUrl");
842
- }
843
-
844
- function currentCommand() {
845
- return commands.find((command) => command.id === state.commandId) ?? commands[0];
846
- }
847
-
848
- function missingFields(command) {
849
- const transferOutputs = command.id === "wallet-transfer-notes"
850
- ? currentTransferOutputs()
851
- : null;
852
- const optionalFields = new Set(command.optionalFields ?? []);
853
- return command.fields.filter((fieldKey) => {
854
- const fieldConfig = fieldCatalog[fieldKey];
855
- if (fieldConfig?.optional || optionalFields.has(fieldKey)) {
856
- return false;
857
- }
858
- if (fieldConfig?.type === "checkbox") {
859
- return state[fieldKey] !== true;
860
- }
861
- if (fieldKey === "amounts" && command.id === "wallet-mint-notes") {
862
- return currentMintAmounts().length === 0;
863
- }
864
- if ((fieldKey === "recipients" || fieldKey === "amounts") && command.id === "wallet-transfer-notes") {
865
- if (fieldKey === "amounts") {
866
- return false;
867
- }
868
- return transferOutputs.recipients.length === 0 || transferOutputs.hasPartialPair;
869
- }
870
- return !String(state[fieldKey] ?? "").trim();
871
- });
872
- }
873
-
874
- function buildCommand({ maskSecrets }) {
875
- const command = currentCommand();
876
- const parts = [cliEntry, ...(command.display ?? command.id).split(" ")];
877
- const transferOutputs = command.id === "wallet-transfer-notes"
878
- ? currentTransferOutputs()
879
- : null;
880
-
881
- for (const fieldKey of command.fields) {
882
- if (fieldKey === "rpcUrl" && !String(state.rpcUrl ?? "").trim()) {
883
- continue;
884
- }
885
-
886
- const value = fieldKey === "amounts" && command.id === "wallet-mint-notes"
887
- ? JSON.stringify(currentMintAmounts())
888
- : fieldKey === "amounts" && command.id === "wallet-transfer-notes"
889
- ? (!transferOutputs.hasPartialPair && transferOutputs.amounts.length > 0
890
- ? JSON.stringify(transferOutputs.amounts)
891
- : "")
892
- : fieldKey === "recipients" && command.id === "wallet-transfer-notes"
893
- ? (!transferOutputs.hasPartialPair && transferOutputs.recipients.length > 0
894
- ? JSON.stringify(transferOutputs.recipients)
895
- : "")
896
- : state[fieldKey];
897
- if (!value) {
898
- continue;
899
- }
900
- const renderedValue = maskSecrets && Object.hasOwn(maskedSecretValues, fieldKey)
901
- ? maskedSecretValues[fieldKey]
902
- : value;
903
-
904
- const fieldConfig = fieldCatalog[fieldKey];
905
- if (fieldConfig?.type === "checkbox") {
906
- parts.push(fieldConfig.option);
907
- } else if (fieldConfig?.option) {
908
- appendOption(parts, fieldConfig.option, renderedValue);
909
- }
910
- }
911
-
912
- return parts.join(" ");
913
- }
914
-
915
- async function loadWorkspaceDirectoryOptions(handle) {
916
- const byNetwork = {};
917
-
918
- for await (const [networkFolderName, networkHandle] of handle.entries()) {
919
- if (networkHandle.kind !== "directory") {
920
- continue;
921
- }
922
-
923
- const channelEntries = [];
924
- for await (const [channelFolderName, channelHandle] of networkHandle.entries()) {
925
- if (channelHandle.kind !== "directory") {
926
- continue;
927
- }
928
-
929
- const channelDirHandle = await channelHandle.getDirectoryHandle("channel").catch(() => null);
930
- if (!channelDirHandle) {
931
- continue;
932
- }
933
-
934
- const workspaceFileHandle = await channelDirHandle.getFileHandle("workspace.json").catch(() => null);
935
- if (!workspaceFileHandle) {
936
- continue;
937
- }
938
-
939
- let workspaceJson;
940
- try {
941
- workspaceJson = JSON.parse(await (await workspaceFileHandle.getFile()).text());
942
- } catch {
943
- continue;
944
- }
945
-
946
- const resolvedNetworkName = typeof workspaceJson.network === "string" && workspaceJson.network.length > 0
947
- ? workspaceJson.network
948
- : networkFolderName;
949
- const channelName = typeof workspaceJson.channelName === "string" && workspaceJson.channelName.length > 0
950
- ? workspaceJson.channelName
951
- : channelFolderName;
952
-
953
- const walletsHandle = await channelHandle.getDirectoryHandle("wallets").catch(() => null);
954
- const walletEntries = [];
955
- if (walletsHandle) {
956
- for await (const [walletFolderName, walletHandle] of walletsHandle.entries()) {
957
- if (walletHandle.kind !== "directory") {
958
- continue;
959
- }
960
- const addressMatch = /-(0x[a-fA-F0-9]{40})$/.exec(walletFolderName);
961
- walletEntries.push({
962
- name: addressMatch ? `${channelName}-${addressMatch[1]}` : walletFolderName,
963
- handle: walletHandle,
964
- l1Address: addressMatch ? addressMatch[1] : "",
965
- });
966
- }
967
- }
968
-
969
- if (!byNetwork[resolvedNetworkName]) {
970
- byNetwork[resolvedNetworkName] = [];
971
- }
972
- byNetwork[resolvedNetworkName].push({
973
- channelName,
974
- walletEntries,
975
- });
976
- }
977
- }
978
-
979
- return Object.fromEntries(
980
- Object.entries(byNetwork).map(([networkName, channelEntries]) => {
981
- channelEntries.sort((left, right) => left.channelName.localeCompare(right.channelName));
982
- const walletEntriesByName = {};
983
- for (const entry of channelEntries) {
984
- for (const walletEntry of entry.walletEntries) {
985
- walletEntriesByName[walletEntry.name] = walletEntry;
986
- }
987
- }
988
- return [networkName, {
989
- channelOptions: channelEntries.map((entry) => entry.channelName),
990
- walletOptions: [...new Set(channelEntries.flatMap((entry) => entry.walletEntries.map((walletEntry) => walletEntry.name)))]
991
- .sort((left, right) => left.localeCompare(right)),
992
- walletEntriesByName,
993
- }];
994
- }),
995
- );
996
- }
997
-
998
- function applyWorkspaceDirectoryOptions(handle, byNetwork) {
999
- workspaceDirectoryState.handle = handle;
1000
- workspaceDirectoryState.byNetwork = byNetwork;
1001
- workspaceDirectoryState.loaded = true;
1002
- const currentOptions = currentWorkspaceOptions();
1003
- if (
1004
- !currentOptions.channelOptions.includes(state.channelName)
1005
- && currentCommand().id !== "channel-create"
1006
- && currentCommand().id !== "channel-recover-workspace"
1007
- ) {
1008
- state.channelName = currentOptions.channelOptions[0] ?? "";
1009
- }
1010
- const filteredWalletOptions = walletOptionsForCommand();
1011
- if (!filteredWalletOptions.includes(state.wallet)) {
1012
- state.wallet = filteredWalletOptions[0] ?? "";
1013
- }
1014
- clearWalletNoteState();
1015
- renderWorkspacePathLabel();
1016
- }
1017
-
1018
- async function rescanSelectedWorkspaceDirectory() {
1019
- if (!workspaceDirectoryState.handle) {
1020
- return false;
1021
- }
1022
-
1023
- try {
1024
- setWorkspaceStatus("Rescanning workspace directory...");
1025
- const byNetwork = await loadWorkspaceDirectoryOptions(workspaceDirectoryState.handle);
1026
- applyWorkspaceDirectoryOptions(workspaceDirectoryState.handle, byNetwork);
1027
- setWorkspaceStatus(buildWorkspaceStatusMessage());
1028
- return true;
1029
- } catch {
1030
- showWorkspaceSelectionError("Failed to read the selected workspace directory.");
1031
- return false;
1032
- }
1033
- }
1034
-
1035
- async function chooseWorkspaceDirectory() {
1036
- if (typeof window.showDirectoryPicker !== "function") {
1037
- showWorkspaceSelectionError("This browser does not support directory selection.");
1038
- return;
1039
- }
1040
-
1041
- try {
1042
- const handle = await window.showDirectoryPicker();
1043
- if (handle.name !== "workspace") {
1044
- showWorkspaceSelectionError('Selected directory must be named "workspace".');
1045
- return;
1046
- }
1047
- const byNetwork = await loadWorkspaceDirectoryOptions(handle);
1048
- applyWorkspaceDirectoryOptions(handle, byNetwork);
1049
- persistState();
1050
- renderWorkspacePicker();
1051
- await refreshWalletNoteOptions();
1052
- renderCommandFields();
1053
- renderPreview();
1054
- setWorkspaceStatus(buildWorkspaceStatusMessage());
1055
- } catch (error) {
1056
- if (error?.name === "AbortError") {
1057
- return;
1058
- }
1059
- showWorkspaceSelectionError("Failed to read the selected workspace directory.");
1060
- }
1061
- }
1062
-
1063
- function renderWorkspacePicker() {
1064
- workspacePickerEl.innerHTML = "";
1065
-
1066
- const button = document.createElement("button");
1067
- button.type = "button";
1068
- button.textContent = "Select workspace directory";
1069
- button.addEventListener("click", async () => {
1070
- await chooseWorkspaceDirectory();
1071
- });
1072
- workspacePickerEl.appendChild(button);
1073
-
1074
- workspaceStatusEl = document.createElement("p");
1075
- workspaceStatusEl.className = "hint";
1076
- workspaceStatusEl.textContent = workspaceDirectoryState.statusMessage || buildWorkspaceStatusMessage();
1077
- workspacePickerEl.appendChild(workspaceStatusEl);
1078
- }
1079
-
1080
- function createWalletField() {
1081
- const wrapper = document.createElement("div");
1082
- wrapper.className = "field-stack";
1083
- const label = document.createElement("label");
1084
- label.textContent = fieldCatalog.wallet.label;
1085
- const select = document.createElement("select");
1086
- const walletOptions = walletOptionsForCommand();
1087
- const placeholder = document.createElement("option");
1088
- placeholder.value = "";
1089
- placeholder.textContent = !workspaceDirectoryState.loaded
1090
- ? "Select a workspace directory for this network first"
1091
- : walletOptions.length === 0
1092
- ? "No wallet exists for this network"
1093
- : "Choose wallet";
1094
- select.appendChild(placeholder);
1095
- for (const option of walletOptions) {
1096
- const optionEl = document.createElement("option");
1097
- optionEl.value = option;
1098
- optionEl.textContent = option;
1099
- select.appendChild(optionEl);
1100
- }
1101
- select.value = walletOptions.includes(state.wallet) ? state.wallet : "";
1102
- select.disabled = walletOptions.length === 0;
1103
- select.addEventListener("change", async (event) => {
1104
- state.wallet = event.target.value;
1105
- clearWalletNoteState();
1106
- persistState();
1107
- await refreshWalletNoteOptions();
1108
- renderCommandFields();
1109
- renderPreview();
1110
- });
1111
- label.appendChild(select);
1112
- wrapper.appendChild(label);
1113
-
1114
- return wrapper;
1115
- }
1116
-
1117
- function createExistingChannelField() {
1118
- const label = document.createElement("label");
1119
- label.textContent = fieldCatalog.channelName.label;
1120
- const select = document.createElement("select");
1121
- const options = currentWorkspaceOptions();
1122
- const placeholder = document.createElement("option");
1123
- placeholder.value = "";
1124
- placeholder.textContent = options.channelOptions.length === 0
1125
- ? "Select a workspace directory for this network first"
1126
- : "Choose channel";
1127
- select.appendChild(placeholder);
1128
- for (const option of options.channelOptions) {
1129
- const optionEl = document.createElement("option");
1130
- optionEl.value = option;
1131
- optionEl.textContent = option;
1132
- select.appendChild(optionEl);
1133
- }
1134
- select.value = options.channelOptions.includes(state.channelName) ? state.channelName : "";
1135
- select.disabled = options.channelOptions.length === 0;
1136
- select.addEventListener("change", (event) => {
1137
- state.channelName = event.target.value;
1138
- persistState();
1139
- renderPreview();
1140
- });
1141
- label.appendChild(select);
1142
- return label;
1143
- }
1144
-
1145
- function createNoteIdsField() {
1146
- const wrapper = document.createElement("div");
1147
- wrapper.className = "field-stack field-span-full";
1148
- const label = document.createElement("label");
1149
- label.textContent = fieldCatalog.noteIds.label;
1150
- wrapper.appendChild(label);
1151
-
1152
- const selectedIds = new Set(parseSelectedNoteIds());
1153
- const optionList = document.createElement("div");
1154
- optionList.className = "note-option-list";
1155
-
1156
- if (walletNoteState.options.length === 0) {
1157
- optionList.classList.add("is-disabled");
1158
- optionList.textContent = "No selectable note IDs are currently available.";
1159
- } else {
1160
- for (const option of walletNoteState.options) {
1161
- const optionLabel = document.createElement("label");
1162
- optionLabel.className = "note-option-item";
1163
-
1164
- const checkbox = document.createElement("input");
1165
- checkbox.type = "checkbox";
1166
- checkbox.value = option.value;
1167
- checkbox.checked = selectedIds.has(option.value);
1168
-
1169
- const copy = document.createElement("span");
1170
- copy.className = "note-option-copy";
1171
- copy.textContent = option.label;
1172
-
1173
- checkbox.addEventListener("change", () => {
1174
- const currentSelection = parseSelectedNoteIds();
1175
- let nextSelection = currentSelection.filter((noteId) => noteId !== option.value);
1176
- if (checkbox.checked) {
1177
- nextSelection = [...nextSelection, option.value].slice(0, noteSelectionLimit());
1178
- }
1179
- state.noteIds = nextSelection.length === 0 ? "" : JSON.stringify(nextSelection);
1180
- persistState();
1181
- rerenderCommandField("noteIds");
1182
- if (currentCommand().id === "wallet-transfer-notes") {
1183
- rerenderCommandField("recipients");
1184
- }
1185
- renderPreview();
1186
- });
1187
-
1188
- optionLabel.appendChild(checkbox);
1189
- optionLabel.appendChild(copy);
1190
- optionList.appendChild(optionLabel);
1191
- }
1192
- }
1193
-
1194
- wrapper.appendChild(optionList);
1195
-
1196
- const hint = document.createElement("p");
1197
- hint.className = "hint";
1198
- hint.textContent = walletNoteState.statusMessage || "Enter note IDs manually. Wallet secrets are not requested by this assistant.";
1199
- wrapper.appendChild(hint);
1200
-
1201
- const totalHint = document.createElement("p");
1202
- totalHint.className = "hint";
1203
- totalHint.textContent = selectedNoteTotalLabel();
1204
- wrapper.appendChild(totalHint);
1205
-
1206
- return wrapper;
1207
- }
1208
-
1209
- function createTransferPairsField() {
1210
- const wrapper = document.createElement("div");
1211
- wrapper.className = "field-stack field-span-full";
1212
-
1213
- const title = document.createElement("label");
1214
- title.textContent = "Transfer Output Pairs";
1215
- wrapper.appendChild(title);
1216
-
1217
- const grid = document.createElement("div");
1218
- grid.className = "transfer-pair-grid";
1219
-
1220
- const pairLimit = transferPairLimit();
1221
- [
1222
- {
1223
- recipientKey: "transferRecipient1",
1224
- amountKey: "transferAmount1",
1225
- recipientLabel: "Recipient 1",
1226
- amountLabel: "Amount 1",
1227
- },
1228
- {
1229
- recipientKey: "transferRecipient2",
1230
- amountKey: "transferAmount2",
1231
- recipientLabel: "Recipient 2",
1232
- amountLabel: "Amount 2",
1233
- },
1234
- ].slice(0, pairLimit).forEach(({ recipientKey, amountKey, recipientLabel, amountLabel }) => {
1235
- const pairField = document.createElement("div");
1236
- pairField.className = "transfer-pair";
1237
-
1238
- const recipientField = document.createElement("div");
1239
- recipientField.className = "field-stack";
1240
-
1241
- const recipientFieldLabel = document.createElement("label");
1242
- recipientFieldLabel.textContent = recipientLabel;
1243
- const recipientRow = document.createElement("div");
1244
- recipientRow.className = "recipient-input-row";
1245
- const recipientInput = document.createElement("input");
1246
- recipientInput.type = "text";
1247
- recipientInput.placeholder = "0xRecipientL2Address";
1248
- recipientInput.value = state[recipientKey] ?? "";
1249
- recipientInput.addEventListener("input", (event) => {
1250
- state[recipientKey] = event.target.value;
1251
- persistState();
1252
- renderPreview();
1253
- });
1254
- recipientRow.appendChild(recipientInput);
1255
- const useSelfButton = document.createElement("button");
1256
- useSelfButton.type = "button";
1257
- useSelfButton.textContent = "Use my L2 address";
1258
- useSelfButton.disabled = !walletNoteState.walletL2Address;
1259
- useSelfButton.addEventListener("click", () => {
1260
- if (!walletNoteState.walletL2Address) {
1261
- return;
1262
- }
1263
- state[recipientKey] = walletNoteState.walletL2Address;
1264
- persistState();
1265
- rerenderCommandField("recipients");
1266
- renderPreview();
1267
- });
1268
- recipientRow.appendChild(useSelfButton);
1269
- recipientFieldLabel.appendChild(recipientRow);
1270
- recipientField.appendChild(recipientFieldLabel);
1271
- pairField.appendChild(recipientField);
1272
-
1273
- const amountField = document.createElement("div");
1274
- amountField.className = "field-stack";
1275
- const amountFieldLabel = document.createElement("label");
1276
- const amountInput = document.createElement("input");
1277
- amountInput.type = "text";
1278
- amountInput.placeholder = "3";
1279
- amountInput.value = state[amountKey] ?? "";
1280
- amountInput.addEventListener("input", (event) => {
1281
- state[amountKey] = event.target.value;
1282
- persistState();
1283
- renderPreview();
1284
- });
1285
- amountFieldLabel.textContent = amountLabel;
1286
- amountFieldLabel.appendChild(amountInput);
1287
- amountField.appendChild(amountFieldLabel);
1288
- pairField.appendChild(amountField);
1289
-
1290
- grid.appendChild(pairField);
1291
- });
1292
-
1293
- wrapper.appendChild(grid);
1294
-
1295
- const hint = document.createElement("p");
1296
- hint.className = "hint";
1297
- hint.textContent = transferPairHintLabel();
1298
- wrapper.appendChild(hint);
1299
-
1300
- return wrapper;
1301
- }
1302
-
1303
- function createMintAmountsField() {
1304
- const wrapper = document.createElement("div");
1305
- wrapper.className = "field-stack";
1306
-
1307
- const title = document.createElement("label");
1308
- title.textContent = "Mint Amount Entries";
1309
- wrapper.appendChild(title);
1310
-
1311
- const grid = document.createElement("div");
1312
- grid.className = "fields";
1313
-
1314
- [
1315
- { key: "mintAmount1", label: "Amount 1" },
1316
- { key: "mintAmount2", label: "Amount 2" },
1317
- ].forEach(({ key, label }) => {
1318
- const amountLabel = document.createElement("label");
1319
- amountLabel.textContent = label;
1320
- const input = document.createElement("input");
1321
- input.type = "text";
1322
- input.placeholder = "3";
1323
- input.value = state[key] ?? "";
1324
- input.addEventListener("input", (event) => {
1325
- state[key] = event.target.value;
1326
- persistState();
1327
- totalHint.textContent = mintAmountsTotalLabel();
1328
- renderPreview();
1329
- });
1330
- amountLabel.appendChild(input);
1331
- grid.appendChild(amountLabel);
1332
- });
1333
-
1334
- wrapper.appendChild(grid);
1335
-
1336
- const totalHint = document.createElement("p");
1337
- totalHint.className = "hint";
1338
- totalHint.textContent = mintAmountsTotalLabel();
1339
- wrapper.appendChild(totalHint);
1340
-
1341
- return wrapper;
1342
- }
1343
-
1344
- function createField(fieldKey) {
1345
- const config = fieldCatalog[fieldKey];
1346
- if (fieldKey === "wallet") {
1347
- return createWalletField();
1348
- }
1349
- if (fieldKey === "amounts" && currentCommand().id === "wallet-mint-notes") {
1350
- return createMintAmountsField();
1351
- }
1352
- if (fieldKey === "recipients" && currentCommand().id === "wallet-transfer-notes") {
1353
- return createTransferPairsField();
1354
- }
1355
- if (fieldKey === "noteIds" && commandNeedsWalletNotes()) {
1356
- return createNoteIdsField();
1357
- }
1358
- if (
1359
- fieldKey === "channelName"
1360
- && currentCommand().id !== "channel-create"
1361
- && currentCommand().id !== "channel-recover-workspace"
1362
- ) {
1363
- return createExistingChannelField();
1364
- }
1365
- const label = document.createElement("label");
1366
- label.textContent = config.label;
1367
-
1368
- let input;
1369
- if (config.type === "select") {
1370
- input = document.createElement("select");
1371
- for (const option of config.options) {
1372
- const optionEl = document.createElement("option");
1373
- optionEl.value = option;
1374
- optionEl.textContent = option;
1375
- input.appendChild(optionEl);
1376
- }
1377
- } else if (config.type === "checkbox") {
1378
- input = document.createElement("input");
1379
- input.type = "checkbox";
1380
- } else if (config.type === "textarea") {
1381
- input = document.createElement("textarea");
1382
- } else {
1383
- input = document.createElement("input");
1384
- input.type = config.type;
1385
- }
1386
-
1387
- if (config.type === "checkbox") {
1388
- input.checked = state[fieldKey] === true;
1389
- } else {
1390
- input.value = state[fieldKey] ?? "";
1391
- input.placeholder = config.placeholder;
1392
- }
1393
- input.addEventListener("input", async (event) => {
1394
- state[fieldKey] = config.type === "checkbox" ? event.target.checked : event.target.value;
1395
- if (fieldKey === "network") {
1396
- syncWorkspaceSelectionsForCurrentNetwork();
1397
- workspaceDirectoryState.statusMessage = buildWorkspaceStatusMessage();
1398
- clearWalletNoteState();
1399
- }
1400
- persistState();
1401
- if (fieldKey === "network") {
1402
- await refreshWalletNoteOptions();
1403
- }
1404
- renderPreview();
1405
- if (fieldKey === "network") {
1406
- renderWorkspacePicker();
1407
- renderCommandFields();
1408
- }
1409
- });
1410
-
1411
- label.appendChild(input);
1412
- if (config.type === "checkbox" && config.hint) {
1413
- const hint = document.createElement("p");
1414
- hint.className = "hint";
1415
- hint.textContent = config.hint;
1416
- label.appendChild(hint);
1417
- }
1418
- return label;
1419
- }
1420
-
1421
- function renderMemoryFields() {
1422
- memoryFieldsEl.innerHTML = "";
1423
- [
1424
- "network",
1425
- "rpcUrl",
1426
- "account",
1427
- ].forEach((fieldKey) => {
1428
- memoryFieldsEl.appendChild(createField(fieldKey));
1429
- });
1430
- memoryStatusEl.textContent = "Use set rpc once before bridge-facing commands, then use account import --private-key-file before signing commands.";
1431
- }
1432
-
1433
- function renderCommandSelect() {
1434
- commandSelectEl.innerHTML = "";
1435
- for (const command of commands) {
1436
- const option = document.createElement("option");
1437
- option.value = command.id;
1438
- option.textContent = command.display ?? command.id;
1439
- commandSelectEl.appendChild(option);
1440
- }
1441
- commandSelectEl.value = state.commandId;
1442
- commandSelectEl.addEventListener("change", async (event) => {
1443
- state.commandId = event.target.value;
1444
- await rescanSelectedWorkspaceDirectory();
1445
- persistState();
1446
- await render();
1447
- });
1448
- }
1449
-
1450
- function renderCommandFields() {
1451
- const command = currentCommand();
1452
- commandDescriptionEl.textContent = command.description;
1453
- commandFieldsEl.innerHTML = "";
1454
- const commandOnlyFields = command.fields.filter((fieldKey) => !sharedFieldKeys.includes(fieldKey));
1455
-
1456
- if (commandOnlyFields.length === 0) {
1457
- commandFieldsEl.textContent = "This command does not need extra command-specific inputs.";
1458
- return;
1459
- }
1460
-
1461
- for (const fieldKey of commandOnlyFields) {
1462
- if (fieldKey === "rpcUrl" && !acceptsRpcUrl(command)) {
1463
- continue;
1464
- }
1465
- if (fieldKey === "amounts" && command.id === "wallet-transfer-notes") {
1466
- continue;
1467
- }
1468
- const fieldElement = createField(fieldKey);
1469
- if (fieldKey === "noteIds") {
1470
- fieldElement.classList.add("field-span-full");
1471
- }
1472
- fieldElement.dataset.fieldKey = fieldKey;
1473
- commandFieldsEl.appendChild(fieldElement);
1474
- }
1475
- }
1476
-
1477
- function renderPreview() {
1478
- const command = currentCommand();
1479
- const missing = missingFields(command);
1480
- previewEl.textContent = buildCommand({ maskSecrets: true });
1481
- const workspaceWarnings = [
1482
- commandNeedsExistingChannel(command) && workspaceDirectoryState.handle === null
1483
- ? " Select a workspace directory to populate the channel dropdown."
1484
- : "",
1485
- commandNeedsWallet(command) && workspaceDirectoryState.handle === null
1486
- ? " Select a workspace directory to populate the wallet dropdown."
1487
- : "",
1488
- ].join("");
1489
- warningEl.textContent = missing.length === 0
1490
- ? `Ready. Bridge-facing commands use the saved network RPC configuration.${workspaceWarnings}`
1491
- : `Missing inputs: ${missing.map((fieldKey) => fieldCatalog[fieldKey].label).join(", ")}.${workspaceWarnings}`;
1492
- }
1493
-
1494
- async function copyText(value) {
1495
- if (navigator.clipboard?.writeText) {
1496
- await navigator.clipboard.writeText(value);
1497
- return;
1498
- }
1499
-
1500
- const textarea = document.createElement("textarea");
1501
- textarea.value = value;
1502
- textarea.setAttribute("readonly", "");
1503
- textarea.style.position = "fixed";
1504
- textarea.style.opacity = "0";
1505
- document.body.appendChild(textarea);
1506
- textarea.select();
1507
- document.execCommand("copy");
1508
- textarea.remove();
1509
- }
1510
-
1511
- async function render() {
1512
- syncWorkspaceSelectionsForCurrentNetwork();
1513
- workspaceDirectoryState.statusMessage = buildWorkspaceStatusMessage();
1514
- await refreshWalletNoteOptions();
1515
- renderWorkspacePathLabel();
1516
- renderWorkspacePicker();
1517
- renderMemoryFields();
1518
- renderCommandFields();
1519
- renderPreview();
1520
- }
1521
-
1522
- async function init() {
1523
- renderCommandSelect();
1524
- await render();
1525
- }
1526
-
1527
- init().catch((error) => {
1528
- console.error(error);
1529
- warningEl.textContent = "Assistant initialization failed. Reload the page and try again.";
1530
- setStatus("Assistant initialization failed.");
1531
- });
1532
- </script>
1533
- </body>
1534
- </html>