@tokamak-private-dapps/private-state-cli 2.4.1 → 2.4.3
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 +14 -0
- package/README.md +22 -5
- package/agents.md +9 -5
- package/commands/index.mjs +2 -6
- package/lib/private-state-cli-command-registry.mjs +11 -7
- package/lib/runtime.mjs +594 -157
- package/package.json +1 -1
package/lib/runtime.mjs
CHANGED
|
@@ -166,6 +166,8 @@ const CLI_ERROR_CODES = Object.freeze({
|
|
|
166
166
|
MISSING_CHANNEL_REGISTRATION: "MISSING_CHANNEL_REGISTRATION",
|
|
167
167
|
STALE_WORKSPACE: "STALE_WORKSPACE",
|
|
168
168
|
STALE_CHANNEL_ROOT: "STALE_CHANNEL_ROOT",
|
|
169
|
+
TX_DRY_RUN_FAILED: "TX_DRY_RUN_FAILED",
|
|
170
|
+
TX_SUBMIT_FAILED: "TX_SUBMIT_FAILED",
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
class PrivateStateCliError extends Error {
|
|
@@ -288,7 +290,13 @@ function printImmutableChannelPolicyWarning({
|
|
|
288
290
|
"Do not sign if any snapshot value is unexpected or has not been reviewed.",
|
|
289
291
|
);
|
|
290
292
|
}
|
|
291
|
-
|
|
293
|
+
cliOutput.warning("channel-policy", details.join("\n"), {
|
|
294
|
+
action,
|
|
295
|
+
channelName,
|
|
296
|
+
channelId: channelId.toString(),
|
|
297
|
+
channelManager,
|
|
298
|
+
policySnapshot,
|
|
299
|
+
});
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
const ACTION_IMPACT_SUMMARIES = Object.freeze({
|
|
@@ -483,7 +491,16 @@ function printActionImpactSummary(summary, details) {
|
|
|
483
491
|
lines.push(`- Exchange-controlled address warning: ${summary.exchangeControlledAddressWarning}`);
|
|
484
492
|
}
|
|
485
493
|
lines.push(`- Confirmation: pass --acknowledge-action-impact or type the exact confirmation phrase when prompted.`);
|
|
486
|
-
|
|
494
|
+
cliOutput.warning("action-impact", lines.join("\n"), {
|
|
495
|
+
command: summary.display,
|
|
496
|
+
l1PublicEvent: summary.l1PublicEvent,
|
|
497
|
+
privateNoteState: summary.privateNoteState,
|
|
498
|
+
publicFields: normalizeImpactLines(summary.publicFields, details),
|
|
499
|
+
notPublic: normalizeImpactLines(summary.notPublic, details),
|
|
500
|
+
noteProvenance: summary.noteProvenance,
|
|
501
|
+
policy: summary.policy,
|
|
502
|
+
exchangeControlledAddressWarning: summary.exchangeControlledAddressWarning ?? null,
|
|
503
|
+
});
|
|
487
504
|
}
|
|
488
505
|
|
|
489
506
|
function normalizeImpactLines(value, details) {
|
|
@@ -609,8 +626,15 @@ async function handleChannelCreate({ args, network, provider }) {
|
|
|
609
626
|
channelId,
|
|
610
627
|
policySnapshot,
|
|
611
628
|
});
|
|
612
|
-
const receipt =
|
|
613
|
-
|
|
629
|
+
const receipt = await dryRunThenSubmitTransaction({
|
|
630
|
+
operationName: "channel create",
|
|
631
|
+
call: contractTxCall(
|
|
632
|
+
bridgeCore.createChannel,
|
|
633
|
+
[channelId, dappId, joinToll, dapp.metadataDigest],
|
|
634
|
+
undefined,
|
|
635
|
+
bridgeCore.interface,
|
|
636
|
+
),
|
|
637
|
+
});
|
|
614
638
|
const channelInfo = await bridgeCore.getChannel(channelId);
|
|
615
639
|
|
|
616
640
|
const workspaceResult = await syncChannelWorkspace({
|
|
@@ -625,7 +649,7 @@ async function handleChannelCreate({ args, network, provider }) {
|
|
|
625
649
|
progressAction: "channel create",
|
|
626
650
|
});
|
|
627
651
|
|
|
628
|
-
|
|
652
|
+
cliOutput.result({
|
|
629
653
|
action: "channel create",
|
|
630
654
|
channelName,
|
|
631
655
|
channelId: channelId.toString(),
|
|
@@ -767,7 +791,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
767
791
|
})
|
|
768
792
|
: null;
|
|
769
793
|
|
|
770
|
-
|
|
794
|
+
cliOutput.result({
|
|
771
795
|
action: "channel recover-workspace",
|
|
772
796
|
source: workspace.recoverySource ?? recoverySource,
|
|
773
797
|
workspace: workspaceName,
|
|
@@ -804,7 +828,7 @@ async function handleGetChannel({ args, network, provider }) {
|
|
|
804
828
|
channelInfo = await bridgeCore.getChannel(channelId);
|
|
805
829
|
} catch (error) {
|
|
806
830
|
if (isContractError(error, bridgeCore.interface, "UnknownChannel")) {
|
|
807
|
-
|
|
831
|
+
cliOutput.result({
|
|
808
832
|
action: "channel get-meta",
|
|
809
833
|
channelName,
|
|
810
834
|
channelId: channelId.toString(),
|
|
@@ -843,7 +867,7 @@ async function handleGetChannel({ args, network, provider }) {
|
|
|
843
867
|
readChannelRefundSchedule(channelManager),
|
|
844
868
|
]);
|
|
845
869
|
|
|
846
|
-
|
|
870
|
+
cliOutput.result({
|
|
847
871
|
action: "channel get-meta",
|
|
848
872
|
channelName,
|
|
849
873
|
channelId: channelId.toString(),
|
|
@@ -880,10 +904,18 @@ async function handleSetChannelWorkspaceMirror({ args, network, provider }) {
|
|
|
880
904
|
);
|
|
881
905
|
const channelId = deriveChannelIdFromName(channelName);
|
|
882
906
|
const previousUrl = await readChannelWorkspaceMirror({ bridgeCore, channelId });
|
|
883
|
-
const receipt = await
|
|
907
|
+
const receipt = await dryRunThenSubmitTransaction({
|
|
908
|
+
operationName: "channel set-workspace-mirror",
|
|
909
|
+
call: contractTxCall(
|
|
910
|
+
bridgeCore.setChannelWorkspaceMirror,
|
|
911
|
+
[channelId, url],
|
|
912
|
+
undefined,
|
|
913
|
+
bridgeCore.interface,
|
|
914
|
+
),
|
|
915
|
+
});
|
|
884
916
|
const currentUrl = await readChannelWorkspaceMirror({ bridgeCore, channelId });
|
|
885
917
|
|
|
886
|
-
|
|
918
|
+
cliOutput.result({
|
|
887
919
|
action: "channel set-workspace-mirror",
|
|
888
920
|
channelName,
|
|
889
921
|
channelId: channelId.toString(),
|
|
@@ -2061,7 +2093,8 @@ async function syncChannelWorkspace({
|
|
|
2061
2093
|
throw new Error([
|
|
2062
2094
|
`Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
|
|
2063
2095
|
"The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
|
|
2064
|
-
|
|
2096
|
+
`If a workspace mirror is registered, run channel recover-workspace --channel-name ${channelName} --network ${networkNameFromChainId(network.chainId)} --source mirror first.`,
|
|
2097
|
+
`Use channel recover-workspace --channel-name ${channelName} --network ${networkNameFromChainId(network.chainId)} --source rpc --from-genesis only when no compatible mirror is available.`,
|
|
2065
2098
|
].join(" "));
|
|
2066
2099
|
}
|
|
2067
2100
|
const workspaceBase = {
|
|
@@ -2209,12 +2242,28 @@ async function handleDepositBridge({ args, network, provider }) {
|
|
|
2209
2242
|
signer,
|
|
2210
2243
|
);
|
|
2211
2244
|
let nextNonce = await provider.getTransactionCount(signer.address, "pending");
|
|
2212
|
-
const approveReceipt =
|
|
2213
|
-
|
|
2214
|
-
|
|
2245
|
+
const approveReceipt = await dryRunThenSubmitTransaction({
|
|
2246
|
+
operationName: "account deposit-bridge approve",
|
|
2247
|
+
call: contractTxCall(
|
|
2248
|
+
asset.approve,
|
|
2249
|
+
[bridgeVaultContext.bridgeTokenVaultAddress, amount],
|
|
2250
|
+
{ nonce: nextNonce++ },
|
|
2251
|
+
asset.interface,
|
|
2252
|
+
),
|
|
2253
|
+
});
|
|
2254
|
+
const fundReceipt = await dryRunThenSubmitTransaction({
|
|
2255
|
+
operationName: "account deposit-bridge fund",
|
|
2256
|
+
call: contractTxCall(
|
|
2257
|
+
bridgeTokenVault.fund,
|
|
2258
|
+
[amount],
|
|
2259
|
+
{ nonce: nextNonce++ },
|
|
2260
|
+
bridgeTokenVault.interface,
|
|
2261
|
+
),
|
|
2262
|
+
submittedBefore: [submittedReceiptSummary("account deposit-bridge approve", approveReceipt)],
|
|
2263
|
+
});
|
|
2215
2264
|
const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
|
|
2216
2265
|
|
|
2217
|
-
|
|
2266
|
+
cliOutput.result({
|
|
2218
2267
|
action: "account deposit-bridge",
|
|
2219
2268
|
amountInput,
|
|
2220
2269
|
amountBaseUnits: amount.toString(),
|
|
@@ -2242,7 +2291,7 @@ async function handleAccountGetBridgeFund({ args, provider }) {
|
|
|
2242
2291
|
);
|
|
2243
2292
|
const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
|
|
2244
2293
|
|
|
2245
|
-
|
|
2294
|
+
cliOutput.result({
|
|
2246
2295
|
action: "account get-bridge-fund",
|
|
2247
2296
|
l1Address: signer.address,
|
|
2248
2297
|
bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
|
|
@@ -2392,7 +2441,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2392
2441
|
latestBlock: recoveryEventScan.scanRange.toBlock,
|
|
2393
2442
|
});
|
|
2394
2443
|
|
|
2395
|
-
|
|
2444
|
+
cliOutput.result({
|
|
2396
2445
|
action: "wallet recover-workspace",
|
|
2397
2446
|
status,
|
|
2398
2447
|
wallet: walletName,
|
|
@@ -2490,7 +2539,7 @@ async function handleInstallZkEvm({ args }) {
|
|
|
2490
2539
|
tokamakCliRuntime,
|
|
2491
2540
|
groth16Runtime,
|
|
2492
2541
|
});
|
|
2493
|
-
|
|
2542
|
+
cliOutput.result({
|
|
2494
2543
|
action: "install",
|
|
2495
2544
|
installMode,
|
|
2496
2545
|
selectedVersions,
|
|
@@ -2531,7 +2580,7 @@ async function handleUninstall() {
|
|
|
2531
2580
|
});
|
|
2532
2581
|
const globalPackage = uninstallGlobalPrivateStateCliPackage();
|
|
2533
2582
|
|
|
2534
|
-
|
|
2583
|
+
cliOutput.result({
|
|
2535
2584
|
action: "uninstall",
|
|
2536
2585
|
confirmationAccepted: true,
|
|
2537
2586
|
removedPrivateStateRoots,
|
|
@@ -2558,7 +2607,7 @@ async function handleSetRpc({ args }) {
|
|
|
2558
2607
|
LOG_CHUNK_SIZE: rpcScanLimits.blockRangeCap,
|
|
2559
2608
|
RPC_BLOCK_RANGE_CAP: rpcScanLimits.blockRangeCap,
|
|
2560
2609
|
});
|
|
2561
|
-
|
|
2610
|
+
cliOutput.result({
|
|
2562
2611
|
action: "set rpc",
|
|
2563
2612
|
network: networkName,
|
|
2564
2613
|
rpcConfigPath: rpcConfigEnvPath(networkName),
|
|
@@ -2963,12 +3012,12 @@ async function handleUpdate() {
|
|
|
2963
3012
|
};
|
|
2964
3013
|
|
|
2965
3014
|
if (!updateAvailable) {
|
|
2966
|
-
|
|
3015
|
+
cliOutput.result(result);
|
|
2967
3016
|
return;
|
|
2968
3017
|
}
|
|
2969
3018
|
|
|
2970
3019
|
if (runningFromRepositoryCheckout) {
|
|
2971
|
-
|
|
3020
|
+
cliOutput.result({
|
|
2972
3021
|
...result,
|
|
2973
3022
|
reason: "running from a repository checkout; update the checkout with git/npm instead of mutating source files",
|
|
2974
3023
|
});
|
|
@@ -2976,7 +3025,7 @@ async function handleUpdate() {
|
|
|
2976
3025
|
}
|
|
2977
3026
|
|
|
2978
3027
|
if (!globalPackage.installed) {
|
|
2979
|
-
|
|
3028
|
+
cliOutput.result({
|
|
2980
3029
|
...result,
|
|
2981
3030
|
reason: "global npm package is not installed; install or update the CLI with the printed command",
|
|
2982
3031
|
});
|
|
@@ -2990,7 +3039,7 @@ async function handleUpdate() {
|
|
|
2990
3039
|
stripAnsi(install.stderr || install.stdout).trim(),
|
|
2991
3040
|
].filter(Boolean).join(" "));
|
|
2992
3041
|
}
|
|
2993
|
-
|
|
3042
|
+
cliOutput.result({
|
|
2994
3043
|
...result,
|
|
2995
3044
|
attempted: true,
|
|
2996
3045
|
updated: true,
|
|
@@ -3042,18 +3091,14 @@ function isRepositoryCliPackageRoot(packageRoot) {
|
|
|
3042
3091
|
|
|
3043
3092
|
async function handleDoctor({ args }) {
|
|
3044
3093
|
const report = buildDoctorReport({ probeGpu: args.gpu === true });
|
|
3045
|
-
|
|
3046
|
-
printJson(report);
|
|
3047
|
-
} else {
|
|
3048
|
-
printDoctorHumanReport(report);
|
|
3049
|
-
}
|
|
3094
|
+
cliOutput.result(report);
|
|
3050
3095
|
if (!report.ok) {
|
|
3051
3096
|
process.exitCode = 1;
|
|
3052
3097
|
}
|
|
3053
3098
|
}
|
|
3054
3099
|
|
|
3055
3100
|
function handleObserver() {
|
|
3056
|
-
|
|
3101
|
+
cliOutput.result({
|
|
3057
3102
|
action: "observer",
|
|
3058
3103
|
url: PRIVATE_STATE_OBSERVER_URL,
|
|
3059
3104
|
scope: "Public monitoring observer for Tokamak Private App Channels and the private-state DApp.",
|
|
@@ -3068,7 +3113,7 @@ function handleInvestigator() {
|
|
|
3068
3113
|
const htmlPath = resolveInvestigatorIndexPath();
|
|
3069
3114
|
const fileUrl = pathToFileURL(htmlPath).href;
|
|
3070
3115
|
const browser = openFileInDefaultBrowser(fileUrl);
|
|
3071
|
-
|
|
3116
|
+
cliOutput.result({
|
|
3072
3117
|
action: "investigator",
|
|
3073
3118
|
htmlPath,
|
|
3074
3119
|
fileUrl,
|
|
@@ -3137,7 +3182,7 @@ async function handleTransactionFees({ network, provider, rpcUrl }) {
|
|
|
3137
3182
|
ethUsd,
|
|
3138
3183
|
});
|
|
3139
3184
|
|
|
3140
|
-
|
|
3185
|
+
cliOutput.result({
|
|
3141
3186
|
action: "transaction-fees",
|
|
3142
3187
|
generatedAt: new Date().toISOString(),
|
|
3143
3188
|
network: network.name,
|
|
@@ -3265,7 +3310,7 @@ function trimFixedNumber(value, maxDecimals) {
|
|
|
3265
3310
|
|
|
3266
3311
|
function handleAccountGetL1Address({ args }) {
|
|
3267
3312
|
const signer = requireL1Signer(args);
|
|
3268
|
-
|
|
3313
|
+
cliOutput.result({
|
|
3269
3314
|
action: "account get-l1-address",
|
|
3270
3315
|
l1Address: signer.address,
|
|
3271
3316
|
account: args.account ?? null,
|
|
@@ -3292,7 +3337,7 @@ function handleAccountImport({ args }) {
|
|
|
3292
3337
|
l1Address: getAddress(signer.address),
|
|
3293
3338
|
privateKeyPath,
|
|
3294
3339
|
}, 0o600);
|
|
3295
|
-
|
|
3340
|
+
cliOutput.result({
|
|
3296
3341
|
action: "account import",
|
|
3297
3342
|
account,
|
|
3298
3343
|
network: networkName,
|
|
@@ -3313,7 +3358,7 @@ function handleListLocalWallets({ args }) {
|
|
|
3313
3358
|
channelFilter,
|
|
3314
3359
|
});
|
|
3315
3360
|
|
|
3316
|
-
|
|
3361
|
+
cliOutput.result({
|
|
3317
3362
|
action: "wallet list",
|
|
3318
3363
|
workspaceRoot,
|
|
3319
3364
|
filters: {
|
|
@@ -3381,7 +3426,7 @@ function handleWalletExportBackup({ args }) {
|
|
|
3381
3426
|
archive.writeZip(outputPath);
|
|
3382
3427
|
protectSecretFile(outputPath, "wallet export ZIP");
|
|
3383
3428
|
|
|
3384
|
-
|
|
3429
|
+
cliOutput.result({
|
|
3385
3430
|
action: "wallet export backup",
|
|
3386
3431
|
output: outputPath,
|
|
3387
3432
|
exportMode: manifest.exportMode,
|
|
@@ -3406,7 +3451,7 @@ function handleWalletExportKey({ args, keyKind }) {
|
|
|
3406
3451
|
validateWalletKeyPayload(payload, keyKind);
|
|
3407
3452
|
fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
3408
3453
|
protectSecretFile(outputPath, `${keyKind} key export`);
|
|
3409
|
-
|
|
3454
|
+
cliOutput.result({
|
|
3410
3455
|
action: `wallet export ${keyKind}-key`,
|
|
3411
3456
|
wallet: wallet.walletName,
|
|
3412
3457
|
network: networkName,
|
|
@@ -3451,7 +3496,7 @@ function handleWalletImportBackup({ args }) {
|
|
|
3451
3496
|
|
|
3452
3497
|
commitWalletImportFiles({ targetRoot, plannedWrites });
|
|
3453
3498
|
|
|
3454
|
-
|
|
3499
|
+
cliOutput.result({
|
|
3455
3500
|
action: "wallet import backup",
|
|
3456
3501
|
input: inputPath,
|
|
3457
3502
|
exportMode: manifest.exportMode,
|
|
@@ -3494,7 +3539,7 @@ function handleWalletImportKey({ args, keyKind }) {
|
|
|
3494
3539
|
writeJson(metadataPath, metadata);
|
|
3495
3540
|
}
|
|
3496
3541
|
}
|
|
3497
|
-
|
|
3542
|
+
cliOutput.result({
|
|
3498
3543
|
action: `wallet import ${keyKind}-key`,
|
|
3499
3544
|
input: inputPath,
|
|
3500
3545
|
network: networkName,
|
|
@@ -3636,7 +3681,7 @@ async function handleGuide({ args }) {
|
|
|
3636
3681
|
"help guide --network anvil",
|
|
3637
3682
|
],
|
|
3638
3683
|
});
|
|
3639
|
-
|
|
3684
|
+
cliOutput.result(guide);
|
|
3640
3685
|
return;
|
|
3641
3686
|
}
|
|
3642
3687
|
|
|
@@ -3649,7 +3694,7 @@ async function handleGuide({ args }) {
|
|
|
3649
3694
|
command: "help guide --network <NAME>",
|
|
3650
3695
|
why: `The requested network ${networkName} is not supported by the CLI network config.`,
|
|
3651
3696
|
});
|
|
3652
|
-
|
|
3697
|
+
cliOutput.result(guide);
|
|
3653
3698
|
return;
|
|
3654
3699
|
}
|
|
3655
3700
|
|
|
@@ -3736,7 +3781,7 @@ async function handleGuide({ args }) {
|
|
|
3736
3781
|
|
|
3737
3782
|
destroyGuideProvider(provider);
|
|
3738
3783
|
applyGuideNextAction(guide);
|
|
3739
|
-
|
|
3784
|
+
cliOutput.result(guide);
|
|
3740
3785
|
}
|
|
3741
3786
|
|
|
3742
3787
|
function inspectGuideLocalState(args) {
|
|
@@ -3873,12 +3918,14 @@ async function inspectGuideChannel({ channelName, network, provider, artifactsIn
|
|
|
3873
3918
|
bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
|
|
3874
3919
|
provider,
|
|
3875
3920
|
);
|
|
3876
|
-
const [joinToll, refundSchedule] = await Promise.all([
|
|
3921
|
+
const [joinToll, refundSchedule, workspaceMirror] = await Promise.all([
|
|
3877
3922
|
channelManager.joinToll(),
|
|
3878
3923
|
readChannelRefundSchedule(channelManager),
|
|
3924
|
+
readChannelWorkspaceMirror({ bridgeCore, channelId }),
|
|
3879
3925
|
]);
|
|
3880
3926
|
result.onchain.joinTollBaseUnits = joinToll.toString();
|
|
3881
3927
|
result.onchain.refundSchedule = refundSchedule;
|
|
3928
|
+
result.onchain.workspaceMirror = workspaceMirror;
|
|
3882
3929
|
}
|
|
3883
3930
|
} catch (error) {
|
|
3884
3931
|
result.error = error.message;
|
|
@@ -4054,9 +4101,19 @@ function applyGuideNextAction(guide) {
|
|
|
4054
4101
|
return;
|
|
4055
4102
|
}
|
|
4056
4103
|
if (guide.selectors.channelName && guide.state.channel?.onchain?.exists && !guide.state.channel?.local?.workspaceExists) {
|
|
4104
|
+
const workspaceMirror = typeof guide.state.channel.onchain.workspaceMirror === "string"
|
|
4105
|
+
? guide.state.channel.onchain.workspaceMirror.trim()
|
|
4106
|
+
: "";
|
|
4107
|
+
if (workspaceMirror) {
|
|
4108
|
+
setGuideNextAction(guide, {
|
|
4109
|
+
command: `channel recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --source mirror`,
|
|
4110
|
+
why: "The channel has a registered workspace mirror. Use mirror recovery before considering an explicit RPC genesis rebuild.",
|
|
4111
|
+
});
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4057
4114
|
setGuideNextAction(guide, {
|
|
4058
4115
|
command: `channel recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --source rpc --from-genesis`,
|
|
4059
|
-
why: "The channel exists on-chain, but the local channel workspace has not been recovered yet
|
|
4116
|
+
why: "The channel exists on-chain, but the local channel workspace has not been recovered yet and no workspace mirror is registered. RPC genesis rebuild is the remaining explicit bootstrap path.",
|
|
4060
4117
|
});
|
|
4061
4118
|
return;
|
|
4062
4119
|
}
|
|
@@ -4213,7 +4270,7 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
4213
4270
|
provider,
|
|
4214
4271
|
});
|
|
4215
4272
|
|
|
4216
|
-
|
|
4273
|
+
cliOutput.result({
|
|
4217
4274
|
action: "wallet get-meta",
|
|
4218
4275
|
wallet: wallet.walletName,
|
|
4219
4276
|
...walletLifecycleMetadata(wallet.wallet),
|
|
@@ -4547,7 +4604,7 @@ async function handleWalletGetChannelFund({ args, provider }) {
|
|
|
4547
4604
|
provider,
|
|
4548
4605
|
});
|
|
4549
4606
|
|
|
4550
|
-
|
|
4607
|
+
cliOutput.result({
|
|
4551
4608
|
action: "wallet get-channel-fund",
|
|
4552
4609
|
wallet: wallet.walletName,
|
|
4553
4610
|
network: walletMetadata.network,
|
|
@@ -4628,20 +4685,32 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4628
4685
|
channelId: context.workspace.channelId,
|
|
4629
4686
|
});
|
|
4630
4687
|
if (joinToll !== 0n) {
|
|
4631
|
-
approveReceipt = await
|
|
4632
|
-
|
|
4633
|
-
|
|
4688
|
+
approveReceipt = await dryRunThenSubmitTransaction({
|
|
4689
|
+
operationName: "channel join approve",
|
|
4690
|
+
call: contractTxCall(
|
|
4691
|
+
asset.approve,
|
|
4692
|
+
[context.workspace.bridgeTokenVault, joinToll],
|
|
4693
|
+
{ nonce: nextNonce++ },
|
|
4694
|
+
asset.interface,
|
|
4695
|
+
),
|
|
4696
|
+
});
|
|
4634
4697
|
}
|
|
4635
|
-
receipt = await
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4698
|
+
receipt = await dryRunThenSubmitTransaction({
|
|
4699
|
+
operationName: "channel join",
|
|
4700
|
+
call: contractTxCall(
|
|
4701
|
+
context.bridgeTokenVault.connect(signer).joinChannel,
|
|
4702
|
+
[
|
|
4703
|
+
ethers.toBigInt(context.workspace.channelId),
|
|
4704
|
+
l2Identity.l2Address,
|
|
4705
|
+
storageKey,
|
|
4706
|
+
leafIndex,
|
|
4707
|
+
noteReceiveKeyMaterial.noteReceivePubKey,
|
|
4708
|
+
],
|
|
4642
4709
|
{ nonce: nextNonce++ },
|
|
4710
|
+
context.bridgeTokenVault.interface,
|
|
4643
4711
|
),
|
|
4644
|
-
|
|
4712
|
+
submittedBefore: approveReceipt ? [submittedReceiptSummary("channel join approve", approveReceipt)] : [],
|
|
4713
|
+
});
|
|
4645
4714
|
const registered = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
4646
4715
|
const lifecycleEpoch = await walletEpochFromJoinReceipt({
|
|
4647
4716
|
receipt,
|
|
@@ -4670,7 +4739,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4670
4739
|
rpcUrl,
|
|
4671
4740
|
});
|
|
4672
4741
|
|
|
4673
|
-
|
|
4742
|
+
cliOutput.result({
|
|
4674
4743
|
action: "channel join",
|
|
4675
4744
|
workspace: context.workspaceName,
|
|
4676
4745
|
wallet: walletContext.walletName,
|
|
@@ -4720,16 +4789,22 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4720
4789
|
].join(" "),
|
|
4721
4790
|
);
|
|
4722
4791
|
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(ownerSigner.address);
|
|
4723
|
-
const receipt = await
|
|
4724
|
-
|
|
4725
|
-
|
|
4792
|
+
const receipt = await dryRunThenSubmitTransaction({
|
|
4793
|
+
operationName: "channel exit",
|
|
4794
|
+
call: contractTxCall(
|
|
4795
|
+
context.bridgeTokenVault.connect(ownerSigner).exitChannel,
|
|
4796
|
+
[ethers.toBigInt(context.workspace.channelId)],
|
|
4797
|
+
undefined,
|
|
4798
|
+
context.bridgeTokenVault.interface,
|
|
4799
|
+
),
|
|
4800
|
+
});
|
|
4726
4801
|
const lifecycleEpoch = await markWalletEpochExited({
|
|
4727
4802
|
walletContext,
|
|
4728
4803
|
receipt,
|
|
4729
4804
|
provider,
|
|
4730
4805
|
});
|
|
4731
4806
|
|
|
4732
|
-
|
|
4807
|
+
cliOutput.result({
|
|
4733
4808
|
action: "channel exit",
|
|
4734
4809
|
wallet: walletContext.walletName,
|
|
4735
4810
|
network: walletMetadata.network,
|
|
@@ -4854,16 +4929,22 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4854
4929
|
});
|
|
4855
4930
|
|
|
4856
4931
|
const methodName = direction === "deposit" ? "depositToChannelVault" : "withdrawFromChannelVault";
|
|
4857
|
-
await assertWorkspaceAlignedWithChain(context);
|
|
4858
4932
|
emitProgress(operationName, "submitting");
|
|
4859
|
-
const receipt = await
|
|
4933
|
+
const receipt = await dryRunThenSubmitTransaction({
|
|
4934
|
+
operationName,
|
|
4860
4935
|
context,
|
|
4861
4936
|
walletName: walletContext.walletName,
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4937
|
+
operationDir,
|
|
4938
|
+
precheck: () => precheckGrothRootUpdate({ context, transition, operationName }),
|
|
4939
|
+
call: contractTxCall(
|
|
4940
|
+
bridgeTokenVault[methodName],
|
|
4941
|
+
[
|
|
4942
|
+
ethers.toBigInt(context.workspace.channelId),
|
|
4943
|
+
transition.proof,
|
|
4944
|
+
transition.update,
|
|
4945
|
+
],
|
|
4946
|
+
undefined,
|
|
4947
|
+
bridgeTokenVault.interface,
|
|
4867
4948
|
),
|
|
4868
4949
|
});
|
|
4869
4950
|
const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
|
|
@@ -4887,7 +4968,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4887
4968
|
});
|
|
4888
4969
|
|
|
4889
4970
|
emitProgress(operationName, "done");
|
|
4890
|
-
|
|
4971
|
+
cliOutput.result({
|
|
4891
4972
|
action: operationName,
|
|
4892
4973
|
workspace: context.workspaceName,
|
|
4893
4974
|
wallet: walletContext.walletName,
|
|
@@ -4921,9 +5002,17 @@ async function handleWithdrawBridge({ args, network, provider }) {
|
|
|
4921
5002
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
4922
5003
|
signer,
|
|
4923
5004
|
);
|
|
4924
|
-
const receipt = await
|
|
5005
|
+
const receipt = await dryRunThenSubmitTransaction({
|
|
5006
|
+
operationName: "account withdraw-bridge",
|
|
5007
|
+
call: contractTxCall(
|
|
5008
|
+
bridgeTokenVault.claimToWallet,
|
|
5009
|
+
[amount],
|
|
5010
|
+
undefined,
|
|
5011
|
+
bridgeTokenVault.interface,
|
|
5012
|
+
),
|
|
5013
|
+
});
|
|
4925
5014
|
|
|
4926
|
-
|
|
5015
|
+
cliOutput.result({
|
|
4927
5016
|
action: "account withdraw-bridge",
|
|
4928
5017
|
l1Address: signer.address,
|
|
4929
5018
|
amountInput,
|
|
@@ -5054,7 +5143,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
5054
5143
|
preparedContextResult,
|
|
5055
5144
|
});
|
|
5056
5145
|
|
|
5057
|
-
|
|
5146
|
+
cliOutput.result({
|
|
5058
5147
|
action: "wallet mint-notes",
|
|
5059
5148
|
wallet: wallet.walletName,
|
|
5060
5149
|
workspace: execution.context.workspaceName,
|
|
@@ -5129,7 +5218,7 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
5129
5218
|
preparedContextResult,
|
|
5130
5219
|
});
|
|
5131
5220
|
|
|
5132
|
-
|
|
5221
|
+
cliOutput.result({
|
|
5133
5222
|
action: "wallet redeem-notes",
|
|
5134
5223
|
wallet: wallet.walletName,
|
|
5135
5224
|
workspace: execution.context.workspaceName,
|
|
@@ -5221,7 +5310,7 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
5221
5310
|
})
|
|
5222
5311
|
: null;
|
|
5223
5312
|
|
|
5224
|
-
|
|
5313
|
+
cliOutput.result({
|
|
5225
5314
|
action: "wallet get-notes",
|
|
5226
5315
|
wallet: wallet.walletName,
|
|
5227
5316
|
network: walletMetadata.network,
|
|
@@ -5989,7 +6078,7 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
5989
6078
|
counterpartyDirection: "sent",
|
|
5990
6079
|
});
|
|
5991
6080
|
|
|
5992
|
-
|
|
6081
|
+
cliOutput.result({
|
|
5993
6082
|
action: "wallet transfer-notes",
|
|
5994
6083
|
wallet: wallet.walletName,
|
|
5995
6084
|
workspace: execution.context.workspaceName,
|
|
@@ -7643,7 +7732,7 @@ async function executeWalletTemplateSend({
|
|
|
7643
7732
|
functionName,
|
|
7644
7733
|
templatePayload,
|
|
7645
7734
|
}) {
|
|
7646
|
-
await assertWorkspaceAlignedWithChain(context
|
|
7735
|
+
await assertWorkspaceAlignedWithChain(context);
|
|
7647
7736
|
assertWalletMatchesChannelContext(wallet, l2Identity, context);
|
|
7648
7737
|
await assertChannelProofBackendVersionCompatibility({ context, operationName });
|
|
7649
7738
|
|
|
@@ -7699,19 +7788,26 @@ async function executeWalletTemplateSend({
|
|
|
7699
7788
|
expectedFunctionRoot: context.workspace.functionRoot ?? context.workspace.policySnapshot?.functionRoot,
|
|
7700
7789
|
});
|
|
7701
7790
|
const aPubBlockHash = hashTokamakPublicInputs(payload.aPubBlock);
|
|
7702
|
-
expect(
|
|
7703
|
-
ethers.toBigInt(normalizeBytes32Hex(aPubBlockHash))
|
|
7704
|
-
=== ethers.toBigInt(normalizeBytes32Hex(context.workspace.aPubBlockHash)),
|
|
7705
|
-
"Generated Tokamak proof does not match the channel aPubBlockHash. Check the workspace block_info.json context.",
|
|
7706
|
-
);
|
|
7707
7791
|
|
|
7708
|
-
await assertWorkspaceAlignedWithChain(context);
|
|
7709
7792
|
emitProgress(operationName, "submitting");
|
|
7710
|
-
const receipt = await
|
|
7793
|
+
const receipt = await dryRunThenSubmitTransaction({
|
|
7794
|
+
operationName,
|
|
7711
7795
|
context,
|
|
7712
7796
|
walletName: wallet.walletName,
|
|
7713
|
-
|
|
7714
|
-
|
|
7797
|
+
operationDir,
|
|
7798
|
+
precheck: () => precheckTokamakExecution({
|
|
7799
|
+
context,
|
|
7800
|
+
payload,
|
|
7801
|
+
functionProof,
|
|
7802
|
+
aPubBlockHash,
|
|
7803
|
+
operationName,
|
|
7804
|
+
}),
|
|
7805
|
+
call: contractTxCall(
|
|
7806
|
+
context.channelManager.connect(txSubmitter).executeChannelTransaction,
|
|
7807
|
+
[payload, functionProof],
|
|
7808
|
+
undefined,
|
|
7809
|
+
context.channelManager.interface,
|
|
7810
|
+
),
|
|
7715
7811
|
});
|
|
7716
7812
|
await waitForProviderBlockAtLeast(provider, receipt.blockNumber, { action: operationName });
|
|
7717
7813
|
|
|
@@ -8386,25 +8482,219 @@ function isUnexpectedCurrentRootVectorError(error, context) {
|
|
|
8386
8482
|
return String(error?.message ?? error).includes("UnexpectedCurrentRootVector");
|
|
8387
8483
|
}
|
|
8388
8484
|
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8485
|
+
function precheckGrothRootUpdate({ context, transition, operationName }) {
|
|
8486
|
+
expect(transition && typeof transition === "object", `${operationName} proof precheck failed: missing transition.`);
|
|
8487
|
+
expect(transition.proof && typeof transition.proof === "object", `${operationName} proof precheck failed: missing Groth16 proof.`);
|
|
8488
|
+
expect(transition.update && typeof transition.update === "object", `${operationName} proof precheck failed: missing root update.`);
|
|
8489
|
+
expect(
|
|
8490
|
+
Array.isArray(transition.update.currentRootVector) && transition.update.currentRootVector.length > 0,
|
|
8491
|
+
`${operationName} proof precheck failed: missing current root vector.`,
|
|
8492
|
+
);
|
|
8493
|
+
expect(
|
|
8494
|
+
normalizeBytes32Hex(hashRootVector(transition.update.currentRootVector))
|
|
8495
|
+
=== normalizeBytes32Hex(hashRootVector(context.currentSnapshot.stateRoots)),
|
|
8496
|
+
`${operationName} proof precheck failed: root update does not match the local workspace snapshot.`,
|
|
8497
|
+
);
|
|
8498
|
+
normalizeBytes32Hex(transition.update.updatedRoot);
|
|
8499
|
+
normalizeBytes32Hex(transition.update.currentUserKey);
|
|
8500
|
+
normalizeBytes32Hex(transition.update.updatedUserKey);
|
|
8501
|
+
expect(transition.nextSnapshot?.stateRoots, `${operationName} proof precheck failed: missing next snapshot roots.`);
|
|
8502
|
+
}
|
|
8503
|
+
|
|
8504
|
+
function precheckTokamakExecution({ context, payload, functionProof, aPubBlockHash, operationName }) {
|
|
8505
|
+
expect(payload && typeof payload === "object", `${operationName} proof precheck failed: missing Tokamak payload.`);
|
|
8506
|
+
for (const key of [
|
|
8507
|
+
"proofPart1",
|
|
8508
|
+
"proofPart2",
|
|
8509
|
+
"functionPreprocessPart1",
|
|
8510
|
+
"functionPreprocessPart2",
|
|
8511
|
+
"aPubUser",
|
|
8512
|
+
"aPubBlock",
|
|
8513
|
+
]) {
|
|
8514
|
+
expect(Array.isArray(payload[key]) && payload[key].length > 0, `${operationName} proof precheck failed: payload.${key} is missing.`);
|
|
8515
|
+
}
|
|
8516
|
+
expect(functionProof?.metadata, `${operationName} proof precheck failed: missing function metadata proof.`);
|
|
8517
|
+
expect(Array.isArray(functionProof?.siblings), `${operationName} proof precheck failed: missing function metadata proof siblings.`);
|
|
8518
|
+
expect(
|
|
8519
|
+
ethers.toBigInt(normalizeBytes32Hex(aPubBlockHash))
|
|
8520
|
+
=== ethers.toBigInt(normalizeBytes32Hex(context.workspace.aPubBlockHash)),
|
|
8521
|
+
`${operationName} proof precheck failed: generated aPubBlockHash does not match the channel aPubBlockHash.`,
|
|
8522
|
+
);
|
|
8523
|
+
}
|
|
8524
|
+
|
|
8525
|
+
function contractTxCall(contractMethod, args = [], overrides = undefined, contractInterface = null) {
|
|
8526
|
+
expect(typeof contractMethod === "function", "Internal error: contract transaction method must be callable.");
|
|
8527
|
+
expect(
|
|
8528
|
+
typeof contractMethod.staticCall === "function",
|
|
8529
|
+
"Internal error: contract transaction method must support staticCall.",
|
|
8530
|
+
);
|
|
8531
|
+
const finalArgs = overrides === undefined ? [...args] : [...args, overrides];
|
|
8532
|
+
return {
|
|
8533
|
+
contractInterface,
|
|
8534
|
+
dryRun: () => contractMethod.staticCall(...finalArgs),
|
|
8535
|
+
submit: () => contractMethod(...finalArgs),
|
|
8536
|
+
};
|
|
8537
|
+
}
|
|
8538
|
+
|
|
8539
|
+
async function dryRunThenSubmitTransaction({
|
|
8392
8540
|
operationName,
|
|
8393
|
-
|
|
8541
|
+
call,
|
|
8542
|
+
precheck = null,
|
|
8543
|
+
context = null,
|
|
8544
|
+
walletName = null,
|
|
8545
|
+
operationDir = null,
|
|
8546
|
+
submittedBefore = [],
|
|
8394
8547
|
}) {
|
|
8548
|
+
expect(typeof call?.dryRun === "function", "Internal error: transaction dry-run callback is required.");
|
|
8549
|
+
expect(typeof call?.submit === "function", "Internal error: transaction submit callback is required.");
|
|
8550
|
+
if (precheck) {
|
|
8551
|
+
await precheck();
|
|
8552
|
+
}
|
|
8395
8553
|
try {
|
|
8396
|
-
|
|
8554
|
+
await call.dryRun();
|
|
8397
8555
|
} catch (error) {
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8556
|
+
throw transactionPreflightOrSubmitError({
|
|
8557
|
+
phase: "dry-run",
|
|
8558
|
+
operationName,
|
|
8559
|
+
cause: error,
|
|
8560
|
+
context,
|
|
8561
|
+
walletName,
|
|
8562
|
+
operationDir,
|
|
8563
|
+
submittedBefore,
|
|
8564
|
+
contractInterface: call.contractInterface,
|
|
8565
|
+
});
|
|
8566
|
+
}
|
|
8567
|
+
try {
|
|
8568
|
+
return await waitForReceipt(await call.submit());
|
|
8569
|
+
} catch (error) {
|
|
8570
|
+
throw transactionPreflightOrSubmitError({
|
|
8571
|
+
phase: "submit",
|
|
8572
|
+
operationName,
|
|
8573
|
+
cause: error,
|
|
8574
|
+
context,
|
|
8575
|
+
walletName,
|
|
8576
|
+
operationDir,
|
|
8577
|
+
submittedBefore,
|
|
8578
|
+
contractInterface: call.contractInterface,
|
|
8579
|
+
});
|
|
8580
|
+
}
|
|
8581
|
+
}
|
|
8582
|
+
|
|
8583
|
+
function transactionPreflightOrSubmitError({
|
|
8584
|
+
phase,
|
|
8585
|
+
operationName,
|
|
8586
|
+
cause,
|
|
8587
|
+
context = null,
|
|
8588
|
+
walletName = null,
|
|
8589
|
+
operationDir = null,
|
|
8590
|
+
submittedBefore = [],
|
|
8591
|
+
contractInterface = null,
|
|
8592
|
+
}) {
|
|
8593
|
+
if (context && isUnexpectedCurrentRootVectorError(cause, context)) {
|
|
8594
|
+
return staleChannelRootError({
|
|
8595
|
+
cause,
|
|
8596
|
+
context,
|
|
8597
|
+
walletName,
|
|
8598
|
+
operationName,
|
|
8599
|
+
phase,
|
|
8600
|
+
});
|
|
8601
|
+
}
|
|
8602
|
+
const decodedError = decodeTransactionContractError(cause, [
|
|
8603
|
+
contractInterface,
|
|
8604
|
+
context?.channelManager?.interface,
|
|
8605
|
+
context?.bridgeTokenVault?.interface,
|
|
8606
|
+
]);
|
|
8607
|
+
const details = [
|
|
8608
|
+
phase === "dry-run"
|
|
8609
|
+
? `${operationName} pre-submit dry-run failed. No ${operationName} transaction was submitted.`
|
|
8610
|
+
: `${operationName} transaction submission failed.`,
|
|
8611
|
+
];
|
|
8612
|
+
const submitted = normalizeSubmittedBefore(submittedBefore);
|
|
8613
|
+
if (submitted.length > 0) {
|
|
8614
|
+
details.push(`Already submitted before this failure: ${submitted.join("; ")}.`);
|
|
8615
|
+
}
|
|
8616
|
+
if (walletName) {
|
|
8617
|
+
details.push(`Wallet: ${walletName}.`);
|
|
8618
|
+
}
|
|
8619
|
+
if (operationDir) {
|
|
8620
|
+
details.push(`Operation directory: ${operationDir}.`);
|
|
8621
|
+
}
|
|
8622
|
+
if (decodedError) {
|
|
8623
|
+
details.push(`Decoded contract error: ${decodedError}.`);
|
|
8624
|
+
}
|
|
8625
|
+
details.push(`Provider error: ${extractProviderErrorMessage(cause)}.`);
|
|
8626
|
+
const error = cliError(
|
|
8627
|
+
phase === "dry-run" ? CLI_ERROR_CODES.TX_DRY_RUN_FAILED : CLI_ERROR_CODES.TX_SUBMIT_FAILED,
|
|
8628
|
+
details.join(" "),
|
|
8629
|
+
{ cause },
|
|
8630
|
+
);
|
|
8631
|
+
error.phase = phase;
|
|
8632
|
+
error.operationName = operationName;
|
|
8633
|
+
error.transactionSubmitted = phase !== "dry-run";
|
|
8634
|
+
error.submittedBefore = submitted;
|
|
8635
|
+
error.walletName = walletName;
|
|
8636
|
+
error.operationDir = operationDir;
|
|
8637
|
+
error.decodedContractError = decodedError;
|
|
8638
|
+
error.providerError = extractProviderErrorMessage(cause);
|
|
8639
|
+
if (context) {
|
|
8640
|
+
error.channelName = context.workspace?.channelName;
|
|
8641
|
+
error.networkName = context.workspace?.network;
|
|
8642
|
+
}
|
|
8643
|
+
return error;
|
|
8644
|
+
}
|
|
8645
|
+
|
|
8646
|
+
function submittedReceiptSummary(label, receipt) {
|
|
8647
|
+
return `${label} tx ${receipt?.hash ?? "<unknown>"} in block ${receipt?.blockNumber ?? "<unknown>"}`;
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
function normalizeSubmittedBefore(entries) {
|
|
8651
|
+
return entries
|
|
8652
|
+
.filter(Boolean)
|
|
8653
|
+
.map((entry) => {
|
|
8654
|
+
if (typeof entry === "string") {
|
|
8655
|
+
return entry;
|
|
8656
|
+
}
|
|
8657
|
+
if (entry?.hash) {
|
|
8658
|
+
return submittedReceiptSummary(entry.label ?? "transaction", entry);
|
|
8659
|
+
}
|
|
8660
|
+
return String(entry);
|
|
8661
|
+
});
|
|
8662
|
+
}
|
|
8663
|
+
|
|
8664
|
+
function decodeTransactionContractError(error, contractInterfaces) {
|
|
8665
|
+
if (error?.revert?.name) {
|
|
8666
|
+
return formatDecodedContractError(error.revert.name, error.revert.args ?? []);
|
|
8667
|
+
}
|
|
8668
|
+
for (const contractInterface of contractInterfaces.filter(Boolean)) {
|
|
8669
|
+
for (const errorData of extractContractErrorDataCandidates(error)) {
|
|
8670
|
+
try {
|
|
8671
|
+
const parsed = contractInterface.parseError(errorData);
|
|
8672
|
+
if (parsed) {
|
|
8673
|
+
return formatDecodedContractError(parsed.name, parsed.args ?? []);
|
|
8674
|
+
}
|
|
8675
|
+
} catch {
|
|
8676
|
+
// Keep scanning provider error payloads and interfaces.
|
|
8677
|
+
}
|
|
8405
8678
|
}
|
|
8406
|
-
throw error;
|
|
8407
8679
|
}
|
|
8680
|
+
return null;
|
|
8681
|
+
}
|
|
8682
|
+
|
|
8683
|
+
function formatDecodedContractError(name, args) {
|
|
8684
|
+
const renderedArgs = Array.from(args ?? [])
|
|
8685
|
+
.map((value) => serializeBigInts(normalizeCliOutputValue(value, [])));
|
|
8686
|
+
return `${name}(${renderedArgs.map((value) => JSON.stringify(value)).join(", ")})`;
|
|
8687
|
+
}
|
|
8688
|
+
|
|
8689
|
+
function extractProviderErrorMessage(error) {
|
|
8690
|
+
return String(
|
|
8691
|
+
error?.shortMessage
|
|
8692
|
+
?? error?.reason
|
|
8693
|
+
?? error?.info?.error?.message
|
|
8694
|
+
?? error?.error?.message
|
|
8695
|
+
?? error?.message
|
|
8696
|
+
?? error,
|
|
8697
|
+
);
|
|
8408
8698
|
}
|
|
8409
8699
|
|
|
8410
8700
|
function staleChannelRootError({
|
|
@@ -8412,17 +8702,24 @@ function staleChannelRootError({
|
|
|
8412
8702
|
context,
|
|
8413
8703
|
walletName,
|
|
8414
8704
|
operationName,
|
|
8705
|
+
phase = "submit",
|
|
8415
8706
|
}) {
|
|
8416
8707
|
const message = [
|
|
8417
|
-
|
|
8708
|
+
phase === "dry-run"
|
|
8709
|
+
? `${operationName} pre-submit dry-run failed because the generated proof targets an older channel root. No ${operationName} transaction was submitted.`
|
|
8710
|
+
: `${operationName} failed because the submitted proof was generated for an older channel root.`,
|
|
8418
8711
|
"The rejected proof cannot be reused.",
|
|
8419
8712
|
"Do not change recipients, amounts, note counts, function arity, or split the command as recovery.",
|
|
8420
8713
|
"Refresh the channel workspace, re-check affected wallet state when the command uses notes, then rerun the original intended command so the CLI regenerates a proof from a fresh snapshot.",
|
|
8421
8714
|
].join(" ");
|
|
8422
8715
|
const error = cliError(CLI_ERROR_CODES.STALE_CHANNEL_ROOT, message, { cause });
|
|
8716
|
+
error.phase = phase;
|
|
8717
|
+
error.operationName = operationName;
|
|
8718
|
+
error.transactionSubmitted = phase !== "dry-run";
|
|
8423
8719
|
error.channelName = context.workspace.channelName;
|
|
8424
8720
|
error.networkName = context.workspace.network;
|
|
8425
8721
|
error.walletName = walletName;
|
|
8722
|
+
error.providerError = extractProviderErrorMessage(cause);
|
|
8426
8723
|
error.retryPolicy = "recover_workspace_then_regenerate_proof";
|
|
8427
8724
|
error.semanticMutationAllowed = false;
|
|
8428
8725
|
error.reuseProofAllowed = false;
|
|
@@ -9729,15 +10026,11 @@ function assertVersionArgs(args) {
|
|
|
9729
10026
|
}
|
|
9730
10027
|
|
|
9731
10028
|
function printVersion() {
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
});
|
|
9738
|
-
return;
|
|
9739
|
-
}
|
|
9740
|
-
console.log(privateStateCliPackageJson.version);
|
|
10029
|
+
cliOutput.result({
|
|
10030
|
+
action: "version",
|
|
10031
|
+
packageName: privateStateCliPackageJson.name,
|
|
10032
|
+
version: privateStateCliPackageJson.version,
|
|
10033
|
+
});
|
|
9741
10034
|
}
|
|
9742
10035
|
|
|
9743
10036
|
function requireWorkspaceName(args) {
|
|
@@ -10782,38 +11075,53 @@ function buildWalletViewingKeyMetadata(wallet) {
|
|
|
10782
11075
|
}
|
|
10783
11076
|
|
|
10784
11077
|
function printHelp() {
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
` ${command.description}`,
|
|
10788
|
-
...(command.help ?? []).map((line) => ` ${line}`),
|
|
10789
|
-
].join("\n")).join("\n\n");
|
|
10790
|
-
console.log(`
|
|
10791
|
-
Commands:
|
|
10792
|
-
${commandHelp}
|
|
10793
|
-
|
|
10794
|
-
Secret source options:
|
|
10795
|
-
Use account import --private-key-file once to create a protected local account secret.
|
|
10796
|
-
L1 signing commands use --account only.
|
|
10797
|
-
A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
|
|
10798
|
-
Create one before joining a channel, for example:
|
|
10799
|
-
openssl rand -hex 32 > ./wallet-secret.txt
|
|
10800
|
-
private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt --acknowledge-action-impact
|
|
10801
|
-
Configure each network RPC endpoint once with set rpc. The CLI reads RPC_URL, LOG_CHUNK_SIZE,
|
|
10802
|
-
and LOG_REQUESTS_PER_SECOND from ~/tokamak-private-channels/workspace/<network>/rpc-config.env.
|
|
10803
|
-
Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
|
|
10804
|
-
Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
|
|
10805
|
-
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
10806
|
-
|
|
10807
|
-
Options:
|
|
10808
|
-
--version
|
|
10809
|
-
Print the private-state CLI package version and exit.
|
|
11078
|
+
cliOutput.result(buildHelpCommandsResult());
|
|
11079
|
+
}
|
|
10810
11080
|
|
|
10811
|
-
|
|
10812
|
-
|
|
11081
|
+
function buildHelpCommandsResult() {
|
|
11082
|
+
return {
|
|
11083
|
+
action: "help commands",
|
|
11084
|
+
commands: PRIVATE_STATE_CLI_COMMANDS.map(buildHelpCommandEntry),
|
|
11085
|
+
secretSourceOptions: [
|
|
11086
|
+
"Use account import --private-key-file once to create a protected local account secret.",
|
|
11087
|
+
"L1 signing commands use --account only.",
|
|
11088
|
+
"A wallet secret source file is arbitrary high-entropy secret text read once by channel join.",
|
|
11089
|
+
"Configure each network RPC endpoint once with set rpc.",
|
|
11090
|
+
"Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.",
|
|
11091
|
+
"Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but canonical CLI secret files remain protected.",
|
|
11092
|
+
],
|
|
11093
|
+
globalOptions: [
|
|
11094
|
+
{
|
|
11095
|
+
option: "--version",
|
|
11096
|
+
description: "Print the private-state CLI package version and exit.",
|
|
11097
|
+
},
|
|
11098
|
+
{
|
|
11099
|
+
option: "--json",
|
|
11100
|
+
description: "Print the final success or failure result as JSON on stdout. Progress, warning, and info events are JSONL on stderr.",
|
|
11101
|
+
},
|
|
11102
|
+
{
|
|
11103
|
+
option: "--help",
|
|
11104
|
+
description: "Show this help. Equivalent to help commands.",
|
|
11105
|
+
},
|
|
11106
|
+
],
|
|
11107
|
+
};
|
|
11108
|
+
}
|
|
10813
11109
|
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
11110
|
+
function buildHelpCommandEntry(command) {
|
|
11111
|
+
const requiredFields = privateStateCliCommandRequiredOptionKeys(command);
|
|
11112
|
+
const fields = command.fields ?? [];
|
|
11113
|
+
return {
|
|
11114
|
+
id: command.id,
|
|
11115
|
+
display: privateStateCliCommandDisplay(command),
|
|
11116
|
+
synopsis: privateStateCliCommandSynopsis(command),
|
|
11117
|
+
description: command.description,
|
|
11118
|
+
usage: command.usage,
|
|
11119
|
+
fields,
|
|
11120
|
+
requiredFields,
|
|
11121
|
+
optionalFields: fields.filter((field) => !requiredFields.includes(field)),
|
|
11122
|
+
installMode: privateStateCliCommandInstallMode(command),
|
|
11123
|
+
help: command.help ?? [],
|
|
11124
|
+
};
|
|
10817
11125
|
}
|
|
10818
11126
|
|
|
10819
11127
|
function readJson(filePath) {
|
|
@@ -11337,29 +11645,122 @@ function loadWalletCommandRuntime(args, { prepareArtifacts = false } = {}) {
|
|
|
11337
11645
|
}
|
|
11338
11646
|
|
|
11339
11647
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
11648
|
+
doctor: printDoctorHumanReport,
|
|
11340
11649
|
guide: printGuideHumanResult,
|
|
11650
|
+
"help commands": printHelpCommandsHumanResult,
|
|
11341
11651
|
investigator: printInvestigatorHumanResult,
|
|
11342
11652
|
observer: printObserverHumanResult,
|
|
11343
11653
|
"transaction-fees": printTransactionFeesHumanResult,
|
|
11344
11654
|
update: printUpdateHumanResult,
|
|
11655
|
+
version: printVersionHumanResult,
|
|
11345
11656
|
});
|
|
11346
11657
|
|
|
11347
11658
|
function normalizePrivateKey(value) {
|
|
11348
11659
|
return value.startsWith("0x") ? value : `0x${value}`;
|
|
11349
11660
|
}
|
|
11350
11661
|
|
|
11351
|
-
|
|
11352
|
-
|
|
11662
|
+
const cliOutput = Object.freeze({
|
|
11663
|
+
result(value) {
|
|
11664
|
+
const normalized = normalizeCliOutput(value);
|
|
11665
|
+
if (isJsonOutputRequested()) {
|
|
11666
|
+
console.log(JSON.stringify(buildJsonSuccessPayload(normalized), null, 2));
|
|
11667
|
+
return;
|
|
11668
|
+
}
|
|
11669
|
+
const renderer = HUMAN_RESULT_RENDERERS[normalized?.action];
|
|
11670
|
+
if (renderer) {
|
|
11671
|
+
renderer(normalized);
|
|
11672
|
+
return;
|
|
11673
|
+
}
|
|
11674
|
+
printHumanResult(normalized);
|
|
11675
|
+
},
|
|
11676
|
+
error(error, args = {}) {
|
|
11677
|
+
if (isJsonOutputRequested()) {
|
|
11678
|
+
console.log(JSON.stringify(normalizeCliOutput(buildJsonErrorPayload(error, args)), null, 2));
|
|
11679
|
+
return;
|
|
11680
|
+
}
|
|
11681
|
+
console.error(formatHumanError(error, args));
|
|
11682
|
+
},
|
|
11683
|
+
progress(action, phase, details = {}) {
|
|
11684
|
+
emitOutputEvent({
|
|
11685
|
+
event: "progress",
|
|
11686
|
+
action,
|
|
11687
|
+
phase,
|
|
11688
|
+
message: details.message ?? `[${action}] ${phase}`,
|
|
11689
|
+
details,
|
|
11690
|
+
});
|
|
11691
|
+
},
|
|
11692
|
+
warning(kind, message, details = {}) {
|
|
11693
|
+
emitOutputEvent({
|
|
11694
|
+
event: "warning",
|
|
11695
|
+
kind,
|
|
11696
|
+
message,
|
|
11697
|
+
details,
|
|
11698
|
+
});
|
|
11699
|
+
},
|
|
11700
|
+
});
|
|
11701
|
+
|
|
11702
|
+
function buildJsonSuccessPayload(value) {
|
|
11703
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11704
|
+
return Object.hasOwn(value, "ok") ? value : { ok: true, ...value };
|
|
11705
|
+
}
|
|
11706
|
+
return {
|
|
11707
|
+
ok: true,
|
|
11708
|
+
action: "result",
|
|
11709
|
+
value,
|
|
11710
|
+
};
|
|
11711
|
+
}
|
|
11712
|
+
|
|
11713
|
+
function emitOutputEvent(event) {
|
|
11714
|
+
const normalized = normalizeCliOutput({
|
|
11715
|
+
timestamp: new Date().toISOString(),
|
|
11716
|
+
...event,
|
|
11717
|
+
});
|
|
11353
11718
|
if (isJsonOutputRequested()) {
|
|
11354
|
-
console.
|
|
11719
|
+
console.error(JSON.stringify(normalized));
|
|
11355
11720
|
return;
|
|
11356
11721
|
}
|
|
11357
|
-
const
|
|
11358
|
-
if (
|
|
11359
|
-
|
|
11722
|
+
const message = event.message ?? `[${event.action ?? event.kind ?? "cli"}] ${event.phase ?? event.event}`;
|
|
11723
|
+
if (event.event === "warning") {
|
|
11724
|
+
console.error(message);
|
|
11360
11725
|
return;
|
|
11361
11726
|
}
|
|
11362
|
-
|
|
11727
|
+
console.log(message);
|
|
11728
|
+
}
|
|
11729
|
+
|
|
11730
|
+
function printVersionHumanResult(result) {
|
|
11731
|
+
console.log(result.version);
|
|
11732
|
+
}
|
|
11733
|
+
|
|
11734
|
+
function printHelpCommandsHumanResult(help) {
|
|
11735
|
+
const commandHelp = (help.commands ?? []).map((command) => [
|
|
11736
|
+
` ${command.synopsis}`,
|
|
11737
|
+
` ${command.description}`,
|
|
11738
|
+
...(command.help ?? []).map((line) => ` ${line}`),
|
|
11739
|
+
].join("\n")).join("\n\n");
|
|
11740
|
+
const globalOptions = (help.globalOptions ?? []).map((option) => [
|
|
11741
|
+
` ${option.option}`,
|
|
11742
|
+
` ${option.description}`,
|
|
11743
|
+
].join("\n")).join("\n\n");
|
|
11744
|
+
console.log(`
|
|
11745
|
+
Commands:
|
|
11746
|
+
${commandHelp}
|
|
11747
|
+
|
|
11748
|
+
Secret source options:
|
|
11749
|
+
Use account import --private-key-file once to create a protected local account secret.
|
|
11750
|
+
L1 signing commands use --account only.
|
|
11751
|
+
A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
|
|
11752
|
+
Create one before joining a channel, for example:
|
|
11753
|
+
openssl rand -hex 32 > ./wallet-secret.txt
|
|
11754
|
+
private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt --acknowledge-action-impact
|
|
11755
|
+
Configure each network RPC endpoint once with set rpc. The CLI reads RPC_URL, LOG_CHUNK_SIZE,
|
|
11756
|
+
and LOG_REQUESTS_PER_SECOND from ~/tokamak-private-channels/workspace/<network>/rpc-config.env.
|
|
11757
|
+
Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
|
|
11758
|
+
Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
|
|
11759
|
+
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
11760
|
+
|
|
11761
|
+
Options:
|
|
11762
|
+
${globalOptions}
|
|
11763
|
+
`);
|
|
11363
11764
|
}
|
|
11364
11765
|
|
|
11365
11766
|
function printGuideHumanResult(guide) {
|
|
@@ -11591,12 +11992,7 @@ function humanizeLabel(value) {
|
|
|
11591
11992
|
}
|
|
11592
11993
|
|
|
11593
11994
|
function emitProgress(action, phase) {
|
|
11594
|
-
|
|
11595
|
-
if (isJsonOutputRequested()) {
|
|
11596
|
-
console.error(line);
|
|
11597
|
-
} else {
|
|
11598
|
-
console.log(line);
|
|
11599
|
-
}
|
|
11995
|
+
cliOutput.progress(action, phase);
|
|
11600
11996
|
}
|
|
11601
11997
|
|
|
11602
11998
|
function createByteDownloadProgress({ action, label, url }) {
|
|
@@ -11732,7 +12128,7 @@ function createRpcLogScanProgress({ action, label }) {
|
|
|
11732
12128
|
};
|
|
11733
12129
|
}
|
|
11734
12130
|
|
|
11735
|
-
function
|
|
12131
|
+
function formatHumanError(error, args = {}) {
|
|
11736
12132
|
const message = String(error?.message ?? error);
|
|
11737
12133
|
const hints = buildRecoveryHints(error, args);
|
|
11738
12134
|
if (hints.length === 0) {
|
|
@@ -11745,6 +12141,41 @@ function formatCliErrorForDisplay(error, args = {}) {
|
|
|
11745
12141
|
].join("\n");
|
|
11746
12142
|
}
|
|
11747
12143
|
|
|
12144
|
+
function buildJsonErrorPayload(error, args = {}) {
|
|
12145
|
+
const message = String(error?.message ?? error);
|
|
12146
|
+
const hints = buildRecoveryHints(error, args);
|
|
12147
|
+
return {
|
|
12148
|
+
ok: false,
|
|
12149
|
+
action: "error",
|
|
12150
|
+
error: {
|
|
12151
|
+
name: error?.name ?? "Error",
|
|
12152
|
+
code: error?.code ?? "ERROR",
|
|
12153
|
+
command: typeof args.command === "string" ? args.command : null,
|
|
12154
|
+
message,
|
|
12155
|
+
hints,
|
|
12156
|
+
phase: error?.phase ?? null,
|
|
12157
|
+
operationName: error?.operationName ?? null,
|
|
12158
|
+
transactionSubmitted: typeof error?.transactionSubmitted === "boolean"
|
|
12159
|
+
? error.transactionSubmitted
|
|
12160
|
+
: null,
|
|
12161
|
+
submittedBefore: Array.isArray(error?.submittedBefore) ? error.submittedBefore : [],
|
|
12162
|
+
decodedContractError: error?.decodedContractError ?? null,
|
|
12163
|
+
providerError: error?.providerError ?? null,
|
|
12164
|
+
channelName: error?.channelName ?? (typeof args.channelName === "string" ? args.channelName : null),
|
|
12165
|
+
networkName: error?.networkName ?? (typeof args.network === "string" ? args.network : null),
|
|
12166
|
+
walletName: error?.walletName ?? (typeof args.wallet === "string" ? args.wallet : null),
|
|
12167
|
+
operationDir: error?.operationDir ?? null,
|
|
12168
|
+
retryPolicy: error?.retryPolicy ?? null,
|
|
12169
|
+
semanticMutationAllowed: typeof error?.semanticMutationAllowed === "boolean"
|
|
12170
|
+
? error.semanticMutationAllowed
|
|
12171
|
+
: null,
|
|
12172
|
+
reuseProofAllowed: typeof error?.reuseProofAllowed === "boolean"
|
|
12173
|
+
? error.reuseProofAllowed
|
|
12174
|
+
: null,
|
|
12175
|
+
},
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
|
|
11748
12179
|
function buildRecoveryHints(error, args = {}) {
|
|
11749
12180
|
const message = String(error?.message ?? error);
|
|
11750
12181
|
const hints = [];
|
|
@@ -11802,7 +12233,9 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
11802
12233
|
}
|
|
11803
12234
|
|
|
11804
12235
|
if (error?.code === CLI_ERROR_CODES.STALE_WORKSPACE) {
|
|
11805
|
-
hints.push(`private-state-cli channel
|
|
12236
|
+
hints.push(`private-state-cli channel get-meta --channel-name ${channelName} --network ${networkName}`);
|
|
12237
|
+
hints.push(`if workspaceMirror is set: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source mirror`);
|
|
12238
|
+
hints.push(`otherwise use indexed RPC recovery: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
|
|
11806
12239
|
hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName}`);
|
|
11807
12240
|
}
|
|
11808
12241
|
|
|
@@ -11810,7 +12243,9 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
11810
12243
|
error?.code === CLI_ERROR_CODES.STALE_CHANNEL_ROOT
|
|
11811
12244
|
|| message.includes("UnexpectedCurrentRootVector")
|
|
11812
12245
|
) {
|
|
11813
|
-
hints.push(`private-state-cli channel
|
|
12246
|
+
hints.push(`private-state-cli channel get-meta --channel-name ${channelName} --network ${networkName}`);
|
|
12247
|
+
hints.push(`if workspaceMirror is set: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source mirror`);
|
|
12248
|
+
hints.push(`otherwise use indexed RPC recovery: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
|
|
11814
12249
|
if (walletName !== "<WALLET>") {
|
|
11815
12250
|
hints.push(`private-state-cli wallet get-notes --wallet ${walletName} --network ${networkName}`);
|
|
11816
12251
|
}
|
|
@@ -11818,7 +12253,9 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
11818
12253
|
}
|
|
11819
12254
|
|
|
11820
12255
|
if (message.includes("Workspace recovery index is missing or unusable")) {
|
|
11821
|
-
hints.push(`private-state-cli channel
|
|
12256
|
+
hints.push(`private-state-cli channel get-meta --channel-name ${channelName} --network ${networkName}`);
|
|
12257
|
+
hints.push(`if workspaceMirror is set: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source mirror`);
|
|
12258
|
+
hints.push(`only if no compatible workspace mirror is available: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source rpc --from-genesis`);
|
|
11822
12259
|
}
|
|
11823
12260
|
|
|
11824
12261
|
if (message.includes("Wallet note recovery index is missing or unusable")) {
|
|
@@ -11931,5 +12368,5 @@ export {
|
|
|
11931
12368
|
loadExplicitCommandRuntime,
|
|
11932
12369
|
loadWalletCommandRuntime,
|
|
11933
12370
|
assertProviderChainIdMatchesNetwork,
|
|
11934
|
-
|
|
12371
|
+
cliOutput,
|
|
11935
12372
|
};
|