@tokamak-private-dapps/private-state-cli 1.0.1 → 1.0.2
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,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.2 - 2026-05-06
|
|
6
|
+
|
|
7
|
+
- Added `update`, which checks npm registry for the latest private-state CLI package and updates
|
|
8
|
+
global npm installs when a newer version exists.
|
|
9
|
+
- Kept repository checkouts and non-global installs read-only during `update`; those modes print
|
|
10
|
+
the exact `npm install -g @tokamak-private-dapps/private-state-cli@latest` command instead.
|
|
11
|
+
- Reused runtime-management output parsing helpers for `update` and `uninstall` instead of
|
|
12
|
+
duplicating npm JSON and ANSI-output handling in the CLI entrypoint.
|
|
5
13
|
- Added `transaction-fees`, which reads packaged measured gas data from `assets/tx-fees.json`,
|
|
6
14
|
combines it with live RPC fee data and live ETH/USD pricing, and prints a per-command ETH/USD
|
|
7
15
|
fee table.
|
|
@@ -15,6 +23,12 @@
|
|
|
15
23
|
guiding new users through `join-channel`.
|
|
16
24
|
- Added RPC log scan progress output to `recover-workspace` and `recover-wallet`, with progress
|
|
17
25
|
routed to stderr in `--json` mode so machine-readable command results stay valid.
|
|
26
|
+
- Added `recover-wallet --from-genesis` and removed implicit genesis replay fallback from
|
|
27
|
+
`recover-workspace` and `recover-wallet`; both commands now require a usable recovery index
|
|
28
|
+
unless the user explicitly requests `--from-genesis`.
|
|
29
|
+
- Changed `get-my-wallet-meta`, `get-my-channel-fund`, and `get-my-notes` to use indexed
|
|
30
|
+
recovery only before reading channel state, with `get-my-notes` also validating the wallet
|
|
31
|
+
note-receive recovery index before scanning delivery logs.
|
|
18
32
|
- Unified wallet command workspace refresh through the same recovery-indexed path used by
|
|
19
33
|
`recover-workspace`, and shared received-note recovery through the wallet's
|
|
20
34
|
`noteReceiveLastScannedBlock` index.
|
package/README.md
CHANGED
|
@@ -57,6 +57,16 @@ Print only the installed CLI package version with:
|
|
|
57
57
|
private-state-cli --version
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
Check npm registry for a newer CLI package and update a global npm install when possible:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
private-state-cli update
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`update` keeps `--version` suitable for scripts by using a separate command for registry checks. If the CLI is running
|
|
67
|
+
from a repository checkout or npm does not report a global install, it does not edit local source files; it prints the
|
|
68
|
+
recommended `npm install -g @tokamak-private-dapps/private-state-cli@latest` command instead.
|
|
69
|
+
|
|
60
70
|
Remove all local private-state CLI data with:
|
|
61
71
|
|
|
62
72
|
```bash
|
|
@@ -86,6 +96,24 @@ A common private-state flow is:
|
|
|
86
96
|
|
|
87
97
|
Use `private-state-cli --help` for the full command list and required options.
|
|
88
98
|
|
|
99
|
+
Workspace recovery commands use the saved recovery index by default. If the local workspace is missing, corrupted, or
|
|
100
|
+
does not contain a usable index, `recover-workspace` and `recover-wallet` stop with an explicit error instead of
|
|
101
|
+
silently replaying logs from channel genesis. Use `--from-genesis` only when you intentionally want to rebuild from the
|
|
102
|
+
channel creation block:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
private-state-cli recover-workspace --channel-name <CHANNEL> --network mainnet --from-genesis
|
|
106
|
+
private-state-cli recover-wallet --channel-name <CHANNEL> --network mainnet --account <ACCOUNT> --from-genesis
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`create-channel` is the exception: after the channel is created on-chain, the CLI initializes that new local workspace
|
|
110
|
+
by replaying from the channel's genesis block because no prior recovery index can exist for a new channel.
|
|
111
|
+
|
|
112
|
+
Wallet getter commands that need channel state, including `get-my-wallet-meta`, `get-my-channel-fund`, and
|
|
113
|
+
`get-my-notes`, follow the same indexed recovery rule before reading local or on-chain state. `get-my-notes` also uses
|
|
114
|
+
the wallet's saved note-receive scan index for encrypted note delivery logs. If either index is unusable, the command
|
|
115
|
+
stops and asks the user to run the appropriate recovery command with `--from-genesis`.
|
|
116
|
+
|
|
89
117
|
Estimate live transaction costs before sending commands with:
|
|
90
118
|
|
|
91
119
|
```bash
|
|
@@ -205,6 +233,9 @@ Operating rules:
|
|
|
205
233
|
`<channelName>-<l1Address>`.
|
|
206
234
|
- The network RPC URL is the endpoint used to read and write chain state. It can be supplied once with `--rpc-url`
|
|
207
235
|
on a bridge-facing command, after which the CLI saves it under the selected network.
|
|
236
|
+
- A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
|
|
237
|
+
without replaying the channel from its creation block. If it is missing, explain `--from-genesis` before using it
|
|
238
|
+
because genesis replay can take much longer.
|
|
208
239
|
- When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
|
|
209
240
|
selected network. They can obtain one from an infrastructure provider such as Alchemy, Infura, QuickNode, or from
|
|
210
241
|
their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only the endpoint URL
|
|
@@ -167,6 +167,16 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
167
167
|
fields: [],
|
|
168
168
|
usage: "no options",
|
|
169
169
|
},
|
|
170
|
+
{
|
|
171
|
+
id: "update",
|
|
172
|
+
description: "Check npm registry for the latest private-state CLI package and update global installs when possible.",
|
|
173
|
+
fields: ["json"],
|
|
174
|
+
usage: "optional --json",
|
|
175
|
+
help: [
|
|
176
|
+
"Global npm installs are updated with npm install -g when a newer registry version exists",
|
|
177
|
+
"Repository checkouts and non-global installs print the required update command instead of modifying source files",
|
|
178
|
+
],
|
|
179
|
+
},
|
|
170
180
|
{
|
|
171
181
|
id: "doctor",
|
|
172
182
|
description: "Check private-state CLI package versions, runtime install state, Docker mode, CUDA mode, and deployment artifacts.",
|
|
@@ -207,7 +217,10 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
207
217
|
description: "Create a bridge channel and initialize its workspace.",
|
|
208
218
|
fields: ["channelName", "joinToll", "network", "account", "rpcUrl"],
|
|
209
219
|
usage: "--channel-name, --join-toll, --network, --account, and optional --rpc-url",
|
|
210
|
-
help: [
|
|
220
|
+
help: [
|
|
221
|
+
"Prints the immutable policy snapshot before sending the transaction",
|
|
222
|
+
"Initializes the local channel workspace by replaying channel logs from channel genesis",
|
|
223
|
+
],
|
|
211
224
|
},
|
|
212
225
|
{
|
|
213
226
|
id: "recover-workspace",
|
|
@@ -216,6 +229,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
216
229
|
usage: "--channel-name, --network, optional --from-genesis, and optional --rpc-url",
|
|
217
230
|
help: [
|
|
218
231
|
"By default, resumes RPC log scanning from the workspace recovery index when available",
|
|
232
|
+
"Fails instead of falling back to genesis when no usable recovery index exists",
|
|
219
233
|
"Use --from-genesis to ignore the recovery index and replay logs from channel genesis",
|
|
220
234
|
"Prints RPC log scan progress while rebuilding the workspace",
|
|
221
235
|
],
|
|
@@ -247,11 +261,14 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
247
261
|
{
|
|
248
262
|
id: "recover-wallet",
|
|
249
263
|
description: "Rebuild a recoverable local wallet from on-chain channel state.",
|
|
250
|
-
fields: ["channelName", "network", "account", "rpcUrl"],
|
|
251
|
-
usage: "--channel-name, --network, --account, and optional --rpc-url",
|
|
264
|
+
fields: ["channelName", "network", "account", "fromGenesis", "rpcUrl"],
|
|
265
|
+
usage: "--channel-name, --network, --account, optional --from-genesis, and optional --rpc-url",
|
|
252
266
|
help: [
|
|
253
267
|
"Requires the protected wallet-local secret imported during join-channel to exist at the canonical secret path",
|
|
254
268
|
"Does not create or recover the wallet secret itself",
|
|
269
|
+
"By default, resumes RPC log scanning from the workspace recovery index when available",
|
|
270
|
+
"Fails instead of falling back to genesis when no usable recovery index exists",
|
|
271
|
+
"Use --from-genesis to ignore the recovery index and replay channel logs from channel genesis",
|
|
255
272
|
"Prints RPC log scan progress while rebuilding channel state and received-note state",
|
|
256
273
|
],
|
|
257
274
|
},
|
|
@@ -270,6 +287,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
270
287
|
description: "Check whether a wallet matches the on-chain channel registration.",
|
|
271
288
|
fields: ["wallet", "network"],
|
|
272
289
|
usage: "--wallet and --network",
|
|
290
|
+
help: ["Refreshes channel state through the workspace recovery index before reading registration metadata"],
|
|
273
291
|
},
|
|
274
292
|
{
|
|
275
293
|
id: "get-my-l1-address",
|
|
@@ -301,6 +319,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
301
319
|
description: "Read the current channel L2 accounting balance.",
|
|
302
320
|
fields: ["wallet", "network"],
|
|
303
321
|
usage: "--wallet and --network",
|
|
322
|
+
help: ["Refreshes channel state through the workspace recovery index before reading the L2 accounting balance"],
|
|
304
323
|
},
|
|
305
324
|
{
|
|
306
325
|
id: "exit-channel",
|
|
@@ -334,6 +353,10 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
334
353
|
description: "Show the wallet's tracked note state and refresh received notes.",
|
|
335
354
|
fields: ["wallet", "network"],
|
|
336
355
|
usage: "--wallet and --network",
|
|
356
|
+
help: [
|
|
357
|
+
"Refreshes channel state through the workspace recovery index before reading notes",
|
|
358
|
+
"Refreshes received-note logs through the wallet note recovery index",
|
|
359
|
+
],
|
|
337
360
|
},
|
|
338
361
|
]);
|
|
339
362
|
|
|
@@ -1300,9 +1300,11 @@ export {
|
|
|
1300
1300
|
installGroth16RuntimeForPrivateState,
|
|
1301
1301
|
installPrivateStateCliArtifacts,
|
|
1302
1302
|
writePrivateStateCliInstallManifest,
|
|
1303
|
+
parseJsonReport,
|
|
1303
1304
|
resolveArtifactCacheBaseRoot,
|
|
1304
1305
|
privateStateCliArtifactPaths,
|
|
1305
1306
|
inspectGroth16Runtime,
|
|
1307
|
+
stripAnsi,
|
|
1306
1308
|
resolveActiveGroth16ProverRuntime,
|
|
1307
1309
|
resolveActiveTokamakCliInvocation,
|
|
1308
1310
|
readTokamakCliPackageReport,
|
package/package.json
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
installPrivateStateCliArtifacts,
|
|
69
69
|
installTokamakCliRuntimeForPrivateState,
|
|
70
70
|
inspectGroth16Runtime,
|
|
71
|
+
parseJsonReport,
|
|
71
72
|
printDoctorHumanReport,
|
|
72
73
|
privateStateCliArtifactPaths,
|
|
73
74
|
readTokamakCliPackageReport,
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
resolveArtifactCacheBaseRoot,
|
|
78
79
|
resolvePrivateStateInstallRuntimeVersions,
|
|
79
80
|
resolveTokamakCliResourceDirForRuntimeRoot,
|
|
81
|
+
stripAnsi,
|
|
80
82
|
writePrivateStateCliInstallManifest,
|
|
81
83
|
} from "./lib/private-state-runtime-management.mjs";
|
|
82
84
|
import {
|
|
@@ -127,6 +129,7 @@ const secretRoot = path.resolve(os.homedir(), "tokamak-private-channels", "secre
|
|
|
127
129
|
const flatDeploymentArtifactPathsByChainId = new Map();
|
|
128
130
|
const PRIVATE_STATE_UNINSTALL_CONFIRMATION =
|
|
129
131
|
"I understand that the wallet secrets deleted due to this decision cannot be recovered";
|
|
132
|
+
const PRIVATE_STATE_CLI_PACKAGE_NAME = privateStateCliPackageJson.name;
|
|
130
133
|
const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
|
|
131
134
|
const TOKAMAK_ZKEVM_CLI_PACKAGE_NAME = "@tokamak-zk-evm/cli";
|
|
132
135
|
let jsonOutputRequested = false;
|
|
@@ -338,6 +341,12 @@ async function main() {
|
|
|
338
341
|
return;
|
|
339
342
|
}
|
|
340
343
|
|
|
344
|
+
if (args.command === "update") {
|
|
345
|
+
assertUpdateArgs(args);
|
|
346
|
+
await handleUpdate();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
341
350
|
if (args.command === "doctor") {
|
|
342
351
|
assertDoctorArgs(args);
|
|
343
352
|
await handleDoctor({ args });
|
|
@@ -525,6 +534,8 @@ async function handleChannelCreate({ args, network, provider }) {
|
|
|
525
534
|
provider,
|
|
526
535
|
bridgeResources,
|
|
527
536
|
persist: true,
|
|
537
|
+
fromGenesis: true,
|
|
538
|
+
progressAction: "create-channel",
|
|
528
539
|
});
|
|
529
540
|
|
|
530
541
|
printJson({
|
|
@@ -822,6 +833,13 @@ async function initializeChannelWorkspace({
|
|
|
822
833
|
managedStorageAddresses,
|
|
823
834
|
})
|
|
824
835
|
: null;
|
|
836
|
+
if (useWorkspaceRecoveryIndex && !fromGenesis && !localSnapshotReusable && !recoveryIndex) {
|
|
837
|
+
throw new Error([
|
|
838
|
+
`Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
|
|
839
|
+
"The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
|
|
840
|
+
"Run recover-workspace --from-genesis or recover-wallet --from-genesis to rebuild from channel genesis.",
|
|
841
|
+
].join(" "));
|
|
842
|
+
}
|
|
825
843
|
const reconstruction = localSnapshotReusable
|
|
826
844
|
? {
|
|
827
845
|
currentSnapshot: existingArtifacts.stateSnapshot,
|
|
@@ -993,6 +1011,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
993
1011
|
persist: true,
|
|
994
1012
|
allowExistingWorkspaceSync: true,
|
|
995
1013
|
useWorkspaceRecoveryIndex: true,
|
|
1014
|
+
fromGenesis: args.fromGenesis === true,
|
|
996
1015
|
progressAction: "recover-wallet",
|
|
997
1016
|
});
|
|
998
1017
|
const context = {
|
|
@@ -1423,31 +1442,44 @@ function assertSafeManagedRoot(rootPath, label) {
|
|
|
1423
1442
|
}
|
|
1424
1443
|
}
|
|
1425
1444
|
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1445
|
+
function npmCommandName() {
|
|
1446
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function inspectGlobalPrivateStateCliPackage() {
|
|
1450
|
+
const list = runCaptured(npmCommandName(), ["ls", "-g", PRIVATE_STATE_CLI_PACKAGE_NAME, "--depth=0", "--json"]);
|
|
1451
|
+
const report = parseJsonReport(list.stdout);
|
|
1452
|
+
const packageReport = report?.dependencies?.[PRIVATE_STATE_CLI_PACKAGE_NAME] ?? null;
|
|
1453
|
+
const installed = Boolean(packageReport);
|
|
1454
|
+
if (!installed) {
|
|
1431
1455
|
const missing = /empty|missing|not found|not installed/iu.test(`${list.stdout}\n${list.stderr}`);
|
|
1432
1456
|
return {
|
|
1433
|
-
attempted: false,
|
|
1434
1457
|
installed: false,
|
|
1435
|
-
|
|
1458
|
+
version: null,
|
|
1436
1459
|
status: list.status,
|
|
1460
|
+
reason: missing || report ? "global package is not installed" : "unable to inspect global npm package",
|
|
1437
1461
|
stderr: stripAnsi(list.stderr).trim(),
|
|
1438
1462
|
};
|
|
1439
1463
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1464
|
+
return {
|
|
1465
|
+
installed: true,
|
|
1466
|
+
version: packageReport.version ?? null,
|
|
1467
|
+
status: list.status,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function uninstallGlobalPrivateStateCliPackage() {
|
|
1472
|
+
const inspection = inspectGlobalPrivateStateCliPackage();
|
|
1473
|
+
if (!inspection.installed) {
|
|
1443
1474
|
return {
|
|
1444
1475
|
attempted: false,
|
|
1445
1476
|
installed: false,
|
|
1446
|
-
reason:
|
|
1447
|
-
status:
|
|
1477
|
+
reason: inspection.reason,
|
|
1478
|
+
status: inspection.status,
|
|
1479
|
+
stderr: inspection.stderr,
|
|
1448
1480
|
};
|
|
1449
1481
|
}
|
|
1450
|
-
const uninstall = runCaptured(
|
|
1482
|
+
const uninstall = runCaptured(npmCommandName(), ["uninstall", "-g", PRIVATE_STATE_CLI_PACKAGE_NAME]);
|
|
1451
1483
|
return {
|
|
1452
1484
|
attempted: true,
|
|
1453
1485
|
installed: true,
|
|
@@ -1458,6 +1490,106 @@ function uninstallGlobalPrivateStateCliPackage() {
|
|
|
1458
1490
|
};
|
|
1459
1491
|
}
|
|
1460
1492
|
|
|
1493
|
+
async function handleUpdate() {
|
|
1494
|
+
const currentVersion = privateStateCliPackageJson.version;
|
|
1495
|
+
const latestVersion = await fetchLatestPrivateStateCliVersion();
|
|
1496
|
+
const registryComparison = compareSemver(currentVersion, latestVersion);
|
|
1497
|
+
const globalPackage = inspectGlobalPrivateStateCliPackage();
|
|
1498
|
+
const runningFromRepositoryCheckout = isRepositoryCliPackageRoot(privateStateCliPackageRoot);
|
|
1499
|
+
const updateAvailable = registryComparison < 0;
|
|
1500
|
+
|
|
1501
|
+
const result = {
|
|
1502
|
+
action: "update",
|
|
1503
|
+
packageName: PRIVATE_STATE_CLI_PACKAGE_NAME,
|
|
1504
|
+
currentVersion,
|
|
1505
|
+
latestVersion,
|
|
1506
|
+
updateAvailable,
|
|
1507
|
+
registryState: registryComparison > 0 ? "local-version-ahead-of-registry" : "normal",
|
|
1508
|
+
runningFromRepositoryCheckout,
|
|
1509
|
+
globalPackage,
|
|
1510
|
+
attempted: false,
|
|
1511
|
+
updated: false,
|
|
1512
|
+
command: `npm install -g ${PRIVATE_STATE_CLI_PACKAGE_NAME}@latest`,
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
if (!updateAvailable) {
|
|
1516
|
+
printJson(result);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (runningFromRepositoryCheckout) {
|
|
1521
|
+
printJson({
|
|
1522
|
+
...result,
|
|
1523
|
+
reason: "running from a repository checkout; update the checkout with git/npm instead of mutating source files",
|
|
1524
|
+
});
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (!globalPackage.installed) {
|
|
1529
|
+
printJson({
|
|
1530
|
+
...result,
|
|
1531
|
+
reason: "global npm package is not installed; install or update the CLI with the printed command",
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const install = runCaptured(npmCommandName(), ["install", "-g", `${PRIVATE_STATE_CLI_PACKAGE_NAME}@latest`]);
|
|
1537
|
+
if (install.status !== 0) {
|
|
1538
|
+
throw new Error([
|
|
1539
|
+
`Unable to update ${PRIVATE_STATE_CLI_PACKAGE_NAME} to ${latestVersion}.`,
|
|
1540
|
+
stripAnsi(install.stderr || install.stdout).trim(),
|
|
1541
|
+
].filter(Boolean).join(" "));
|
|
1542
|
+
}
|
|
1543
|
+
printJson({
|
|
1544
|
+
...result,
|
|
1545
|
+
attempted: true,
|
|
1546
|
+
updated: true,
|
|
1547
|
+
installedVersion: latestVersion,
|
|
1548
|
+
stdout: stripAnsi(install.stdout).trim(),
|
|
1549
|
+
stderr: stripAnsi(install.stderr).trim(),
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async function fetchLatestPrivateStateCliVersion() {
|
|
1554
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(PRIVATE_STATE_CLI_PACKAGE_NAME)}/latest`;
|
|
1555
|
+
const response = await fetch(url, {
|
|
1556
|
+
redirect: "follow",
|
|
1557
|
+
signal: typeof globalThis.AbortSignal?.timeout === "function" ? globalThis.AbortSignal.timeout(10_000) : undefined,
|
|
1558
|
+
});
|
|
1559
|
+
if (!response.ok) {
|
|
1560
|
+
throw new Error(`Unable to fetch ${PRIVATE_STATE_CLI_PACKAGE_NAME} latest version from npm registry: HTTP ${response.status}.`);
|
|
1561
|
+
}
|
|
1562
|
+
const metadata = await response.json();
|
|
1563
|
+
const version = metadata?.version;
|
|
1564
|
+
if (typeof version !== "string" || version.trim() === "") {
|
|
1565
|
+
throw new Error(`npm registry response for ${PRIVATE_STATE_CLI_PACKAGE_NAME} did not include a version.`);
|
|
1566
|
+
}
|
|
1567
|
+
return version;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function compareSemver(left, right) {
|
|
1571
|
+
const parse = (value) => String(value).split("-", 1)[0].split(".").map((part) => {
|
|
1572
|
+
const parsed = Number.parseInt(part, 10);
|
|
1573
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1574
|
+
});
|
|
1575
|
+
const leftParts = parse(left);
|
|
1576
|
+
const rightParts = parse(right);
|
|
1577
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1578
|
+
for (let index = 0; index < length; index += 1) {
|
|
1579
|
+
const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
1580
|
+
if (delta !== 0) {
|
|
1581
|
+
return delta < 0 ? -1 : 1;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return 0;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function isRepositoryCliPackageRoot(packageRoot) {
|
|
1588
|
+
const segments = path.resolve(packageRoot).split(path.sep);
|
|
1589
|
+
const suffix = ["packages", "apps", "private-state", "cli"];
|
|
1590
|
+
return suffix.every((segment, index) => segments[segments.length - suffix.length + index] === segment);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1461
1593
|
async function handleDoctor({ args }) {
|
|
1462
1594
|
const report = buildDoctorReport({ probeGpu: args.gpu === true });
|
|
1463
1595
|
if (isJsonOutputRequested()) {
|
|
@@ -2129,8 +2261,8 @@ function applyGuideNextAction(guide) {
|
|
|
2129
2261
|
}
|
|
2130
2262
|
if (guide.selectors.channelName && guide.state.channel?.onchain?.exists && !guide.state.channel?.local?.workspaceExists) {
|
|
2131
2263
|
setGuideNextAction(guide, {
|
|
2132
|
-
command: `recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network}`,
|
|
2133
|
-
why: "The channel exists on-chain, but the local channel workspace has not been recovered yet.",
|
|
2264
|
+
command: `recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --from-genesis`,
|
|
2265
|
+
why: "The channel exists on-chain, but the local channel workspace has not been recovered yet, so there is no local recovery index to resume from.",
|
|
2134
2266
|
});
|
|
2135
2267
|
return;
|
|
2136
2268
|
}
|
|
@@ -2269,21 +2401,24 @@ function redactRpcUrl(rpcUrl) {
|
|
|
2269
2401
|
|
|
2270
2402
|
async function handleGetMyWalletMeta({ args, provider }) {
|
|
2271
2403
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2274
|
-
args,
|
|
2275
|
-
networkName: walletMetadata.network,
|
|
2404
|
+
const contextResult = await loadPreferredWalletChannelContext({
|
|
2405
|
+
walletContext: wallet,
|
|
2276
2406
|
provider,
|
|
2407
|
+
progressAction: "get-my-wallet-meta",
|
|
2408
|
+
});
|
|
2409
|
+
const context = contextResult.context;
|
|
2410
|
+
const {
|
|
2411
|
+
signer,
|
|
2412
|
+
l2Identity,
|
|
2413
|
+
registration,
|
|
2414
|
+
expectedStorageKey,
|
|
2415
|
+
matchesWallet,
|
|
2416
|
+
} = await loadWalletChannelRegistrationState({
|
|
2277
2417
|
walletContext: wallet,
|
|
2418
|
+
context,
|
|
2419
|
+
provider,
|
|
2278
2420
|
});
|
|
2279
2421
|
|
|
2280
|
-
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2281
|
-
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
2282
|
-
const matchesWallet = registration.exists
|
|
2283
|
-
&& ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
|
|
2284
|
-
&& ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2285
|
-
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
|
|
2286
|
-
|
|
2287
2422
|
printJson({
|
|
2288
2423
|
action: "get-my-wallet-meta",
|
|
2289
2424
|
wallet: wallet.walletName,
|
|
@@ -2301,31 +2436,23 @@ async function handleGetMyWalletMeta({ args, provider }) {
|
|
|
2301
2436
|
}
|
|
2302
2437
|
|
|
2303
2438
|
async function loadWalletChannelFundState({ walletContext, provider, progressAction = null }) {
|
|
2304
|
-
const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
|
|
2305
2439
|
const contextResult = await loadPreferredWalletChannelContext({
|
|
2306
2440
|
walletContext,
|
|
2307
2441
|
provider,
|
|
2308
2442
|
progressAction,
|
|
2309
2443
|
});
|
|
2310
2444
|
const context = contextResult.context;
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
"The local wallet L2 address does not match the registered channel L2 address.",
|
|
2323
|
-
);
|
|
2324
|
-
expect(
|
|
2325
|
-
ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2326
|
-
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey)),
|
|
2327
|
-
"The local wallet L2 storage key does not match the registered channelTokenVault key.",
|
|
2328
|
-
);
|
|
2445
|
+
const {
|
|
2446
|
+
signer,
|
|
2447
|
+
l2Identity,
|
|
2448
|
+
registration,
|
|
2449
|
+
expectedStorageKey,
|
|
2450
|
+
} = await loadWalletChannelRegistrationState({
|
|
2451
|
+
walletContext,
|
|
2452
|
+
context,
|
|
2453
|
+
provider,
|
|
2454
|
+
requireRegistration: true,
|
|
2455
|
+
});
|
|
2329
2456
|
|
|
2330
2457
|
const stateManager = await buildStateManager(context.currentSnapshot, context.contractCodes);
|
|
2331
2458
|
const channelDeposit = await currentStorageBigInt(
|
|
@@ -2344,6 +2471,43 @@ async function loadWalletChannelFundState({ walletContext, provider, progressAct
|
|
|
2344
2471
|
};
|
|
2345
2472
|
}
|
|
2346
2473
|
|
|
2474
|
+
async function loadWalletChannelRegistrationState({
|
|
2475
|
+
walletContext,
|
|
2476
|
+
context,
|
|
2477
|
+
provider,
|
|
2478
|
+
requireRegistration = false,
|
|
2479
|
+
}) {
|
|
2480
|
+
const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
|
|
2481
|
+
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2482
|
+
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
2483
|
+
const matchesWallet = registration.exists
|
|
2484
|
+
&& ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
|
|
2485
|
+
&& ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2486
|
+
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
|
|
2487
|
+
|
|
2488
|
+
if (requireRegistration) {
|
|
2489
|
+
expect(
|
|
2490
|
+
registration.exists,
|
|
2491
|
+
cliError(
|
|
2492
|
+
CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
|
|
2493
|
+
`No channelTokenVault registration exists for ${signer.address}. Run join-channel first.`,
|
|
2494
|
+
),
|
|
2495
|
+
);
|
|
2496
|
+
expect(
|
|
2497
|
+
matchesWallet,
|
|
2498
|
+
"The local wallet L2 address or storage key does not match the registered channelTokenVault state.",
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
return {
|
|
2503
|
+
signer,
|
|
2504
|
+
l2Identity,
|
|
2505
|
+
registration,
|
|
2506
|
+
expectedStorageKey,
|
|
2507
|
+
matchesWallet,
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2347
2511
|
async function handleGetMyChannelFund({ args, provider }) {
|
|
2348
2512
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
2349
2513
|
const {
|
|
@@ -2353,7 +2517,11 @@ async function handleGetMyChannelFund({ args, provider }) {
|
|
|
2353
2517
|
registration,
|
|
2354
2518
|
expectedStorageKey,
|
|
2355
2519
|
channelFund,
|
|
2356
|
-
} = await loadWalletChannelFundState({
|
|
2520
|
+
} = await loadWalletChannelFundState({
|
|
2521
|
+
walletContext: wallet,
|
|
2522
|
+
provider,
|
|
2523
|
+
progressAction: "get-my-channel-fund",
|
|
2524
|
+
});
|
|
2357
2525
|
|
|
2358
2526
|
printJson({
|
|
2359
2527
|
action: "get-my-channel-fund",
|
|
@@ -2376,9 +2544,9 @@ async function handleGetMyChannelFund({ args, provider }) {
|
|
|
2376
2544
|
}
|
|
2377
2545
|
|
|
2378
2546
|
async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
2379
|
-
const context = await
|
|
2547
|
+
const context = await loadJoinChannelContext({
|
|
2380
2548
|
args,
|
|
2381
|
-
|
|
2549
|
+
network,
|
|
2382
2550
|
provider,
|
|
2383
2551
|
});
|
|
2384
2552
|
const signer = requireL1Signer(args, provider);
|
|
@@ -3323,11 +3491,12 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
3323
3491
|
noteReceivePrivateKey,
|
|
3324
3492
|
progressAction = null,
|
|
3325
3493
|
}) {
|
|
3326
|
-
const scanStartBlock = Math.max(
|
|
3327
|
-
Number(walletContext.wallet.noteReceiveLastScannedBlock),
|
|
3328
|
-
Number(context.workspace.genesisBlockNumber),
|
|
3329
|
-
);
|
|
3330
3494
|
const latestBlock = await provider.getBlockNumber();
|
|
3495
|
+
const scanStartBlock = requireUsableWalletNoteReceiveRecoveryIndex({
|
|
3496
|
+
walletContext,
|
|
3497
|
+
context,
|
|
3498
|
+
latestBlock,
|
|
3499
|
+
});
|
|
3331
3500
|
const scanRange = {
|
|
3332
3501
|
fromBlock: scanStartBlock,
|
|
3333
3502
|
toBlock: latestBlock,
|
|
@@ -3431,6 +3600,23 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
3431
3600
|
};
|
|
3432
3601
|
}
|
|
3433
3602
|
|
|
3603
|
+
function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, latestBlock }) {
|
|
3604
|
+
const nextBlock = Number(walletContext.wallet.noteReceiveLastScannedBlock);
|
|
3605
|
+
const genesisBlockNumber = Number(context.workspace.genesisBlockNumber);
|
|
3606
|
+
if (
|
|
3607
|
+
!Number.isInteger(nextBlock)
|
|
3608
|
+
|| nextBlock < genesisBlockNumber
|
|
3609
|
+
|| nextBlock > Number(latestBlock) + 1
|
|
3610
|
+
) {
|
|
3611
|
+
throw new Error([
|
|
3612
|
+
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
3613
|
+
`Expected noteReceiveLastScannedBlock to be an integer between ${genesisBlockNumber} and ${Number(latestBlock) + 1}.`,
|
|
3614
|
+
"Run recover-wallet --from-genesis to rebuild wallet note state from channel genesis.",
|
|
3615
|
+
].join(" "));
|
|
3616
|
+
}
|
|
3617
|
+
return nextBlock;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3434
3620
|
function extractEncryptedNoteValueFromBridgeLog(log) {
|
|
3435
3621
|
if (!Array.isArray(log?.topics) || log.topics.length !== 1) {
|
|
3436
3622
|
return null;
|
|
@@ -4191,24 +4377,10 @@ async function loadWorkspaceContext(workspaceName, networkName, provider) {
|
|
|
4191
4377
|
};
|
|
4192
4378
|
}
|
|
4193
4379
|
|
|
4194
|
-
async function
|
|
4380
|
+
async function loadJoinChannelContext({ args, network, provider }) {
|
|
4195
4381
|
const chainId = Number((await provider.getNetwork()).chainId);
|
|
4196
|
-
const resolvedNetworkName =
|
|
4197
|
-
const channelName = args.channelName
|
|
4198
|
-
if (args.channelName && walletContext) {
|
|
4199
|
-
expect(
|
|
4200
|
-
args.channelName === walletContext.wallet.channelName,
|
|
4201
|
-
[
|
|
4202
|
-
`The provided --channel-name (${args.channelName}) does not match the wallet channel`,
|
|
4203
|
-
`(${walletContext.wallet.channelName}).`,
|
|
4204
|
-
].join(" "),
|
|
4205
|
-
);
|
|
4206
|
-
}
|
|
4207
|
-
if (!channelName) {
|
|
4208
|
-
throw new Error(
|
|
4209
|
-
"Missing channel selector. Provide either --channel-name or --wallet bound to a channel.",
|
|
4210
|
-
);
|
|
4211
|
-
}
|
|
4382
|
+
const resolvedNetworkName = network?.name ?? networkNameFromChainId(chainId);
|
|
4383
|
+
const channelName = requireArg(args.channelName, "--channel-name");
|
|
4212
4384
|
|
|
4213
4385
|
const bridgeResources = loadBridgeResources({ chainId });
|
|
4214
4386
|
const initialized = await initializeChannelWorkspace({
|
|
@@ -5953,6 +6125,10 @@ function assertUninstallArgs(args) {
|
|
|
5953
6125
|
assertAllowedCommandSchema(args, "uninstall");
|
|
5954
6126
|
}
|
|
5955
6127
|
|
|
6128
|
+
function assertUpdateArgs(args) {
|
|
6129
|
+
assertAllowedCommandSchema(args, "update");
|
|
6130
|
+
}
|
|
6131
|
+
|
|
5956
6132
|
function assertDoctorArgs(args) {
|
|
5957
6133
|
assertAllowedCommandSchema(args, "doctor");
|
|
5958
6134
|
if (args.gpu !== undefined && args.gpu !== true) {
|
|
@@ -6051,6 +6227,9 @@ function assertExplicitSignerCommandArgs(args, commandName) {
|
|
|
6051
6227
|
|
|
6052
6228
|
function assertRecoverWalletArgs(args) {
|
|
6053
6229
|
assertExplicitSignerCommandArgs(args, "recover-wallet");
|
|
6230
|
+
if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
|
|
6231
|
+
throw new Error("recover-wallet option --from-genesis does not accept a value.");
|
|
6232
|
+
}
|
|
6054
6233
|
}
|
|
6055
6234
|
|
|
6056
6235
|
function assertJoinChannelArgs(args) {
|
|
@@ -6577,6 +6756,7 @@ function loadWalletCommandRuntime(args) {
|
|
|
6577
6756
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
6578
6757
|
guide: printGuideHumanResult,
|
|
6579
6758
|
"transaction-fees": printTransactionFeesHumanResult,
|
|
6759
|
+
update: printUpdateHumanResult,
|
|
6580
6760
|
});
|
|
6581
6761
|
|
|
6582
6762
|
function normalizePrivateKey(value) {
|
|
@@ -6668,6 +6848,35 @@ function printTransactionFeesHumanResult(report) {
|
|
|
6668
6848
|
console.log(lines.join("\n"));
|
|
6669
6849
|
}
|
|
6670
6850
|
|
|
6851
|
+
function printUpdateHumanResult(report) {
|
|
6852
|
+
const lines = [
|
|
6853
|
+
"Private-State CLI Update",
|
|
6854
|
+
`Package: ${formatHumanValue(report.packageName)}`,
|
|
6855
|
+
`Current version: ${formatHumanValue(report.currentVersion)}`,
|
|
6856
|
+
`Latest registry version: ${formatHumanValue(report.latestVersion)}`,
|
|
6857
|
+
];
|
|
6858
|
+
if (report.registryState === "local-version-ahead-of-registry") {
|
|
6859
|
+
lines.push("Status: local version is newer than the npm registry latest tag.");
|
|
6860
|
+
} else if (!report.updateAvailable) {
|
|
6861
|
+
lines.push("Status: up to date.");
|
|
6862
|
+
} else if (report.updated) {
|
|
6863
|
+
lines.push("Status: updated global npm install.");
|
|
6864
|
+
} else {
|
|
6865
|
+
lines.push(
|
|
6866
|
+
"Status: update available.",
|
|
6867
|
+
`Reason: ${formatHumanValue(report.reason)}`,
|
|
6868
|
+
`Command: ${formatHumanValue(report.command)}`,
|
|
6869
|
+
);
|
|
6870
|
+
}
|
|
6871
|
+
lines.push(
|
|
6872
|
+
`Global install: ${report.globalPackage?.installed ? `yes (${formatHumanValue(report.globalPackage.version)})` : "no"}`,
|
|
6873
|
+
`Repository checkout: ${report.runningFromRepositoryCheckout ? "yes" : "no"}`,
|
|
6874
|
+
"",
|
|
6875
|
+
"Run with --json to inspect the full update report.",
|
|
6876
|
+
);
|
|
6877
|
+
console.log(lines.join("\n"));
|
|
6878
|
+
}
|
|
6879
|
+
|
|
6671
6880
|
function formatHumanTable(headers, rows) {
|
|
6672
6881
|
const values = [headers, ...rows].map((row) => row.map((value) => String(value ?? "")));
|
|
6673
6882
|
const widths = headers.map((_header, columnIndex) =>
|
|
@@ -6888,6 +7097,15 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
6888
7097
|
hints.push(`private-state-cli guide --network ${networkName} --channel-name ${channelName}`);
|
|
6889
7098
|
}
|
|
6890
7099
|
|
|
7100
|
+
if (message.includes("Workspace recovery index is missing or unusable")) {
|
|
7101
|
+
hints.push(`private-state-cli recover-workspace --channel-name ${channelName} --network ${networkName} --from-genesis`);
|
|
7102
|
+
hints.push(`private-state-cli recover-wallet --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
|
|
7103
|
+
}
|
|
7104
|
+
|
|
7105
|
+
if (message.includes("Wallet note recovery index is missing or unusable")) {
|
|
7106
|
+
hints.push(`private-state-cli recover-wallet --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
|
|
7107
|
+
}
|
|
7108
|
+
|
|
6891
7109
|
if (message.includes("Missing channel selector")) {
|
|
6892
7110
|
hints.push(`private-state-cli list-local-wallets --network ${networkName}`);
|
|
6893
7111
|
hints.push(`private-state-cli guide --network ${networkName} --channel-name <CHANNEL> --wallet <WALLET>`);
|