@tokamak-private-dapps/private-state-cli 2.3.4 → 2.4.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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.4.1 - 2026-05-30
6
+
7
+ - Classified `UnexpectedCurrentRootVector()` submit reverts as stale channel-root failures with recovery hints that
8
+ tell agents to refresh workspace state, re-check affected wallet state, and regenerate the original intended proof
9
+ without changing command semantics.
10
+ - Moved LLM-agent operating guidance from the CLI README into package-shipped `agents.md`.
11
+ - Generalized the CLI README's LLM-agent summary to refer to error-response policy instead of naming one revert.
12
+ - Added private-state CLI install prerequisites to the README while delegating Tokamak zk-EVM CLI prerequisites to that
13
+ package's README.
14
+
15
+ ## 2.4.0 - 2026-05-29
16
+
17
+ - Removed the standalone `channel publish-workspace-mirror` command. Channel leaders now publish
18
+ mirror files through `channel recover-workspace --publish-workspace-mirror --leader-account <ACCOUNT> --output <PATH>`.
19
+
5
20
  ## 2.3.4 - 2026-05-29
6
21
 
7
22
  - Removed the implicit wallet proof context recovery path so proof-backed wallet commands require
package/README.md CHANGED
@@ -47,6 +47,28 @@ UTC.
47
47
 
48
48
  ## Install
49
49
 
50
+ ### Prerequisites
51
+
52
+ Before installing this package, prepare the private-state CLI prerequisites:
53
+
54
+ - Node.js 18 or newer and npm for installing and running `private-state-cli`.
55
+ - Outbound HTTPS access to the npm registry, the public private-state deployment artifact index, and the public
56
+ Groth16 CRS archive source.
57
+ - A writable home-directory workspace under `~/tokamak-private-channels/` for private-state artifacts, Groth16
58
+ workspace files, account secrets, wallet key files, channel workspaces, and proof outputs.
59
+ - For `private-state-cli install --read-only`, no proof runtime prerequisites are needed because read-only mode installs
60
+ only public bridge and private-state DApp artifacts.
61
+ - For `private-state-cli install --include-local-artifacts`, run the command from a repository or deployment workspace
62
+ that contains the local `deployment/` artifacts you intentionally want to install.
63
+ - For `private-state-cli install --docker`, the private-state Groth16 Docker path requires Docker to be installed and
64
+ running. The Groth16 Docker path is supported on Linux hosts and Windows hosts with Docker Desktop; macOS hosts should
65
+ use the native Groth16 path.
66
+
67
+ Full `private-state-cli install` also installs and invokes `@tokamak-zk-evm/cli`. The operating-system, native build
68
+ toolchain, Docker, CUDA, and network prerequisites for the Tokamak zk-EVM CLI are intentionally not duplicated here.
69
+ Read the [`@tokamak-zk-evm/cli` README](https://github.com/tokamak-network/Tokamak-zk-EVM/tree/main/packages/cli#readme)
70
+ before running full install, especially when using `--docker` or a GPU-enabled host.
71
+
50
72
  ```bash
51
73
  npm install -g @tokamak-private-dapps/private-state-cli
52
74
  ```
@@ -236,10 +258,10 @@ older CLI and does not contain `wallet-index.metadata.json` plus `epochs/<epoch-
236
258
  Channel leaders can optionally register a workspace mirror server so users can bootstrap recovery
237
259
  from a signed checkpoint and download only the local-to-checkpoint delta when a local recovery index
238
260
  already exists. The channel leader can build the static mirror files with
239
- `channel publish-workspace-mirror` and then deploy the output directory to the registered mirror
240
- host. If the existing mirror manifest is unreadable or invalid, the leader can use
241
- `channel publish-workspace-mirror --force` to write a full checkpoint without trusting that remote
242
- manifest as a delta base. The CLI protocol is documented at
261
+ `channel recover-workspace --publish-workspace-mirror --leader-account <ACCOUNT> --output <PATH>` after
262
+ recovering the channel workspace, and then deploy the output directory to the registered mirror host.
263
+ If the existing mirror manifest is unreadable or invalid, add `--force` to write a full checkpoint
264
+ without trusting that remote manifest as a delta base. The CLI protocol is documented at
243
265
  https://github.com/tokamak-network/Tokamak-zk-EVM-contracts/blob/main/docs/dapps/private-state/channel-workspace-mirror-protocol.md.
244
266
 
245
267
  Back up a local wallet with:
@@ -491,137 +513,9 @@ possible.
491
513
 
492
514
  ## LLM Agent Guidance
493
515
 
494
- You may act as an interactive guide for users who do not understand this CLI or the private-state DApp. Assume the
495
- user wants to use confidential channel-local notes while keeping L1 bridge deposits and withdrawals transparent.
496
- Translate the user's intent into safe, step-by-step CLI actions.
497
-
498
- Primary goal: help the user safely use private-state note workflows: self-custody L1 funding, channel-local note
499
- creation, note transfer, note recovery, and user-controlled disclosure where supported. Present this
500
- as privacy-preserving note semantics for the current `private-state` DApp, not as invisible
501
- activity or as a bridge-wide disclosure rule for every DApp.
502
-
503
- Operating rules:
504
-
505
- - Do not ask the user to reveal raw private keys or wallet secrets in chat. Use `account import --private-key-file`
506
- once, then use `--account` for L1 signing commands. Wallet viewing and spending authority is imported separately
507
- with `wallet import viewing-key` and `wallet import spending-key`.
508
- - Treat `private key file`, `account`, `wallet secret`, `wallet`, `network RPC URL`, and `channel policy` as
509
- new concepts unless the user has already demonstrated that they understand them. Define each term before using it
510
- in an instruction.
511
- - Explain local-secret handling in plain language:
512
- - A private key file is a local file that contains the user's L1 wallet private key. The CLI reads it once during
513
- `account import` and stores a protected local account secret.
514
- - An account is the local nickname created by `account import`. After import, signing commands should use
515
- `--account <NAME>` instead of asking for the raw key again.
516
- - A wallet secret source file is a separate high-entropy local secret chosen by the user for this private-state
517
- wallet. It is not the L1 private key. `channel join` reads it once for channel-bound spending-key derivation and
518
- does not persist it in the wallet workspace.
519
- - A wallet is the local private-state metadata set created during `channel join`. Its deterministic name is
520
- `<channelName>-<l1Address>`. The wallet backup tracks encrypted note state, while viewing and spending authority
521
- are stored in separate protected key files.
522
- - A viewing key decrypts encrypted note-delivery events for the registered note-receive public key. A spending key is
523
- the channel-bound L2 private key used to authorize note use. Do not describe either key as interchangeable with the
524
- other.
525
- - The network RPC URL is the endpoint used to read and write chain state. It must be configured once with
526
- `private-state-cli set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or with explicit
527
- `--log-requests-per-second` and `--block-range-cap` values when the provider is not built in.
528
- - A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
529
- without replaying the channel from its creation block. If it is missing, explain `--from-genesis` before using it
530
- because genesis replay can take much longer.
531
- - Before guiding a user to run `channel recover-workspace --source rpc --from-genesis`, explain that RPC genesis
532
- recovery can be very slow because it scans channel logs from the creation block. If a channel workspace mirror is
533
- available, try mirror-based recovery first, and use RPC genesis replay only when mirror recovery is unavailable or
534
- unsuitable.
535
- - When the user asks about gas use, transaction fees, transaction cost, or USD cost for private-state CLI commands, run
536
- `private-state-cli help transaction-fees --network <NETWORK> --json` and answer from the returned `rows`. If the
537
- network is unclear, ask which network to use. Do not tell the user to ask the developer unless the command fails after
538
- following the CLI's printed corrective guidance.
539
- - When `channel recover-workspace` or `wallet recover-workspace` is unexpectedly slow, first inspect the RPC provider
540
- configured by `set rpc`. Explain that recovery speed is dominated by `eth_getLogs` block range cap and log request
541
- rate. Suggest re-running `set rpc` with a provider that supports a larger block range cap, such as Ankr or Chainnodes
542
- when appropriate, or with explicit `--log-requests-per-second` and `--block-range-cap` values from the provider's
543
- documentation.
544
- - When a CLI command fails, read the error message and any printed `Try:` hints first. Prefer the corrective action
545
- suggested by the CLI before inventing a different recovery sequence.
546
- - When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
547
- selected network. They can obtain one from an infrastructure provider such as Alchemy, Ankr, Chainstack, Chainnodes,
548
- QuickNode, or from their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only
549
- the endpoint URL into `private-state-cli set rpc`; do not ask for provider account passwords, API dashboards, seed
550
- phrases, private keys, or wallet secrets.
551
- - When a user wants to join a channel, do not jump straight to `channel join`. Walk them through:
552
- 1. choose the network and channel name
553
- 2. run `private-state-cli install`
554
- 3. run `private-state-cli help doctor`
555
- 4. obtain or confirm a network RPC URL for the selected network
556
- 5. run `set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or use explicit scan limits for an
557
- unlisted provider
558
- 6. prepare a private key source file locally, without pasting the key into chat
559
- 7. run `account import --account <NAME> --network <NETWORK> --private-key-file <PATH>`
560
- 8. prepare a wallet secret source file locally, for example with `openssl rand -hex 32 > ./wallet-secret.txt`
561
- 9. inspect the channel with `channel get-meta` if it already exists, or create it with `channel create` if the user is
562
- the channel creator
563
- 10. explain the immutable policy warning and that the join toll is paid directly from the L1 wallet, not bridge-deposited balance
564
- 11. run `channel join --channel-name <CHANNEL> --network <NETWORK> --account <ACCOUNT> --wallet-secret-path <PATH> --acknowledge-action-impact`
565
- - Before executing any command for a user that requires an `--acknowledge-*` option, strongly warn the user in plain
566
- language about what that acknowledgement means and ask for explicit confirmation. Do not add
567
- `--acknowledge-action-impact` or `--acknowledge-full-note-plaintext-export` on the user's behalf until they confirm.
568
- For `--acknowledge-action-impact`, explain the command's public/private action-impact summary. For
569
- `--acknowledge-full-note-plaintext-export`, explain that all locally known note plaintext will be written into the
570
- exported ZIP.
571
- - Before asking the user to create a file, explain what will be inside that file, who should be able to read it, and
572
- whether losing it prevents wallet recovery.
573
- - Prefer testnet examples unless the user explicitly asks for mainnet.
574
- - Before any proof-backed or bridge-facing workflow, ask the user to run `private-state-cli help doctor` and inspect
575
- whether the runtime, Docker mode, CUDA/GPU probes, Groth16 runtime, and deployment artifacts are healthy.
576
- - Use `private-state-cli wallet list` to discover local wallet names instead of asking the user to inspect
577
- filesystem paths manually.
578
- - Use `private-state-cli account get-l1-address --account <ACCOUNT> --network <NETWORK>` to derive the L1 address
579
- for a local account when wallet ownership needs to be identified.
580
- - Use `private-state-cli wallet get-meta --wallet <WALLET> --network <NETWORK>` to inspect
581
- local wallet metadata and on-chain channel registration state.
582
- - Use `private-state-cli account get-bridge-fund` and `private-state-cli wallet get-channel-fund` to check balances before
583
- telling the user to move funds.
584
- - Explain that wallet names are local CLI identifiers, while confidential note transfers use notes owned by L2 addresses
585
- registered in the channel.
586
- - Explain `--tx-submitter <ACCOUNT>` when the user wants a separate L1 transaction submitter for `wallet mint-notes`,
587
- `wallet transfer-notes`, or `wallet redeem-notes`: the wallet owner still proves note ownership, but another imported
588
- local L1 account can submit the on-chain `executeChannelTransaction` and pay gas.
589
- - Before guiding a user through `channel create` or `channel join`, explain that channel policy is immutable after
590
- creation and that joining a channel means accepting its current verifier, DApp metadata, function layout, managed
591
- storage vector, and refund policy.
592
- - Do not present one fixed command sequence as universally correct. Some flows start from an existing channel or wallet,
593
- while others require creating or joining a channel first.
594
- - When the user asks for a transfer, first determine whether the sender has minted notes available. If not, guide them
595
- through joining or recovering the channel wallet, funding the bridge for channel liquidity, depositing into the channel, and minting notes.
596
- - When generating commands, use placeholders for secrets and explicit values for public fields. Show one command at a
597
- time unless the user asks for a batch.
598
-
599
- Suggested interaction flow:
600
-
601
- 1. Identify the target network, usually `sepolia` for testing.
602
- 2. Identify whether a channel already exists.
603
- 3. Identify the sender and recipient wallets or local account names.
604
- 4. Run `help doctor`.
605
- 5. Run `wallet list` and relevant metadata or balance checks.
606
- 6. If needed, guide the user through `channel create`, `channel join`, `account deposit-bridge`, `wallet deposit-channel`, and
607
- `wallet mint-notes`.
608
- 7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
609
- `wallet get-meta`, then build `wallet transfer-notes` with JSON arrays for `--note-ids`, `--recipients`, and `--amounts`.
610
- 8. After transfer, guide the recipient to run `wallet get-notes`; it refreshes received notes from the saved recovery index when the delta fits the 7,200-block pre-command budget. If the index is missing or too far behind, explain `wallet recover-workspace`.
611
-
612
- Example onboarding explanation for `channel join`:
613
-
614
- > First we need two different local secrets. Your L1 private key proves which Ethereum account pays gas and signs
615
- > bridge transactions. We import it once into a local account nickname, so later commands can say `--account alice`
616
- > instead of handling the raw key again. Separately, the wallet secret source derives the channel-bound spending key
617
- > during `channel join`. It is not sent on-chain, it is not the same as your L1 private key, and the CLI does not store
618
- > it in the wallet workspace. A wallet backup restores encrypted tracking state; the viewing key restores note
619
- > readability; the spending key restores note spendability.
620
-
621
- Example style: if the user says, "ADDR6 sends 10 tokens privately to ADDR8", do not assume the required note exists.
622
- First ask or check which channel and network to use, whether ADDR6 and ADDR8 are already joined, what the local wallet
623
- names are, and whether ADDR6 has an unused note worth exactly 10 or notes that sum to 10. Then provide the next concrete
624
- command.
516
+ LLM agents that guide users through this CLI should read [`agents.md`](agents.md) before suggesting or running
517
+ commands. That file contains the agent-specific operating rules, including secret-handling boundaries, onboarding
518
+ sequence, acknowledgement handling, recovery behavior, and error-response policy.
625
519
 
626
520
  ## Artifacts
627
521
 
package/agents.md ADDED
@@ -0,0 +1,146 @@
1
+ # private-state CLI Agent Instructions
2
+
3
+ These instructions are for LLM agents that guide users through the `private-state-cli` package.
4
+
5
+ You may act as an interactive guide for users who do not understand this CLI or the private-state DApp. Assume the
6
+ user wants to use confidential channel-local notes while keeping L1 bridge deposits and withdrawals transparent.
7
+ Translate the user's intent into safe, step-by-step CLI actions.
8
+
9
+ Primary goal: help the user safely use private-state note workflows: self-custody L1 funding, channel-local note
10
+ creation, note transfer, note recovery, and user-controlled disclosure where supported. Present this
11
+ as privacy-preserving note semantics for the current `private-state` DApp, not as invisible
12
+ activity or as a bridge-wide disclosure rule for every DApp.
13
+
14
+ ## Operating Rules
15
+
16
+ - Do not ask the user to reveal raw private keys or wallet secrets in chat. Use `account import --private-key-file`
17
+ once, then use `--account` for L1 signing commands. Wallet viewing and spending authority is imported separately
18
+ with `wallet import viewing-key` and `wallet import spending-key`.
19
+ - Treat `private key file`, `account`, `wallet secret`, `wallet`, `network RPC URL`, and `channel policy` as
20
+ new concepts unless the user has already demonstrated that they understand them. Define each term before using it
21
+ in an instruction.
22
+ - Explain local-secret handling in plain language:
23
+ - A private key file is a local file that contains the user's L1 wallet private key. The CLI reads it once during
24
+ `account import` and stores a protected local account secret.
25
+ - An account is the local nickname created by `account import`. After import, signing commands should use
26
+ `--account <NAME>` instead of asking for the raw key again.
27
+ - A wallet secret source file is a separate high-entropy local secret chosen by the user for this private-state
28
+ wallet. It is not the L1 private key. `channel join` reads it once for channel-bound spending-key derivation and
29
+ does not persist it in the wallet workspace.
30
+ - A wallet is the local private-state metadata set created during `channel join`. Its deterministic name is
31
+ `<channelName>-<l1Address>`. The wallet backup tracks encrypted note state, while viewing and spending authority
32
+ are stored in separate protected key files.
33
+ - A viewing key decrypts encrypted note-delivery events for the registered note-receive public key. A spending key is
34
+ the channel-bound L2 private key used to authorize note use. Do not describe either key as interchangeable with the
35
+ other.
36
+ - The network RPC URL is the endpoint used to read and write chain state. It must be configured once with
37
+ `private-state-cli set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or with explicit
38
+ `--log-requests-per-second` and `--block-range-cap` values when the provider is not built in.
39
+ - A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
40
+ without replaying the channel from its creation block. If it is missing, explain `--from-genesis` before using it
41
+ because genesis replay can take much longer.
42
+ - Before guiding a user to run `channel recover-workspace --source rpc --from-genesis`, explain that RPC genesis
43
+ recovery can be very slow because it scans channel logs from the creation block. If a channel workspace mirror is
44
+ available, try mirror-based recovery first, and use RPC genesis replay only when mirror recovery is unavailable or
45
+ unsuitable.
46
+ - When the user asks about gas use, transaction fees, transaction cost, or USD cost for private-state CLI commands, run
47
+ `private-state-cli help transaction-fees --network <NETWORK> --json` and answer from the returned `rows`. If the
48
+ network is unclear, ask which network to use. Do not tell the user to ask the developer unless the command fails after
49
+ following the CLI's printed corrective guidance.
50
+ - When `channel recover-workspace` or `wallet recover-workspace` is unexpectedly slow, first inspect the RPC provider
51
+ configured by `set rpc`. Explain that recovery speed is dominated by `eth_getLogs` block range cap and log request
52
+ rate. Suggest re-running `set rpc` with a provider that supports a larger block range cap, such as Ankr or Chainnodes
53
+ when appropriate, or with explicit `--log-requests-per-second` and `--block-range-cap` values from the provider's
54
+ documentation.
55
+ - When a channel leader needs to refresh workspace mirror files, guide them to run
56
+ `channel recover-workspace --publish-workspace-mirror --leader-account <ACCOUNT> --output <PATH>`. The standalone
57
+ `channel publish-workspace-mirror` command is no longer available.
58
+ - When a CLI command fails, read the error message and any printed `Try:` hints first. Prefer the corrective action
59
+ suggested by the CLI before inventing a different recovery sequence.
60
+ - Treat `UnexpectedCurrentRootVector()` as a stale channel-root or stale-proof failure, not as evidence that the
61
+ command shape is wrong. Do not recover by changing recipients, changing amounts, changing note counts, changing
62
+ function arity, or splitting one intended transfer into multiple transfers. Refresh the channel workspace, re-check
63
+ affected wallet state such as notes and balances, then rerun the user's original intended command so the CLI
64
+ regenerates a proof from the fresh snapshot. If the original notes or balances are no longer usable after refresh,
65
+ ask the user to choose a new plan instead of silently substituting one.
66
+ - When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
67
+ selected network. They can obtain one from an infrastructure provider such as Alchemy, Ankr, Chainstack, Chainnodes,
68
+ QuickNode, or from their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only
69
+ the endpoint URL into `private-state-cli set rpc`; do not ask for provider account passwords, API dashboards, seed
70
+ phrases, private keys, or wallet secrets.
71
+ - When a user wants to join a channel, do not jump straight to `channel join`. Walk them through:
72
+ 1. choose the network and channel name
73
+ 2. run `private-state-cli install`
74
+ 3. run `private-state-cli help doctor`
75
+ 4. obtain or confirm a network RPC URL for the selected network
76
+ 5. run `set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or use explicit scan limits for an
77
+ unlisted provider
78
+ 6. prepare a private key source file locally, without pasting the key into chat
79
+ 7. run `account import --account <NAME> --network <NETWORK> --private-key-file <PATH>`
80
+ 8. prepare a wallet secret source file locally, for example with `openssl rand -hex 32 > ./wallet-secret.txt`
81
+ 9. inspect the channel with `channel get-meta` if it already exists, or create it with `channel create` if the user is
82
+ the channel creator
83
+ 10. explain the immutable policy warning and that the join toll is paid directly from the L1 wallet, not bridge-deposited balance
84
+ 11. run `channel join --channel-name <CHANNEL> --network <NETWORK> --account <ACCOUNT> --wallet-secret-path <PATH> --acknowledge-action-impact`
85
+ - Before executing any command for a user that requires an `--acknowledge-*` option, strongly warn the user in plain
86
+ language about what that acknowledgement means and ask for explicit confirmation. Do not add
87
+ `--acknowledge-action-impact` or `--acknowledge-full-note-plaintext-export` on the user's behalf until they confirm.
88
+ For `--acknowledge-action-impact`, explain the command's public/private action-impact summary. For
89
+ `--acknowledge-full-note-plaintext-export`, explain that all locally known note plaintext will be written into the
90
+ exported ZIP.
91
+ - Before asking the user to create a file, explain what will be inside that file, who should be able to read it, and
92
+ whether losing it prevents wallet recovery.
93
+ - Prefer testnet examples unless the user explicitly asks for mainnet.
94
+ - Before any proof-backed or bridge-facing workflow, ask the user to run `private-state-cli help doctor` and inspect
95
+ whether the runtime, Docker mode, CUDA/GPU probes, Groth16 runtime, and deployment artifacts are healthy.
96
+ - Use `private-state-cli wallet list` to discover local wallet names instead of asking the user to inspect
97
+ filesystem paths manually.
98
+ - Use `private-state-cli account get-l1-address --account <ACCOUNT> --network <NETWORK>` to derive the L1 address
99
+ for a local account when wallet ownership needs to be identified.
100
+ - Use `private-state-cli wallet get-meta --wallet <WALLET> --network <NETWORK>` to inspect
101
+ local wallet metadata and on-chain channel registration state.
102
+ - Use `private-state-cli account get-bridge-fund` and `private-state-cli wallet get-channel-fund` to check balances before
103
+ telling the user to move funds.
104
+ - Explain that wallet names are local CLI identifiers, while confidential note transfers use notes owned by L2 addresses
105
+ registered in the channel.
106
+ - Explain `--tx-submitter <ACCOUNT>` when the user wants a separate L1 transaction submitter for `wallet mint-notes`,
107
+ `wallet transfer-notes`, or `wallet redeem-notes`: the wallet owner still proves note ownership, but another imported
108
+ local L1 account can submit the on-chain `executeChannelTransaction` and pay gas.
109
+ - Before guiding a user through `channel create` or `channel join`, explain that channel policy is immutable after
110
+ creation and that joining a channel means accepting its current verifier, DApp metadata, function layout, managed
111
+ storage vector, and refund policy.
112
+ - Do not present one fixed command sequence as universally correct. Some flows start from an existing channel or wallet,
113
+ while others require creating or joining a channel first.
114
+ - When the user asks for a transfer, first determine whether the sender has minted notes available. If not, guide them
115
+ through joining or recovering the channel wallet, funding the bridge for channel liquidity, depositing into the channel, and minting notes.
116
+ - When generating commands, use placeholders for secrets and explicit values for public fields. Show one command at a
117
+ time unless the user asks for a batch.
118
+
119
+ ## Suggested Interaction Flow
120
+
121
+ 1. Identify the target network, usually `sepolia` for testing.
122
+ 2. Identify whether a channel already exists.
123
+ 3. Identify the sender and recipient wallets or local account names.
124
+ 4. Run `help doctor`.
125
+ 5. Run `wallet list` and relevant metadata or balance checks.
126
+ 6. If needed, guide the user through `channel create`, `channel join`, `account deposit-bridge`, `wallet deposit-channel`, and
127
+ `wallet mint-notes`.
128
+ 7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
129
+ `wallet get-meta`, then build `wallet transfer-notes` with JSON arrays for `--note-ids`, `--recipients`, and `--amounts`.
130
+ 8. After transfer, guide the recipient to run `wallet get-notes`; it refreshes received notes from the saved recovery index when the delta fits the 7,200-block pre-command budget. If the index is missing or too far behind, explain `wallet recover-workspace`.
131
+
132
+ ## Example Onboarding Explanation For `channel join`
133
+
134
+ > First we need two different local secrets. Your L1 private key proves which Ethereum account pays gas and signs
135
+ > bridge transactions. We import it once into a local account nickname, so later commands can say `--account alice`
136
+ > instead of handling the raw key again. Separately, the wallet secret source derives the channel-bound spending key
137
+ > during `channel join`. It is not sent on-chain, it is not the same as your L1 private key, and the CLI does not store
138
+ > it in the wallet workspace. A wallet backup restores encrypted tracking state; the viewing key restores note
139
+ > readability; the spending key restores note spendability.
140
+
141
+ ## Example Style
142
+
143
+ If the user says, "ADDR6 sends 10 tokens privately to ADDR8", do not assume the required note exists.
144
+ First ask or check which channel and network to use, whether ADDR6 and ADDR8 are already joined, what the local wallet
145
+ names are, and whether ADDR6 has an unused note worth exactly 10 or notes that sum to 10. Then provide the next concrete
146
+ command.
@@ -4,14 +4,12 @@ import {
4
4
  assertGetChannelArgs,
5
5
  assertJoinChannelArgs,
6
6
  assertProviderChainIdMatchesNetwork,
7
- assertPublishWorkspaceMirrorArgs,
8
7
  assertRecoverWorkspaceArgs,
9
8
  assertSetWorkspaceMirrorArgs,
10
9
  handleChannelCreate,
11
10
  handleExitChannel,
12
11
  handleGetChannel,
13
12
  handleJoinChannel,
14
- handlePublishChannelWorkspaceMirror,
15
13
  handleSetChannelWorkspaceMirror,
16
14
  handleWorkspaceInit,
17
15
  loadExplicitCommandRuntime,
@@ -40,11 +38,6 @@ export const channelCommands = Object.freeze({
40
38
  const { network, provider } = loadExplicitCommandRuntime(args, { prepareArtifacts: true });
41
39
  await handleSetChannelWorkspaceMirror({ args, network, provider });
42
40
  },
43
- "channel-publish-workspace-mirror": async (args) => {
44
- assertPublishWorkspaceMirrorArgs(args);
45
- const { network, provider } = loadExplicitCommandRuntime(args, { prepareArtifacts: true });
46
- await handlePublishChannelWorkspaceMirror({ args, network, provider });
47
- },
48
41
  "channel-join": async (args) => {
49
42
  assertJoinChannelArgs(args);
50
43
  const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args, { prepareArtifacts: true });
@@ -55,6 +55,15 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
55
55
  valueLabel: "<NAME>",
56
56
  option: "--account",
57
57
  },
58
+ leaderAccount: {
59
+ label: "Channel Leader Account",
60
+ type: "text",
61
+ placeholder: "leader-account",
62
+ valueLabel: "<ACCOUNT>",
63
+ hint: "Required with --publish-workspace-mirror. Signs the workspace mirror manifest and must match the on-chain channel leader.",
64
+ option: "--leader-account",
65
+ optional: true,
66
+ },
58
67
  txSubmitter: {
59
68
  label: "Transaction Submitter",
60
69
  type: "text",
@@ -212,6 +221,13 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
212
221
  option: "--output-raw",
213
222
  optional: true,
214
223
  },
224
+ publishWorkspaceMirror: {
225
+ label: "Publish Workspace Mirror",
226
+ type: "checkbox",
227
+ hint: "After channel recovery, write the registered workspace mirror manifest, checkpoint, and any delta bundle.",
228
+ option: "--publish-workspace-mirror",
229
+ optional: true,
230
+ },
215
231
  source: {
216
232
  label: "Recovery Source",
217
233
  type: "select",
@@ -330,7 +346,11 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
330
346
  fields: ["network", "channelName", "account", "wallet"],
331
347
  optionalFields: ["network", "channelName", "account", "wallet"],
332
348
  usage: "optional --network, --channel-name, --account, and --wallet",
333
- help: ["Does not accept --rpc-url and never writes RPC configuration", "Recommends bridge deposits only after a wallet is joined and needs channel liquidity"],
349
+ help: [
350
+ "Does not accept --rpc-url and never writes RPC configuration",
351
+ "Recommends bridge deposits only after a wallet is joined and needs channel liquidity",
352
+ "Channel leaders publish workspace mirror files through channel recover-workspace --publish-workspace-mirror, not a standalone publish command",
353
+ ],
334
354
  },
335
355
  {
336
356
  id: "help-observer",
@@ -405,8 +425,9 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
405
425
  display: "channel recover-workspace",
406
426
  description: "Rebuild the local channel workspace from bridge state.",
407
427
  installMode: "read-only",
408
- fields: ["channelName", "network", "source", "fromGenesis", "outputRaw"],
409
- usage: "--channel-name, --network, optional --source, optional --from-genesis, optional --output-raw",
428
+ fields: ["channelName", "network", "source", "fromGenesis", "outputRaw", "publishWorkspaceMirror", "leaderAccount", "output", "force"],
429
+ optionalFields: ["source", "fromGenesis", "outputRaw", "publishWorkspaceMirror", "leaderAccount", "output", "force"],
430
+ usage: "--channel-name, --network, optional --source, optional --from-genesis, optional --output-raw, optional --publish-workspace-mirror with --leader-account and --output, optional --force",
410
431
  help: [
411
432
  "By default, --source rpc resumes RPC log scanning from the workspace recovery index when available",
412
433
  "--source mirror validates the channel leader's registered checkpoint manifest, downloads only the needed checkpoint or delta bundle, and then replays RPC logs to latest",
@@ -416,6 +437,10 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
416
437
  "Use --source rpc --from-genesis to ignore the recovery index and replay logs from channel genesis",
417
438
  "--output-raw with --source rpc appends raw JSON-RPC request and response history to method-specific JSON files under the channel workspace rpcCallHistory directory; eth_getLogs is split by event",
418
439
  "--from-genesis moves the existing local channel workspace to workspace-rebuild-backups before writing the current-format workspace; local secrets are preserved",
440
+ "--publish-workspace-mirror writes manifest.json, checkpoint.zip, and any needed delta bundle after recovery",
441
+ "--publish-workspace-mirror requires --leader-account <ACCOUNT> and --output <PATH>; the leader account signs the mirror manifest and must match the on-chain channel leader",
442
+ "--force with --publish-workspace-mirror ignores an unreadable or invalid existing mirror manifest and publishes a full checkpoint without a delta",
443
+ "Workspace mirror publishing does not upload files to a remote server; deploy the output directory to the registered mirror host",
419
444
  "Prints RPC log scan progress while rebuilding the workspace",
420
445
  ],
421
446
  },
@@ -431,20 +456,6 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
431
456
  "The URL points to a server implementing the private-state channel workspace mirror protocol",
432
457
  ],
433
458
  },
434
- {
435
- id: "channel-publish-workspace-mirror",
436
- display: "channel publish-workspace-mirror",
437
- description: "Build static workspace mirror files for the registered mirror URL.",
438
- installMode: "read-only",
439
- fields: ["channelName", "network", "account", "output", "force"],
440
- usage: "--channel-name, --network, --account, --output, optional --force",
441
- help: [
442
- "Requires the local channel workspace to be current and ahead of the registered mirror checkpoint",
443
- "--force ignores an unreadable or invalid existing mirror manifest and publishes a full checkpoint without a delta",
444
- "Writes manifest.json, checkpoint.zip, and any needed delta bundle under the workspace mirror static path",
445
- "Does not upload files to a remote server; deploy the output directory to the registered HTTPS mirror host",
446
- ],
447
- },
448
459
  {
449
460
  id: "channel-get-meta",
450
461
  display: "channel get-meta",
package/lib/runtime.mjs CHANGED
@@ -165,6 +165,7 @@ const CLI_ERROR_CODES = Object.freeze({
165
165
  MISSING_DEPLOYMENT_ARTIFACTS: "MISSING_DEPLOYMENT_ARTIFACTS",
166
166
  MISSING_CHANNEL_REGISTRATION: "MISSING_CHANNEL_REGISTRATION",
167
167
  STALE_WORKSPACE: "STALE_WORKSPACE",
168
+ STALE_CHANNEL_ROOT: "STALE_CHANNEL_ROOT",
168
169
  });
169
170
 
170
171
  class PrivateStateCliError extends Error {
@@ -729,6 +730,8 @@ async function handleWorkspaceInit({ args, network, provider }) {
729
730
  workspaceDir,
730
731
  workspace,
731
732
  currentSnapshot,
733
+ blockInfo,
734
+ contractCodes,
732
735
  cleanRebuildBackup,
733
736
  rpcCallHistory,
734
737
  } = await syncChannelWorkspace({
@@ -746,6 +749,24 @@ async function handleWorkspaceInit({ args, network, provider }) {
746
749
  progressAction: "channel recover-workspace",
747
750
  });
748
751
 
752
+ const publishedWorkspaceMirror = args.publishWorkspaceMirror === true
753
+ ? await publishChannelWorkspaceMirrorFromRecoveredWorkspace({
754
+ args,
755
+ network,
756
+ provider,
757
+ bridgeResources,
758
+ channelName,
759
+ local: {
760
+ workspace,
761
+ stateSnapshot: currentSnapshot,
762
+ blockInfo,
763
+ contractCodes,
764
+ recoveryLastScannedBlock: workspace.recoveryLastScannedBlock,
765
+ recoveryRootVectorHash: workspace.recoveryRootVectorHash,
766
+ },
767
+ })
768
+ : null;
769
+
749
770
  printJson({
750
771
  action: "channel recover-workspace",
751
772
  source: workspace.recoverySource ?? recoverySource,
@@ -764,6 +785,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
764
785
  recoveryScanRange: workspace.recoveryScanRange,
765
786
  rpcCallHistory,
766
787
  workspaceMirror: workspace.workspaceMirror ?? null,
788
+ publishedWorkspaceMirror,
767
789
  });
768
790
  }
769
791
 
@@ -875,12 +897,17 @@ async function handleSetChannelWorkspaceMirror({ args, network, provider }) {
875
897
  });
876
898
  }
877
899
 
878
- async function handlePublishChannelWorkspaceMirror({ args, network, provider }) {
879
- const channelName = requireArg(args.channelName, "--channel-name");
900
+ async function publishChannelWorkspaceMirrorFromRecoveredWorkspace({
901
+ args,
902
+ network,
903
+ provider,
904
+ bridgeResources,
905
+ channelName,
906
+ local,
907
+ }) {
880
908
  const outputRoot = path.resolve(String(requireArg(args.output, "--output")));
881
909
  const force = args.force === true;
882
- const signer = requireL1Signer(args, provider);
883
- const bridgeResources = loadBridgeResources({ chainId: network.chainId });
910
+ const { signer, account: leaderAccount } = requireLeaderSigner(args, provider);
884
911
  const { bridgeDeployment, bridgeAbiManifest } = bridgeResources;
885
912
  const bridgeCore = new Contract(
886
913
  bridgeDeployment.bridgeCore,
@@ -906,13 +933,6 @@ async function handlePublishChannelWorkspaceMirror({ args, network, provider })
906
933
  channelId,
907
934
  });
908
935
 
909
- const local = await loadPublishableLocalWorkspaceMirrorCheckpoint({
910
- channelName,
911
- network,
912
- provider,
913
- bridgeResources,
914
- channelInfo,
915
- });
916
936
  const remote = await readRemoteWorkspaceMirrorCheckpoint({
917
937
  manifestUrl,
918
938
  chainId: network.chainId,
@@ -927,9 +947,9 @@ async function handlePublishChannelWorkspaceMirror({ args, network, provider })
927
947
  expect(
928
948
  !remote.exists || Number(local.recoveryLastScannedBlock) > Number(remote.recoveryLastScannedBlock),
929
949
  [
930
- `Local workspace recovery index ${local.recoveryLastScannedBlock} is not ahead of the registered mirror`,
950
+ `Recovered workspace index ${local.recoveryLastScannedBlock} is not ahead of the registered mirror`,
931
951
  `checkpoint ${remote.exists ? remote.recoveryLastScannedBlock : "<missing>"}.`,
932
- "Run channel recover-workspace first if the local workspace is stale.",
952
+ "No newer workspace mirror checkpoint can be published.",
933
953
  ].join(" "),
934
954
  );
935
955
 
@@ -1023,11 +1043,13 @@ async function handlePublishChannelWorkspaceMirror({ args, network, provider })
1023
1043
  };
1024
1044
  writeJson(publishTarget.manifestPath, manifest);
1025
1045
 
1026
- printJson({
1027
- action: "channel publish-workspace-mirror",
1046
+ return {
1047
+ action: "channel recover-workspace publish-workspace-mirror",
1028
1048
  channelName,
1029
1049
  channelId: channelId.toString(),
1030
1050
  force,
1051
+ leaderAccount,
1052
+ leader: getAddress(signer.address),
1031
1053
  outputRoot,
1032
1054
  mirrorDir,
1033
1055
  manifestPath: publishTarget.manifestPath,
@@ -1053,7 +1075,7 @@ async function handlePublishChannelWorkspaceMirror({ args, network, provider })
1053
1075
  sizeBytes: checkpointBundle.bytes.length,
1054
1076
  },
1055
1077
  deltaBundles,
1056
- });
1078
+ };
1057
1079
  }
1058
1080
 
1059
1081
  function resolveWorkspaceRecoverySource(args) {
@@ -1084,84 +1106,6 @@ async function readChannelWorkspaceMirror({ bridgeCore, channelId }) {
1084
1106
  return String(await bridgeCore.getChannelWorkspaceMirror(channelId));
1085
1107
  }
1086
1108
 
1087
- async function loadPublishableLocalWorkspaceMirrorCheckpoint({
1088
- channelName,
1089
- network,
1090
- provider,
1091
- bridgeResources,
1092
- channelInfo,
1093
- }) {
1094
- const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), channelName);
1095
- const existingArtifacts = loadExistingWorkspaceArtifacts(workspaceDir);
1096
- const workspace = existingArtifacts.workspace;
1097
- expect(workspace, `Local channel workspace is missing at ${channelWorkspaceConfigPath(workspaceDir)}.`);
1098
- const stateSnapshot = existingArtifacts.stateSnapshot;
1099
- expect(stateSnapshot, `Local channel state snapshot is missing under ${channelWorkspaceCurrentPath(workspaceDir)}.`);
1100
- expect(existingArtifacts.blockInfo, `Local channel block_info.json is missing under ${channelWorkspaceCurrentPath(workspaceDir)}.`);
1101
- expect(existingArtifacts.contractCodes, `Local channel contract_codes.json is missing under ${channelWorkspaceCurrentPath(workspaceDir)}.`);
1102
- const channelManager = new Contract(
1103
- channelInfo.manager,
1104
- bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
1105
- provider,
1106
- );
1107
- const [
1108
- currentRootVectorHash,
1109
- genesisBlockNumber,
1110
- latestBlock,
1111
- managedStorageAddresses,
1112
- ] = await Promise.all([
1113
- channelManager.currentRootVectorHash(),
1114
- channelManager.genesisBlockNumber(),
1115
- provider.getBlockNumber(),
1116
- channelManager.getManagedStorageAddresses(),
1117
- ]);
1118
- const normalizedManagedStorageAddresses = normalizedAddressVector(managedStorageAddresses);
1119
- const recoveryIndex = getUsableWorkspaceRecoveryIndex({
1120
- existingArtifacts,
1121
- genesisBlockNumber: Number(genesisBlockNumber),
1122
- latestBlock,
1123
- managedStorageAddresses: normalizedManagedStorageAddresses,
1124
- });
1125
- expect(
1126
- recoveryIndex,
1127
- [
1128
- "Local channel workspace does not contain a usable recovery index.",
1129
- `Run channel recover-workspace --channel-name ${channelName} --network ${networkNameFromChainId(network.chainId)} first.`,
1130
- ].join(" "),
1131
- );
1132
- expect(
1133
- canReuseLocalWorkspaceSnapshot({
1134
- existingArtifacts,
1135
- currentRootVectorHash,
1136
- managedStorageAddresses: normalizedManagedStorageAddresses,
1137
- }),
1138
- [
1139
- "Local channel workspace is stale relative to the on-chain channel state.",
1140
- `Run channel recover-workspace --channel-name ${channelName} --network ${networkNameFromChainId(network.chainId)} first.`,
1141
- ].join(" "),
1142
- );
1143
- expect(Number(workspace.chainId) === Number(network.chainId), "Local workspace chainId does not match --network.");
1144
- expect(ethers.toBigInt(workspace.channelId) === ethers.toBigInt(deriveChannelIdFromName(channelName)), "Local workspace channelId mismatch.");
1145
- expect(String(workspace.channelName) === channelName, "Local workspace channelName mismatch.");
1146
- expect(
1147
- ethers.toBigInt(getAddress(workspace.channelManager)) === ethers.toBigInt(getAddress(channelInfo.manager)),
1148
- "Local workspace channelManager mismatch.",
1149
- );
1150
- expect(
1151
- ethers.toBigInt(getAddress(workspace.bridgeTokenVault)) === ethers.toBigInt(getAddress(channelInfo.bridgeTokenVault)),
1152
- "Local workspace bridgeTokenVault mismatch.",
1153
- );
1154
-
1155
- return {
1156
- workspace,
1157
- stateSnapshot,
1158
- blockInfo: existingArtifacts.blockInfo,
1159
- contractCodes: existingArtifacts.contractCodes,
1160
- recoveryLastScannedBlock: recoveryIndex.nextBlock,
1161
- recoveryRootVectorHash: recoveryIndex.recoveryRootVectorHash,
1162
- };
1163
- }
1164
-
1165
1109
  async function readRemoteWorkspaceMirrorCheckpoint({
1166
1110
  manifestUrl,
1167
1111
  chainId,
@@ -3661,6 +3605,7 @@ async function handleGuide({ args }) {
3661
3605
  why: null,
3662
3606
  candidateCommands: [],
3663
3607
  privacyTip: "For wallet mint-notes, wallet transfer-notes, and wallet redeem-notes, add --tx-submitter <ACCOUNT> to let a separate local L1 account submit executeChannelTransaction and pay gas.",
3608
+ mirrorTip: "Channel leaders refresh mirror files with channel recover-workspace --publish-workspace-mirror --leader-account <ACCOUNT> --output <PATH>; the standalone channel publish-workspace-mirror command is no longer available.",
3664
3609
  };
3665
3610
 
3666
3611
  guide.state.local = inspectGuideLocalState(args);
@@ -4911,9 +4856,16 @@ async function handleGrothVaultMove({ args, provider, direction }) {
4911
4856
  const methodName = direction === "deposit" ? "depositToChannelVault" : "withdrawFromChannelVault";
4912
4857
  await assertWorkspaceAlignedWithChain(context);
4913
4858
  emitProgress(operationName, "submitting");
4914
- const receipt = await waitForReceipt(
4915
- await bridgeTokenVault[methodName](ethers.toBigInt(context.workspace.channelId), transition.proof, transition.update),
4916
- );
4859
+ const receipt = await submitProofBackedRootUpdate({
4860
+ context,
4861
+ walletName: walletContext.walletName,
4862
+ operationName,
4863
+ submit: () => bridgeTokenVault[methodName](
4864
+ ethers.toBigInt(context.workspace.channelId),
4865
+ transition.proof,
4866
+ transition.update,
4867
+ ),
4868
+ });
4917
4869
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
4918
4870
  expect(
4919
4871
  onchainRootVectorHash === normalizeBytes32Hex(hashRootVector(transition.nextSnapshot.stateRoots)),
@@ -7755,10 +7707,12 @@ async function executeWalletTemplateSend({
7755
7707
 
7756
7708
  await assertWorkspaceAlignedWithChain(context);
7757
7709
  emitProgress(operationName, "submitting");
7758
- const receipt =
7759
- await waitForReceipt(
7760
- await context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
7761
- );
7710
+ const receipt = await submitProofBackedRootUpdate({
7711
+ context,
7712
+ walletName: wallet.walletName,
7713
+ operationName,
7714
+ submit: () => context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
7715
+ });
7762
7716
  await waitForProviderBlockAtLeast(provider, receipt.blockNumber, { action: operationName });
7763
7717
 
7764
7718
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
@@ -8425,6 +8379,56 @@ function isContractError(error, contractInterface, errorName) {
8425
8379
  return false;
8426
8380
  }
8427
8381
 
8382
+ function isUnexpectedCurrentRootVectorError(error, context) {
8383
+ if (isContractError(error, context.channelManager.interface, "UnexpectedCurrentRootVector")) {
8384
+ return true;
8385
+ }
8386
+ return String(error?.message ?? error).includes("UnexpectedCurrentRootVector");
8387
+ }
8388
+
8389
+ async function submitProofBackedRootUpdate({
8390
+ context,
8391
+ walletName,
8392
+ operationName,
8393
+ submit,
8394
+ }) {
8395
+ try {
8396
+ return await waitForReceipt(await submit());
8397
+ } catch (error) {
8398
+ if (isUnexpectedCurrentRootVectorError(error, context)) {
8399
+ throw staleChannelRootError({
8400
+ cause: error,
8401
+ context,
8402
+ walletName,
8403
+ operationName,
8404
+ });
8405
+ }
8406
+ throw error;
8407
+ }
8408
+ }
8409
+
8410
+ function staleChannelRootError({
8411
+ cause,
8412
+ context,
8413
+ walletName,
8414
+ operationName,
8415
+ }) {
8416
+ const message = [
8417
+ `${operationName} failed because the submitted proof was generated for an older channel root.`,
8418
+ "The rejected proof cannot be reused.",
8419
+ "Do not change recipients, amounts, note counts, function arity, or split the command as recovery.",
8420
+ "Refresh the channel workspace, re-check affected wallet state when the command uses notes, then rerun the original intended command so the CLI regenerates a proof from a fresh snapshot.",
8421
+ ].join(" ");
8422
+ const error = cliError(CLI_ERROR_CODES.STALE_CHANNEL_ROOT, message, { cause });
8423
+ error.channelName = context.workspace.channelName;
8424
+ error.networkName = context.workspace.network;
8425
+ error.walletName = walletName;
8426
+ error.retryPolicy = "recover_workspace_then_regenerate_proof";
8427
+ error.semanticMutationAllowed = false;
8428
+ error.reuseProofAllowed = false;
8429
+ return error;
8430
+ }
8431
+
8428
8432
  function extractContractErrorDataCandidates(error) {
8429
8433
  return [
8430
8434
  error?.data,
@@ -9764,6 +9768,19 @@ function requireL1Signer(args, provider) {
9764
9768
  return new Wallet(resolvePrivateKeySource(args), provider);
9765
9769
  }
9766
9770
 
9771
+ function requireLeaderSigner(args, provider) {
9772
+ const networkName = requireNetworkName(args);
9773
+ const account = String(requireArg(args.leaderAccount, "--leader-account")).trim();
9774
+ expect(account.length > 0, "--leader-account requires a local account name.");
9775
+ return {
9776
+ signer: new Wallet(
9777
+ normalizePrivateKey(readSecretFile(accountPrivateKeyPath(networkName, account), "--leader-account")),
9778
+ provider,
9779
+ ),
9780
+ account,
9781
+ };
9782
+ }
9783
+
9767
9784
  function resolveTxSubmitterSigner({ args, ownerSigner, provider }) {
9768
9785
  if (args.txSubmitter === undefined) {
9769
9786
  expect(
@@ -10453,9 +10470,25 @@ function assertRecoverWorkspaceArgs(args) {
10453
10470
  const source = resolveWorkspaceRecoverySource(args);
10454
10471
  assertBooleanFlag(args, "fromGenesis", "channel recover-workspace option --from-genesis");
10455
10472
  assertBooleanFlag(args, "outputRaw", "channel recover-workspace option --output-raw");
10473
+ assertBooleanFlag(args, "publishWorkspaceMirror", "channel recover-workspace option --publish-workspace-mirror");
10474
+ assertBooleanFlag(args, "force", "channel recover-workspace option --force");
10456
10475
  if (args.outputRaw === true && source !== "rpc") {
10457
10476
  throw new Error("channel recover-workspace option --output-raw requires --source rpc.");
10458
10477
  }
10478
+ if (args.publishWorkspaceMirror === true) {
10479
+ requireArg(args.leaderAccount, "--leader-account");
10480
+ requireArg(args.output, "--output");
10481
+ } else {
10482
+ if (args.leaderAccount !== undefined) {
10483
+ throw new Error("channel recover-workspace option --leader-account requires --publish-workspace-mirror.");
10484
+ }
10485
+ if (args.output !== undefined) {
10486
+ throw new Error("channel recover-workspace option --output requires --publish-workspace-mirror.");
10487
+ }
10488
+ if (args.force !== undefined) {
10489
+ throw new Error("channel recover-workspace option --force requires --publish-workspace-mirror.");
10490
+ }
10491
+ }
10459
10492
  }
10460
10493
 
10461
10494
  function assertGetChannelArgs(args) {
@@ -10467,12 +10500,6 @@ function assertSetWorkspaceMirrorArgs(args) {
10467
10500
  requireWorkspaceMirrorUrl(args.url);
10468
10501
  }
10469
10502
 
10470
- function assertPublishWorkspaceMirrorArgs(args) {
10471
- assertAllowedCommandSchema(args, "channel-publish-workspace-mirror");
10472
- requireArg(args.output, "--output");
10473
- assertBooleanFlag(args, "force", "channel publish-workspace-mirror option --force");
10474
- }
10475
-
10476
10503
  function assertDepositBridgeArgs(args) {
10477
10504
  assertAllowedCommandSchema(args, "account-deposit-bridge");
10478
10505
  assertActionImpactArg(args, "account deposit-bridge");
@@ -11369,6 +11396,11 @@ function printGuideHumanResult(guide) {
11369
11396
  "Privacy Tip",
11370
11397
  formatHumanValue(guide.privacyTip),
11371
11398
  );
11399
+ lines.push(
11400
+ "",
11401
+ "Mirror Tip",
11402
+ formatHumanValue(guide.mirrorTip),
11403
+ );
11372
11404
  console.log(lines.join("\n"));
11373
11405
  }
11374
11406
 
@@ -11718,16 +11750,16 @@ function buildRecoveryHints(error, args = {}) {
11718
11750
  const hints = [];
11719
11751
  const networkName = typeof args.network === "string" && args.network.length > 0
11720
11752
  ? args.network
11721
- : "<NETWORK>";
11753
+ : error?.networkName ?? "<NETWORK>";
11722
11754
  const channelName = typeof args.channelName === "string" && args.channelName.length > 0
11723
11755
  ? args.channelName
11724
- : "<CHANNEL>";
11756
+ : error?.channelName ?? "<CHANNEL>";
11725
11757
  const accountName = typeof args.account === "string" && args.account.length > 0
11726
11758
  ? args.account
11727
11759
  : "<ACCOUNT>";
11728
11760
  const walletName = typeof args.wallet === "string" && args.wallet.length > 0
11729
11761
  ? args.wallet
11730
- : extractUnknownWalletName(message) ?? "<WALLET>";
11762
+ : error?.walletName ?? extractUnknownWalletName(message) ?? "<WALLET>";
11731
11763
 
11732
11764
  if (
11733
11765
  error?.code === CLI_ERROR_CODES.MISSING_RPC_URL
@@ -11774,6 +11806,17 @@ function buildRecoveryHints(error, args = {}) {
11774
11806
  hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName}`);
11775
11807
  }
11776
11808
 
11809
+ if (
11810
+ error?.code === CLI_ERROR_CODES.STALE_CHANNEL_ROOT
11811
+ || message.includes("UnexpectedCurrentRootVector")
11812
+ ) {
11813
+ hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
11814
+ if (walletName !== "<WALLET>") {
11815
+ hints.push(`private-state-cli wallet get-notes --wallet ${walletName} --network ${networkName}`);
11816
+ }
11817
+ hints.push("rerun the original proof-backed command unchanged so the CLI regenerates a fresh proof");
11818
+ }
11819
+
11777
11820
  if (message.includes("Workspace recovery index is missing or unusable")) {
11778
11821
  hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
11779
11822
  }
@@ -11847,7 +11890,6 @@ export {
11847
11890
  assertRecoverWorkspaceArgs,
11848
11891
  assertGetChannelArgs,
11849
11892
  assertSetWorkspaceMirrorArgs,
11850
- assertPublishWorkspaceMirrorArgs,
11851
11893
  assertDepositBridgeArgs,
11852
11894
  assertWithdrawBridgeArgs,
11853
11895
  assertAccountGetBridgeFundArgs,
@@ -11881,7 +11923,6 @@ export {
11881
11923
  handleWorkspaceInit,
11882
11924
  handleGetChannel,
11883
11925
  handleSetChannelWorkspaceMirror,
11884
- handlePublishChannelWorkspaceMirror,
11885
11926
  handleDepositBridge,
11886
11927
  handleWithdrawBridge,
11887
11928
  handleAccountGetBridgeFund,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "2.3.4",
3
+ "version": "2.4.1",
4
4
  "description": "Command-line client for the Tokamak private-state DApp.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "Tokamak Network",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "README.md",
29
+ "agents.md",
29
30
  "CHANGELOG.md",
30
31
  "LICENSE",
31
32
  "private-state-bridge-cli.mjs",