@tokamak-private-dapps/private-state-cli 1.0.0 → 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 +35 -0
- package/README.md +104 -0
- package/assets/tx-fees.json +170 -0
- package/lib/private-state-cli-command-registry.mjs +56 -9
- package/lib/private-state-runtime-management.mjs +2 -0
- package/package.json +2 -1
- package/private-state-bridge-cli.mjs +794 -99
|
@@ -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 {
|
|
@@ -120,12 +122,14 @@ import {
|
|
|
120
122
|
|
|
121
123
|
const require = createRequire(import.meta.url);
|
|
122
124
|
const defaultCommandCwd = process.cwd();
|
|
125
|
+
const privateStateCliPackageJson = require("./package.json");
|
|
123
126
|
const privateStateCliPackageRoot = path.dirname(require.resolve("./package.json"));
|
|
124
127
|
const workspaceRoot = path.resolve(os.homedir(), "tokamak-private-channels", "workspace");
|
|
125
128
|
const secretRoot = path.resolve(os.homedir(), "tokamak-private-channels", "secrets");
|
|
126
129
|
const flatDeploymentArtifactPathsByChainId = new Map();
|
|
127
130
|
const PRIVATE_STATE_UNINSTALL_CONFIRMATION =
|
|
128
131
|
"I understand that the wallet secrets deleted due to this decision cannot be recovered";
|
|
132
|
+
const PRIVATE_STATE_CLI_PACKAGE_NAME = privateStateCliPackageJson.name;
|
|
129
133
|
const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
|
|
130
134
|
const TOKAMAK_ZKEVM_CLI_PACKAGE_NAME = "@tokamak-zk-evm/cli";
|
|
131
135
|
let jsonOutputRequested = false;
|
|
@@ -314,6 +318,12 @@ async function main() {
|
|
|
314
318
|
activeCliArgs = args;
|
|
315
319
|
configureOutput(args);
|
|
316
320
|
|
|
321
|
+
if (args.version !== undefined) {
|
|
322
|
+
assertVersionArgs(args);
|
|
323
|
+
printVersion();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
317
327
|
if (args.help || !args.command) {
|
|
318
328
|
printHelp();
|
|
319
329
|
return;
|
|
@@ -331,6 +341,12 @@ async function main() {
|
|
|
331
341
|
return;
|
|
332
342
|
}
|
|
333
343
|
|
|
344
|
+
if (args.command === "update") {
|
|
345
|
+
assertUpdateArgs(args);
|
|
346
|
+
await handleUpdate();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
334
350
|
if (args.command === "doctor") {
|
|
335
351
|
assertDoctorArgs(args);
|
|
336
352
|
await handleDoctor({ args });
|
|
@@ -343,6 +359,13 @@ async function main() {
|
|
|
343
359
|
return;
|
|
344
360
|
}
|
|
345
361
|
|
|
362
|
+
if (args.command === "transaction-fees") {
|
|
363
|
+
assertTransactionFeesArgs(args);
|
|
364
|
+
const { network, provider, rpcUrl } = loadExplicitCommandRuntime(args);
|
|
365
|
+
await handleTransactionFees({ network, provider, rpcUrl });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
346
369
|
if (args.command === "get-my-l1-address") {
|
|
347
370
|
assertGetMyL1AddressArgs(args);
|
|
348
371
|
handleGetMyL1Address({ args });
|
|
@@ -511,6 +534,8 @@ async function handleChannelCreate({ args, network, provider }) {
|
|
|
511
534
|
provider,
|
|
512
535
|
bridgeResources,
|
|
513
536
|
persist: true,
|
|
537
|
+
fromGenesis: true,
|
|
538
|
+
progressAction: "create-channel",
|
|
514
539
|
});
|
|
515
540
|
|
|
516
541
|
printJson({
|
|
@@ -622,6 +647,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
622
647
|
allowExistingWorkspaceSync: true,
|
|
623
648
|
useWorkspaceRecoveryIndex: true,
|
|
624
649
|
fromGenesis: args.fromGenesis === true,
|
|
650
|
+
progressAction: "recover-workspace",
|
|
625
651
|
});
|
|
626
652
|
|
|
627
653
|
printJson({
|
|
@@ -729,6 +755,7 @@ async function initializeChannelWorkspace({
|
|
|
729
755
|
allowExistingWorkspaceSync = false,
|
|
730
756
|
useWorkspaceRecoveryIndex = false,
|
|
731
757
|
fromGenesis = false,
|
|
758
|
+
progressAction = null,
|
|
732
759
|
}) {
|
|
733
760
|
const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), workspaceName);
|
|
734
761
|
const channelDir = channelDataPath(workspaceDir);
|
|
@@ -806,6 +833,13 @@ async function initializeChannelWorkspace({
|
|
|
806
833
|
managedStorageAddresses,
|
|
807
834
|
})
|
|
808
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
|
+
}
|
|
809
843
|
const reconstruction = localSnapshotReusable
|
|
810
844
|
? {
|
|
811
845
|
currentSnapshot: existingArtifacts.stateSnapshot,
|
|
@@ -831,6 +865,7 @@ async function initializeChannelWorkspace({
|
|
|
831
865
|
baseSnapshot: recoveryIndex?.stateSnapshot ?? null,
|
|
832
866
|
fromBlock: recoveryIndex?.nextBlock ?? genesisBlockNumber,
|
|
833
867
|
toBlock: latestBlock,
|
|
868
|
+
progressAction,
|
|
834
869
|
});
|
|
835
870
|
const currentSnapshot = reconstruction.currentSnapshot;
|
|
836
871
|
const recoveryRootVectorHash = normalizeBytes32Hex(hashRootVector(currentSnapshot.stateRoots));
|
|
@@ -975,6 +1010,9 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
975
1010
|
bridgeResources,
|
|
976
1011
|
persist: true,
|
|
977
1012
|
allowExistingWorkspaceSync: true,
|
|
1013
|
+
useWorkspaceRecoveryIndex: true,
|
|
1014
|
+
fromGenesis: args.fromGenesis === true,
|
|
1015
|
+
progressAction: "recover-wallet",
|
|
978
1016
|
});
|
|
979
1017
|
const context = {
|
|
980
1018
|
workspaceName: channelName,
|
|
@@ -1056,6 +1094,14 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
1056
1094
|
});
|
|
1057
1095
|
|
|
1058
1096
|
if (existingWallet) {
|
|
1097
|
+
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
1098
|
+
walletContext: existingWallet,
|
|
1099
|
+
context,
|
|
1100
|
+
provider,
|
|
1101
|
+
signer,
|
|
1102
|
+
noteReceiveKeyMaterial,
|
|
1103
|
+
progressAction: "recover-wallet",
|
|
1104
|
+
});
|
|
1059
1105
|
printJson({
|
|
1060
1106
|
action: "recover-wallet",
|
|
1061
1107
|
status: "already-recovered",
|
|
@@ -1071,6 +1117,10 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
1071
1117
|
l2StorageKey: storageKey,
|
|
1072
1118
|
leafIndex: registration.leafIndex.toString(),
|
|
1073
1119
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
1120
|
+
l2Nonce: existingWallet.wallet.l2Nonce,
|
|
1121
|
+
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
1122
|
+
scannedDeliveryLogs: recoveredDeliveryState.scannedLogs,
|
|
1123
|
+
noteReceiveScanRange: recoveredDeliveryState.scanRange,
|
|
1074
1124
|
});
|
|
1075
1125
|
return;
|
|
1076
1126
|
}
|
|
@@ -1091,11 +1141,13 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
1091
1141
|
walletContext.wallet.l2Nonce = 0;
|
|
1092
1142
|
persistWallet(walletContext);
|
|
1093
1143
|
|
|
1094
|
-
const recoveredDeliveryState = await
|
|
1144
|
+
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
1095
1145
|
walletContext,
|
|
1096
1146
|
context,
|
|
1097
1147
|
provider,
|
|
1098
|
-
|
|
1148
|
+
signer,
|
|
1149
|
+
noteReceiveKeyMaterial,
|
|
1150
|
+
progressAction: "recover-wallet",
|
|
1099
1151
|
});
|
|
1100
1152
|
|
|
1101
1153
|
printJson({
|
|
@@ -1390,31 +1442,44 @@ function assertSafeManagedRoot(rootPath, label) {
|
|
|
1390
1442
|
}
|
|
1391
1443
|
}
|
|
1392
1444
|
|
|
1393
|
-
function
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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) {
|
|
1398
1455
|
const missing = /empty|missing|not found|not installed/iu.test(`${list.stdout}\n${list.stderr}`);
|
|
1399
1456
|
return {
|
|
1400
|
-
attempted: false,
|
|
1401
1457
|
installed: false,
|
|
1402
|
-
|
|
1458
|
+
version: null,
|
|
1403
1459
|
status: list.status,
|
|
1460
|
+
reason: missing || report ? "global package is not installed" : "unable to inspect global npm package",
|
|
1404
1461
|
stderr: stripAnsi(list.stderr).trim(),
|
|
1405
1462
|
};
|
|
1406
1463
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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) {
|
|
1410
1474
|
return {
|
|
1411
1475
|
attempted: false,
|
|
1412
1476
|
installed: false,
|
|
1413
|
-
reason:
|
|
1414
|
-
status:
|
|
1477
|
+
reason: inspection.reason,
|
|
1478
|
+
status: inspection.status,
|
|
1479
|
+
stderr: inspection.stderr,
|
|
1415
1480
|
};
|
|
1416
1481
|
}
|
|
1417
|
-
const uninstall = runCaptured(
|
|
1482
|
+
const uninstall = runCaptured(npmCommandName(), ["uninstall", "-g", PRIVATE_STATE_CLI_PACKAGE_NAME]);
|
|
1418
1483
|
return {
|
|
1419
1484
|
attempted: true,
|
|
1420
1485
|
installed: true,
|
|
@@ -1425,6 +1490,106 @@ function uninstallGlobalPrivateStateCliPackage() {
|
|
|
1425
1490
|
};
|
|
1426
1491
|
}
|
|
1427
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
|
+
|
|
1428
1593
|
async function handleDoctor({ args }) {
|
|
1429
1594
|
const report = buildDoctorReport({ probeGpu: args.gpu === true });
|
|
1430
1595
|
if (isJsonOutputRequested()) {
|
|
@@ -1437,6 +1602,144 @@ async function handleDoctor({ args }) {
|
|
|
1437
1602
|
}
|
|
1438
1603
|
}
|
|
1439
1604
|
|
|
1605
|
+
async function handleTransactionFees({ network, provider, rpcUrl }) {
|
|
1606
|
+
const feeAsset = loadTransactionFeeAsset();
|
|
1607
|
+
const feeData = await provider.getFeeData();
|
|
1608
|
+
const gasPrices = requireTransactionFeeGasPrices(feeData);
|
|
1609
|
+
const ethUsd = await fetchEthUsdPrice();
|
|
1610
|
+
const rows = buildTransactionFeeRows({
|
|
1611
|
+
commands: feeAsset.commands,
|
|
1612
|
+
typicalGasPriceWei: gasPrices.typical,
|
|
1613
|
+
worstCaseGasPriceWei: gasPrices.worstCase,
|
|
1614
|
+
ethUsd,
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
printJson({
|
|
1618
|
+
action: "transaction-fees",
|
|
1619
|
+
generatedAt: new Date().toISOString(),
|
|
1620
|
+
network: network.name,
|
|
1621
|
+
chainId: Number(network.chainId),
|
|
1622
|
+
rpcUrl,
|
|
1623
|
+
asset: {
|
|
1624
|
+
schema: feeAsset.schema,
|
|
1625
|
+
measuredAt: feeAsset.measuredAt,
|
|
1626
|
+
measurementBasis: feeAsset.measurementBasis,
|
|
1627
|
+
notes: feeAsset.notes,
|
|
1628
|
+
},
|
|
1629
|
+
livePricing: {
|
|
1630
|
+
typicalGasPriceWei: gasPrices.typical.toString(),
|
|
1631
|
+
typicalGasPriceGwei: formatGwei(gasPrices.typical),
|
|
1632
|
+
typicalGasPriceSource: gasPrices.typicalSource,
|
|
1633
|
+
worstCaseGasPriceWei: gasPrices.worstCase.toString(),
|
|
1634
|
+
worstCaseGasPriceGwei: formatGwei(gasPrices.worstCase),
|
|
1635
|
+
worstCaseGasPriceSource: gasPrices.worstCaseSource,
|
|
1636
|
+
maxFeePerGasWei: feeData.maxFeePerGas?.toString() ?? null,
|
|
1637
|
+
maxPriorityFeePerGasWei: feeData.maxPriorityFeePerGas?.toString() ?? null,
|
|
1638
|
+
gasPriceWei: feeData.gasPrice?.toString() ?? null,
|
|
1639
|
+
ethUsd,
|
|
1640
|
+
ethUsdSource: "CoinGecko simple price API",
|
|
1641
|
+
},
|
|
1642
|
+
rows,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function loadTransactionFeeAsset() {
|
|
1647
|
+
const assetPath = path.join(privateStateCliPackageRoot, "assets", "tx-fees.json");
|
|
1648
|
+
const asset = readJson(assetPath);
|
|
1649
|
+
expect(asset.schema === "tokamak-private-state-cli-tx-fees.v1", `Unsupported transaction fee asset schema: ${assetPath}`);
|
|
1650
|
+
expect(Array.isArray(asset.commands), `Transaction fee asset is missing commands: ${assetPath}`);
|
|
1651
|
+
return asset;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function requireTransactionFeeGasPrices(feeData) {
|
|
1655
|
+
const typical = feeData.gasPrice ?? feeData.maxFeePerGas;
|
|
1656
|
+
const worstCase = feeData.maxFeePerGas ?? feeData.gasPrice;
|
|
1657
|
+
if (typical === null || typical === undefined || worstCase === null || worstCase === undefined) {
|
|
1658
|
+
throw new Error("RPC provider did not return gasPrice or maxFeePerGas.");
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
typical: ethers.toBigInt(typical),
|
|
1662
|
+
typicalSource: feeData.gasPrice ? "gasPrice" : "maxFeePerGas",
|
|
1663
|
+
worstCase: ethers.toBigInt(worstCase),
|
|
1664
|
+
worstCaseSource: feeData.maxFeePerGas ? "maxFeePerGas" : "gasPrice",
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
async function fetchEthUsdPrice() {
|
|
1669
|
+
const url = "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd";
|
|
1670
|
+
const response = await fetch(url, {
|
|
1671
|
+
headers: {
|
|
1672
|
+
accept: "application/json",
|
|
1673
|
+
"user-agent": `${privateStateCliPackageJson.name}/${privateStateCliPackageJson.version}`,
|
|
1674
|
+
},
|
|
1675
|
+
});
|
|
1676
|
+
if (!response.ok) {
|
|
1677
|
+
throw new Error(`Unable to fetch live ETH/USD price from CoinGecko: HTTP ${response.status}.`);
|
|
1678
|
+
}
|
|
1679
|
+
const payload = await response.json();
|
|
1680
|
+
const value = Number(payload?.ethereum?.usd);
|
|
1681
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1682
|
+
throw new Error("CoinGecko response did not include a valid ethereum.usd price.");
|
|
1683
|
+
}
|
|
1684
|
+
return value;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function buildTransactionFeeRows({ commands, typicalGasPriceWei, worstCaseGasPriceWei, ethUsd }) {
|
|
1688
|
+
return commands.map((entry) => {
|
|
1689
|
+
const transactions = expectTransactionFeeTransactions(entry);
|
|
1690
|
+
const gasUsed = transactions.reduce((sum, transaction) => sum + Number(transaction.gasUsed), 0);
|
|
1691
|
+
const typicalEth = ethers.formatEther(BigInt(gasUsed) * typicalGasPriceWei);
|
|
1692
|
+
const worstCaseEth = ethers.formatEther(BigInt(gasUsed) * worstCaseGasPriceWei);
|
|
1693
|
+
return {
|
|
1694
|
+
command: entry.command,
|
|
1695
|
+
description: entry.description,
|
|
1696
|
+
transactions: transactions.map((transaction) => transaction.label).join(" + "),
|
|
1697
|
+
gasUsed,
|
|
1698
|
+
typicalGasPriceGwei: formatGwei(typicalGasPriceWei),
|
|
1699
|
+
typicalEth: formatEthForDisplay(typicalEth),
|
|
1700
|
+
typicalUsd: formatUsdForDisplay(Number(typicalEth) * ethUsd),
|
|
1701
|
+
worstCaseGasPriceGwei: formatGwei(worstCaseGasPriceWei),
|
|
1702
|
+
worstCaseEth: formatEthForDisplay(worstCaseEth),
|
|
1703
|
+
worstCaseUsd: formatUsdForDisplay(Number(worstCaseEth) * ethUsd),
|
|
1704
|
+
sources: [...new Set(transactions.map((transaction) => transaction.source))].join(", "),
|
|
1705
|
+
sourceDetails: transactions.map((transaction) => transaction.sourceDetail),
|
|
1706
|
+
};
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function expectTransactionFeeTransactions(entry) {
|
|
1711
|
+
expect(typeof entry?.command === "string" && entry.command.length > 0, "Transaction fee asset contains a command without command name.");
|
|
1712
|
+
expect(Array.isArray(entry.transactions) && entry.transactions.length > 0, `Transaction fee asset command ${entry.command} has no transactions.`);
|
|
1713
|
+
for (const transaction of entry.transactions) {
|
|
1714
|
+
expect(Number.isInteger(transaction.gasUsed) && transaction.gasUsed > 0, `Transaction fee asset command ${entry.command} has invalid gasUsed.`);
|
|
1715
|
+
}
|
|
1716
|
+
return entry.transactions;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function formatGwei(wei) {
|
|
1720
|
+
return trimFixedNumber(ethers.formatUnits(wei, "gwei"), 6);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function formatEthForDisplay(value) {
|
|
1724
|
+
return trimFixedNumber(value, 8);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function formatUsdForDisplay(value) {
|
|
1728
|
+
if (value > 0 && value < 0.01) {
|
|
1729
|
+
return "<0.01";
|
|
1730
|
+
}
|
|
1731
|
+
return value.toFixed(2);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function trimFixedNumber(value, maxDecimals) {
|
|
1735
|
+
const [integer, decimals = ""] = String(value).split(".");
|
|
1736
|
+
if (!decimals || maxDecimals <= 0) {
|
|
1737
|
+
return integer;
|
|
1738
|
+
}
|
|
1739
|
+
const trimmed = decimals.slice(0, maxDecimals).replace(/0+$/u, "");
|
|
1740
|
+
return trimmed ? `${integer}.${trimmed}` : integer;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1440
1743
|
function handleGetMyL1Address({ args }) {
|
|
1441
1744
|
const signer = requireL1Signer(args);
|
|
1442
1745
|
printJson({
|
|
@@ -1513,6 +1816,7 @@ async function handleGuide({ args }) {
|
|
|
1513
1816
|
nextSafeAction: null,
|
|
1514
1817
|
why: null,
|
|
1515
1818
|
candidateCommands: [],
|
|
1819
|
+
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
1820
|
};
|
|
1517
1821
|
|
|
1518
1822
|
guide.state.local = inspectGuideLocalState(args);
|
|
@@ -1957,8 +2261,8 @@ function applyGuideNextAction(guide) {
|
|
|
1957
2261
|
}
|
|
1958
2262
|
if (guide.selectors.channelName && guide.state.channel?.onchain?.exists && !guide.state.channel?.local?.workspaceExists) {
|
|
1959
2263
|
setGuideNextAction(guide, {
|
|
1960
|
-
command: `recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network}`,
|
|
1961
|
-
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.",
|
|
1962
2266
|
});
|
|
1963
2267
|
return;
|
|
1964
2268
|
}
|
|
@@ -2006,18 +2310,18 @@ function applyGuideNextAction(guide) {
|
|
|
2006
2310
|
}
|
|
2007
2311
|
if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
|
|
2008
2312
|
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.",
|
|
2313
|
+
command: `mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
2314
|
+
why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
2011
2315
|
});
|
|
2012
2316
|
return;
|
|
2013
2317
|
}
|
|
2014
2318
|
if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
|
|
2015
2319
|
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.",
|
|
2320
|
+
command: `transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
2321
|
+
why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
2018
2322
|
candidates: [
|
|
2019
2323
|
`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
|
|
2324
|
+
`redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
|
|
2021
2325
|
],
|
|
2022
2326
|
});
|
|
2023
2327
|
return;
|
|
@@ -2097,21 +2401,24 @@ function redactRpcUrl(rpcUrl) {
|
|
|
2097
2401
|
|
|
2098
2402
|
async function handleGetMyWalletMeta({ args, provider }) {
|
|
2099
2403
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
args,
|
|
2103
|
-
networkName: walletMetadata.network,
|
|
2404
|
+
const contextResult = await loadPreferredWalletChannelContext({
|
|
2405
|
+
walletContext: wallet,
|
|
2104
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({
|
|
2105
2417
|
walletContext: wallet,
|
|
2418
|
+
context,
|
|
2419
|
+
provider,
|
|
2106
2420
|
});
|
|
2107
2421
|
|
|
2108
|
-
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2109
|
-
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
2110
|
-
const matchesWallet = registration.exists
|
|
2111
|
-
&& ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
|
|
2112
|
-
&& ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2113
|
-
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
|
|
2114
|
-
|
|
2115
2422
|
printJson({
|
|
2116
2423
|
action: "get-my-wallet-meta",
|
|
2117
2424
|
wallet: wallet.walletName,
|
|
@@ -2128,28 +2435,24 @@ async function handleGetMyWalletMeta({ args, provider }) {
|
|
|
2128
2435
|
});
|
|
2129
2436
|
}
|
|
2130
2437
|
|
|
2131
|
-
async function loadWalletChannelFundState({ walletContext, provider }) {
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2438
|
+
async function loadWalletChannelFundState({ walletContext, provider, progressAction = null }) {
|
|
2439
|
+
const contextResult = await loadPreferredWalletChannelContext({
|
|
2440
|
+
walletContext,
|
|
2441
|
+
provider,
|
|
2442
|
+
progressAction,
|
|
2443
|
+
});
|
|
2134
2444
|
const context = contextResult.context;
|
|
2135
|
-
const
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
"The local wallet L2 address does not match the registered channel L2 address.",
|
|
2147
|
-
);
|
|
2148
|
-
expect(
|
|
2149
|
-
ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2150
|
-
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey)),
|
|
2151
|
-
"The local wallet L2 storage key does not match the registered channelTokenVault key.",
|
|
2152
|
-
);
|
|
2445
|
+
const {
|
|
2446
|
+
signer,
|
|
2447
|
+
l2Identity,
|
|
2448
|
+
registration,
|
|
2449
|
+
expectedStorageKey,
|
|
2450
|
+
} = await loadWalletChannelRegistrationState({
|
|
2451
|
+
walletContext,
|
|
2452
|
+
context,
|
|
2453
|
+
provider,
|
|
2454
|
+
requireRegistration: true,
|
|
2455
|
+
});
|
|
2153
2456
|
|
|
2154
2457
|
const stateManager = await buildStateManager(context.currentSnapshot, context.contractCodes);
|
|
2155
2458
|
const channelDeposit = await currentStorageBigInt(
|
|
@@ -2168,6 +2471,43 @@ async function loadWalletChannelFundState({ walletContext, provider }) {
|
|
|
2168
2471
|
};
|
|
2169
2472
|
}
|
|
2170
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
|
+
|
|
2171
2511
|
async function handleGetMyChannelFund({ args, provider }) {
|
|
2172
2512
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
2173
2513
|
const {
|
|
@@ -2177,7 +2517,11 @@ async function handleGetMyChannelFund({ args, provider }) {
|
|
|
2177
2517
|
registration,
|
|
2178
2518
|
expectedStorageKey,
|
|
2179
2519
|
channelFund,
|
|
2180
|
-
} = await loadWalletChannelFundState({
|
|
2520
|
+
} = await loadWalletChannelFundState({
|
|
2521
|
+
walletContext: wallet,
|
|
2522
|
+
provider,
|
|
2523
|
+
progressAction: "get-my-channel-fund",
|
|
2524
|
+
});
|
|
2181
2525
|
|
|
2182
2526
|
printJson({
|
|
2183
2527
|
action: "get-my-channel-fund",
|
|
@@ -2200,9 +2544,9 @@ async function handleGetMyChannelFund({ args, provider }) {
|
|
|
2200
2544
|
}
|
|
2201
2545
|
|
|
2202
2546
|
async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
2203
|
-
const context = await
|
|
2547
|
+
const context = await loadJoinChannelContext({
|
|
2204
2548
|
args,
|
|
2205
|
-
|
|
2549
|
+
network,
|
|
2206
2550
|
provider,
|
|
2207
2551
|
});
|
|
2208
2552
|
const signer = requireL1Signer(args, provider);
|
|
@@ -2357,7 +2701,11 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
2357
2701
|
: "withdraw";
|
|
2358
2702
|
emitProgress(operationName, "loading");
|
|
2359
2703
|
const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
|
|
2360
|
-
const contextResult = await loadPreferredWalletChannelContext({
|
|
2704
|
+
const contextResult = await loadPreferredWalletChannelContext({
|
|
2705
|
+
walletContext,
|
|
2706
|
+
provider,
|
|
2707
|
+
progressAction: operationName,
|
|
2708
|
+
});
|
|
2361
2709
|
const context = contextResult.context;
|
|
2362
2710
|
const network = contextResult.network;
|
|
2363
2711
|
expect(
|
|
@@ -2403,6 +2751,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
2403
2751
|
"The derived L2 address does not match the registered channel L2 address.",
|
|
2404
2752
|
);
|
|
2405
2753
|
|
|
2754
|
+
await assertWorkspaceAlignedWithChain(context);
|
|
2406
2755
|
const stateManager = await buildStateManager(context.currentSnapshot, context.contractCodes);
|
|
2407
2756
|
const keyHex = storageKey;
|
|
2408
2757
|
const currentValue = await currentStorageBigInt(stateManager, context.workspace.l2AccountingVault, keyHex);
|
|
@@ -2578,6 +2927,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
2578
2927
|
const { channelFund } = await loadWalletChannelFundState({
|
|
2579
2928
|
walletContext: wallet,
|
|
2580
2929
|
provider,
|
|
2930
|
+
progressAction: "mint-notes",
|
|
2581
2931
|
});
|
|
2582
2932
|
expect(
|
|
2583
2933
|
totalMintAmount <= channelFund,
|
|
@@ -2591,6 +2941,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
2591
2941
|
baseUnitAmounts: baseUnitAmounts.map(({ amountBaseUnits }) => amountBaseUnits),
|
|
2592
2942
|
});
|
|
2593
2943
|
const { execution, contextResult, recoveredWorkspace } = await executeWalletDirectTemplateCommand({
|
|
2944
|
+
args,
|
|
2594
2945
|
wallet,
|
|
2595
2946
|
provider,
|
|
2596
2947
|
operationName: "mint-notes",
|
|
@@ -2602,7 +2953,10 @@ async function handleMintNotes({ args, provider }) {
|
|
|
2602
2953
|
wallet: wallet.walletName,
|
|
2603
2954
|
workspace: execution.context.workspaceName,
|
|
2604
2955
|
operationDir: execution.operationDir,
|
|
2605
|
-
l1Submitter: execution.
|
|
2956
|
+
l1Submitter: execution.txSubmitter.address,
|
|
2957
|
+
l1WalletOwner: execution.signer.address,
|
|
2958
|
+
txSubmitterSource: execution.txSubmitterSource,
|
|
2959
|
+
txSubmitterAccount: execution.txSubmitterAccount,
|
|
2606
2960
|
l2Address: execution.l2Identity.l2Address,
|
|
2607
2961
|
underlyingMethod: templatePayload.method,
|
|
2608
2962
|
nonce: execution.nonce,
|
|
@@ -2631,6 +2985,7 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
2631
2985
|
inputNotes,
|
|
2632
2986
|
});
|
|
2633
2987
|
const { execution, contextResult, recoveredWorkspace } = await executeWalletDirectTemplateCommand({
|
|
2988
|
+
args,
|
|
2634
2989
|
wallet,
|
|
2635
2990
|
provider,
|
|
2636
2991
|
operationName: "redeem-notes",
|
|
@@ -2642,7 +2997,10 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
2642
2997
|
wallet: wallet.walletName,
|
|
2643
2998
|
workspace: execution.context.workspaceName,
|
|
2644
2999
|
operationDir: execution.operationDir,
|
|
2645
|
-
l1Submitter: execution.
|
|
3000
|
+
l1Submitter: execution.txSubmitter.address,
|
|
3001
|
+
l1WalletOwner: execution.signer.address,
|
|
3002
|
+
txSubmitterSource: execution.txSubmitterSource,
|
|
3003
|
+
txSubmitterAccount: execution.txSubmitterAccount,
|
|
2646
3004
|
l2Address: execution.l2Identity.l2Address,
|
|
2647
3005
|
receiver: wallet.wallet.l2Address,
|
|
2648
3006
|
underlyingMethod: templatePayload.method,
|
|
@@ -2669,20 +3027,18 @@ async function handleGetMyNotes({ args, provider }) {
|
|
|
2669
3027
|
`Wallet ${wallet.walletName} is missing the stored controller address.`,
|
|
2670
3028
|
);
|
|
2671
3029
|
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,
|
|
3030
|
+
const { context } = await loadPreferredWalletChannelContext({
|
|
3031
|
+
walletContext: wallet,
|
|
3032
|
+
provider,
|
|
3033
|
+
progressAction: "get-my-notes",
|
|
2680
3034
|
});
|
|
2681
|
-
const
|
|
3035
|
+
const signer = restoreWalletSigner(wallet, provider);
|
|
3036
|
+
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2682
3037
|
walletContext: wallet,
|
|
2683
3038
|
context,
|
|
2684
3039
|
provider,
|
|
2685
|
-
|
|
3040
|
+
signer,
|
|
3041
|
+
progressAction: "get-my-notes",
|
|
2686
3042
|
});
|
|
2687
3043
|
|
|
2688
3044
|
const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
|
|
@@ -2728,7 +3084,11 @@ async function handleGetMyNotes({ args, provider }) {
|
|
|
2728
3084
|
async function handleTransferNotes({ args, provider }) {
|
|
2729
3085
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
2730
3086
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
2731
|
-
const preparedContextResult = await loadPreferredWalletChannelContext({
|
|
3087
|
+
const preparedContextResult = await loadPreferredWalletChannelContext({
|
|
3088
|
+
walletContext: wallet,
|
|
3089
|
+
provider,
|
|
3090
|
+
progressAction: "transfer-notes",
|
|
3091
|
+
});
|
|
2732
3092
|
const context = preparedContextResult.context;
|
|
2733
3093
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
2734
3094
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
@@ -2760,6 +3120,7 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
2760
3120
|
outputAmounts,
|
|
2761
3121
|
});
|
|
2762
3122
|
const { execution, contextResult, recoveredWorkspace } = await executeWalletDirectTemplateCommand({
|
|
3123
|
+
args,
|
|
2763
3124
|
wallet,
|
|
2764
3125
|
provider,
|
|
2765
3126
|
operationName: "transfer-notes",
|
|
@@ -2777,7 +3138,10 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
2777
3138
|
wallet: wallet.walletName,
|
|
2778
3139
|
workspace: execution.context.workspaceName,
|
|
2779
3140
|
operationDir: execution.operationDir,
|
|
2780
|
-
l1Submitter: execution.
|
|
3141
|
+
l1Submitter: execution.txSubmitter.address,
|
|
3142
|
+
l1WalletOwner: execution.signer.address,
|
|
3143
|
+
txSubmitterSource: execution.txSubmitterSource,
|
|
3144
|
+
txSubmitterAccount: execution.txSubmitterAccount,
|
|
2781
3145
|
l2Address: execution.l2Identity.l2Address,
|
|
2782
3146
|
underlyingMethod: templatePayload.method,
|
|
2783
3147
|
nonce: execution.nonce,
|
|
@@ -3092,17 +3456,47 @@ function buildLifecycleTrackedOutputs({
|
|
|
3092
3456
|
}));
|
|
3093
3457
|
}
|
|
3094
3458
|
|
|
3459
|
+
async function recoverWalletReceivedNotes({
|
|
3460
|
+
walletContext,
|
|
3461
|
+
context,
|
|
3462
|
+
provider,
|
|
3463
|
+
signer,
|
|
3464
|
+
noteReceiveKeyMaterial = null,
|
|
3465
|
+
progressAction = null,
|
|
3466
|
+
}) {
|
|
3467
|
+
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ?? await deriveNoteReceiveKeyMaterial({
|
|
3468
|
+
signer,
|
|
3469
|
+
chainId: context.workspace.chainId,
|
|
3470
|
+
channelId: context.workspace.channelId,
|
|
3471
|
+
channelName: context.workspace.channelName,
|
|
3472
|
+
account: signer.address,
|
|
3473
|
+
});
|
|
3474
|
+
const recoveredDeliveryState = await recoverDeliveredNotesFromEventLogs({
|
|
3475
|
+
walletContext,
|
|
3476
|
+
context,
|
|
3477
|
+
provider,
|
|
3478
|
+
noteReceivePrivateKey: resolvedNoteReceiveKeyMaterial.privateKey,
|
|
3479
|
+
progressAction,
|
|
3480
|
+
});
|
|
3481
|
+
return {
|
|
3482
|
+
noteReceiveKeyMaterial: resolvedNoteReceiveKeyMaterial,
|
|
3483
|
+
recoveredDeliveryState,
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3095
3487
|
async function recoverDeliveredNotesFromEventLogs({
|
|
3096
3488
|
walletContext,
|
|
3097
3489
|
context,
|
|
3098
3490
|
provider,
|
|
3099
3491
|
noteReceivePrivateKey,
|
|
3492
|
+
progressAction = null,
|
|
3100
3493
|
}) {
|
|
3101
|
-
const scanStartBlock = Math.max(
|
|
3102
|
-
Number(walletContext.wallet.noteReceiveLastScannedBlock),
|
|
3103
|
-
Number(context.workspace.genesisBlockNumber),
|
|
3104
|
-
);
|
|
3105
3494
|
const latestBlock = await provider.getBlockNumber();
|
|
3495
|
+
const scanStartBlock = requireUsableWalletNoteReceiveRecoveryIndex({
|
|
3496
|
+
walletContext,
|
|
3497
|
+
context,
|
|
3498
|
+
latestBlock,
|
|
3499
|
+
});
|
|
3106
3500
|
const scanRange = {
|
|
3107
3501
|
fromBlock: scanStartBlock,
|
|
3108
3502
|
toBlock: latestBlock,
|
|
@@ -3128,6 +3522,9 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
3128
3522
|
topics: [NOTE_VALUE_ENCRYPTED_TOPIC],
|
|
3129
3523
|
fromBlock: scanStartBlock,
|
|
3130
3524
|
toBlock: latestBlock,
|
|
3525
|
+
onProgress: progressAction
|
|
3526
|
+
? createRpcLogScanProgress({ action: progressAction, label: "note-delivery events" })
|
|
3527
|
+
: null,
|
|
3131
3528
|
});
|
|
3132
3529
|
|
|
3133
3530
|
const importedCandidates = [];
|
|
@@ -3203,6 +3600,23 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
3203
3600
|
};
|
|
3204
3601
|
}
|
|
3205
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
|
+
|
|
3206
3620
|
function extractEncryptedNoteValueFromBridgeLog(log) {
|
|
3207
3621
|
if (!Array.isArray(log?.topics) || log.topics.length !== 1) {
|
|
3208
3622
|
return null;
|
|
@@ -3666,10 +4080,15 @@ function walletChannelWorkspaceIsReady(walletContext) {
|
|
|
3666
4080
|
&& fs.existsSync(path.join(channelWorkspaceCurrentPath(workspaceDir), "contract_codes.json"));
|
|
3667
4081
|
}
|
|
3668
4082
|
|
|
3669
|
-
async function loadPreferredWalletChannelContext({
|
|
4083
|
+
async function loadPreferredWalletChannelContext({
|
|
4084
|
+
walletContext,
|
|
4085
|
+
provider,
|
|
4086
|
+
forceRecover = false,
|
|
4087
|
+
progressAction = null,
|
|
4088
|
+
}) {
|
|
3670
4089
|
let recoveredWorkspace = false;
|
|
3671
4090
|
if (forceRecover || !walletChannelWorkspaceIsReady(walletContext)) {
|
|
3672
|
-
await recoverWalletChannelWorkspace({ walletContext, provider });
|
|
4091
|
+
await recoverWalletChannelWorkspace({ walletContext, provider, progressAction });
|
|
3673
4092
|
recoveredWorkspace = true;
|
|
3674
4093
|
}
|
|
3675
4094
|
let context = await loadWorkspaceContext(walletContext.wallet.channelName, walletContext.wallet.network, provider);
|
|
@@ -3679,7 +4098,7 @@ async function loadPreferredWalletChannelContext({ walletContext, provider, forc
|
|
|
3679
4098
|
if (!isRecoverableWalletWorkspaceFailure(error)) {
|
|
3680
4099
|
throw error;
|
|
3681
4100
|
}
|
|
3682
|
-
await recoverWalletChannelWorkspace({ walletContext, provider });
|
|
4101
|
+
await recoverWalletChannelWorkspace({ walletContext, provider, progressAction });
|
|
3683
4102
|
recoveredWorkspace = true;
|
|
3684
4103
|
context = await loadWorkspaceContext(walletContext.wallet.channelName, walletContext.wallet.network, provider);
|
|
3685
4104
|
await assertWorkspaceAlignedWithChain(context);
|
|
@@ -3692,7 +4111,7 @@ async function loadPreferredWalletChannelContext({ walletContext, provider, forc
|
|
|
3692
4111
|
};
|
|
3693
4112
|
}
|
|
3694
4113
|
|
|
3695
|
-
async function recoverWalletChannelWorkspace({ walletContext, provider }) {
|
|
4114
|
+
async function recoverWalletChannelWorkspace({ walletContext, provider, progressAction = null }) {
|
|
3696
4115
|
const networkName = walletContext.wallet.network ?? networkNameFromChainId(Number(walletContext.wallet.chainId));
|
|
3697
4116
|
const network = resolveCliNetwork(networkName);
|
|
3698
4117
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
@@ -3704,6 +4123,8 @@ async function recoverWalletChannelWorkspace({ walletContext, provider }) {
|
|
|
3704
4123
|
bridgeResources,
|
|
3705
4124
|
persist: true,
|
|
3706
4125
|
allowExistingWorkspaceSync: true,
|
|
4126
|
+
useWorkspaceRecoveryIndex: true,
|
|
4127
|
+
progressAction,
|
|
3707
4128
|
});
|
|
3708
4129
|
}
|
|
3709
4130
|
|
|
@@ -3725,6 +4146,7 @@ function assertWalletMatchesChannelContext(walletContext, l2Identity, context) {
|
|
|
3725
4146
|
}
|
|
3726
4147
|
|
|
3727
4148
|
async function executeWalletDirectTemplateCommand({
|
|
4149
|
+
args,
|
|
3728
4150
|
wallet,
|
|
3729
4151
|
provider,
|
|
3730
4152
|
operationName,
|
|
@@ -3732,13 +4154,29 @@ async function executeWalletDirectTemplateCommand({
|
|
|
3732
4154
|
}) {
|
|
3733
4155
|
emitProgress(operationName, "loading");
|
|
3734
4156
|
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
3735
|
-
|
|
4157
|
+
const {
|
|
4158
|
+
txSubmitter,
|
|
4159
|
+
source: txSubmitterSource,
|
|
4160
|
+
account: txSubmitterAccount,
|
|
4161
|
+
} = resolveTxSubmitterSigner({
|
|
4162
|
+
args,
|
|
4163
|
+
ownerSigner: signer,
|
|
4164
|
+
provider,
|
|
4165
|
+
});
|
|
4166
|
+
let contextResult = await loadPreferredWalletChannelContext({
|
|
4167
|
+
walletContext: wallet,
|
|
4168
|
+
provider,
|
|
4169
|
+
progressAction: operationName,
|
|
4170
|
+
});
|
|
3736
4171
|
let recoveredWorkspace = contextResult.recoveredWorkspace;
|
|
3737
4172
|
|
|
3738
4173
|
try {
|
|
3739
4174
|
const execution = await executeWalletTemplateSend({
|
|
3740
4175
|
wallet,
|
|
3741
4176
|
signer,
|
|
4177
|
+
txSubmitter,
|
|
4178
|
+
txSubmitterSource,
|
|
4179
|
+
txSubmitterAccount,
|
|
3742
4180
|
l2Identity,
|
|
3743
4181
|
context: contextResult.context,
|
|
3744
4182
|
operationName,
|
|
@@ -3761,11 +4199,15 @@ async function executeWalletDirectTemplateCommand({
|
|
|
3761
4199
|
walletContext: wallet,
|
|
3762
4200
|
provider,
|
|
3763
4201
|
forceRecover: true,
|
|
4202
|
+
progressAction: operationName,
|
|
3764
4203
|
});
|
|
3765
4204
|
recoveredWorkspace = contextResult.recoveredWorkspace;
|
|
3766
4205
|
const execution = await executeWalletTemplateSend({
|
|
3767
4206
|
wallet,
|
|
3768
4207
|
signer,
|
|
4208
|
+
txSubmitter,
|
|
4209
|
+
txSubmitterSource,
|
|
4210
|
+
txSubmitterAccount,
|
|
3769
4211
|
l2Identity,
|
|
3770
4212
|
context: contextResult.context,
|
|
3771
4213
|
operationName,
|
|
@@ -3783,6 +4225,9 @@ async function executeWalletDirectTemplateCommand({
|
|
|
3783
4225
|
async function executeWalletTemplateSend({
|
|
3784
4226
|
wallet,
|
|
3785
4227
|
signer,
|
|
4228
|
+
txSubmitter,
|
|
4229
|
+
txSubmitterSource,
|
|
4230
|
+
txSubmitterAccount,
|
|
3786
4231
|
l2Identity,
|
|
3787
4232
|
context,
|
|
3788
4233
|
operationName,
|
|
@@ -3860,7 +4305,7 @@ async function executeWalletTemplateSend({
|
|
|
3860
4305
|
emitProgress(operationName, "submitting");
|
|
3861
4306
|
const receipt =
|
|
3862
4307
|
await waitForReceipt(
|
|
3863
|
-
await context.channelManager.connect(
|
|
4308
|
+
await context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
|
|
3864
4309
|
);
|
|
3865
4310
|
|
|
3866
4311
|
const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
|
|
@@ -3884,6 +4329,9 @@ async function executeWalletTemplateSend({
|
|
|
3884
4329
|
return {
|
|
3885
4330
|
wallet,
|
|
3886
4331
|
signer,
|
|
4332
|
+
txSubmitter,
|
|
4333
|
+
txSubmitterSource,
|
|
4334
|
+
txSubmitterAccount,
|
|
3887
4335
|
l2Identity,
|
|
3888
4336
|
context,
|
|
3889
4337
|
noteLifecycle,
|
|
@@ -3929,24 +4377,10 @@ async function loadWorkspaceContext(workspaceName, networkName, provider) {
|
|
|
3929
4377
|
};
|
|
3930
4378
|
}
|
|
3931
4379
|
|
|
3932
|
-
async function
|
|
4380
|
+
async function loadJoinChannelContext({ args, network, provider }) {
|
|
3933
4381
|
const chainId = Number((await provider.getNetwork()).chainId);
|
|
3934
|
-
const resolvedNetworkName =
|
|
3935
|
-
const channelName = args.channelName
|
|
3936
|
-
if (args.channelName && walletContext) {
|
|
3937
|
-
expect(
|
|
3938
|
-
args.channelName === walletContext.wallet.channelName,
|
|
3939
|
-
[
|
|
3940
|
-
`The provided --channel-name (${args.channelName}) does not match the wallet channel`,
|
|
3941
|
-
`(${walletContext.wallet.channelName}).`,
|
|
3942
|
-
].join(" "),
|
|
3943
|
-
);
|
|
3944
|
-
}
|
|
3945
|
-
if (!channelName) {
|
|
3946
|
-
throw new Error(
|
|
3947
|
-
"Missing channel selector. Provide either --channel-name or --wallet bound to a channel.",
|
|
3948
|
-
);
|
|
3949
|
-
}
|
|
4382
|
+
const resolvedNetworkName = network?.name ?? networkNameFromChainId(chainId);
|
|
4383
|
+
const channelName = requireArg(args.channelName, "--channel-name");
|
|
3950
4384
|
|
|
3951
4385
|
const bridgeResources = loadBridgeResources({ chainId });
|
|
3952
4386
|
const initialized = await initializeChannelWorkspace({
|
|
@@ -4732,6 +5166,7 @@ async function reconstructChannelSnapshot({
|
|
|
4732
5166
|
baseSnapshot = null,
|
|
4733
5167
|
fromBlock = genesisBlockNumber,
|
|
4734
5168
|
toBlock = null,
|
|
5169
|
+
progressAction = null,
|
|
4735
5170
|
}) {
|
|
4736
5171
|
let startingSnapshot = baseSnapshot;
|
|
4737
5172
|
if (!startingSnapshot) {
|
|
@@ -4763,6 +5198,9 @@ async function reconstructChannelSnapshot({
|
|
|
4763
5198
|
]],
|
|
4764
5199
|
fromBlock: scanFromBlock,
|
|
4765
5200
|
toBlock: latestBlock,
|
|
5201
|
+
onProgress: progressAction
|
|
5202
|
+
? createRpcLogScanProgress({ action: progressAction, label: "channel-manager events" })
|
|
5203
|
+
: null,
|
|
4766
5204
|
});
|
|
4767
5205
|
const channelManagerEvents = channelManagerLogs.map((log) => {
|
|
4768
5206
|
const topic0 = log.topics[0] ? normalizeBytes32Hex(log.topics[0]) : null;
|
|
@@ -4781,6 +5219,9 @@ async function reconstructChannelSnapshot({
|
|
|
4781
5219
|
eventName: "StorageWriteObserved",
|
|
4782
5220
|
fromBlock: scanFromBlock,
|
|
4783
5221
|
toBlock: latestBlock,
|
|
5222
|
+
onProgress: progressAction
|
|
5223
|
+
? createRpcLogScanProgress({ action: progressAction, label: "bridge-vault events" })
|
|
5224
|
+
: null,
|
|
4784
5225
|
});
|
|
4785
5226
|
|
|
4786
5227
|
const groupedEvents = new Map();
|
|
@@ -4900,15 +5341,34 @@ async function fetchLogsChunked(provider, {
|
|
|
4900
5341
|
fromBlock,
|
|
4901
5342
|
toBlock,
|
|
4902
5343
|
initialChunkSize = DEFAULT_LOG_CHUNK_SIZE,
|
|
5344
|
+
onProgress = null,
|
|
4903
5345
|
}) {
|
|
4904
5346
|
const normalizedFromBlock = Number(fromBlock);
|
|
4905
5347
|
const resolvedToBlock = toBlock === "latest" ? await provider.getBlockNumber() : Number(toBlock);
|
|
4906
5348
|
const aggregatedLogs = [];
|
|
4907
5349
|
|
|
4908
5350
|
if (normalizedFromBlock > resolvedToBlock) {
|
|
5351
|
+
onProgress?.({
|
|
5352
|
+
status: "skipped",
|
|
5353
|
+
fromBlock: normalizedFromBlock,
|
|
5354
|
+
toBlock: resolvedToBlock,
|
|
5355
|
+
scannedBlocks: 0,
|
|
5356
|
+
totalBlocks: 0,
|
|
5357
|
+
logsFound: 0,
|
|
5358
|
+
});
|
|
4909
5359
|
return aggregatedLogs;
|
|
4910
5360
|
}
|
|
4911
5361
|
|
|
5362
|
+
const totalBlocks = resolvedToBlock - normalizedFromBlock + 1;
|
|
5363
|
+
onProgress?.({
|
|
5364
|
+
status: "start",
|
|
5365
|
+
fromBlock: normalizedFromBlock,
|
|
5366
|
+
toBlock: resolvedToBlock,
|
|
5367
|
+
scannedBlocks: 0,
|
|
5368
|
+
totalBlocks,
|
|
5369
|
+
logsFound: 0,
|
|
5370
|
+
});
|
|
5371
|
+
|
|
4912
5372
|
let chunkSize = Math.max(1, Number(initialChunkSize));
|
|
4913
5373
|
let cursor = normalizedFromBlock;
|
|
4914
5374
|
while (cursor <= resolvedToBlock) {
|
|
@@ -4922,6 +5382,17 @@ async function fetchLogsChunked(provider, {
|
|
|
4922
5382
|
toBlock: chunkToBlock,
|
|
4923
5383
|
});
|
|
4924
5384
|
aggregatedLogs.push(...logs);
|
|
5385
|
+
onProgress?.({
|
|
5386
|
+
status: "progress",
|
|
5387
|
+
fromBlock: normalizedFromBlock,
|
|
5388
|
+
toBlock: resolvedToBlock,
|
|
5389
|
+
chunkFromBlock: cursor,
|
|
5390
|
+
chunkToBlock,
|
|
5391
|
+
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
5392
|
+
totalBlocks,
|
|
5393
|
+
logsFound: aggregatedLogs.length,
|
|
5394
|
+
chunkLogs: logs.length,
|
|
5395
|
+
});
|
|
4925
5396
|
cursor = chunkToBlock + 1;
|
|
4926
5397
|
} catch (error) {
|
|
4927
5398
|
if (isRateLimitError(error)) {
|
|
@@ -4938,6 +5409,15 @@ async function fetchLogsChunked(provider, {
|
|
|
4938
5409
|
}
|
|
4939
5410
|
}
|
|
4940
5411
|
|
|
5412
|
+
onProgress?.({
|
|
5413
|
+
status: "done",
|
|
5414
|
+
fromBlock: normalizedFromBlock,
|
|
5415
|
+
toBlock: resolvedToBlock,
|
|
5416
|
+
scannedBlocks: totalBlocks,
|
|
5417
|
+
totalBlocks,
|
|
5418
|
+
logsFound: aggregatedLogs.length,
|
|
5419
|
+
});
|
|
5420
|
+
|
|
4941
5421
|
return aggregatedLogs;
|
|
4942
5422
|
}
|
|
4943
5423
|
|
|
@@ -4995,6 +5475,7 @@ async function queryContractEventsChunked({
|
|
|
4995
5475
|
eventName,
|
|
4996
5476
|
fromBlock,
|
|
4997
5477
|
toBlock,
|
|
5478
|
+
onProgress = null,
|
|
4998
5479
|
}) {
|
|
4999
5480
|
const eventFragment = contract.interface.getEvent(eventName);
|
|
5000
5481
|
const eventTopic = contract.interface.getEvent(eventName).topicHash;
|
|
@@ -5006,6 +5487,7 @@ async function queryContractEventsChunked({
|
|
|
5006
5487
|
topics: [eventTopic],
|
|
5007
5488
|
fromBlock,
|
|
5008
5489
|
toBlock,
|
|
5490
|
+
onProgress,
|
|
5009
5491
|
});
|
|
5010
5492
|
|
|
5011
5493
|
return logs.map((log) => {
|
|
@@ -5278,6 +5760,33 @@ function requireArg(value, label) {
|
|
|
5278
5760
|
return value;
|
|
5279
5761
|
}
|
|
5280
5762
|
|
|
5763
|
+
function assertVersionArgs(args) {
|
|
5764
|
+
if (args.version !== true) {
|
|
5765
|
+
throw new Error("--version does not accept a value.");
|
|
5766
|
+
}
|
|
5767
|
+
if (args.command) {
|
|
5768
|
+
throw new Error("--version must be used without a command.");
|
|
5769
|
+
}
|
|
5770
|
+
const allowedKeys = new Set(["version", "json"]);
|
|
5771
|
+
const unknownKeys = Object.keys(args)
|
|
5772
|
+
.filter((key) => key !== "positional" && key !== "command" && !allowedKeys.has(key));
|
|
5773
|
+
if (unknownKeys.length > 0) {
|
|
5774
|
+
throw new Error(`Unsupported --version option(s): ${unknownKeys.map(toKebabCase).join(", ")}.`);
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
function printVersion() {
|
|
5779
|
+
if (isJsonOutputRequested()) {
|
|
5780
|
+
printJson({
|
|
5781
|
+
action: "version",
|
|
5782
|
+
packageName: privateStateCliPackageJson.name,
|
|
5783
|
+
version: privateStateCliPackageJson.version,
|
|
5784
|
+
});
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
console.log(privateStateCliPackageJson.version);
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5281
5790
|
function requireWorkspaceName(args) {
|
|
5282
5791
|
const value = typeof args === "string" ? args : args.workspace;
|
|
5283
5792
|
if (!value) {
|
|
@@ -5306,6 +5815,29 @@ function requireL1Signer(args, provider) {
|
|
|
5306
5815
|
return new Wallet(resolvePrivateKeySource(args), provider);
|
|
5307
5816
|
}
|
|
5308
5817
|
|
|
5818
|
+
function resolveTxSubmitterSigner({ args, ownerSigner, provider }) {
|
|
5819
|
+
if (args.txSubmitter === undefined) {
|
|
5820
|
+
return {
|
|
5821
|
+
txSubmitter: ownerSigner,
|
|
5822
|
+
source: "wallet-owner",
|
|
5823
|
+
account: null,
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
if (args.txSubmitter === true || String(args.txSubmitter).trim() === "") {
|
|
5827
|
+
throw new Error("--tx-submitter requires a local account name.");
|
|
5828
|
+
}
|
|
5829
|
+
const networkName = requireNetworkName(args);
|
|
5830
|
+
const account = String(args.txSubmitter).trim();
|
|
5831
|
+
return {
|
|
5832
|
+
txSubmitter: new Wallet(
|
|
5833
|
+
normalizePrivateKey(readSecretFile(accountPrivateKeyPath(networkName, account), "--tx-submitter")),
|
|
5834
|
+
provider,
|
|
5835
|
+
),
|
|
5836
|
+
source: "tx-submitter-account",
|
|
5837
|
+
account,
|
|
5838
|
+
};
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5309
5841
|
function resolvePrivateKeySource(args) {
|
|
5310
5842
|
const networkName = requireNetworkName(args);
|
|
5311
5843
|
const account = requireAccountName(args);
|
|
@@ -5593,6 +6125,10 @@ function assertUninstallArgs(args) {
|
|
|
5593
6125
|
assertAllowedCommandSchema(args, "uninstall");
|
|
5594
6126
|
}
|
|
5595
6127
|
|
|
6128
|
+
function assertUpdateArgs(args) {
|
|
6129
|
+
assertAllowedCommandSchema(args, "update");
|
|
6130
|
+
}
|
|
6131
|
+
|
|
5596
6132
|
function assertDoctorArgs(args) {
|
|
5597
6133
|
assertAllowedCommandSchema(args, "doctor");
|
|
5598
6134
|
if (args.gpu !== undefined && args.gpu !== true) {
|
|
@@ -5616,12 +6152,17 @@ function assertGuideArgs(args) {
|
|
|
5616
6152
|
assertAllowedCommandSchema(args, "guide");
|
|
5617
6153
|
}
|
|
5618
6154
|
|
|
6155
|
+
function assertTransactionFeesArgs(args) {
|
|
6156
|
+
assertAllowedCommandSchema(args, "transaction-fees");
|
|
6157
|
+
}
|
|
6158
|
+
|
|
5619
6159
|
function assertAccountImportArgs(args) {
|
|
5620
6160
|
assertAllowedCommandSchema(args, "account-import");
|
|
5621
6161
|
}
|
|
5622
6162
|
|
|
5623
6163
|
function assertMintNotesArgs(args) {
|
|
5624
6164
|
assertAllowedCommandSchema(args, "mint-notes");
|
|
6165
|
+
assertTxSubmitterArg(args);
|
|
5625
6166
|
parseAmountVector(args.amounts, {
|
|
5626
6167
|
allowZeroEntries: true,
|
|
5627
6168
|
requireAnyPositive: true,
|
|
@@ -5630,11 +6171,13 @@ function assertMintNotesArgs(args) {
|
|
|
5630
6171
|
|
|
5631
6172
|
function assertRedeemNotesArgs(args) {
|
|
5632
6173
|
assertAllowedCommandSchema(args, "redeem-notes");
|
|
6174
|
+
assertTxSubmitterArg(args);
|
|
5633
6175
|
selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
|
|
5634
6176
|
}
|
|
5635
6177
|
|
|
5636
6178
|
function assertTransferNotesArgs(args) {
|
|
5637
6179
|
assertAllowedCommandSchema(args, "transfer-notes");
|
|
6180
|
+
assertTxSubmitterArg(args);
|
|
5638
6181
|
const noteIds = parseNoteIdVector(args.noteIds);
|
|
5639
6182
|
const recipients = parseRecipientVector(args.recipients);
|
|
5640
6183
|
const amounts = parseAmountVector(args.amounts);
|
|
@@ -5645,6 +6188,15 @@ function assertTransferNotesArgs(args) {
|
|
|
5645
6188
|
selectTransferNotesMethod(noteIds.length, recipients.length);
|
|
5646
6189
|
}
|
|
5647
6190
|
|
|
6191
|
+
function assertTxSubmitterArg(args) {
|
|
6192
|
+
if (args.txSubmitter === undefined) {
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
if (args.txSubmitter === true || String(args.txSubmitter).trim() === "") {
|
|
6196
|
+
throw new Error("--tx-submitter requires a local account name.");
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
|
|
5648
6200
|
function assertGetMyNotesArgs(args) {
|
|
5649
6201
|
assertWalletSecretArgs(args, "get-my-notes");
|
|
5650
6202
|
}
|
|
@@ -5675,6 +6227,9 @@ function assertExplicitSignerCommandArgs(args, commandName) {
|
|
|
5675
6227
|
|
|
5676
6228
|
function assertRecoverWalletArgs(args) {
|
|
5677
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
|
+
}
|
|
5678
6233
|
}
|
|
5679
6234
|
|
|
5680
6235
|
function assertJoinChannelArgs(args) {
|
|
@@ -5769,6 +6324,9 @@ Secret source options:
|
|
|
5769
6324
|
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
5770
6325
|
|
|
5771
6326
|
Options:
|
|
6327
|
+
--version
|
|
6328
|
+
Print the private-state CLI package version and exit.
|
|
6329
|
+
|
|
5772
6330
|
--json
|
|
5773
6331
|
Print the command result as JSON. Without --json, commands print human-readable output.
|
|
5774
6332
|
|
|
@@ -6197,6 +6755,8 @@ function loadWalletCommandRuntime(args) {
|
|
|
6197
6755
|
|
|
6198
6756
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
6199
6757
|
guide: printGuideHumanResult,
|
|
6758
|
+
"transaction-fees": printTransactionFeesHumanResult,
|
|
6759
|
+
update: printUpdateHumanResult,
|
|
6200
6760
|
});
|
|
6201
6761
|
|
|
6202
6762
|
function normalizePrivateKey(value) {
|
|
@@ -6246,9 +6806,90 @@ function printGuideHumanResult(guide) {
|
|
|
6246
6806
|
}
|
|
6247
6807
|
|
|
6248
6808
|
lines.push("", "Run with --json to inspect the full guide state.");
|
|
6809
|
+
lines.push(
|
|
6810
|
+
"",
|
|
6811
|
+
"Privacy Tip",
|
|
6812
|
+
formatHumanValue(guide.privacyTip),
|
|
6813
|
+
);
|
|
6814
|
+
console.log(lines.join("\n"));
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
function printTransactionFeesHumanResult(report) {
|
|
6818
|
+
const lines = [
|
|
6819
|
+
"Transaction Fees",
|
|
6820
|
+
`Generated: ${formatHumanValue(report.generatedAt)}`,
|
|
6821
|
+
`Network: ${formatHumanValue(report.network)} (${formatHumanValue(report.chainId)})`,
|
|
6822
|
+
`Typical gas price: ${formatHumanValue(report.livePricing?.typicalGasPriceGwei)} gwei (${formatHumanValue(report.livePricing?.typicalGasPriceSource)})`,
|
|
6823
|
+
`Worst-case gas price: ${formatHumanValue(report.livePricing?.worstCaseGasPriceGwei)} gwei (${formatHumanValue(report.livePricing?.worstCaseGasPriceSource)})`,
|
|
6824
|
+
`ETH/USD: $${formatHumanValue(report.livePricing?.ethUsd)} (${formatHumanValue(report.livePricing?.ethUsdSource)})`,
|
|
6825
|
+
`Measured gas asset: ${formatHumanValue(report.asset?.schema)}, measured ${formatHumanValue(report.asset?.measuredAt)}`,
|
|
6826
|
+
"",
|
|
6827
|
+
formatHumanTable(
|
|
6828
|
+
["Command", "Transactions", "Gas", "Typical ETH", "Typical USD", "Worst ETH", "Worst USD", "Source"],
|
|
6829
|
+
(report.rows ?? []).map((row) => [
|
|
6830
|
+
row.command,
|
|
6831
|
+
row.transactions,
|
|
6832
|
+
String(row.gasUsed),
|
|
6833
|
+
row.typicalEth,
|
|
6834
|
+
`$${row.typicalUsd}`,
|
|
6835
|
+
row.worstCaseEth,
|
|
6836
|
+
`$${row.worstCaseUsd}`,
|
|
6837
|
+
row.sources,
|
|
6838
|
+
]),
|
|
6839
|
+
),
|
|
6840
|
+
];
|
|
6841
|
+
if (Array.isArray(report.asset?.notes) && report.asset.notes.length > 0) {
|
|
6842
|
+
lines.push(
|
|
6843
|
+
"",
|
|
6844
|
+
"Notes",
|
|
6845
|
+
...report.asset.notes.map((note) => `- ${note}`),
|
|
6846
|
+
);
|
|
6847
|
+
}
|
|
6249
6848
|
console.log(lines.join("\n"));
|
|
6250
6849
|
}
|
|
6251
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
|
+
|
|
6880
|
+
function formatHumanTable(headers, rows) {
|
|
6881
|
+
const values = [headers, ...rows].map((row) => row.map((value) => String(value ?? "")));
|
|
6882
|
+
const widths = headers.map((_header, columnIndex) =>
|
|
6883
|
+
Math.max(...values.map((row) => row[columnIndex].length)),
|
|
6884
|
+
);
|
|
6885
|
+
const formatRow = (row) => `| ${row.map((value, index) => value.padEnd(widths[index])).join(" | ")} |`;
|
|
6886
|
+
return [
|
|
6887
|
+
formatRow(values[0]),
|
|
6888
|
+
formatRow(widths.map((width) => "-".repeat(width))),
|
|
6889
|
+
...values.slice(1).map(formatRow),
|
|
6890
|
+
].join("\n");
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6252
6893
|
function formatGuideSelector(value) {
|
|
6253
6894
|
return value === null || value === undefined || value === "" ? "not selected" : String(value);
|
|
6254
6895
|
}
|
|
@@ -6327,6 +6968,51 @@ function emitProgress(action, phase) {
|
|
|
6327
6968
|
}
|
|
6328
6969
|
}
|
|
6329
6970
|
|
|
6971
|
+
function createRpcLogScanProgress({ action, label }) {
|
|
6972
|
+
let lastBucket = -1;
|
|
6973
|
+
return (event) => {
|
|
6974
|
+
const totalBlocks = Number(event.totalBlocks ?? 0);
|
|
6975
|
+
const scannedBlocks = Number(event.scannedBlocks ?? 0);
|
|
6976
|
+
const logsFound = Number(event.logsFound ?? 0);
|
|
6977
|
+
if (event.status === "skipped") {
|
|
6978
|
+
emitProgress(action, `rpc-log-scan ${label}: skipped (no blocks to scan, ${logsFound} logs)`);
|
|
6979
|
+
return;
|
|
6980
|
+
}
|
|
6981
|
+
if (event.status === "start") {
|
|
6982
|
+
lastBucket = 0;
|
|
6983
|
+
emitProgress(
|
|
6984
|
+
action,
|
|
6985
|
+
`rpc-log-scan ${label}: 0% (0/${totalBlocks} blocks, ${logsFound} logs, blocks ${event.fromBlock}-${event.toBlock})`,
|
|
6986
|
+
);
|
|
6987
|
+
return;
|
|
6988
|
+
}
|
|
6989
|
+
if (event.status === "done") {
|
|
6990
|
+
emitProgress(
|
|
6991
|
+
action,
|
|
6992
|
+
`rpc-log-scan ${label}: 100% (${totalBlocks}/${totalBlocks} blocks, ${logsFound} logs, done)`,
|
|
6993
|
+
);
|
|
6994
|
+
return;
|
|
6995
|
+
}
|
|
6996
|
+
if (event.status !== "progress" || totalBlocks <= 0) {
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6999
|
+
|
|
7000
|
+
const percent = Math.min(100, Math.floor((scannedBlocks * 100) / totalBlocks));
|
|
7001
|
+
if (percent >= 100) {
|
|
7002
|
+
return;
|
|
7003
|
+
}
|
|
7004
|
+
const bucket = Math.floor(percent / 10) * 10;
|
|
7005
|
+
if (bucket <= lastBucket) {
|
|
7006
|
+
return;
|
|
7007
|
+
}
|
|
7008
|
+
lastBucket = bucket;
|
|
7009
|
+
emitProgress(
|
|
7010
|
+
action,
|
|
7011
|
+
`rpc-log-scan ${label}: ${percent}% (${scannedBlocks}/${totalBlocks} blocks, ${logsFound} logs)`,
|
|
7012
|
+
);
|
|
7013
|
+
};
|
|
7014
|
+
}
|
|
7015
|
+
|
|
6330
7016
|
function formatCliErrorForDisplay(error, args = {}) {
|
|
6331
7017
|
const message = String(error?.message ?? error);
|
|
6332
7018
|
const hints = buildRecoveryHints(error, args);
|
|
@@ -6411,6 +7097,15 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
6411
7097
|
hints.push(`private-state-cli guide --network ${networkName} --channel-name ${channelName}`);
|
|
6412
7098
|
}
|
|
6413
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
|
+
|
|
6414
7109
|
if (message.includes("Missing channel selector")) {
|
|
6415
7110
|
hints.push(`private-state-cli list-local-wallets --network ${networkName}`);
|
|
6416
7111
|
hints.push(`private-state-cli guide --network ${networkName} --channel-name <CHANNEL> --wallet <WALLET>`);
|