@tokamak-private-dapps/private-state-cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +73 -0
- package/assets/tx-fees.json +170 -0
- package/lib/private-state-cli-command-registry.mjs +30 -6
- package/package.json +2 -1
- package/private-state-bridge-cli.mjs +507 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Added `transaction-fees`, which reads packaged measured gas data from `assets/tx-fees.json`,
|
|
6
|
+
combines it with live RPC fee data and live ETH/USD pricing, and prints a per-command ETH/USD
|
|
7
|
+
fee table.
|
|
8
|
+
- Split `transaction-fees` estimates into typical cost from RPC `gasPrice` and worst-case cost
|
|
9
|
+
from EIP-1559 `maxFeePerGas`.
|
|
10
|
+
- Added optional `--tx-submitter <ACCOUNT>` support to `mint-notes`, `transfer-notes`, and
|
|
11
|
+
`redeem-notes` so proof-backed note owners can separate note ownership from the L1 account
|
|
12
|
+
that submits `executeChannelTransaction` and pays gas.
|
|
13
|
+
- Expanded LLM-agent README guidance so agents explain private key files, local account aliases,
|
|
14
|
+
wallet secret source files, network RPC URLs, and immutable channel policy step by step before
|
|
15
|
+
guiding new users through `join-channel`.
|
|
16
|
+
- Added RPC log scan progress output to `recover-workspace` and `recover-wallet`, with progress
|
|
17
|
+
routed to stderr in `--json` mode so machine-readable command results stay valid.
|
|
18
|
+
- Unified wallet command workspace refresh through the same recovery-indexed path used by
|
|
19
|
+
`recover-workspace`, and shared received-note recovery through the wallet's
|
|
20
|
+
`noteReceiveLastScannedBlock` index.
|
|
21
|
+
|
|
22
|
+
## 1.0.1 - 2026-05-05
|
|
23
|
+
|
|
24
|
+
- Added global `--version` output for scripts that need the installed private-state CLI package
|
|
25
|
+
version without running `doctor`.
|
|
5
26
|
- Changed the channel-bound L2 identity derivation signing domain and mode from password wording
|
|
6
27
|
to wallet-secret wording. Existing local wallets from the pre-1.0.0 cleanup path are not
|
|
7
28
|
compatibility targets.
|
package/README.md
CHANGED
|
@@ -51,6 +51,12 @@ Check the installed package and runtime state with:
|
|
|
51
51
|
private-state-cli doctor
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
Print only the installed CLI package version with:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
private-state-cli --version
|
|
58
|
+
```
|
|
59
|
+
|
|
54
60
|
Remove all local private-state CLI data with:
|
|
55
61
|
|
|
56
62
|
```bash
|
|
@@ -80,6 +86,28 @@ A common private-state flow is:
|
|
|
80
86
|
|
|
81
87
|
Use `private-state-cli --help` for the full command list and required options.
|
|
82
88
|
|
|
89
|
+
Estimate live transaction costs before sending commands with:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
private-state-cli transaction-fees --network mainnet --rpc-url <RPC_URL>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`transaction-fees` uses the measured gas data packaged in `assets/tx-fees.json`, the selected network's live fee data,
|
|
96
|
+
and live ETH/USD pricing to print an ETH/USD fee table for transaction-sending commands. The table separates typical
|
|
97
|
+
cost, based on the RPC `gasPrice`, from worst-case cost, based on `maxFeePerGas` when the network reports EIP-1559 fee
|
|
98
|
+
data.
|
|
99
|
+
|
|
100
|
+
Proof-backed note commands can use a separate L1 transaction submitter:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
private-state-cli mint-notes --wallet <WALLET> --network mainnet --amounts '[1]' --tx-submitter <ACCOUNT>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--tx-submitter <ACCOUNT>` is available on `mint-notes`, `transfer-notes`, and `redeem-notes`. The wallet still proves
|
|
107
|
+
note ownership and builds the ZK proof, but the selected local account submits `executeChannelTransaction` and pays gas.
|
|
108
|
+
Use this option when you want stronger privacy by avoiding a direct on-chain link between the note owner's wallet L1
|
|
109
|
+
account and the proof-submission transaction.
|
|
110
|
+
|
|
83
111
|
Channel policy warning:
|
|
84
112
|
|
|
85
113
|
- `create-channel` commits to an immutable channel policy: verifier bindings, DApp execution metadata, function layout,
|
|
@@ -162,6 +190,40 @@ Operating rules:
|
|
|
162
190
|
|
|
163
191
|
- Do not ask the user to reveal raw private keys or wallet secrets in chat. Use `account import --private-key-file`
|
|
164
192
|
once, then use `--account` for L1 signing commands. Wallet commands use wallet-local default secret files.
|
|
193
|
+
- Treat `private key file`, `account`, `wallet secret`, `wallet`, `network RPC URL`, and `channel policy` as
|
|
194
|
+
new concepts unless the user has already demonstrated that they understand them. Define each term before using it
|
|
195
|
+
in an instruction.
|
|
196
|
+
- Explain local-secret handling in plain language:
|
|
197
|
+
- A private key file is a local file that contains the user's L1 wallet private key. The CLI reads it once during
|
|
198
|
+
`account import` and stores a protected local account secret.
|
|
199
|
+
- An account is the local nickname created by `account import`. After import, signing commands should use
|
|
200
|
+
`--account <NAME>` instead of asking for the raw key again.
|
|
201
|
+
- A wallet secret source file is a separate high-entropy local secret chosen by the user for this private-state
|
|
202
|
+
wallet. It is not the L1 private key. `join-channel` imports it once and uses it to protect and recover the
|
|
203
|
+
channel-local wallet.
|
|
204
|
+
- A wallet is the encrypted local private-state wallet created during `join-channel`. Its deterministic name is
|
|
205
|
+
`<channelName>-<l1Address>`.
|
|
206
|
+
- The network RPC URL is the endpoint used to read and write chain state. It can be supplied once with `--rpc-url`
|
|
207
|
+
on a bridge-facing command, after which the CLI saves it under the selected network.
|
|
208
|
+
- When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
|
|
209
|
+
selected network. They can obtain one from an infrastructure provider such as Alchemy, Infura, QuickNode, or from
|
|
210
|
+
their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only the endpoint URL
|
|
211
|
+
into the CLI command that accepts `--rpc-url`; do not ask for provider account passwords, API dashboards, seed phrases,
|
|
212
|
+
private keys, or wallet secrets.
|
|
213
|
+
- When a user wants to join a channel, do not jump straight to `join-channel`. Walk them through:
|
|
214
|
+
1. choose the network and channel name
|
|
215
|
+
2. run `private-state-cli install`
|
|
216
|
+
3. run `private-state-cli doctor`
|
|
217
|
+
4. obtain or confirm a network RPC URL for the selected network
|
|
218
|
+
5. prepare a private key source file locally, without pasting the key into chat
|
|
219
|
+
6. run `account import --account <NAME> --network <NETWORK> --private-key-file <PATH>`
|
|
220
|
+
7. prepare a wallet secret source file locally, for example with `openssl rand -hex 32 > ./wallet-secret.txt`
|
|
221
|
+
8. inspect the channel with `get-channel` if it already exists, or create it with `create-channel` if the user is
|
|
222
|
+
the channel creator
|
|
223
|
+
9. explain the immutable policy warning printed by the CLI
|
|
224
|
+
10. run `join-channel --channel-name <CHANNEL> --network <NETWORK> --account <ACCOUNT> --wallet-secret-path <PATH>`
|
|
225
|
+
- Before asking the user to create a file, explain what will be inside that file, who should be able to read it, and
|
|
226
|
+
whether losing it prevents wallet recovery.
|
|
165
227
|
- Prefer testnet examples unless the user explicitly asks for mainnet.
|
|
166
228
|
- Before any proof-backed or bridge-facing workflow, ask the user to run `private-state-cli doctor` and inspect
|
|
167
229
|
whether the runtime, Docker mode, CUDA/GPU probes, Groth16 runtime, and deployment artifacts are healthy.
|
|
@@ -175,6 +237,9 @@ Operating rules:
|
|
|
175
237
|
telling the user to move funds.
|
|
176
238
|
- Explain that wallet names are local CLI identifiers, while private transfers use notes owned by L2 addresses
|
|
177
239
|
registered in the channel.
|
|
240
|
+
- Explain `--tx-submitter <ACCOUNT>` when the user wants stronger privacy for `mint-notes`, `transfer-notes`, or
|
|
241
|
+
`redeem-notes`: the wallet owner still proves note ownership, but another imported local L1 account can submit the
|
|
242
|
+
on-chain `executeChannelTransaction` and pay gas.
|
|
178
243
|
- Before guiding a user through `create-channel` or `join-channel`, explain that channel policy is immutable after
|
|
179
244
|
creation and that joining a channel means accepting its current verifier, DApp metadata, function layout, managed
|
|
180
245
|
storage vector, and refund policy.
|
|
@@ -198,6 +263,14 @@ Suggested interaction flow:
|
|
|
198
263
|
`get-my-wallet-meta`, then build `transfer-notes`.
|
|
199
264
|
8. After transfer, guide the recipient to run `get-my-notes` to recover received notes from event logs.
|
|
200
265
|
|
|
266
|
+
Example onboarding explanation for `join-channel`:
|
|
267
|
+
|
|
268
|
+
> First we need two different local secrets. Your L1 private key proves which Ethereum account pays gas and signs
|
|
269
|
+
> bridge transactions. We import it once into a local account nickname, so later commands can say `--account alice`
|
|
270
|
+
> instead of handling the raw key again. Separately, the wallet secret protects the encrypted private-state wallet for
|
|
271
|
+
> this channel. It is not sent on-chain and it is not the same as your L1 private key. If you lose the wallet secret,
|
|
272
|
+
> recovering this channel wallet can become impossible.
|
|
273
|
+
|
|
201
274
|
Example style: if the user says, "ADDR6 sends 10 tokens privately to ADDR8", do not assume the required note exists.
|
|
202
275
|
First ask or check which channel and network to use, whether ADDR6 and ADDR8 are already joined, what the local wallet
|
|
203
276
|
names are, and whether ADDR6 has an unused note worth exactly 10 or notes that sum to 10. Then provide the next concrete
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "tokamak-private-state-cli-tx-fees.v1",
|
|
3
|
+
"measuredAt": "2026-05-05",
|
|
4
|
+
"measurementBasis": "Measured directly from the current repository worktree with forge test --root bridge --gas-report and completed local CLI e2e transaction receipts.",
|
|
5
|
+
"notes": [
|
|
6
|
+
"Gas values are protocol transaction gasUsed values, not a guarantee for every future calldata shape or verifier implementation.",
|
|
7
|
+
"ERC-20 approval transactions are listed as separate approve components when the CLI command sends an approval transaction.",
|
|
8
|
+
"transfer-notes and redeem-notes use the same proof-backed executeChannelTransaction baseline measured from the successful mint-notes CLI e2e receipt because the current e2e run stopped before producing fresh transfer/redeem receipts."
|
|
9
|
+
],
|
|
10
|
+
"commands": [
|
|
11
|
+
{
|
|
12
|
+
"command": "create-channel",
|
|
13
|
+
"description": "Create a bridge channel and initialize its policy snapshot.",
|
|
14
|
+
"transactions": [
|
|
15
|
+
{
|
|
16
|
+
"label": "createChannel",
|
|
17
|
+
"contract": "BridgeCore",
|
|
18
|
+
"function": "createChannel",
|
|
19
|
+
"gasUsed": 2736229,
|
|
20
|
+
"source": "forge-gas-report",
|
|
21
|
+
"sourceDetail": "BridgeCore.createChannel max successful path from forge test --root bridge --gas-report."
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"command": "deposit-bridge",
|
|
27
|
+
"description": "Deposit canonical tokens into the shared bridge vault.",
|
|
28
|
+
"transactions": [
|
|
29
|
+
{
|
|
30
|
+
"label": "approve",
|
|
31
|
+
"contract": "MockERC20",
|
|
32
|
+
"function": "approve",
|
|
33
|
+
"gasUsed": 45729,
|
|
34
|
+
"source": "cast-local-measurement",
|
|
35
|
+
"sourceDetail": "Measured on local anvil by deploying bridge/src/mocks/MockERC20.sol and sending approve(address,uint256)."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"label": "fund",
|
|
39
|
+
"contract": "L1TokenVault",
|
|
40
|
+
"function": "fund",
|
|
41
|
+
"gasUsed": 68343,
|
|
42
|
+
"source": "forge-gas-report",
|
|
43
|
+
"sourceDetail": "L1TokenVault.fund from forge test --root bridge --gas-report."
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"command": "withdraw-bridge",
|
|
49
|
+
"description": "Withdraw canonical tokens from the shared bridge vault.",
|
|
50
|
+
"transactions": [
|
|
51
|
+
{
|
|
52
|
+
"label": "claimToWallet",
|
|
53
|
+
"contract": "L1TokenVault",
|
|
54
|
+
"function": "claimToWallet",
|
|
55
|
+
"gasUsed": 33824,
|
|
56
|
+
"source": "forge-gas-report",
|
|
57
|
+
"sourceDetail": "L1TokenVault.claimToWallet from forge test --root bridge --gas-report."
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"command": "join-channel",
|
|
63
|
+
"description": "Pay the channel join toll and register a wallet identity.",
|
|
64
|
+
"transactions": [
|
|
65
|
+
{
|
|
66
|
+
"label": "approve",
|
|
67
|
+
"contract": "MockERC20",
|
|
68
|
+
"function": "approve",
|
|
69
|
+
"gasUsed": 45729,
|
|
70
|
+
"source": "cast-local-measurement",
|
|
71
|
+
"sourceDetail": "Measured on local anvil by deploying bridge/src/mocks/MockERC20.sol and sending approve(address,uint256). join-channel sends this approval only when the channel join toll is nonzero."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"label": "joinChannel",
|
|
75
|
+
"contract": "L1TokenVault",
|
|
76
|
+
"function": "joinChannel",
|
|
77
|
+
"gasUsed": 341775,
|
|
78
|
+
"source": "forge-gas-report",
|
|
79
|
+
"sourceDetail": "L1TokenVault.joinChannel max successful path from forge test --root bridge --gas-report."
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"command": "deposit-channel",
|
|
85
|
+
"description": "Move bridged funds into the channel L2 accounting vault.",
|
|
86
|
+
"transactions": [
|
|
87
|
+
{
|
|
88
|
+
"label": "depositToChannelVault",
|
|
89
|
+
"contract": "L1TokenVault",
|
|
90
|
+
"function": "depositToChannelVault",
|
|
91
|
+
"gasUsed": 336496,
|
|
92
|
+
"gasUsedMin": 336464,
|
|
93
|
+
"gasUsedMax": 336496,
|
|
94
|
+
"source": "cli-e2e-receipts",
|
|
95
|
+
"sourceDetail": "Three completed local CLI e2e deposit-channel receipts from the current worktree."
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"command": "withdraw-channel",
|
|
101
|
+
"description": "Move channel L2 accounting vault funds back into the shared bridge vault.",
|
|
102
|
+
"transactions": [
|
|
103
|
+
{
|
|
104
|
+
"label": "withdrawFromChannelVault",
|
|
105
|
+
"contract": "L1TokenVault",
|
|
106
|
+
"function": "withdrawFromChannelVault",
|
|
107
|
+
"gasUsed": 95861,
|
|
108
|
+
"source": "forge-gas-report",
|
|
109
|
+
"sourceDetail": "L1TokenVault.withdrawFromChannelVault max successful path from forge test --root bridge --gas-report."
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"command": "exit-channel",
|
|
115
|
+
"description": "Exit a channel after the channel balance is zero.",
|
|
116
|
+
"transactions": [
|
|
117
|
+
{
|
|
118
|
+
"label": "exitChannel",
|
|
119
|
+
"contract": "L1TokenVault",
|
|
120
|
+
"function": "exitChannel",
|
|
121
|
+
"gasUsed": 119031,
|
|
122
|
+
"source": "forge-gas-report",
|
|
123
|
+
"sourceDetail": "L1TokenVault.exitChannel max successful path from forge test --root bridge --gas-report."
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"command": "mint-notes",
|
|
129
|
+
"description": "Submit a proof-backed private-state mint transaction.",
|
|
130
|
+
"transactions": [
|
|
131
|
+
{
|
|
132
|
+
"label": "executeChannelTransaction",
|
|
133
|
+
"contract": "ChannelManager",
|
|
134
|
+
"function": "executeChannelTransaction",
|
|
135
|
+
"gasUsed": 861627,
|
|
136
|
+
"source": "cli-e2e-receipt",
|
|
137
|
+
"sourceDetail": "Completed local CLI e2e mint-notes bridge-submit-receipt.json from the current worktree."
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"command": "transfer-notes",
|
|
143
|
+
"description": "Submit a proof-backed private-state transfer transaction.",
|
|
144
|
+
"transactions": [
|
|
145
|
+
{
|
|
146
|
+
"label": "executeChannelTransaction",
|
|
147
|
+
"contract": "ChannelManager",
|
|
148
|
+
"function": "executeChannelTransaction",
|
|
149
|
+
"gasUsed": 861627,
|
|
150
|
+
"source": "cli-e2e-baseline",
|
|
151
|
+
"sourceDetail": "Uses the current successful mint-notes executeChannelTransaction receipt as the shared proof-backed DApp command baseline."
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"command": "redeem-notes",
|
|
157
|
+
"description": "Submit a proof-backed private-state redeem transaction.",
|
|
158
|
+
"transactions": [
|
|
159
|
+
{
|
|
160
|
+
"label": "executeChannelTransaction",
|
|
161
|
+
"contract": "ChannelManager",
|
|
162
|
+
"function": "executeChannelTransaction",
|
|
163
|
+
"gasUsed": 861627,
|
|
164
|
+
"source": "cli-e2e-baseline",
|
|
165
|
+
"sourceDetail": "Uses the current successful mint-notes executeChannelTransaction receipt as the shared proof-backed DApp command baseline."
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
@@ -29,6 +29,15 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
|
|
|
29
29
|
valueLabel: "<NAME>",
|
|
30
30
|
option: "--account",
|
|
31
31
|
},
|
|
32
|
+
txSubmitter: {
|
|
33
|
+
label: "Transaction Submitter",
|
|
34
|
+
type: "text",
|
|
35
|
+
placeholder: "relayer-account",
|
|
36
|
+
valueLabel: "<ACCOUNT>",
|
|
37
|
+
hint: "Optional for proof-backed note commands. Uses a separate local L1 account to submit executeChannelTransaction.",
|
|
38
|
+
option: "--tx-submitter",
|
|
39
|
+
optional: true,
|
|
40
|
+
},
|
|
32
41
|
privateKeyFile: {
|
|
33
42
|
label: "Private Key File",
|
|
34
43
|
type: "text",
|
|
@@ -176,6 +185,16 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
176
185
|
usage: "optional --network, --channel-name, --account, and --wallet",
|
|
177
186
|
help: ["Does not accept --rpc-url and never writes RPC configuration"],
|
|
178
187
|
},
|
|
188
|
+
{
|
|
189
|
+
id: "transaction-fees",
|
|
190
|
+
description: "Estimate ETH and USD fees for transaction-sending commands from packaged measured gas data and live network fee data.",
|
|
191
|
+
fields: ["network", "rpcUrl", "json"],
|
|
192
|
+
usage: "--network, optional --rpc-url, and optional --json",
|
|
193
|
+
help: [
|
|
194
|
+
"Uses packages/apps/private-state/cli/assets/tx-fees.json as the measured gas source packaged with the CLI",
|
|
195
|
+
"Reads live fee data from the selected network RPC and live ETH/USD from CoinGecko",
|
|
196
|
+
],
|
|
197
|
+
},
|
|
179
198
|
{
|
|
180
199
|
id: "account-import",
|
|
181
200
|
display: "account import",
|
|
@@ -198,6 +217,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
198
217
|
help: [
|
|
199
218
|
"By default, resumes RPC log scanning from the workspace recovery index when available",
|
|
200
219
|
"Use --from-genesis to ignore the recovery index and replay logs from channel genesis",
|
|
220
|
+
"Prints RPC log scan progress while rebuilding the workspace",
|
|
201
221
|
],
|
|
202
222
|
},
|
|
203
223
|
{
|
|
@@ -232,6 +252,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
232
252
|
help: [
|
|
233
253
|
"Requires the protected wallet-local secret imported during join-channel to exist at the canonical secret path",
|
|
234
254
|
"Does not create or recover the wallet secret itself",
|
|
255
|
+
"Prints RPC log scan progress while rebuilding channel state and received-note state",
|
|
235
256
|
],
|
|
236
257
|
},
|
|
237
258
|
{
|
|
@@ -290,20 +311,23 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
290
311
|
{
|
|
291
312
|
id: "mint-notes",
|
|
292
313
|
description: "Mint one or two private-state notes from the wallet's channel balance.",
|
|
293
|
-
fields: ["wallet", "network", "amounts"],
|
|
294
|
-
usage: "--wallet, --network, and --
|
|
314
|
+
fields: ["wallet", "network", "amounts", "txSubmitter"],
|
|
315
|
+
usage: "--wallet, --network, --amounts, and optional --tx-submitter",
|
|
316
|
+
help: ["Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy"],
|
|
295
317
|
},
|
|
296
318
|
{
|
|
297
319
|
id: "transfer-notes",
|
|
298
320
|
description: "Spend input notes into the registered 1->1, 1->2, or 2->1 private transfer shapes.",
|
|
299
|
-
fields: ["wallet", "network", "noteIds", "recipients", "amounts"],
|
|
300
|
-
usage: "--wallet, --network, --note-ids, --recipients, and --
|
|
321
|
+
fields: ["wallet", "network", "noteIds", "recipients", "amounts", "txSubmitter"],
|
|
322
|
+
usage: "--wallet, --network, --note-ids, --recipients, --amounts, and optional --tx-submitter",
|
|
323
|
+
help: ["Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy"],
|
|
301
324
|
},
|
|
302
325
|
{
|
|
303
326
|
id: "redeem-notes",
|
|
304
327
|
description: "Redeem one tracked note back into the wallet's channel balance.",
|
|
305
|
-
fields: ["wallet", "network", "noteIds"],
|
|
306
|
-
usage: "--wallet, --network, and --
|
|
328
|
+
fields: ["wallet", "network", "noteIds", "txSubmitter"],
|
|
329
|
+
usage: "--wallet, --network, --note-ids, and optional --tx-submitter",
|
|
330
|
+
help: ["Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy"],
|
|
307
331
|
},
|
|
308
332
|
{
|
|
309
333
|
id: "get-my-notes",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokamak-private-dapps/private-state-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"LICENSE",
|
|
31
31
|
"private-state-bridge-cli.mjs",
|
|
32
32
|
"cli-assistant.html",
|
|
33
|
+
"assets",
|
|
33
34
|
"lib"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
@@ -120,6 +120,7 @@ import {
|
|
|
120
120
|
|
|
121
121
|
const require = createRequire(import.meta.url);
|
|
122
122
|
const defaultCommandCwd = process.cwd();
|
|
123
|
+
const privateStateCliPackageJson = require("./package.json");
|
|
123
124
|
const privateStateCliPackageRoot = path.dirname(require.resolve("./package.json"));
|
|
124
125
|
const workspaceRoot = path.resolve(os.homedir(), "tokamak-private-channels", "workspace");
|
|
125
126
|
const secretRoot = path.resolve(os.homedir(), "tokamak-private-channels", "secrets");
|
|
@@ -314,6 +315,12 @@ async function main() {
|
|
|
314
315
|
activeCliArgs = args;
|
|
315
316
|
configureOutput(args);
|
|
316
317
|
|
|
318
|
+
if (args.version !== undefined) {
|
|
319
|
+
assertVersionArgs(args);
|
|
320
|
+
printVersion();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
317
324
|
if (args.help || !args.command) {
|
|
318
325
|
printHelp();
|
|
319
326
|
return;
|
|
@@ -343,6 +350,13 @@ async function main() {
|
|
|
343
350
|
return;
|
|
344
351
|
}
|
|
345
352
|
|
|
353
|
+
if (args.command === "transaction-fees") {
|
|
354
|
+
assertTransactionFeesArgs(args);
|
|
355
|
+
const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
|
|
356
|
+
await handleTransactionFees({ network, provider, rpcUrl });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
346
360
|
if (args.command === "get-my-l1-address") {
|
|
347
361
|
assertGetMyL1AddressArgs(args);
|
|
348
362
|
handleGetMyL1Address({ args });
|
|
@@ -622,6 +636,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
622
636
|
allowExistingWorkspaceSync: true,
|
|
623
637
|
useWorkspaceRecoveryIndex: true,
|
|
624
638
|
fromGenesis: args.fromGenesis === true,
|
|
639
|
+
progressAction: "recover-workspace",
|
|
625
640
|
});
|
|
626
641
|
|
|
627
642
|
printJson({
|
|
@@ -729,6 +744,7 @@ async function initializeChannelWorkspace({
|
|
|
729
744
|
allowExistingWorkspaceSync = false,
|
|
730
745
|
useWorkspaceRecoveryIndex = false,
|
|
731
746
|
fromGenesis = false,
|
|
747
|
+
progressAction = null,
|
|
732
748
|
}) {
|
|
733
749
|
const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), workspaceName);
|
|
734
750
|
const channelDir = channelDataPath(workspaceDir);
|
|
@@ -831,6 +847,7 @@ async function initializeChannelWorkspace({
|
|
|
831
847
|
baseSnapshot: recoveryIndex?.stateSnapshot ?? null,
|
|
832
848
|
fromBlock: recoveryIndex?.nextBlock ?? genesisBlockNumber,
|
|
833
849
|
toBlock: latestBlock,
|
|
850
|
+
progressAction,
|
|
834
851
|
});
|
|
835
852
|
const currentSnapshot = reconstruction.currentSnapshot;
|
|
836
853
|
const recoveryRootVectorHash = normalizeBytes32Hex(hashRootVector(currentSnapshot.stateRoots));
|
|
@@ -975,6 +992,8 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
975
992
|
bridgeResources,
|
|
976
993
|
persist: true,
|
|
977
994
|
allowExistingWorkspaceSync: true,
|
|
995
|
+
useWorkspaceRecoveryIndex: true,
|
|
996
|
+
progressAction: "recover-wallet",
|
|
978
997
|
});
|
|
979
998
|
const context = {
|
|
980
999
|
workspaceName: channelName,
|
|
@@ -1056,6 +1075,14 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
1056
1075
|
});
|
|
1057
1076
|
|
|
1058
1077
|
if (existingWallet) {
|
|
1078
|
+
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
1079
|
+
walletContext: existingWallet,
|
|
1080
|
+
context,
|
|
1081
|
+
provider,
|
|
1082
|
+
signer,
|
|
1083
|
+
noteReceiveKeyMaterial,
|
|
1084
|
+
progressAction: "recover-wallet",
|
|
1085
|
+
});
|
|
1059
1086
|
printJson({
|
|
1060
1087
|
action: "recover-wallet",
|
|
1061
1088
|
status: "already-recovered",
|
|
@@ -1071,6 +1098,10 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
1071
1098
|
l2StorageKey: storageKey,
|
|
1072
1099
|
leafIndex: registration.leafIndex.toString(),
|
|
1073
1100
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
1101
|
+
l2Nonce: existingWallet.wallet.l2Nonce,
|
|
1102
|
+
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
1103
|
+
scannedDeliveryLogs: recoveredDeliveryState.scannedLogs,
|
|
1104
|
+
noteReceiveScanRange: recoveredDeliveryState.scanRange,
|
|
1074
1105
|
});
|
|
1075
1106
|
return;
|
|
1076
1107
|
}
|
|
@@ -1091,11 +1122,13 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
1091
1122
|
walletContext.wallet.l2Nonce = 0;
|
|
1092
1123
|
persistWallet(walletContext);
|
|
1093
1124
|
|
|
1094
|
-
const recoveredDeliveryState = await
|
|
1125
|
+
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
1095
1126
|
walletContext,
|
|
1096
1127
|
context,
|
|
1097
1128
|
provider,
|
|
1098
|
-
|
|
1129
|
+
signer,
|
|
1130
|
+
noteReceiveKeyMaterial,
|
|
1131
|
+
progressAction: "recover-wallet",
|
|
1099
1132
|
});
|
|
1100
1133
|
|
|
1101
1134
|
printJson({
|
|
@@ -1437,6 +1470,144 @@ async function handleDoctor({ args }) {
|
|
|
1437
1470
|
}
|
|
1438
1471
|
}
|
|
1439
1472
|
|
|
1473
|
+
async function handleTransactionFees({ network, provider, rpcUrl }) {
|
|
1474
|
+
const feeAsset = loadTransactionFeeAsset();
|
|
1475
|
+
const feeData = await provider.getFeeData();
|
|
1476
|
+
const gasPrices = requireTransactionFeeGasPrices(feeData);
|
|
1477
|
+
const ethUsd = await fetchEthUsdPrice();
|
|
1478
|
+
const rows = buildTransactionFeeRows({
|
|
1479
|
+
commands: feeAsset.commands,
|
|
1480
|
+
typicalGasPriceWei: gasPrices.typical,
|
|
1481
|
+
worstCaseGasPriceWei: gasPrices.worstCase,
|
|
1482
|
+
ethUsd,
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
printJson({
|
|
1486
|
+
action: "transaction-fees",
|
|
1487
|
+
generatedAt: new Date().toISOString(),
|
|
1488
|
+
network: network.name,
|
|
1489
|
+
chainId: Number(network.chainId),
|
|
1490
|
+
rpcUrl,
|
|
1491
|
+
asset: {
|
|
1492
|
+
schema: feeAsset.schema,
|
|
1493
|
+
measuredAt: feeAsset.measuredAt,
|
|
1494
|
+
measurementBasis: feeAsset.measurementBasis,
|
|
1495
|
+
notes: feeAsset.notes,
|
|
1496
|
+
},
|
|
1497
|
+
livePricing: {
|
|
1498
|
+
typicalGasPriceWei: gasPrices.typical.toString(),
|
|
1499
|
+
typicalGasPriceGwei: formatGwei(gasPrices.typical),
|
|
1500
|
+
typicalGasPriceSource: gasPrices.typicalSource,
|
|
1501
|
+
worstCaseGasPriceWei: gasPrices.worstCase.toString(),
|
|
1502
|
+
worstCaseGasPriceGwei: formatGwei(gasPrices.worstCase),
|
|
1503
|
+
worstCaseGasPriceSource: gasPrices.worstCaseSource,
|
|
1504
|
+
maxFeePerGasWei: feeData.maxFeePerGas?.toString() ?? null,
|
|
1505
|
+
maxPriorityFeePerGasWei: feeData.maxPriorityFeePerGas?.toString() ?? null,
|
|
1506
|
+
gasPriceWei: feeData.gasPrice?.toString() ?? null,
|
|
1507
|
+
ethUsd,
|
|
1508
|
+
ethUsdSource: "CoinGecko simple price API",
|
|
1509
|
+
},
|
|
1510
|
+
rows,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function loadTransactionFeeAsset() {
|
|
1515
|
+
const assetPath = path.join(privateStateCliPackageRoot, "assets", "tx-fees.json");
|
|
1516
|
+
const asset = readJson(assetPath);
|
|
1517
|
+
expect(asset.schema === "tokamak-private-state-cli-tx-fees.v1", `Unsupported transaction fee asset schema: ${assetPath}`);
|
|
1518
|
+
expect(Array.isArray(asset.commands), `Transaction fee asset is missing commands: ${assetPath}`);
|
|
1519
|
+
return asset;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function requireTransactionFeeGasPrices(feeData) {
|
|
1523
|
+
const typical = feeData.gasPrice ?? feeData.maxFeePerGas;
|
|
1524
|
+
const worstCase = feeData.maxFeePerGas ?? feeData.gasPrice;
|
|
1525
|
+
if (typical === null || typical === undefined || worstCase === null || worstCase === undefined) {
|
|
1526
|
+
throw new Error("RPC provider did not return gasPrice or maxFeePerGas.");
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
typical: ethers.toBigInt(typical),
|
|
1530
|
+
typicalSource: feeData.gasPrice ? "gasPrice" : "maxFeePerGas",
|
|
1531
|
+
worstCase: ethers.toBigInt(worstCase),
|
|
1532
|
+
worstCaseSource: feeData.maxFeePerGas ? "maxFeePerGas" : "gasPrice",
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
async function fetchEthUsdPrice() {
|
|
1537
|
+
const url = "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd";
|
|
1538
|
+
const response = await fetch(url, {
|
|
1539
|
+
headers: {
|
|
1540
|
+
accept: "application/json",
|
|
1541
|
+
"user-agent": `${privateStateCliPackageJson.name}/${privateStateCliPackageJson.version}`,
|
|
1542
|
+
},
|
|
1543
|
+
});
|
|
1544
|
+
if (!response.ok) {
|
|
1545
|
+
throw new Error(`Unable to fetch live ETH/USD price from CoinGecko: HTTP ${response.status}.`);
|
|
1546
|
+
}
|
|
1547
|
+
const payload = await response.json();
|
|
1548
|
+
const value = Number(payload?.ethereum?.usd);
|
|
1549
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1550
|
+
throw new Error("CoinGecko response did not include a valid ethereum.usd price.");
|
|
1551
|
+
}
|
|
1552
|
+
return value;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function buildTransactionFeeRows({ commands, typicalGasPriceWei, worstCaseGasPriceWei, ethUsd }) {
|
|
1556
|
+
return commands.map((entry) => {
|
|
1557
|
+
const transactions = expectTransactionFeeTransactions(entry);
|
|
1558
|
+
const gasUsed = transactions.reduce((sum, transaction) => sum + Number(transaction.gasUsed), 0);
|
|
1559
|
+
const typicalEth = ethers.formatEther(BigInt(gasUsed) * typicalGasPriceWei);
|
|
1560
|
+
const worstCaseEth = ethers.formatEther(BigInt(gasUsed) * worstCaseGasPriceWei);
|
|
1561
|
+
return {
|
|
1562
|
+
command: entry.command,
|
|
1563
|
+
description: entry.description,
|
|
1564
|
+
transactions: transactions.map((transaction) => transaction.label).join(" + "),
|
|
1565
|
+
gasUsed,
|
|
1566
|
+
typicalGasPriceGwei: formatGwei(typicalGasPriceWei),
|
|
1567
|
+
typicalEth: formatEthForDisplay(typicalEth),
|
|
1568
|
+
typicalUsd: formatUsdForDisplay(Number(typicalEth) * ethUsd),
|
|
1569
|
+
worstCaseGasPriceGwei: formatGwei(worstCaseGasPriceWei),
|
|
1570
|
+
worstCaseEth: formatEthForDisplay(worstCaseEth),
|
|
1571
|
+
worstCaseUsd: formatUsdForDisplay(Number(worstCaseEth) * ethUsd),
|
|
1572
|
+
sources: [...new Set(transactions.map((transaction) => transaction.source))].join(", "),
|
|
1573
|
+
sourceDetails: transactions.map((transaction) => transaction.sourceDetail),
|
|
1574
|
+
};
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function expectTransactionFeeTransactions(entry) {
|
|
1579
|
+
expect(typeof entry?.command === "string" && entry.command.length > 0, "Transaction fee asset contains a command without command name.");
|
|
1580
|
+
expect(Array.isArray(entry.transactions) && entry.transactions.length > 0, `Transaction fee asset command ${entry.command} has no transactions.`);
|
|
1581
|
+
for (const transaction of entry.transactions) {
|
|
1582
|
+
expect(Number.isInteger(transaction.gasUsed) && transaction.gasUsed > 0, `Transaction fee asset command ${entry.command} has invalid gasUsed.`);
|
|
1583
|
+
}
|
|
1584
|
+
return entry.transactions;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function formatGwei(wei) {
|
|
1588
|
+
return trimFixedNumber(ethers.formatUnits(wei, "gwei"), 6);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function formatEthForDisplay(value) {
|
|
1592
|
+
return trimFixedNumber(value, 8);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function formatUsdForDisplay(value) {
|
|
1596
|
+
if (value > 0 && value < 0.01) {
|
|
1597
|
+
return "<0.01";
|
|
1598
|
+
}
|
|
1599
|
+
return value.toFixed(2);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function trimFixedNumber(value, maxDecimals) {
|
|
1603
|
+
const [integer, decimals = ""] = String(value).split(".");
|
|
1604
|
+
if (!decimals || maxDecimals <= 0) {
|
|
1605
|
+
return integer;
|
|
1606
|
+
}
|
|
1607
|
+
const trimmed = decimals.slice(0, maxDecimals).replace(/0+$/u, "");
|
|
1608
|
+
return trimmed ? `${integer}.${trimmed}` : integer;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1440
1611
|
function handleGetMyL1Address({ args }) {
|
|
1441
1612
|
const signer = requireL1Signer(args);
|
|
1442
1613
|
printJson({
|
|
@@ -1513,6 +1684,7 @@ async function handleGuide({ args }) {
|
|
|
1513
1684
|
nextSafeAction: null,
|
|
1514
1685
|
why: null,
|
|
1515
1686
|
candidateCommands: [],
|
|
1687
|
+
privacyTip: "For mint-notes, transfer-notes, and redeem-notes, add --tx-submitter <ACCOUNT> to let a separate local L1 account submit executeChannelTransaction and pay gas.",
|
|
1516
1688
|
};
|
|
1517
1689
|
|
|
1518
1690
|
guide.state.local = inspectGuideLocalState(args);
|
|
@@ -2006,18 +2178,18 @@ function applyGuideNextAction(guide) {
|
|
|
2006
2178
|
}
|
|
2007
2179
|
if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
|
|
2008
2180
|
setGuideNextAction(guide, {
|
|
2009
|
-
command: `mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B
|
|
2010
|
-
why: "The wallet has channel L2 balance and no unused private notes yet.",
|
|
2181
|
+
command: `mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
2182
|
+
why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
2011
2183
|
});
|
|
2012
2184
|
return;
|
|
2013
2185
|
}
|
|
2014
2186
|
if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
|
|
2015
2187
|
setGuideNextAction(guide, {
|
|
2016
|
-
command: `transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B
|
|
2017
|
-
why: "The wallet has unused private notes. It can transfer or redeem those notes.",
|
|
2188
|
+
command: `transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
2189
|
+
why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
2018
2190
|
candidates: [
|
|
2019
2191
|
`get-my-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
|
|
2020
|
-
`redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID
|
|
2192
|
+
`redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
|
|
2021
2193
|
],
|
|
2022
2194
|
});
|
|
2023
2195
|
return;
|
|
@@ -2128,9 +2300,13 @@ async function handleGetMyWalletMeta({ args, provider }) {
|
|
|
2128
2300
|
});
|
|
2129
2301
|
}
|
|
2130
2302
|
|
|
2131
|
-
async function loadWalletChannelFundState({ walletContext, provider }) {
|
|
2303
|
+
async function loadWalletChannelFundState({ walletContext, provider, progressAction = null }) {
|
|
2132
2304
|
const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
|
|
2133
|
-
const contextResult = await loadPreferredWalletChannelContext({
|
|
2305
|
+
const contextResult = await loadPreferredWalletChannelContext({
|
|
2306
|
+
walletContext,
|
|
2307
|
+
provider,
|
|
2308
|
+
progressAction,
|
|
2309
|
+
});
|
|
2134
2310
|
const context = contextResult.context;
|
|
2135
2311
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2136
2312
|
expect(
|
|
@@ -2357,7 +2533,11 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
2357
2533
|
: "withdraw";
|
|
2358
2534
|
emitProgress(operationName, "loading");
|
|
2359
2535
|
const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
|
|
2360
|
-
const contextResult = await loadPreferredWalletChannelContext({
|
|
2536
|
+
const contextResult = await loadPreferredWalletChannelContext({
|
|
2537
|
+
walletContext,
|
|
2538
|
+
provider,
|
|
2539
|
+
progressAction: operationName,
|
|
2540
|
+
});
|
|
2361
2541
|
const context = contextResult.context;
|
|
2362
2542
|
const network = contextResult.network;
|
|
2363
2543
|
expect(
|
|
@@ -2403,6 +2583,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
2403
2583
|
"The derived L2 address does not match the registered channel L2 address.",
|
|
2404
2584
|
);
|
|
2405
2585
|
|
|
2586
|
+
await assertWorkspaceAlignedWithChain(context);
|
|
2406
2587
|
const stateManager = await buildStateManager(context.currentSnapshot, context.contractCodes);
|
|
2407
2588
|
const keyHex = storageKey;
|
|
2408
2589
|
const currentValue = await currentStorageBigInt(stateManager, context.workspace.l2AccountingVault, keyHex);
|
|
@@ -2578,6 +2759,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
2578
2759
|
const { channelFund } = await loadWalletChannelFundState({
|
|
2579
2760
|
walletContext: wallet,
|
|
2580
2761
|
provider,
|
|
2762
|
+
progressAction: "mint-notes",
|
|
2581
2763
|
});
|
|
2582
2764
|
expect(
|
|
2583
2765
|
totalMintAmount <= channelFund,
|
|
@@ -2591,6 +2773,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
2591
2773
|
baseUnitAmounts: baseUnitAmounts.map(({ amountBaseUnits }) => amountBaseUnits),
|
|
2592
2774
|
});
|
|
2593
2775
|
const { execution, contextResult, recoveredWorkspace } = await executeWalletDirectTemplateCommand({
|
|
2776
|
+
args,
|
|
2594
2777
|
wallet,
|
|
2595
2778
|
provider,
|
|
2596
2779
|
operationName: "mint-notes",
|
|
@@ -2602,7 +2785,10 @@ async function handleMintNotes({ args, provider }) {
|
|
|
2602
2785
|
wallet: wallet.walletName,
|
|
2603
2786
|
workspace: execution.context.workspaceName,
|
|
2604
2787
|
operationDir: execution.operationDir,
|
|
2605
|
-
l1Submitter: execution.
|
|
2788
|
+
l1Submitter: execution.txSubmitter.address,
|
|
2789
|
+
l1WalletOwner: execution.signer.address,
|
|
2790
|
+
txSubmitterSource: execution.txSubmitterSource,
|
|
2791
|
+
txSubmitterAccount: execution.txSubmitterAccount,
|
|
2606
2792
|
l2Address: execution.l2Identity.l2Address,
|
|
2607
2793
|
underlyingMethod: templatePayload.method,
|
|
2608
2794
|
nonce: execution.nonce,
|
|
@@ -2631,6 +2817,7 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
2631
2817
|
inputNotes,
|
|
2632
2818
|
});
|
|
2633
2819
|
const { execution, contextResult, recoveredWorkspace } = await executeWalletDirectTemplateCommand({
|
|
2820
|
+
args,
|
|
2634
2821
|
wallet,
|
|
2635
2822
|
provider,
|
|
2636
2823
|
operationName: "redeem-notes",
|
|
@@ -2642,7 +2829,10 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
2642
2829
|
wallet: wallet.walletName,
|
|
2643
2830
|
workspace: execution.context.workspaceName,
|
|
2644
2831
|
operationDir: execution.operationDir,
|
|
2645
|
-
l1Submitter: execution.
|
|
2832
|
+
l1Submitter: execution.txSubmitter.address,
|
|
2833
|
+
l1WalletOwner: execution.signer.address,
|
|
2834
|
+
txSubmitterSource: execution.txSubmitterSource,
|
|
2835
|
+
txSubmitterAccount: execution.txSubmitterAccount,
|
|
2646
2836
|
l2Address: execution.l2Identity.l2Address,
|
|
2647
2837
|
receiver: wallet.wallet.l2Address,
|
|
2648
2838
|
underlyingMethod: templatePayload.method,
|
|
@@ -2669,20 +2859,18 @@ async function handleGetMyNotes({ args, provider }) {
|
|
|
2669
2859
|
`Wallet ${wallet.walletName} is missing the stored controller address.`,
|
|
2670
2860
|
);
|
|
2671
2861
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
2672
|
-
const { context } = await loadPreferredWalletChannelContext({
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
chainId: context.workspace.chainId,
|
|
2677
|
-
channelId: context.workspace.channelId,
|
|
2678
|
-
channelName: context.workspace.channelName,
|
|
2679
|
-
account: signer.address,
|
|
2862
|
+
const { context } = await loadPreferredWalletChannelContext({
|
|
2863
|
+
walletContext: wallet,
|
|
2864
|
+
provider,
|
|
2865
|
+
progressAction: "get-my-notes",
|
|
2680
2866
|
});
|
|
2681
|
-
const
|
|
2867
|
+
const signer = restoreWalletSigner(wallet, provider);
|
|
2868
|
+
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2682
2869
|
walletContext: wallet,
|
|
2683
2870
|
context,
|
|
2684
2871
|
provider,
|
|
2685
|
-
|
|
2872
|
+
signer,
|
|
2873
|
+
progressAction: "get-my-notes",
|
|
2686
2874
|
});
|
|
2687
2875
|
|
|
2688
2876
|
const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
|
|
@@ -2728,7 +2916,11 @@ async function handleGetMyNotes({ args, provider }) {
|
|
|
2728
2916
|
async function handleTransferNotes({ args, provider }) {
|
|
2729
2917
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
2730
2918
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
2731
|
-
const preparedContextResult = await loadPreferredWalletChannelContext({
|
|
2919
|
+
const preparedContextResult = await loadPreferredWalletChannelContext({
|
|
2920
|
+
walletContext: wallet,
|
|
2921
|
+
provider,
|
|
2922
|
+
progressAction: "transfer-notes",
|
|
2923
|
+
});
|
|
2732
2924
|
const context = preparedContextResult.context;
|
|
2733
2925
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
2734
2926
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
@@ -2760,6 +2952,7 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
2760
2952
|
outputAmounts,
|
|
2761
2953
|
});
|
|
2762
2954
|
const { execution, contextResult, recoveredWorkspace } = await executeWalletDirectTemplateCommand({
|
|
2955
|
+
args,
|
|
2763
2956
|
wallet,
|
|
2764
2957
|
provider,
|
|
2765
2958
|
operationName: "transfer-notes",
|
|
@@ -2777,7 +2970,10 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
2777
2970
|
wallet: wallet.walletName,
|
|
2778
2971
|
workspace: execution.context.workspaceName,
|
|
2779
2972
|
operationDir: execution.operationDir,
|
|
2780
|
-
l1Submitter: execution.
|
|
2973
|
+
l1Submitter: execution.txSubmitter.address,
|
|
2974
|
+
l1WalletOwner: execution.signer.address,
|
|
2975
|
+
txSubmitterSource: execution.txSubmitterSource,
|
|
2976
|
+
txSubmitterAccount: execution.txSubmitterAccount,
|
|
2781
2977
|
l2Address: execution.l2Identity.l2Address,
|
|
2782
2978
|
underlyingMethod: templatePayload.method,
|
|
2783
2979
|
nonce: execution.nonce,
|
|
@@ -3092,11 +3288,40 @@ function buildLifecycleTrackedOutputs({
|
|
|
3092
3288
|
}));
|
|
3093
3289
|
}
|
|
3094
3290
|
|
|
3291
|
+
async function recoverWalletReceivedNotes({
|
|
3292
|
+
walletContext,
|
|
3293
|
+
context,
|
|
3294
|
+
provider,
|
|
3295
|
+
signer,
|
|
3296
|
+
noteReceiveKeyMaterial = null,
|
|
3297
|
+
progressAction = null,
|
|
3298
|
+
}) {
|
|
3299
|
+
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ?? await deriveNoteReceiveKeyMaterial({
|
|
3300
|
+
signer,
|
|
3301
|
+
chainId: context.workspace.chainId,
|
|
3302
|
+
channelId: context.workspace.channelId,
|
|
3303
|
+
channelName: context.workspace.channelName,
|
|
3304
|
+
account: signer.address,
|
|
3305
|
+
});
|
|
3306
|
+
const recoveredDeliveryState = await recoverDeliveredNotesFromEventLogs({
|
|
3307
|
+
walletContext,
|
|
3308
|
+
context,
|
|
3309
|
+
provider,
|
|
3310
|
+
noteReceivePrivateKey: resolvedNoteReceiveKeyMaterial.privateKey,
|
|
3311
|
+
progressAction,
|
|
3312
|
+
});
|
|
3313
|
+
return {
|
|
3314
|
+
noteReceiveKeyMaterial: resolvedNoteReceiveKeyMaterial,
|
|
3315
|
+
recoveredDeliveryState,
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3095
3319
|
async function recoverDeliveredNotesFromEventLogs({
|
|
3096
3320
|
walletContext,
|
|
3097
3321
|
context,
|
|
3098
3322
|
provider,
|
|
3099
3323
|
noteReceivePrivateKey,
|
|
3324
|
+
progressAction = null,
|
|
3100
3325
|
}) {
|
|
3101
3326
|
const scanStartBlock = Math.max(
|
|
3102
3327
|
Number(walletContext.wallet.noteReceiveLastScannedBlock),
|
|
@@ -3128,6 +3353,9 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
3128
3353
|
topics: [NOTE_VALUE_ENCRYPTED_TOPIC],
|
|
3129
3354
|
fromBlock: scanStartBlock,
|
|
3130
3355
|
toBlock: latestBlock,
|
|
3356
|
+
onProgress: progressAction
|
|
3357
|
+
? createRpcLogScanProgress({ action: progressAction, label: "note-delivery events" })
|
|
3358
|
+
: null,
|
|
3131
3359
|
});
|
|
3132
3360
|
|
|
3133
3361
|
const importedCandidates = [];
|
|
@@ -3666,10 +3894,15 @@ function walletChannelWorkspaceIsReady(walletContext) {
|
|
|
3666
3894
|
&& fs.existsSync(path.join(channelWorkspaceCurrentPath(workspaceDir), "contract_codes.json"));
|
|
3667
3895
|
}
|
|
3668
3896
|
|
|
3669
|
-
async function loadPreferredWalletChannelContext({
|
|
3897
|
+
async function loadPreferredWalletChannelContext({
|
|
3898
|
+
walletContext,
|
|
3899
|
+
provider,
|
|
3900
|
+
forceRecover = false,
|
|
3901
|
+
progressAction = null,
|
|
3902
|
+
}) {
|
|
3670
3903
|
let recoveredWorkspace = false;
|
|
3671
3904
|
if (forceRecover || !walletChannelWorkspaceIsReady(walletContext)) {
|
|
3672
|
-
await recoverWalletChannelWorkspace({ walletContext, provider });
|
|
3905
|
+
await recoverWalletChannelWorkspace({ walletContext, provider, progressAction });
|
|
3673
3906
|
recoveredWorkspace = true;
|
|
3674
3907
|
}
|
|
3675
3908
|
let context = await loadWorkspaceContext(walletContext.wallet.channelName, walletContext.wallet.network, provider);
|
|
@@ -3679,7 +3912,7 @@ async function loadPreferredWalletChannelContext({ walletContext, provider, forc
|
|
|
3679
3912
|
if (!isRecoverableWalletWorkspaceFailure(error)) {
|
|
3680
3913
|
throw error;
|
|
3681
3914
|
}
|
|
3682
|
-
await recoverWalletChannelWorkspace({ walletContext, provider });
|
|
3915
|
+
await recoverWalletChannelWorkspace({ walletContext, provider, progressAction });
|
|
3683
3916
|
recoveredWorkspace = true;
|
|
3684
3917
|
context = await loadWorkspaceContext(walletContext.wallet.channelName, walletContext.wallet.network, provider);
|
|
3685
3918
|
await assertWorkspaceAlignedWithChain(context);
|
|
@@ -3692,7 +3925,7 @@ async function loadPreferredWalletChannelContext({ walletContext, provider, forc
|
|
|
3692
3925
|
};
|
|
3693
3926
|
}
|
|
3694
3927
|
|
|
3695
|
-
async function recoverWalletChannelWorkspace({ walletContext, provider }) {
|
|
3928
|
+
async function recoverWalletChannelWorkspace({ walletContext, provider, progressAction = null }) {
|
|
3696
3929
|
const networkName = walletContext.wallet.network ?? networkNameFromChainId(Number(walletContext.wallet.chainId));
|
|
3697
3930
|
const network = resolveCliNetwork(networkName);
|
|
3698
3931
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
@@ -3704,6 +3937,8 @@ async function recoverWalletChannelWorkspace({ walletContext, provider }) {
|
|
|
3704
3937
|
bridgeResources,
|
|
3705
3938
|
persist: true,
|
|
3706
3939
|
allowExistingWorkspaceSync: true,
|
|
3940
|
+
useWorkspaceRecoveryIndex: true,
|
|
3941
|
+
progressAction,
|
|
3707
3942
|
});
|
|
3708
3943
|
}
|
|
3709
3944
|
|
|
@@ -3725,6 +3960,7 @@ function assertWalletMatchesChannelContext(walletContext, l2Identity, context) {
|
|
|
3725
3960
|
}
|
|
3726
3961
|
|
|
3727
3962
|
async function executeWalletDirectTemplateCommand({
|
|
3963
|
+
args,
|
|
3728
3964
|
wallet,
|
|
3729
3965
|
provider,
|
|
3730
3966
|
operationName,
|
|
@@ -3732,13 +3968,29 @@ async function executeWalletDirectTemplateCommand({
|
|
|
3732
3968
|
}) {
|
|
3733
3969
|
emitProgress(operationName, "loading");
|
|
3734
3970
|
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
3735
|
-
|
|
3971
|
+
const {
|
|
3972
|
+
txSubmitter,
|
|
3973
|
+
source: txSubmitterSource,
|
|
3974
|
+
account: txSubmitterAccount,
|
|
3975
|
+
} = resolveTxSubmitterSigner({
|
|
3976
|
+
args,
|
|
3977
|
+
ownerSigner: signer,
|
|
3978
|
+
provider,
|
|
3979
|
+
});
|
|
3980
|
+
let contextResult = await loadPreferredWalletChannelContext({
|
|
3981
|
+
walletContext: wallet,
|
|
3982
|
+
provider,
|
|
3983
|
+
progressAction: operationName,
|
|
3984
|
+
});
|
|
3736
3985
|
let recoveredWorkspace = contextResult.recoveredWorkspace;
|
|
3737
3986
|
|
|
3738
3987
|
try {
|
|
3739
3988
|
const execution = await executeWalletTemplateSend({
|
|
3740
3989
|
wallet,
|
|
3741
3990
|
signer,
|
|
3991
|
+
txSubmitter,
|
|
3992
|
+
txSubmitterSource,
|
|
3993
|
+
txSubmitterAccount,
|
|
3742
3994
|
l2Identity,
|
|
3743
3995
|
context: contextResult.context,
|
|
3744
3996
|
operationName,
|
|
@@ -3761,11 +4013,15 @@ async function executeWalletDirectTemplateCommand({
|
|
|
3761
4013
|
walletContext: wallet,
|
|
3762
4014
|
provider,
|
|
3763
4015
|
forceRecover: true,
|
|
4016
|
+
progressAction: operationName,
|
|
3764
4017
|
});
|
|
3765
4018
|
recoveredWorkspace = contextResult.recoveredWorkspace;
|
|
3766
4019
|
const execution = await executeWalletTemplateSend({
|
|
3767
4020
|
wallet,
|
|
3768
4021
|
signer,
|
|
4022
|
+
txSubmitter,
|
|
4023
|
+
txSubmitterSource,
|
|
4024
|
+
txSubmitterAccount,
|
|
3769
4025
|
l2Identity,
|
|
3770
4026
|
context: contextResult.context,
|
|
3771
4027
|
operationName,
|
|
@@ -3783,6 +4039,9 @@ async function executeWalletDirectTemplateCommand({
|
|
|
3783
4039
|
async function executeWalletTemplateSend({
|
|
3784
4040
|
wallet,
|
|
3785
4041
|
signer,
|
|
4042
|
+
txSubmitter,
|
|
4043
|
+
txSubmitterSource,
|
|
4044
|
+
txSubmitterAccount,
|
|
3786
4045
|
l2Identity,
|
|
3787
4046
|
context,
|
|
3788
4047
|
operationName,
|
|
@@ -3860,7 +4119,7 @@ async function executeWalletTemplateSend({
|
|
|
3860
4119
|
emitProgress(operationName, "submitting");
|
|
3861
4120
|
const receipt =
|
|
3862
4121
|
await waitForReceipt(
|
|
3863
|
-
await context.channelManager.connect(
|
|
4122
|
+
await context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
|
|
3864
4123
|
);
|
|
3865
4124
|
|
|
3866
4125
|
const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
|
|
@@ -3884,6 +4143,9 @@ async function executeWalletTemplateSend({
|
|
|
3884
4143
|
return {
|
|
3885
4144
|
wallet,
|
|
3886
4145
|
signer,
|
|
4146
|
+
txSubmitter,
|
|
4147
|
+
txSubmitterSource,
|
|
4148
|
+
txSubmitterAccount,
|
|
3887
4149
|
l2Identity,
|
|
3888
4150
|
context,
|
|
3889
4151
|
noteLifecycle,
|
|
@@ -4732,6 +4994,7 @@ async function reconstructChannelSnapshot({
|
|
|
4732
4994
|
baseSnapshot = null,
|
|
4733
4995
|
fromBlock = genesisBlockNumber,
|
|
4734
4996
|
toBlock = null,
|
|
4997
|
+
progressAction = null,
|
|
4735
4998
|
}) {
|
|
4736
4999
|
let startingSnapshot = baseSnapshot;
|
|
4737
5000
|
if (!startingSnapshot) {
|
|
@@ -4763,6 +5026,9 @@ async function reconstructChannelSnapshot({
|
|
|
4763
5026
|
]],
|
|
4764
5027
|
fromBlock: scanFromBlock,
|
|
4765
5028
|
toBlock: latestBlock,
|
|
5029
|
+
onProgress: progressAction
|
|
5030
|
+
? createRpcLogScanProgress({ action: progressAction, label: "channel-manager events" })
|
|
5031
|
+
: null,
|
|
4766
5032
|
});
|
|
4767
5033
|
const channelManagerEvents = channelManagerLogs.map((log) => {
|
|
4768
5034
|
const topic0 = log.topics[0] ? normalizeBytes32Hex(log.topics[0]) : null;
|
|
@@ -4781,6 +5047,9 @@ async function reconstructChannelSnapshot({
|
|
|
4781
5047
|
eventName: "StorageWriteObserved",
|
|
4782
5048
|
fromBlock: scanFromBlock,
|
|
4783
5049
|
toBlock: latestBlock,
|
|
5050
|
+
onProgress: progressAction
|
|
5051
|
+
? createRpcLogScanProgress({ action: progressAction, label: "bridge-vault events" })
|
|
5052
|
+
: null,
|
|
4784
5053
|
});
|
|
4785
5054
|
|
|
4786
5055
|
const groupedEvents = new Map();
|
|
@@ -4900,15 +5169,34 @@ async function fetchLogsChunked(provider, {
|
|
|
4900
5169
|
fromBlock,
|
|
4901
5170
|
toBlock,
|
|
4902
5171
|
initialChunkSize = DEFAULT_LOG_CHUNK_SIZE,
|
|
5172
|
+
onProgress = null,
|
|
4903
5173
|
}) {
|
|
4904
5174
|
const normalizedFromBlock = Number(fromBlock);
|
|
4905
5175
|
const resolvedToBlock = toBlock === "latest" ? await provider.getBlockNumber() : Number(toBlock);
|
|
4906
5176
|
const aggregatedLogs = [];
|
|
4907
5177
|
|
|
4908
5178
|
if (normalizedFromBlock > resolvedToBlock) {
|
|
5179
|
+
onProgress?.({
|
|
5180
|
+
status: "skipped",
|
|
5181
|
+
fromBlock: normalizedFromBlock,
|
|
5182
|
+
toBlock: resolvedToBlock,
|
|
5183
|
+
scannedBlocks: 0,
|
|
5184
|
+
totalBlocks: 0,
|
|
5185
|
+
logsFound: 0,
|
|
5186
|
+
});
|
|
4909
5187
|
return aggregatedLogs;
|
|
4910
5188
|
}
|
|
4911
5189
|
|
|
5190
|
+
const totalBlocks = resolvedToBlock - normalizedFromBlock + 1;
|
|
5191
|
+
onProgress?.({
|
|
5192
|
+
status: "start",
|
|
5193
|
+
fromBlock: normalizedFromBlock,
|
|
5194
|
+
toBlock: resolvedToBlock,
|
|
5195
|
+
scannedBlocks: 0,
|
|
5196
|
+
totalBlocks,
|
|
5197
|
+
logsFound: 0,
|
|
5198
|
+
});
|
|
5199
|
+
|
|
4912
5200
|
let chunkSize = Math.max(1, Number(initialChunkSize));
|
|
4913
5201
|
let cursor = normalizedFromBlock;
|
|
4914
5202
|
while (cursor <= resolvedToBlock) {
|
|
@@ -4922,6 +5210,17 @@ async function fetchLogsChunked(provider, {
|
|
|
4922
5210
|
toBlock: chunkToBlock,
|
|
4923
5211
|
});
|
|
4924
5212
|
aggregatedLogs.push(...logs);
|
|
5213
|
+
onProgress?.({
|
|
5214
|
+
status: "progress",
|
|
5215
|
+
fromBlock: normalizedFromBlock,
|
|
5216
|
+
toBlock: resolvedToBlock,
|
|
5217
|
+
chunkFromBlock: cursor,
|
|
5218
|
+
chunkToBlock,
|
|
5219
|
+
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
5220
|
+
totalBlocks,
|
|
5221
|
+
logsFound: aggregatedLogs.length,
|
|
5222
|
+
chunkLogs: logs.length,
|
|
5223
|
+
});
|
|
4925
5224
|
cursor = chunkToBlock + 1;
|
|
4926
5225
|
} catch (error) {
|
|
4927
5226
|
if (isRateLimitError(error)) {
|
|
@@ -4938,6 +5237,15 @@ async function fetchLogsChunked(provider, {
|
|
|
4938
5237
|
}
|
|
4939
5238
|
}
|
|
4940
5239
|
|
|
5240
|
+
onProgress?.({
|
|
5241
|
+
status: "done",
|
|
5242
|
+
fromBlock: normalizedFromBlock,
|
|
5243
|
+
toBlock: resolvedToBlock,
|
|
5244
|
+
scannedBlocks: totalBlocks,
|
|
5245
|
+
totalBlocks,
|
|
5246
|
+
logsFound: aggregatedLogs.length,
|
|
5247
|
+
});
|
|
5248
|
+
|
|
4941
5249
|
return aggregatedLogs;
|
|
4942
5250
|
}
|
|
4943
5251
|
|
|
@@ -4995,6 +5303,7 @@ async function queryContractEventsChunked({
|
|
|
4995
5303
|
eventName,
|
|
4996
5304
|
fromBlock,
|
|
4997
5305
|
toBlock,
|
|
5306
|
+
onProgress = null,
|
|
4998
5307
|
}) {
|
|
4999
5308
|
const eventFragment = contract.interface.getEvent(eventName);
|
|
5000
5309
|
const eventTopic = contract.interface.getEvent(eventName).topicHash;
|
|
@@ -5006,6 +5315,7 @@ async function queryContractEventsChunked({
|
|
|
5006
5315
|
topics: [eventTopic],
|
|
5007
5316
|
fromBlock,
|
|
5008
5317
|
toBlock,
|
|
5318
|
+
onProgress,
|
|
5009
5319
|
});
|
|
5010
5320
|
|
|
5011
5321
|
return logs.map((log) => {
|
|
@@ -5278,6 +5588,33 @@ function requireArg(value, label) {
|
|
|
5278
5588
|
return value;
|
|
5279
5589
|
}
|
|
5280
5590
|
|
|
5591
|
+
function assertVersionArgs(args) {
|
|
5592
|
+
if (args.version !== true) {
|
|
5593
|
+
throw new Error("--version does not accept a value.");
|
|
5594
|
+
}
|
|
5595
|
+
if (args.command) {
|
|
5596
|
+
throw new Error("--version must be used without a command.");
|
|
5597
|
+
}
|
|
5598
|
+
const allowedKeys = new Set(["version", "json"]);
|
|
5599
|
+
const unknownKeys = Object.keys(args)
|
|
5600
|
+
.filter((key) => key !== "positional" && key !== "command" && !allowedKeys.has(key));
|
|
5601
|
+
if (unknownKeys.length > 0) {
|
|
5602
|
+
throw new Error(`Unsupported --version option(s): ${unknownKeys.map(toKebabCase).join(", ")}.`);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
function printVersion() {
|
|
5607
|
+
if (isJsonOutputRequested()) {
|
|
5608
|
+
printJson({
|
|
5609
|
+
action: "version",
|
|
5610
|
+
packageName: privateStateCliPackageJson.name,
|
|
5611
|
+
version: privateStateCliPackageJson.version,
|
|
5612
|
+
});
|
|
5613
|
+
return;
|
|
5614
|
+
}
|
|
5615
|
+
console.log(privateStateCliPackageJson.version);
|
|
5616
|
+
}
|
|
5617
|
+
|
|
5281
5618
|
function requireWorkspaceName(args) {
|
|
5282
5619
|
const value = typeof args === "string" ? args : args.workspace;
|
|
5283
5620
|
if (!value) {
|
|
@@ -5306,6 +5643,29 @@ function requireL1Signer(args, provider) {
|
|
|
5306
5643
|
return new Wallet(resolvePrivateKeySource(args), provider);
|
|
5307
5644
|
}
|
|
5308
5645
|
|
|
5646
|
+
function resolveTxSubmitterSigner({ args, ownerSigner, provider }) {
|
|
5647
|
+
if (args.txSubmitter === undefined) {
|
|
5648
|
+
return {
|
|
5649
|
+
txSubmitter: ownerSigner,
|
|
5650
|
+
source: "wallet-owner",
|
|
5651
|
+
account: null,
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
if (args.txSubmitter === true || String(args.txSubmitter).trim() === "") {
|
|
5655
|
+
throw new Error("--tx-submitter requires a local account name.");
|
|
5656
|
+
}
|
|
5657
|
+
const networkName = requireNetworkName(args);
|
|
5658
|
+
const account = String(args.txSubmitter).trim();
|
|
5659
|
+
return {
|
|
5660
|
+
txSubmitter: new Wallet(
|
|
5661
|
+
normalizePrivateKey(readSecretFile(accountPrivateKeyPath(networkName, account), "--tx-submitter")),
|
|
5662
|
+
provider,
|
|
5663
|
+
),
|
|
5664
|
+
source: "tx-submitter-account",
|
|
5665
|
+
account,
|
|
5666
|
+
};
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5309
5669
|
function resolvePrivateKeySource(args) {
|
|
5310
5670
|
const networkName = requireNetworkName(args);
|
|
5311
5671
|
const account = requireAccountName(args);
|
|
@@ -5616,12 +5976,17 @@ function assertGuideArgs(args) {
|
|
|
5616
5976
|
assertAllowedCommandSchema(args, "guide");
|
|
5617
5977
|
}
|
|
5618
5978
|
|
|
5979
|
+
function assertTransactionFeesArgs(args) {
|
|
5980
|
+
assertAllowedCommandSchema(args, "transaction-fees");
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5619
5983
|
function assertAccountImportArgs(args) {
|
|
5620
5984
|
assertAllowedCommandSchema(args, "account-import");
|
|
5621
5985
|
}
|
|
5622
5986
|
|
|
5623
5987
|
function assertMintNotesArgs(args) {
|
|
5624
5988
|
assertAllowedCommandSchema(args, "mint-notes");
|
|
5989
|
+
assertTxSubmitterArg(args);
|
|
5625
5990
|
parseAmountVector(args.amounts, {
|
|
5626
5991
|
allowZeroEntries: true,
|
|
5627
5992
|
requireAnyPositive: true,
|
|
@@ -5630,11 +5995,13 @@ function assertMintNotesArgs(args) {
|
|
|
5630
5995
|
|
|
5631
5996
|
function assertRedeemNotesArgs(args) {
|
|
5632
5997
|
assertAllowedCommandSchema(args, "redeem-notes");
|
|
5998
|
+
assertTxSubmitterArg(args);
|
|
5633
5999
|
selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
|
|
5634
6000
|
}
|
|
5635
6001
|
|
|
5636
6002
|
function assertTransferNotesArgs(args) {
|
|
5637
6003
|
assertAllowedCommandSchema(args, "transfer-notes");
|
|
6004
|
+
assertTxSubmitterArg(args);
|
|
5638
6005
|
const noteIds = parseNoteIdVector(args.noteIds);
|
|
5639
6006
|
const recipients = parseRecipientVector(args.recipients);
|
|
5640
6007
|
const amounts = parseAmountVector(args.amounts);
|
|
@@ -5645,6 +6012,15 @@ function assertTransferNotesArgs(args) {
|
|
|
5645
6012
|
selectTransferNotesMethod(noteIds.length, recipients.length);
|
|
5646
6013
|
}
|
|
5647
6014
|
|
|
6015
|
+
function assertTxSubmitterArg(args) {
|
|
6016
|
+
if (args.txSubmitter === undefined) {
|
|
6017
|
+
return;
|
|
6018
|
+
}
|
|
6019
|
+
if (args.txSubmitter === true || String(args.txSubmitter).trim() === "") {
|
|
6020
|
+
throw new Error("--tx-submitter requires a local account name.");
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
|
|
5648
6024
|
function assertGetMyNotesArgs(args) {
|
|
5649
6025
|
assertWalletSecretArgs(args, "get-my-notes");
|
|
5650
6026
|
}
|
|
@@ -5769,6 +6145,9 @@ Secret source options:
|
|
|
5769
6145
|
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
5770
6146
|
|
|
5771
6147
|
Options:
|
|
6148
|
+
--version
|
|
6149
|
+
Print the private-state CLI package version and exit.
|
|
6150
|
+
|
|
5772
6151
|
--json
|
|
5773
6152
|
Print the command result as JSON. Without --json, commands print human-readable output.
|
|
5774
6153
|
|
|
@@ -6197,6 +6576,7 @@ function loadWalletCommandRuntime(args) {
|
|
|
6197
6576
|
|
|
6198
6577
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
6199
6578
|
guide: printGuideHumanResult,
|
|
6579
|
+
"transaction-fees": printTransactionFeesHumanResult,
|
|
6200
6580
|
});
|
|
6201
6581
|
|
|
6202
6582
|
function normalizePrivateKey(value) {
|
|
@@ -6246,9 +6626,61 @@ function printGuideHumanResult(guide) {
|
|
|
6246
6626
|
}
|
|
6247
6627
|
|
|
6248
6628
|
lines.push("", "Run with --json to inspect the full guide state.");
|
|
6629
|
+
lines.push(
|
|
6630
|
+
"",
|
|
6631
|
+
"Privacy Tip",
|
|
6632
|
+
formatHumanValue(guide.privacyTip),
|
|
6633
|
+
);
|
|
6249
6634
|
console.log(lines.join("\n"));
|
|
6250
6635
|
}
|
|
6251
6636
|
|
|
6637
|
+
function printTransactionFeesHumanResult(report) {
|
|
6638
|
+
const lines = [
|
|
6639
|
+
"Transaction Fees",
|
|
6640
|
+
`Generated: ${formatHumanValue(report.generatedAt)}`,
|
|
6641
|
+
`Network: ${formatHumanValue(report.network)} (${formatHumanValue(report.chainId)})`,
|
|
6642
|
+
`Typical gas price: ${formatHumanValue(report.livePricing?.typicalGasPriceGwei)} gwei (${formatHumanValue(report.livePricing?.typicalGasPriceSource)})`,
|
|
6643
|
+
`Worst-case gas price: ${formatHumanValue(report.livePricing?.worstCaseGasPriceGwei)} gwei (${formatHumanValue(report.livePricing?.worstCaseGasPriceSource)})`,
|
|
6644
|
+
`ETH/USD: $${formatHumanValue(report.livePricing?.ethUsd)} (${formatHumanValue(report.livePricing?.ethUsdSource)})`,
|
|
6645
|
+
`Measured gas asset: ${formatHumanValue(report.asset?.schema)}, measured ${formatHumanValue(report.asset?.measuredAt)}`,
|
|
6646
|
+
"",
|
|
6647
|
+
formatHumanTable(
|
|
6648
|
+
["Command", "Transactions", "Gas", "Typical ETH", "Typical USD", "Worst ETH", "Worst USD", "Source"],
|
|
6649
|
+
(report.rows ?? []).map((row) => [
|
|
6650
|
+
row.command,
|
|
6651
|
+
row.transactions,
|
|
6652
|
+
String(row.gasUsed),
|
|
6653
|
+
row.typicalEth,
|
|
6654
|
+
`$${row.typicalUsd}`,
|
|
6655
|
+
row.worstCaseEth,
|
|
6656
|
+
`$${row.worstCaseUsd}`,
|
|
6657
|
+
row.sources,
|
|
6658
|
+
]),
|
|
6659
|
+
),
|
|
6660
|
+
];
|
|
6661
|
+
if (Array.isArray(report.asset?.notes) && report.asset.notes.length > 0) {
|
|
6662
|
+
lines.push(
|
|
6663
|
+
"",
|
|
6664
|
+
"Notes",
|
|
6665
|
+
...report.asset.notes.map((note) => `- ${note}`),
|
|
6666
|
+
);
|
|
6667
|
+
}
|
|
6668
|
+
console.log(lines.join("\n"));
|
|
6669
|
+
}
|
|
6670
|
+
|
|
6671
|
+
function formatHumanTable(headers, rows) {
|
|
6672
|
+
const values = [headers, ...rows].map((row) => row.map((value) => String(value ?? "")));
|
|
6673
|
+
const widths = headers.map((_header, columnIndex) =>
|
|
6674
|
+
Math.max(...values.map((row) => row[columnIndex].length)),
|
|
6675
|
+
);
|
|
6676
|
+
const formatRow = (row) => `| ${row.map((value, index) => value.padEnd(widths[index])).join(" | ")} |`;
|
|
6677
|
+
return [
|
|
6678
|
+
formatRow(values[0]),
|
|
6679
|
+
formatRow(widths.map((width) => "-".repeat(width))),
|
|
6680
|
+
...values.slice(1).map(formatRow),
|
|
6681
|
+
].join("\n");
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6252
6684
|
function formatGuideSelector(value) {
|
|
6253
6685
|
return value === null || value === undefined || value === "" ? "not selected" : String(value);
|
|
6254
6686
|
}
|
|
@@ -6327,6 +6759,51 @@ function emitProgress(action, phase) {
|
|
|
6327
6759
|
}
|
|
6328
6760
|
}
|
|
6329
6761
|
|
|
6762
|
+
function createRpcLogScanProgress({ action, label }) {
|
|
6763
|
+
let lastBucket = -1;
|
|
6764
|
+
return (event) => {
|
|
6765
|
+
const totalBlocks = Number(event.totalBlocks ?? 0);
|
|
6766
|
+
const scannedBlocks = Number(event.scannedBlocks ?? 0);
|
|
6767
|
+
const logsFound = Number(event.logsFound ?? 0);
|
|
6768
|
+
if (event.status === "skipped") {
|
|
6769
|
+
emitProgress(action, `rpc-log-scan ${label}: skipped (no blocks to scan, ${logsFound} logs)`);
|
|
6770
|
+
return;
|
|
6771
|
+
}
|
|
6772
|
+
if (event.status === "start") {
|
|
6773
|
+
lastBucket = 0;
|
|
6774
|
+
emitProgress(
|
|
6775
|
+
action,
|
|
6776
|
+
`rpc-log-scan ${label}: 0% (0/${totalBlocks} blocks, ${logsFound} logs, blocks ${event.fromBlock}-${event.toBlock})`,
|
|
6777
|
+
);
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
if (event.status === "done") {
|
|
6781
|
+
emitProgress(
|
|
6782
|
+
action,
|
|
6783
|
+
`rpc-log-scan ${label}: 100% (${totalBlocks}/${totalBlocks} blocks, ${logsFound} logs, done)`,
|
|
6784
|
+
);
|
|
6785
|
+
return;
|
|
6786
|
+
}
|
|
6787
|
+
if (event.status !== "progress" || totalBlocks <= 0) {
|
|
6788
|
+
return;
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6791
|
+
const percent = Math.min(100, Math.floor((scannedBlocks * 100) / totalBlocks));
|
|
6792
|
+
if (percent >= 100) {
|
|
6793
|
+
return;
|
|
6794
|
+
}
|
|
6795
|
+
const bucket = Math.floor(percent / 10) * 10;
|
|
6796
|
+
if (bucket <= lastBucket) {
|
|
6797
|
+
return;
|
|
6798
|
+
}
|
|
6799
|
+
lastBucket = bucket;
|
|
6800
|
+
emitProgress(
|
|
6801
|
+
action,
|
|
6802
|
+
`rpc-log-scan ${label}: ${percent}% (${scannedBlocks}/${totalBlocks} blocks, ${logsFound} logs)`,
|
|
6803
|
+
);
|
|
6804
|
+
};
|
|
6805
|
+
}
|
|
6806
|
+
|
|
6330
6807
|
function formatCliErrorForDisplay(error, args = {}) {
|
|
6331
6808
|
const message = String(error?.message ?? error);
|
|
6332
6809
|
const hints = buildRecoveryHints(error, args);
|