@tokamak-private-dapps/private-state-cli 2.1.2 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/runtime.mjs CHANGED
@@ -52,6 +52,7 @@ import {
52
52
  deriveChannelTokenVaultLeafIndex,
53
53
  deriveLiquidBalanceStorageKey,
54
54
  fetchContractCodes,
55
+ getBlockInfoAt,
55
56
  normalizeBytesHex,
56
57
  normalizeBytes32Hex,
57
58
  serializeBigInts,
@@ -82,9 +83,12 @@ import {
82
83
  installPrivateStateCliArtifacts,
83
84
  installTokamakCliRuntimeForPrivateState,
84
85
  inspectGroth16Runtime,
86
+ normalizeInstallMode,
85
87
  parseJsonReport,
86
88
  printDoctorHumanReport,
89
+ privateStateCliArtifactRequiredFiles,
87
90
  privateStateCliArtifactPaths,
91
+ PRIVATE_STATE_INSTALL_MODES,
88
92
  readTokamakCliPackageReport,
89
93
  requireActiveTokamakCliRuntimeRoot,
90
94
  resolveActiveGroth16ProverRuntime,
@@ -99,6 +103,7 @@ import {
99
103
  PRIVATE_STATE_CLI_COMMANDS,
100
104
  PRIVATE_STATE_CLI_FIELD_CATALOG,
101
105
  privateStateCliCommandDisplay,
106
+ privateStateCliCommandInstallMode,
102
107
  privateStateCliCommandOptionKeys,
103
108
  privateStateCliCommandRequiredOptionKeys,
104
109
  privateStateCliCommandSynopsis,
@@ -299,7 +304,7 @@ const ACTION_IMPACT_SUMMARIES = Object.freeze({
299
304
  "No private note owner, value, salt, counterparty, or note provenance is created by this action.",
300
305
  ],
301
306
  noteProvenance: "Not applicable for this bridge-edge action.",
302
- cexWarning: "Do not use a centralized-exchange controlled address as a self-custody bridge source.",
307
+ exchangeControlledAddressWarning: "Do not use an exchange-controlled address as a self-custody bridge source.",
303
308
  policy: "No channel policy is accepted by this action.",
304
309
  },
305
310
  "account-withdraw-bridge": {
@@ -316,7 +321,7 @@ const ACTION_IMPACT_SUMMARIES = Object.freeze({
316
321
  "The private note path that produced any prior channel balance is not reconstructed from this event alone.",
317
322
  ],
318
323
  noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this withdrawal alone.",
319
- cexWarning: "Do not use a centralized-exchange deposit address as the direct bridge withdrawal target unless the user has explicitly accepted the compliance implications. Prefer a self-custody L1 wallet.",
324
+ exchangeControlledAddressWarning: "Do not use an exchange deposit address as the direct bridge withdrawal target unless the user has explicitly accepted the compliance implications. Prefer a self-custody L1 wallet.",
320
325
  policy: "No channel policy is accepted by this action.",
321
326
  },
322
327
  "channel-join": {
@@ -472,8 +477,8 @@ function printActionImpactSummary(summary, details) {
472
477
  `- Secret recovery: Losing wallet secrets, viewing keys, or spending keys can prevent note discovery or note use. The CLI cannot recover lost secrets.`,
473
478
  `- Channel policy: ${summary.policy}`,
474
479
  ];
475
- if (summary.cexWarning) {
476
- lines.push(`- CEX address warning: ${summary.cexWarning}`);
480
+ if (summary.exchangeControlledAddressWarning) {
481
+ lines.push(`- Exchange-controlled address warning: ${summary.exchangeControlledAddressWarning}`);
477
482
  }
478
483
  lines.push(`- Confirmation: pass --acknowledge-action-impact or type the exact confirmation phrase when prompted.`);
479
484
  console.error(lines.join("\n"));
@@ -509,22 +514,36 @@ function normalizeDAppPolicySnapshot({
509
514
  };
510
515
  }
511
516
 
512
- async function prepareDeploymentArtifacts(chainId) {
517
+ function prepareDeploymentArtifacts(chainId, { mode = PRIVATE_STATE_INSTALL_MODES.FULL } = {}) {
513
518
  const normalizedChainId = Number(chainId);
514
- const existingPaths = flatDeploymentArtifactPathsByChainId.get(normalizedChainId);
515
- if (existingPaths) {
516
- return existingPaths.rootDir;
519
+ const normalizedMode = normalizeInstallMode(mode);
520
+ const existingEntry = flatDeploymentArtifactPathsByChainId.get(normalizedChainId);
521
+ if (existingEntry?.preparedModes.has(normalizedMode)) {
522
+ return existingEntry.paths.rootDir;
517
523
  }
518
524
 
519
525
  const cacheBaseRoot = resolveArtifactCacheBaseRoot();
520
- const artifactPaths = privateStateCliArtifactPaths(cacheBaseRoot, normalizedChainId);
521
- requireInstalledDeploymentArtifacts(artifactPaths, normalizedChainId);
522
- flatDeploymentArtifactPathsByChainId.set(normalizedChainId, artifactPaths);
526
+ const artifactPaths = existingEntry?.paths ?? privateStateCliArtifactPaths(cacheBaseRoot, normalizedChainId);
527
+ requireInstalledDeploymentArtifacts(artifactPaths, normalizedChainId, normalizedMode);
528
+ const preparedModes = existingEntry?.preparedModes ?? new Set();
529
+ preparedModes.add(normalizedMode);
530
+ flatDeploymentArtifactPathsByChainId.set(normalizedChainId, {
531
+ paths: artifactPaths,
532
+ preparedModes,
533
+ });
523
534
  return artifactPaths.rootDir;
524
535
  }
525
536
 
537
+ function prepareDeploymentArtifactsForCommand(commandId, chainId) {
538
+ const command = PRIVATE_STATE_CLI_COMMANDS.find((entry) => entry.id === commandId);
539
+ expect(command, `Missing CLI command metadata for ${commandId}.`);
540
+ const mode = privateStateCliCommandInstallMode(command);
541
+ expect(mode !== "none", `${privateStateCliCommandDisplay(command)} does not require installed deployment artifacts.`);
542
+ return prepareDeploymentArtifacts(chainId, { mode });
543
+ }
544
+
526
545
  function flatDeploymentArtifactPathsForChainId(chainId) {
527
- return flatDeploymentArtifactPathsByChainId.get(Number(chainId)) ?? null;
546
+ return flatDeploymentArtifactPathsByChainId.get(Number(chainId))?.paths ?? null;
528
547
  }
529
548
 
530
549
  function requireFlatDeploymentArtifactPathsForChainId(chainId) {
@@ -535,33 +554,26 @@ function requireFlatDeploymentArtifactPathsForChainId(chainId) {
535
554
  return paths;
536
555
  }
537
556
 
538
- function requireInstalledDeploymentArtifacts(artifactPaths, chainId) {
539
- const requiredFiles = [
540
- artifactPaths.bridgeDeploymentPath,
541
- artifactPaths.bridgeAbiManifestPath,
542
- artifactPaths.grothManifestPath,
543
- artifactPaths.grothZkeyPath,
544
- artifactPaths.dappDeploymentPath,
545
- artifactPaths.dappStorageLayoutPath,
546
- artifactPaths.privateStateControllerAbiPath,
547
- artifactPaths.dappRegistrationPath,
548
- ];
549
- try {
550
- for (const filePath of requiredFiles) {
551
- if (!fs.existsSync(filePath)) {
552
- throw new Error(`Missing ${filePath}.`);
553
- }
554
- }
555
- } catch (error) {
556
- throw cliError(
557
- CLI_ERROR_CODES.MISSING_DEPLOYMENT_ARTIFACTS,
558
- [
559
- `Missing installed deployment artifacts for chain ${chainId} under ${artifactPaths.rootDir}.`,
560
- "Run install before running private-state CLI commands for this network.",
561
- `Original error: ${error.message}`,
562
- ].join(" "),
563
- );
557
+ function requireInstalledDeploymentArtifacts(artifactPaths, chainId, mode) {
558
+ const missingFiles = missingInstalledDeploymentArtifactFiles(artifactPaths, mode);
559
+ if (missingFiles.length === 0) {
560
+ return;
564
561
  }
562
+ throw cliError(
563
+ CLI_ERROR_CODES.MISSING_DEPLOYMENT_ARTIFACTS,
564
+ [
565
+ `Missing ${mode} installed deployment artifacts for chain ${chainId} under ${artifactPaths.rootDir}.`,
566
+ mode === PRIVATE_STATE_INSTALL_MODES.FULL
567
+ ? "Run install before running private-state CLI commands that write channel state."
568
+ : "Run install --read-only before running private-state CLI commands that read channel state.",
569
+ `Original error: ${missingFiles.map((entry) => `Missing ${entry.label}: ${entry.path}.`).join(" ")}`,
570
+ ].join(" "),
571
+ );
572
+ }
573
+
574
+ function missingInstalledDeploymentArtifactFiles(artifactPaths, mode) {
575
+ return privateStateCliArtifactRequiredFiles(artifactPaths, mode)
576
+ .filter((entry) => !fs.existsSync(entry.path));
565
577
  }
566
578
 
567
579
  async function handleChannelCreate({ args, network, provider }) {
@@ -710,12 +722,14 @@ async function handleWorkspaceInit({ args, network, provider }) {
710
722
  const workspaceName = channelName;
711
723
  const bridgeResources = loadBridgeResources({ chainId: network.chainId });
712
724
  const recoverySource = resolveWorkspaceRecoverySource(args);
725
+ const outputRawRpcCallHistory = args.outputRaw === true;
713
726
 
714
727
  const {
715
728
  workspaceDir,
716
729
  workspace,
717
730
  currentSnapshot,
718
731
  cleanRebuildBackup,
732
+ rpcCallHistory,
719
733
  } = await syncChannelWorkspace({
720
734
  workspaceName,
721
735
  channelName,
@@ -727,6 +741,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
727
741
  useWorkspaceRecoveryIndex: true,
728
742
  fromGenesis: args.fromGenesis === true,
729
743
  recoverySource,
744
+ outputRawRpcCallHistory,
730
745
  progressAction: "channel recover-workspace",
731
746
  });
732
747
 
@@ -746,6 +761,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
746
761
  recoveryLastScannedBlock: workspace.recoveryLastScannedBlock,
747
762
  recoveryRootVectorHash: workspace.recoveryRootVectorHash,
748
763
  recoveryScanRange: workspace.recoveryScanRange,
764
+ rpcCallHistory,
749
765
  workspaceMirror: workspace.workspaceMirror ?? null,
750
766
  });
751
767
  }
@@ -1957,6 +1973,7 @@ async function syncChannelWorkspace({
1957
1973
  useWorkspaceRecoveryIndex = false,
1958
1974
  fromGenesis = false,
1959
1975
  recoverySource = "rpc",
1976
+ outputRawRpcCallHistory = false,
1960
1977
  minimumToBlock = null,
1961
1978
  progressAction = null,
1962
1979
  }) {
@@ -1982,8 +1999,15 @@ async function syncChannelWorkspace({
1982
1999
  });
1983
2000
  }
1984
2001
 
2002
+ const rpcCallHistoryRecorder = outputRawRpcCallHistory
2003
+ ? createRpcCallHistoryRecorder({ workspaceDir })
2004
+ : null;
2005
+ const activeProvider = rpcCallHistoryRecorder
2006
+ ? attachRpcCallHistoryRecorderToProvider(provider, rpcCallHistoryRecorder)
2007
+ : provider;
2008
+
1985
2009
  const { bridgeDeployment, bridgeAbiManifest } = bridgeResources;
1986
- const bridgeCore = new Contract(bridgeDeployment.bridgeCore, bridgeAbiManifest.contracts.bridgeCore.abi, provider);
2010
+ const bridgeCore = new Contract(bridgeDeployment.bridgeCore, bridgeAbiManifest.contracts.bridgeCore.abi, activeProvider);
1987
2011
  const channelId = deriveChannelIdFromName(channelName);
1988
2012
  const channelInfo = await bridgeCore.getChannel(channelId);
1989
2013
  if (!channelInfo.exists) {
@@ -1993,13 +2017,13 @@ async function syncChannelWorkspace({
1993
2017
  const channelManager = new Contract(
1994
2018
  channelInfo.manager,
1995
2019
  bridgeAbiManifest.contracts.channelManager.abi,
1996
- provider,
2020
+ activeProvider,
1997
2021
  );
1998
2022
  const canonicalAsset = getAddress(channelInfo.asset);
1999
- const canonicalAssetDecimals = await fetchTokenDecimals(provider, canonicalAsset);
2023
+ const canonicalAssetDecimals = await fetchTokenDecimals(activeProvider, canonicalAsset);
2000
2024
  const currentRootVectorHash = normalizeBytes32Hex(await channelManager.currentRootVectorHash());
2001
2025
  const genesisBlockNumber = Number(await channelManager.genesisBlockNumber());
2002
- const observedLatestBlock = await provider.getBlockNumber();
2026
+ const observedLatestBlock = await activeProvider.getBlockNumber();
2003
2027
  const latestBlock = minimumToBlock === null
2004
2028
  ? observedLatestBlock
2005
2029
  : Math.max(observedLatestBlock, Number(minimumToBlock));
@@ -2027,8 +2051,8 @@ async function syncChannelWorkspace({
2027
2051
  `Managed storage vector does not include L2 accounting vault ${l2AccountingVaultAddress}.`,
2028
2052
  );
2029
2053
 
2030
- const contractCodes = await fetchContractCodes(provider, managedStorageAddresses);
2031
- const blockInfo = await fetchChannelBlockInfo(provider, genesisBlockNumber);
2054
+ const contractCodes = await fetchContractCodes(activeProvider, managedStorageAddresses);
2055
+ const blockInfo = await getBlockInfoAt(activeProvider, genesisBlockNumber);
2032
2056
  const derivedAPubBlockHash = normalizeBytes32Hex(hashTokamakPublicInputs(encodeTokamakBlockInfo(blockInfo)));
2033
2057
  expect(
2034
2058
  ethers.toBigInt(derivedAPubBlockHash) === ethers.toBigInt(normalizeBytes32Hex(channelInfo.aPubBlockHash)),
@@ -2143,6 +2167,10 @@ async function syncChannelWorkspace({
2143
2167
  });
2144
2168
  }
2145
2169
  : null;
2170
+ rpcCallHistoryRecorder?.setScanRange({
2171
+ fromBlock: selectedRecoveryIndex?.nextBlock ?? genesisBlockNumber,
2172
+ toBlock: latestBlock,
2173
+ });
2146
2174
  const reconstruction = localSnapshotReusable
2147
2175
  ? {
2148
2176
  currentSnapshot: existingArtifacts.stateSnapshot,
@@ -2162,7 +2190,7 @@ async function syncChannelWorkspace({
2162
2190
  },
2163
2191
  }
2164
2192
  : await reconstructChannelSnapshot({
2165
- provider,
2193
+ provider: activeProvider,
2166
2194
  bridgeAbiManifest,
2167
2195
  channelInfo,
2168
2196
  channelManager,
@@ -2179,7 +2207,9 @@ async function syncChannelWorkspace({
2179
2207
  toBlock: latestBlock,
2180
2208
  progressAction,
2181
2209
  onCheckpoint: persistWorkspaceCheckpoint,
2210
+ rpcCallHistoryRecorder,
2182
2211
  });
2212
+ rpcCallHistoryRecorder?.setScanRange(reconstruction.scanRange);
2183
2213
  const currentSnapshot = reconstruction.currentSnapshot;
2184
2214
  const workspace = buildWorkspaceForSnapshot({
2185
2215
  currentSnapshot,
@@ -2204,6 +2234,7 @@ async function syncChannelWorkspace({
2204
2234
  blockInfo,
2205
2235
  contractCodes,
2206
2236
  cleanRebuildBackup,
2237
+ rpcCallHistory: rpcCallHistoryRecorder?.finish() ?? null,
2207
2238
  };
2208
2239
  }
2209
2240
 
@@ -2450,22 +2481,33 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
2450
2481
  }
2451
2482
 
2452
2483
  async function handleInstallZkEvm({ args }) {
2453
- const selectedVersions = await resolvePrivateStateInstallRuntimeVersions(args);
2454
- const tokamakCliRuntime = await installTokamakCliRuntimeForPrivateState({
2455
- version: selectedVersions.tokamak,
2456
- docker: Boolean(args.docker),
2457
- });
2458
- const groth16Runtime = await installGroth16RuntimeForPrivateState({
2459
- version: selectedVersions.groth16,
2460
- docker: Boolean(args.docker),
2461
- });
2484
+ const installMode = args.readOnly === true
2485
+ ? PRIVATE_STATE_INSTALL_MODES.READ_ONLY
2486
+ : PRIVATE_STATE_INSTALL_MODES.FULL;
2487
+ const selectedVersions = installMode === PRIVATE_STATE_INSTALL_MODES.FULL
2488
+ ? await resolvePrivateStateInstallRuntimeVersions(args)
2489
+ : null;
2490
+ const tokamakCliRuntime = installMode === PRIVATE_STATE_INSTALL_MODES.FULL
2491
+ ? await installTokamakCliRuntimeForPrivateState({
2492
+ version: selectedVersions.tokamak,
2493
+ docker: Boolean(args.docker),
2494
+ })
2495
+ : null;
2496
+ const groth16Runtime = installMode === PRIVATE_STATE_INSTALL_MODES.FULL
2497
+ ? await installGroth16RuntimeForPrivateState({
2498
+ version: selectedVersions.groth16,
2499
+ docker: Boolean(args.docker),
2500
+ })
2501
+ : null;
2462
2502
  const localDeploymentBaseRoot = args.includeLocalArtifacts ? process.cwd() : null;
2463
2503
  const deploymentArtifacts = await installPrivateStateCliArtifacts({
2464
2504
  dappName: PRIVATE_STATE_DAPP_LABEL,
2505
+ installMode,
2465
2506
  localDeploymentBaseRoot,
2466
- groth16CrsVersion: groth16Runtime.compatibleBackendVersion,
2507
+ groth16CrsVersion: groth16Runtime?.compatibleBackendVersion ?? null,
2467
2508
  });
2468
2509
  const installManifest = writePrivateStateCliInstallManifest({
2510
+ installMode,
2469
2511
  dockerRequested: Boolean(args.docker),
2470
2512
  includeLocalArtifacts: Boolean(args.includeLocalArtifacts),
2471
2513
  localDeploymentBaseRoot,
@@ -2476,9 +2518,10 @@ async function handleInstallZkEvm({ args }) {
2476
2518
  });
2477
2519
  printJson({
2478
2520
  action: "install",
2521
+ installMode,
2479
2522
  selectedVersions,
2480
- tokamakCli: tokamakCliRuntime.entryPath,
2481
- runtimeRoot: tokamakCliRuntime.runtimeRoot,
2523
+ tokamakCli: tokamakCliRuntime?.entryPath ?? null,
2524
+ runtimeRoot: tokamakCliRuntime?.runtimeRoot ?? null,
2482
2525
  tokamakCliRuntime,
2483
2526
  groth16Runtime,
2484
2527
  docker: Boolean(args.docker),
@@ -2633,6 +2676,198 @@ function persistChannelWorkspaceFiles({
2633
2676
  writeJsonIfChanged(channelWorkspaceConfigPath(workspaceDir), workspace);
2634
2677
  }
2635
2678
 
2679
+ function channelWorkspaceRpcCallHistoryPath(workspaceDir) {
2680
+ return path.join(channelDataPath(workspaceDir), "rpcCallHistory");
2681
+ }
2682
+
2683
+ function createRpcCallHistoryRecorder({ workspaceDir }) {
2684
+ const historyDir = channelWorkspaceRpcCallHistoryPath(workspaceDir);
2685
+ const entriesByFile = new Map();
2686
+ let scanRange = null;
2687
+ let callCount = 0;
2688
+ ensureDir(historyDir);
2689
+
2690
+ const pushEntry = ({ method, eventName = null, entry }) => {
2691
+ const file = rpcCallHistoryFileName(method, eventName);
2692
+ const entries = entriesByFile.get(file) ?? [];
2693
+ entries.push({
2694
+ method,
2695
+ ...(eventName ? { event: eventName } : {}),
2696
+ ...entry,
2697
+ });
2698
+ entriesByFile.set(file, entries);
2699
+ };
2700
+
2701
+ return {
2702
+ historyDir,
2703
+ setScanRange(nextScanRange) {
2704
+ scanRange = {
2705
+ fromBlock: Number(nextScanRange.fromBlock),
2706
+ toBlock: Number(nextScanRange.toBlock),
2707
+ ...(nextScanRange.mode ? { mode: nextScanRange.mode } : {}),
2708
+ };
2709
+ },
2710
+ recordRpcCall({ method, params, response, error = null }) {
2711
+ if (method === "eth_getLogs") {
2712
+ return;
2713
+ }
2714
+ callCount += 1;
2715
+ pushEntry({
2716
+ method,
2717
+ entry: {
2718
+ recordedAt: new Date().toISOString(),
2719
+ request: buildRawJsonRpcRequest(method, params),
2720
+ ...(error ? { error } : { response }),
2721
+ },
2722
+ });
2723
+ },
2724
+ recordEthGetLogs({ request, logs, groupedValues, chunkFromBlock, chunkToBlock }) {
2725
+ callCount += 1;
2726
+ const eventBuckets = groupRawEthGetLogsByRecoveryEvent({ logs, groupedValues });
2727
+ for (const [eventName, response] of eventBuckets.entries()) {
2728
+ pushEntry({
2729
+ method: "eth_getLogs",
2730
+ eventName,
2731
+ entry: {
2732
+ recordedAt: new Date().toISOString(),
2733
+ chunkRange: { fromBlock: Number(chunkFromBlock), toBlock: Number(chunkToBlock) },
2734
+ request: buildRawEthGetLogsRequest(request),
2735
+ response,
2736
+ },
2737
+ });
2738
+ }
2739
+ },
2740
+ finish() {
2741
+ const files = [...entriesByFile.entries()].map(([file, entries]) =>
2742
+ appendRpcCallHistoryEntries({
2743
+ historyDir,
2744
+ file,
2745
+ entries: entries.map((entry) => ({ scanRange, ...entry })),
2746
+ }));
2747
+ return {
2748
+ historyDir,
2749
+ scanRange,
2750
+ callCount,
2751
+ files: files.sort((left, right) => left.file.localeCompare(right.file)),
2752
+ };
2753
+ },
2754
+ };
2755
+ }
2756
+
2757
+ function attachRpcCallHistoryRecorderToProvider(provider, recorder) {
2758
+ const send = provider.send.bind(provider);
2759
+ provider.send = async (method, params) => {
2760
+ try {
2761
+ const response = await send(method, params);
2762
+ recorder.recordRpcCall({ method, params, response });
2763
+ return response;
2764
+ } catch (error) {
2765
+ recorder.recordRpcCall({ method, params, error: normalizeRpcCallHistoryError(error) });
2766
+ throw error;
2767
+ }
2768
+ };
2769
+ return provider;
2770
+ }
2771
+
2772
+ function normalizeRpcCallHistoryError(error) {
2773
+ return {
2774
+ name: error?.name ?? "Error",
2775
+ code: error?.code ?? null,
2776
+ message: error?.message ?? String(error),
2777
+ };
2778
+ }
2779
+
2780
+ function appendRpcCallHistoryEntries({ historyDir, file, entries }) {
2781
+ const filePath = path.join(historyDir, file);
2782
+ const { method, event: eventName = null } = entries[0];
2783
+ const current = readJsonIfExists(filePath) ?? {
2784
+ method,
2785
+ ...(eventName ? { event: eventName } : {}),
2786
+ entries: [],
2787
+ };
2788
+ expect(current.method === method, `RPC call history file method mismatch: ${filePath}.`);
2789
+ expect(Array.isArray(current.entries), `RPC call history file entries must be an array: ${filePath}.`);
2790
+ if (eventName) {
2791
+ expect(current.event === eventName, `RPC call history file event mismatch: ${filePath}.`);
2792
+ }
2793
+ current.updatedAt = new Date().toISOString();
2794
+ current.entries.push(...entries);
2795
+ writeJson(filePath, current);
2796
+ return {
2797
+ method,
2798
+ ...(eventName ? { event: eventName } : {}),
2799
+ file,
2800
+ path: filePath,
2801
+ entriesAdded: entries.length,
2802
+ totalEntries: current.entries.length,
2803
+ };
2804
+ }
2805
+
2806
+ function buildRawJsonRpcRequest(method, params = []) {
2807
+ return {
2808
+ jsonrpc: "2.0",
2809
+ method,
2810
+ params: params ?? [],
2811
+ };
2812
+ }
2813
+
2814
+ function rpcCallHistoryFileName(method, eventName = null) {
2815
+ const suffix = eventName ? `.${safeRpcCallHistoryFileToken(eventName)}` : "";
2816
+ return `${safeRpcCallHistoryFileToken(method)}${suffix}.json`;
2817
+ }
2818
+
2819
+ function safeRpcCallHistoryFileToken(value) {
2820
+ return String(value).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
2821
+ }
2822
+
2823
+ function groupRawEthGetLogsByRecoveryEvent({ logs, groupedValues }) {
2824
+ if (logs.length === 0) {
2825
+ return new Map([["noLogs", []]]);
2826
+ }
2827
+ const eventNamesByLog = new Map();
2828
+ for (const group of groupedValues) {
2829
+ for (const event of group) {
2830
+ eventNamesByLog.set(recoveryLogHistoryKey(event), channelRecoveryEventName(event));
2831
+ }
2832
+ }
2833
+ const buckets = new Map();
2834
+ for (const log of logs) {
2835
+ const eventName = eventNamesByLog.get(recoveryLogHistoryKey(log)) ?? "unknown";
2836
+ const bucket = buckets.get(eventName) ?? [];
2837
+ bucket.push(log);
2838
+ buckets.set(eventName, bucket);
2839
+ }
2840
+ return buckets;
2841
+ }
2842
+
2843
+ function recoveryLogHistoryKey(log) {
2844
+ return `${normalizeBytes32Hex(log.transactionHash)}:${Number(log.index ?? log.logIndex)}`;
2845
+ }
2846
+
2847
+ function channelRecoveryEventName(event) {
2848
+ if (event.fragment?.name) {
2849
+ return event.fragment.name;
2850
+ }
2851
+ const topic0 = event.topics[0] ? normalizeBytes32Hex(event.topics[0]) : null;
2852
+ if (topic0 === normalizeBytes32Hex(CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC)) {
2853
+ return "StorageKeyObserved";
2854
+ }
2855
+ if (topic0 === normalizeBytes32Hex(VAULT_STORAGE_WRITE_OBSERVED_TOPIC)) {
2856
+ return "LiquidBalanceStorageWriteObserved";
2857
+ }
2858
+ return "unknown";
2859
+ }
2860
+
2861
+ function buildRawEthGetLogsRequest(request) {
2862
+ const filter = {
2863
+ address: request.address,
2864
+ topics: request.topics,
2865
+ fromBlock: ethers.toQuantity(request.fromBlock),
2866
+ toBlock: ethers.toQuantity(request.toBlock),
2867
+ };
2868
+ return buildRawJsonRpcRequest("eth_getLogs", [filter]);
2869
+ }
2870
+
2636
2871
  function nextAvailablePath(basePath) {
2637
2872
  if (!fs.existsSync(basePath)) {
2638
2873
  return basePath;
@@ -3435,15 +3670,19 @@ async function handleGuide({ args }) {
3435
3670
  guide.state.deploymentArtifacts = artifactState;
3436
3671
  guide.checks.push(guideCheck(
3437
3672
  "installed deployment artifacts",
3438
- artifactState.installed ? "ok" : "missing",
3673
+ artifactState.readOnlyInstalled ? "ok" : "missing",
3439
3674
  {
3440
3675
  chainId: networkRuntime.network.chainId,
3441
3676
  rootDir: artifactState.rootDir,
3442
- missingFiles: artifactState.missingFiles,
3677
+ missingFiles: artifactState.readOnlyMissingFiles,
3678
+ fullMissingFiles: artifactState.fullMissingFiles,
3443
3679
  },
3444
3680
  ));
3445
- if (artifactState.installed) {
3446
- flatDeploymentArtifactPathsByChainId.set(Number(networkRuntime.network.chainId), artifactState.paths);
3681
+ if (artifactState.readOnlyInstalled) {
3682
+ flatDeploymentArtifactPathsByChainId.set(Number(networkRuntime.network.chainId), {
3683
+ paths: artifactState.paths,
3684
+ preparedModes: new Set([PRIVATE_STATE_INSTALL_MODES.READ_ONLY]),
3685
+ });
3447
3686
  }
3448
3687
 
3449
3688
  const provider = networkRuntime.provider;
@@ -3452,7 +3691,7 @@ async function handleGuide({ args }) {
3452
3691
  channelName: String(args.channelName),
3453
3692
  network: networkRuntime.network,
3454
3693
  provider,
3455
- artifactsInstalled: artifactState.installed,
3694
+ artifactsInstalled: artifactState.readOnlyInstalled,
3456
3695
  });
3457
3696
  guide.checks.push(guideCheck(
3458
3697
  "channel",
@@ -3474,7 +3713,7 @@ async function handleGuide({ args }) {
3474
3713
  networkName,
3475
3714
  network: networkRuntime.network,
3476
3715
  provider,
3477
- artifactsInstalled: artifactState.installed,
3716
+ artifactsInstalled: artifactState.readOnlyInstalled,
3478
3717
  });
3479
3718
  guide.checks.push(guideCheck(
3480
3719
  "local account secret",
@@ -3493,7 +3732,7 @@ async function handleGuide({ args }) {
3493
3732
  walletName: String(args.wallet),
3494
3733
  networkName,
3495
3734
  provider,
3496
- artifactsInstalled: artifactState.installed,
3735
+ artifactsInstalled: artifactState.readOnlyInstalled,
3497
3736
  });
3498
3737
  guide.checks.push(guideCheck(
3499
3738
  "local wallet",
@@ -3596,21 +3835,18 @@ function inspectGuideNetworkRuntime(networkName) {
3596
3835
 
3597
3836
  function inspectGuideDeploymentArtifacts(chainId) {
3598
3837
  const paths = privateStateCliArtifactPaths(resolveArtifactCacheBaseRoot(), chainId);
3599
- const requiredFiles = [
3600
- paths.bridgeDeploymentPath,
3601
- paths.bridgeAbiManifestPath,
3602
- paths.grothManifestPath,
3603
- paths.grothZkeyPath,
3604
- paths.dappDeploymentPath,
3605
- paths.dappStorageLayoutPath,
3606
- paths.privateStateControllerAbiPath,
3607
- paths.dappRegistrationPath,
3608
- ];
3609
- const missingFiles = requiredFiles.filter((filePath) => !fs.existsSync(filePath));
3838
+ const readOnlyMissingFiles = missingInstalledDeploymentArtifactFiles(paths, PRIVATE_STATE_INSTALL_MODES.READ_ONLY)
3839
+ .map((entry) => entry.path);
3840
+ const fullMissingFiles = missingInstalledDeploymentArtifactFiles(paths, PRIVATE_STATE_INSTALL_MODES.FULL)
3841
+ .map((entry) => entry.path);
3610
3842
  return {
3611
- installed: missingFiles.length === 0,
3843
+ installed: readOnlyMissingFiles.length === 0,
3844
+ readOnlyInstalled: readOnlyMissingFiles.length === 0,
3845
+ fullInstalled: fullMissingFiles.length === 0,
3612
3846
  rootDir: paths.rootDir,
3613
- missingFiles,
3847
+ missingFiles: readOnlyMissingFiles,
3848
+ readOnlyMissingFiles,
3849
+ fullMissingFiles,
3614
3850
  paths,
3615
3851
  };
3616
3852
  }
@@ -8411,39 +8647,6 @@ function appendSplitWord(target, startIndex, value) {
8411
8647
  target[startIndex + 1] = normalized >> 128n;
8412
8648
  }
8413
8649
 
8414
- async function fetchChannelBlockInfo(provider, blockNumber) {
8415
- const blockTag = ethers.toQuantity(blockNumber);
8416
- const block = await provider.send("eth_getBlockByNumber", [blockTag, false]);
8417
- if (!block) {
8418
- throw new Error(`Unable to fetch channel genesis block ${blockNumber}.`);
8419
- }
8420
-
8421
- const prevBlockHashes = [];
8422
- for (let offset = 1; offset <= TOKAMAK_PREVIOUS_BLOCK_HASH_COUNT; offset += 1) {
8423
- if (blockNumber <= offset) {
8424
- prevBlockHashes.push("0x0");
8425
- continue;
8426
- }
8427
- const previousBlock = await provider.send("eth_getBlockByNumber", [ethers.toQuantity(blockNumber - offset), false]);
8428
- if (!previousBlock) {
8429
- throw new Error(`Unable to fetch previous block hash for block ${blockNumber - offset}.`);
8430
- }
8431
- prevBlockHashes.push(previousBlock.hash);
8432
- }
8433
-
8434
- return {
8435
- coinBase: block.miner,
8436
- timeStamp: block.timestamp,
8437
- blockNumber: block.number,
8438
- prevRanDao: block.prevRandao ?? block.mixHash ?? block.difficulty ?? "0x0",
8439
- gasLimit: block.gasLimit,
8440
- chainId: await provider.send("eth_chainId", []),
8441
- selfBalance: "0x0",
8442
- baseFee: block.baseFeePerGas ?? "0x0",
8443
- prevBlockHashes,
8444
- };
8445
- }
8446
-
8447
8650
  async function fetchChannelRecoveryLogs({
8448
8651
  provider,
8449
8652
  bridgeAbiManifest,
@@ -8484,6 +8687,7 @@ async function fetchChannelRecoveryEventGroupsChunked({
8484
8687
  fromBlock,
8485
8688
  toBlock,
8486
8689
  progressAction = null,
8690
+ rpcCallHistoryRecorder = null,
8487
8691
  onChunk,
8488
8692
  }) {
8489
8693
  const recoveryFilter = buildChannelRecoveryLogFilter({
@@ -8501,7 +8705,7 @@ async function fetchChannelRecoveryEventGroupsChunked({
8501
8705
  onProgress: progressAction
8502
8706
  ? createRpcLogScanProgress({ action: progressAction, label: "channel-recovery chunks" })
8503
8707
  : null,
8504
- onChunk: async ({ logs, chunkFromBlock, chunkToBlock }) => {
8708
+ onChunk: async ({ request, logs, chunkFromBlock, chunkToBlock }) => {
8505
8709
  const groupedValues = normalizeWorkspaceMirrorDeltaEventGroups({
8506
8710
  logs,
8507
8711
  channelInfo,
@@ -8509,6 +8713,13 @@ async function fetchChannelRecoveryEventGroupsChunked({
8509
8713
  fromBlock: chunkFromBlock,
8510
8714
  toBlock: chunkToBlock,
8511
8715
  });
8716
+ rpcCallHistoryRecorder?.recordEthGetLogs({
8717
+ request,
8718
+ logs,
8719
+ groupedValues,
8720
+ chunkFromBlock,
8721
+ chunkToBlock,
8722
+ });
8512
8723
  await onChunk?.({
8513
8724
  groupedValues,
8514
8725
  chunkFromBlock,
@@ -8565,6 +8776,7 @@ async function reconstructChannelSnapshot({
8565
8776
  toBlock = null,
8566
8777
  progressAction = null,
8567
8778
  onCheckpoint = null,
8779
+ rpcCallHistoryRecorder = null,
8568
8780
  }) {
8569
8781
  let startingSnapshot = baseSnapshot;
8570
8782
  if (!startingSnapshot) {
@@ -8601,6 +8813,7 @@ async function reconstructChannelSnapshot({
8601
8813
  fromBlock: scanFromBlock,
8602
8814
  toBlock: latestBlock,
8603
8815
  progressAction,
8816
+ rpcCallHistoryRecorder,
8604
8817
  onChunk: async ({ groupedValues, chunkFromBlock, chunkToBlock }) => {
8605
8818
  currentSnapshot = await applyChannelRecoveryEventGroupsToStateManager({
8606
8819
  stateManager,
@@ -8953,13 +9166,14 @@ async function fetchLogsChunked(provider, {
8953
9166
  while (cursor <= resolvedToBlock) {
8954
9167
  const chunkToBlock = Math.min(resolvedToBlock, cursor + chunkSize - 1);
8955
9168
  let logs;
9169
+ const request = {
9170
+ address,
9171
+ topics,
9172
+ fromBlock: cursor,
9173
+ toBlock: chunkToBlock,
9174
+ };
8956
9175
  try {
8957
- logs = await fetchLogsRateLimited(provider, {
8958
- address,
8959
- topics,
8960
- fromBlock: cursor,
8961
- toBlock: chunkToBlock,
8962
- });
9176
+ logs = await fetchLogsRateLimited(provider, request);
8963
9177
  } catch (error) {
8964
9178
  throw buildRpcLogQueryConfigError({
8965
9179
  error,
@@ -8993,6 +9207,7 @@ async function fetchLogsChunked(provider, {
8993
9207
  totalBlocks,
8994
9208
  logsFound,
8995
9209
  chunkLogs: logs.length,
9210
+ request,
8996
9211
  logs,
8997
9212
  });
8998
9213
  cursor = chunkToBlock + 1;
@@ -9976,6 +10191,20 @@ function assertWalletChannelMoveArgs(args, commandName) {
9976
10191
 
9977
10192
  function assertInstallZkEvmArgs(args) {
9978
10193
  assertAllowedCommandSchema(args, "install");
10194
+ if (args.readOnly !== undefined && args.readOnly !== true) {
10195
+ throw new Error("install option --read-only does not accept a value.");
10196
+ }
10197
+ if (args.readOnly === true && args.docker !== undefined) {
10198
+ throw new Error("install --read-only does not accept --docker because proof runtimes are not installed.");
10199
+ }
10200
+ if (args.readOnly === true && args.groth16CliVersion !== undefined) {
10201
+ throw new Error("install --read-only does not accept --groth16-cli-version because Groth16 is not installed.");
10202
+ }
10203
+ if (args.readOnly === true && args.tokamakZkEvmCliVersion !== undefined) {
10204
+ throw new Error(
10205
+ "install --read-only does not accept --tokamak-zk-evm-cli-version because Tokamak zk-EVM is not installed.",
10206
+ );
10207
+ }
9979
10208
  if (args.groth16CliVersion !== undefined) {
9980
10209
  requireSemverVersion(args.groth16CliVersion, "--groth16-cli-version");
9981
10210
  }
@@ -10114,10 +10343,16 @@ function assertCreateChannelArgs(args) {
10114
10343
 
10115
10344
  function assertRecoverWorkspaceArgs(args) {
10116
10345
  assertAllowedCommandSchema(args, "channel-recover-workspace");
10117
- resolveWorkspaceRecoverySource(args);
10346
+ const source = resolveWorkspaceRecoverySource(args);
10118
10347
  if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
10119
10348
  throw new Error("channel recover-workspace option --from-genesis does not accept a value.");
10120
10349
  }
10350
+ if (args.outputRaw !== undefined && args.outputRaw !== true) {
10351
+ throw new Error("channel recover-workspace option --output-raw does not accept a value.");
10352
+ }
10353
+ if (args.outputRaw === true && source !== "rpc") {
10354
+ throw new Error("channel recover-workspace option --output-raw requires --source rpc.");
10355
+ }
10121
10356
  }
10122
10357
 
10123
10358
  function assertGetChannelArgs(args) {
@@ -10919,7 +11154,7 @@ function ensureDir(dirPath) {
10919
11154
  fs.mkdirSync(dirPath, { recursive: true });
10920
11155
  }
10921
11156
 
10922
- function loadExplicitCommandRuntime(args, { staticNetwork = false } = {}) {
11157
+ function loadExplicitCommandRuntime(args, { staticNetwork = false, prepareArtifacts = false } = {}) {
10923
11158
  const networkName = requireNetworkName(args);
10924
11159
  const network = {
10925
11160
  ...resolveCliNetwork(networkName),
@@ -10930,6 +11165,7 @@ function loadExplicitCommandRuntime(args, { staticNetwork = false } = {}) {
10930
11165
  const provider = staticNetwork
10931
11166
  ? new JsonRpcProvider(rpcConfig.rpcUrl, Number(network.chainId), { staticNetwork: true })
10932
11167
  : new JsonRpcProvider(rpcConfig.rpcUrl);
11168
+ if (prepareArtifacts) prepareDeploymentArtifactsForCommand(args.command, network.chainId);
10933
11169
  return {
10934
11170
  network,
10935
11171
  rpcUrl: rpcConfig.rpcUrl,
@@ -10950,7 +11186,7 @@ async function assertProviderChainIdMatchesNetwork({ provider, network, rpcUrl }
10950
11186
  );
10951
11187
  }
10952
11188
 
10953
- function loadWalletCommandRuntime(args) {
11189
+ function loadWalletCommandRuntime(args, { prepareArtifacts = false } = {}) {
10954
11190
  const networkName = requireNetworkName(args);
10955
11191
  loadWalletMetadata(requireWalletName(args), networkName);
10956
11192
  const network = {
@@ -10959,6 +11195,7 @@ function loadWalletCommandRuntime(args) {
10959
11195
  };
10960
11196
  const rpcConfig = resolveCommandRpcConfig(args);
10961
11197
  setActiveRpcLogConfig(rpcConfig);
11198
+ if (prepareArtifacts) prepareDeploymentArtifactsForCommand(args.command, network.chainId);
10962
11199
  return {
10963
11200
  network,
10964
11201
  rpcConfig,
@@ -11525,7 +11762,6 @@ export {
11525
11762
  handleJoinChannel,
11526
11763
  loadExplicitCommandRuntime,
11527
11764
  loadWalletCommandRuntime,
11528
- prepareDeploymentArtifacts,
11529
11765
  assertProviderChainIdMatchesNetwork,
11530
11766
  formatCliErrorForDisplay,
11531
11767
  };