@tokamak-private-dapps/private-state-cli 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +51 -15
- package/investigator/README.md +4 -0
- package/investigator/app.js +20 -3
- package/lib/private-state-cli-command-registry.mjs +18 -7
- package/lib/private-state-note-delivery.mjs +7 -14
- package/lib/private-state-runtime-management.mjs +0 -1
- package/package.json +2 -2
- package/private-state-bridge-cli.mjs +1026 -326
- package/lib/private-state-tokamak-helpers.mjs +0 -184
|
@@ -44,6 +44,18 @@ import {
|
|
|
44
44
|
requireCanonicalCompatibleBackendVersion,
|
|
45
45
|
requireExactSemverVersion,
|
|
46
46
|
} from "@tokamak-private-dapps/common-library/proof-backend-versioning";
|
|
47
|
+
import {
|
|
48
|
+
bigintToHex32,
|
|
49
|
+
buildStateManager,
|
|
50
|
+
buildTokamakTxSnapshot,
|
|
51
|
+
currentStorageBigInt,
|
|
52
|
+
deriveChannelTokenVaultLeafIndex,
|
|
53
|
+
deriveLiquidBalanceStorageKey,
|
|
54
|
+
fetchContractCodes,
|
|
55
|
+
normalizeBytesHex,
|
|
56
|
+
normalizeBytes32Hex,
|
|
57
|
+
serializeBigInts,
|
|
58
|
+
} from "@tokamak-private-dapps/common-library/tokamak-l2-helpers";
|
|
47
59
|
import {
|
|
48
60
|
resolveTokamakBlockInputConfig,
|
|
49
61
|
} from "@tokamak-private-dapps/common-library/tokamak-runtime-paths";
|
|
@@ -107,20 +119,6 @@ import {
|
|
|
107
119
|
normalizeEncryptedNoteValueWords,
|
|
108
120
|
unpackEncryptedNoteValue,
|
|
109
121
|
} from "./lib/private-state-note-delivery.mjs";
|
|
110
|
-
import {
|
|
111
|
-
bigintToHex32,
|
|
112
|
-
buildStateManager,
|
|
113
|
-
buildTokamakTxSnapshot,
|
|
114
|
-
bytes32FromHex,
|
|
115
|
-
currentStorageBigInt,
|
|
116
|
-
deriveChannelTokenVaultLeafIndex,
|
|
117
|
-
deriveLiquidBalanceStorageKey,
|
|
118
|
-
fetchContractCodes,
|
|
119
|
-
normalizeBytesHex,
|
|
120
|
-
normalizeBytes32Hex,
|
|
121
|
-
serializeBigInts,
|
|
122
|
-
} from "./lib/private-state-tokamak-helpers.mjs";
|
|
123
|
-
|
|
124
122
|
const require = createRequire(import.meta.url);
|
|
125
123
|
const defaultCommandCwd = process.cwd();
|
|
126
124
|
const privateStateCliPackageJson = require("./package.json");
|
|
@@ -137,9 +135,11 @@ const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
|
|
|
137
135
|
const TOKAMAK_ZKEVM_CLI_PACKAGE_NAME = "@tokamak-zk-evm/cli";
|
|
138
136
|
const WALLET_BACKUP_EXPORT_FORMAT = "tokamak-private-state-wallet-backup-export";
|
|
139
137
|
const WALLET_KEY_EXPORT_FORMAT = "tokamak-private-state-wallet-key-export";
|
|
138
|
+
const WALLET_INDEX_FORMAT = "tokamak-private-state-wallet-index";
|
|
140
139
|
const WALLET_EVIDENCE_BUNDLE_FORMAT = "tokamak-private-state-raw-evidence-bundle";
|
|
141
140
|
const WALLET_EXPORT_FORMAT_VERSION = 2;
|
|
142
|
-
const
|
|
141
|
+
const WALLET_INDEX_FORMAT_VERSION = 1;
|
|
142
|
+
const WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION = 2;
|
|
143
143
|
const WALLET_WORKSPACE_FORMAT_VERSION = 2;
|
|
144
144
|
const CHANNEL_WORKSPACE_MIRROR_PROTOCOL_VERSION = 2;
|
|
145
145
|
const CHANNEL_WORKSPACE_MIRROR_MANIFEST_PATH_PREFIX =
|
|
@@ -918,7 +918,12 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
918
918
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
919
919
|
const recoverySource = resolveWorkspaceRecoverySource(args);
|
|
920
920
|
|
|
921
|
-
const {
|
|
921
|
+
const {
|
|
922
|
+
workspaceDir,
|
|
923
|
+
workspace,
|
|
924
|
+
currentSnapshot,
|
|
925
|
+
cleanRebuildBackup,
|
|
926
|
+
} = await syncChannelWorkspace({
|
|
922
927
|
workspaceName,
|
|
923
928
|
channelName,
|
|
924
929
|
network,
|
|
@@ -937,6 +942,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
937
942
|
source: workspace.recoverySource ?? recoverySource,
|
|
938
943
|
workspace: workspaceName,
|
|
939
944
|
workspaceDir,
|
|
945
|
+
cleanRebuildBackup: cleanRebuildBackup ?? null,
|
|
940
946
|
channelName,
|
|
941
947
|
channelId: workspace.channelId,
|
|
942
948
|
channelManager: workspace.channelManager,
|
|
@@ -1647,10 +1653,18 @@ async function fetchChannelWorkspaceMirror({
|
|
|
1647
1653
|
|
|
1648
1654
|
const mirrorAheadOfLocal = localRecoveryIndex
|
|
1649
1655
|
&& Number(manifestPrecheck.recoveryLastScannedBlock) > Number(localRecoveryIndex.nextBlock);
|
|
1650
|
-
const
|
|
1656
|
+
const deltaBundleDescriptor = mirrorAheadOfLocal
|
|
1657
|
+
? selectWorkspaceMirrorDeltaBundle({
|
|
1658
|
+
manifest,
|
|
1659
|
+
fromBlock: Number(localRecoveryIndex.nextBlock),
|
|
1660
|
+
toBlock: Number(manifest.checkpoint.recoveryLastScannedBlock) - 1,
|
|
1661
|
+
})
|
|
1662
|
+
: null;
|
|
1663
|
+
const bundleResult = mirrorAheadOfLocal && deltaBundleDescriptor
|
|
1651
1664
|
? await fetchAndApplyWorkspaceMirrorDelta({
|
|
1652
1665
|
manifest,
|
|
1653
1666
|
manifestUrl,
|
|
1667
|
+
bundleDescriptor: deltaBundleDescriptor,
|
|
1654
1668
|
localRecoveryIndex,
|
|
1655
1669
|
chainId,
|
|
1656
1670
|
channelId,
|
|
@@ -2052,6 +2066,7 @@ function validateWorkspaceMirrorCheckpointArchive({
|
|
|
2052
2066
|
async function fetchAndApplyWorkspaceMirrorDelta({
|
|
2053
2067
|
manifest,
|
|
2054
2068
|
manifestUrl,
|
|
2069
|
+
bundleDescriptor,
|
|
2055
2070
|
localRecoveryIndex,
|
|
2056
2071
|
chainId,
|
|
2057
2072
|
channelId,
|
|
@@ -2065,7 +2080,6 @@ async function fetchAndApplyWorkspaceMirrorDelta({
|
|
|
2065
2080
|
}) {
|
|
2066
2081
|
const fromBlock = Number(localRecoveryIndex.nextBlock);
|
|
2067
2082
|
const toBlock = Number(manifest.checkpoint.recoveryLastScannedBlock) - 1;
|
|
2068
|
-
const bundleDescriptor = selectWorkspaceMirrorDeltaBundle({ manifest, fromBlock, toBlock });
|
|
2069
2083
|
expect(
|
|
2070
2084
|
bundleDescriptor,
|
|
2071
2085
|
`Workspace mirror does not provide a delta bundle for local recovery index ${fromBlock} to checkpoint block ${toBlock}.`,
|
|
@@ -2163,6 +2177,7 @@ async function syncChannelWorkspace({
|
|
|
2163
2177
|
}) {
|
|
2164
2178
|
const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), workspaceName);
|
|
2165
2179
|
const channelDir = channelDataPath(workspaceDir);
|
|
2180
|
+
let cleanRebuildBackup = null;
|
|
2166
2181
|
const hasPersistedChannelData = fs.existsSync(channelWorkspaceConfigPath(workspaceDir))
|
|
2167
2182
|
|| fs.existsSync(channelWorkspaceCurrentPath(workspaceDir))
|
|
2168
2183
|
|| fs.existsSync(channelWorkspaceOperationsPath(workspaceDir));
|
|
@@ -2174,6 +2189,13 @@ async function syncChannelWorkspace({
|
|
|
2174
2189
|
const existingArtifacts = persist && hasPersistedChannelData
|
|
2175
2190
|
? loadExistingWorkspaceArtifacts(workspaceDir)
|
|
2176
2191
|
: null;
|
|
2192
|
+
if (persist && useWorkspaceRecoveryIndex && fromGenesis) {
|
|
2193
|
+
cleanRebuildBackup = backupWorkspaceForCleanRebuild({
|
|
2194
|
+
workspaceDir,
|
|
2195
|
+
networkName: networkNameFromChainId(network.chainId),
|
|
2196
|
+
channelName,
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2177
2199
|
|
|
2178
2200
|
const { bridgeDeployment, bridgeAbiManifest } = bridgeResources;
|
|
2179
2201
|
const bridgeCore = new Contract(bridgeDeployment.bridgeCore, bridgeAbiManifest.contracts.bridgeCore.abi, provider);
|
|
@@ -2285,9 +2307,57 @@ async function syncChannelWorkspace({
|
|
|
2285
2307
|
throw new Error([
|
|
2286
2308
|
`Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
|
|
2287
2309
|
"The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
|
|
2288
|
-
"Run channel recover-workspace
|
|
2310
|
+
"Run channel recover-workspace first to refresh the local channel workspace.",
|
|
2289
2311
|
].join(" "));
|
|
2290
2312
|
}
|
|
2313
|
+
const workspaceBase = {
|
|
2314
|
+
name: workspaceName,
|
|
2315
|
+
network: networkNameFromChainId(network.chainId),
|
|
2316
|
+
chainId: network.chainId,
|
|
2317
|
+
appDeploymentPath: deploymentManifestPath,
|
|
2318
|
+
storageLayoutPath: storageLayoutManifestPath,
|
|
2319
|
+
channelId: channelId.toString(),
|
|
2320
|
+
channelName,
|
|
2321
|
+
dappId: Number(channelInfo.dappId),
|
|
2322
|
+
genesisBlockNumber,
|
|
2323
|
+
bridgeCore: getAddress(bridgeDeployment.bridgeCore),
|
|
2324
|
+
channelManager: getAddress(channelInfo.manager),
|
|
2325
|
+
bridgeTokenVault: getAddress(channelInfo.bridgeTokenVault),
|
|
2326
|
+
canonicalAsset,
|
|
2327
|
+
canonicalAssetDecimals,
|
|
2328
|
+
controller: controllerAddress,
|
|
2329
|
+
l2AccountingVault: l2AccountingVaultAddress,
|
|
2330
|
+
aPubBlockHash: normalizeBytes32Hex(channelInfo.aPubBlockHash),
|
|
2331
|
+
dappMetadataDigestSchema: policySnapshot.dappMetadataDigestSchema,
|
|
2332
|
+
dappMetadataDigest: policySnapshot.dappMetadataDigest,
|
|
2333
|
+
functionRoot: policySnapshot.functionRoot,
|
|
2334
|
+
policySnapshot,
|
|
2335
|
+
managedStorageAddresses,
|
|
2336
|
+
liquidBalancesSlot: liquidBalancesSlot.toString(),
|
|
2337
|
+
recoverySource,
|
|
2338
|
+
workspaceMirror: mirrorRecovery.workspaceMirror,
|
|
2339
|
+
};
|
|
2340
|
+
const buildWorkspaceForSnapshot = ({ currentSnapshot, scanRange }) => {
|
|
2341
|
+
const recoveryRootVectorHash = normalizeBytes32Hex(hashRootVector(currentSnapshot.stateRoots));
|
|
2342
|
+
return {
|
|
2343
|
+
...workspaceBase,
|
|
2344
|
+
recoveryLastScannedBlock: Number(scanRange.toBlock) + 1,
|
|
2345
|
+
recoveryRootVectorHash,
|
|
2346
|
+
recoveryScanRange: scanRange,
|
|
2347
|
+
};
|
|
2348
|
+
};
|
|
2349
|
+
const persistWorkspaceCheckpoint = persist
|
|
2350
|
+
? ({ currentSnapshot, scanRange }) => {
|
|
2351
|
+
persistChannelWorkspaceFiles({
|
|
2352
|
+
workspaceDir,
|
|
2353
|
+
channelDir,
|
|
2354
|
+
workspace: buildWorkspaceForSnapshot({ currentSnapshot, scanRange }),
|
|
2355
|
+
currentSnapshot,
|
|
2356
|
+
blockInfo,
|
|
2357
|
+
contractCodes,
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
: null;
|
|
2291
2361
|
const reconstruction = localSnapshotReusable
|
|
2292
2362
|
? {
|
|
2293
2363
|
currentSnapshot: existingArtifacts.stateSnapshot,
|
|
@@ -2323,56 +2393,23 @@ async function syncChannelWorkspace({
|
|
|
2323
2393
|
fromBlock: selectedRecoveryIndex?.nextBlock ?? genesisBlockNumber,
|
|
2324
2394
|
toBlock: latestBlock,
|
|
2325
2395
|
progressAction,
|
|
2396
|
+
onCheckpoint: persistWorkspaceCheckpoint,
|
|
2326
2397
|
});
|
|
2327
2398
|
const currentSnapshot = reconstruction.currentSnapshot;
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
name: workspaceName,
|
|
2333
|
-
network: networkNameFromChainId(network.chainId),
|
|
2334
|
-
chainId: network.chainId,
|
|
2335
|
-
appDeploymentPath: deploymentManifestPath,
|
|
2336
|
-
storageLayoutPath: storageLayoutManifestPath,
|
|
2337
|
-
channelId: channelId.toString(),
|
|
2338
|
-
channelName,
|
|
2339
|
-
dappId: Number(channelInfo.dappId),
|
|
2340
|
-
genesisBlockNumber,
|
|
2341
|
-
bridgeCore: getAddress(bridgeDeployment.bridgeCore),
|
|
2342
|
-
channelManager: getAddress(channelInfo.manager),
|
|
2343
|
-
bridgeTokenVault: getAddress(channelInfo.bridgeTokenVault),
|
|
2344
|
-
canonicalAsset,
|
|
2345
|
-
canonicalAssetDecimals,
|
|
2346
|
-
controller: controllerAddress,
|
|
2347
|
-
l2AccountingVault: l2AccountingVaultAddress,
|
|
2348
|
-
aPubBlockHash: normalizeBytes32Hex(channelInfo.aPubBlockHash),
|
|
2349
|
-
dappMetadataDigestSchema: policySnapshot.dappMetadataDigestSchema,
|
|
2350
|
-
dappMetadataDigest: policySnapshot.dappMetadataDigest,
|
|
2351
|
-
functionRoot: policySnapshot.functionRoot,
|
|
2352
|
-
policySnapshot,
|
|
2353
|
-
managedStorageAddresses,
|
|
2354
|
-
liquidBalancesSlot: liquidBalancesSlot.toString(),
|
|
2355
|
-
recoverySource,
|
|
2356
|
-
workspaceMirror: mirrorRecovery.workspaceMirror,
|
|
2357
|
-
recoveryLastScannedBlock,
|
|
2358
|
-
recoveryRootVectorHash,
|
|
2359
|
-
recoveryScanRange: reconstruction.scanRange,
|
|
2360
|
-
};
|
|
2399
|
+
const workspace = buildWorkspaceForSnapshot({
|
|
2400
|
+
currentSnapshot,
|
|
2401
|
+
scanRange: reconstruction.scanRange,
|
|
2402
|
+
});
|
|
2361
2403
|
|
|
2362
2404
|
if (persist) {
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
writeJsonIfChanged(channelWorkspaceConfigPath(workspaceDir), workspace);
|
|
2369
|
-
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.json"), currentSnapshot);
|
|
2370
|
-
writeJsonIfChanged(
|
|
2371
|
-
path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.normalized.json"),
|
|
2405
|
+
persistChannelWorkspaceFiles({
|
|
2406
|
+
workspaceDir,
|
|
2407
|
+
channelDir,
|
|
2408
|
+
workspace,
|
|
2372
2409
|
currentSnapshot,
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2410
|
+
blockInfo,
|
|
2411
|
+
contractCodes,
|
|
2412
|
+
});
|
|
2376
2413
|
}
|
|
2377
2414
|
|
|
2378
2415
|
return {
|
|
@@ -2381,6 +2418,7 @@ async function syncChannelWorkspace({
|
|
|
2381
2418
|
currentSnapshot,
|
|
2382
2419
|
blockInfo,
|
|
2383
2420
|
contractCodes,
|
|
2421
|
+
cleanRebuildBackup,
|
|
2384
2422
|
};
|
|
2385
2423
|
}
|
|
2386
2424
|
|
|
@@ -2461,39 +2499,13 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2461
2499
|
const channelName = requireArg(args.channelName, "--channel-name");
|
|
2462
2500
|
const signer = requireL1Signer(args, provider);
|
|
2463
2501
|
const walletName = walletNameForChannelAndAddress(channelName, signer.address);
|
|
2464
|
-
const
|
|
2465
|
-
const initialized = await syncChannelWorkspace({
|
|
2466
|
-
workspaceName: channelName,
|
|
2502
|
+
const channelContextResult = await loadFreshChannelWorkspaceContextResult({
|
|
2467
2503
|
channelName,
|
|
2468
|
-
|
|
2504
|
+
networkName: requireNetworkName(args),
|
|
2469
2505
|
provider,
|
|
2470
|
-
bridgeResources,
|
|
2471
|
-
persist: true,
|
|
2472
|
-
allowExistingWorkspaceSync: true,
|
|
2473
|
-
useWorkspaceRecoveryIndex: true,
|
|
2474
|
-
fromGenesis: args.fromGenesis === true,
|
|
2475
2506
|
progressAction: "wallet recover-workspace",
|
|
2476
2507
|
});
|
|
2477
|
-
const context =
|
|
2478
|
-
workspaceName: channelName,
|
|
2479
|
-
workspaceDir: initialized.workspaceDir,
|
|
2480
|
-
persistChannelWorkspace: true,
|
|
2481
|
-
workspace: initialized.workspace,
|
|
2482
|
-
bridgeAbiManifest: bridgeResources.bridgeAbiManifest,
|
|
2483
|
-
currentSnapshot: initialized.currentSnapshot,
|
|
2484
|
-
blockInfo: initialized.blockInfo,
|
|
2485
|
-
contractCodes: initialized.contractCodes,
|
|
2486
|
-
channelManager: new Contract(
|
|
2487
|
-
initialized.workspace.channelManager,
|
|
2488
|
-
bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
|
|
2489
|
-
provider,
|
|
2490
|
-
),
|
|
2491
|
-
bridgeTokenVault: new Contract(
|
|
2492
|
-
initialized.workspace.bridgeTokenVault,
|
|
2493
|
-
bridgeResources.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
2494
|
-
provider,
|
|
2495
|
-
),
|
|
2496
|
-
};
|
|
2508
|
+
const context = channelContextResult.context;
|
|
2497
2509
|
const noteReceiveKeyMaterial = await deriveNoteReceiveKeyMaterial({
|
|
2498
2510
|
signer,
|
|
2499
2511
|
chainId: network.chainId,
|
|
@@ -2502,64 +2514,51 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2502
2514
|
account: signer.address,
|
|
2503
2515
|
});
|
|
2504
2516
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
l2Address: null,
|
|
2520
|
-
l2StorageKey: null,
|
|
2521
|
-
leafIndex: null,
|
|
2522
|
-
reason: "The local wallet existed, but the L1 address is no longer registered in the channel.",
|
|
2523
|
-
nextAction: buildRecoverWalletRemovedNextAction({
|
|
2524
|
-
channelName,
|
|
2525
|
-
networkName: network.name,
|
|
2526
|
-
accountName: args.account,
|
|
2527
|
-
}),
|
|
2528
|
-
});
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
expect(
|
|
2532
|
-
false,
|
|
2533
|
-
cliError(
|
|
2534
|
-
CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
|
|
2535
|
-
`No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
|
|
2536
|
-
),
|
|
2537
|
-
);
|
|
2538
|
-
}
|
|
2517
|
+
const lifecycleEpoch = await resolveWalletLifecycleEpoch({
|
|
2518
|
+
context,
|
|
2519
|
+
provider,
|
|
2520
|
+
l1Address: signer.address,
|
|
2521
|
+
registration,
|
|
2522
|
+
});
|
|
2523
|
+
expect(
|
|
2524
|
+
lifecycleEpoch,
|
|
2525
|
+
cliError(
|
|
2526
|
+
CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
|
|
2527
|
+
`No channelTokenVault registration history exists for ${signer.address}. Run channel join first.`,
|
|
2528
|
+
),
|
|
2529
|
+
);
|
|
2530
|
+
const registeredNoteReceivePubKey = lifecycleEpoch.noteReceivePubKey;
|
|
2539
2531
|
expect(
|
|
2540
|
-
ethers.toBigInt(normalizeBytes32Hex(
|
|
2532
|
+
ethers.toBigInt(normalizeBytes32Hex(registeredNoteReceivePubKey.x))
|
|
2541
2533
|
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
2542
2534
|
"The existing note-receive public key X does not match the derived note-receive public key.",
|
|
2543
2535
|
);
|
|
2544
2536
|
expect(
|
|
2545
|
-
Number(
|
|
2537
|
+
Number(registeredNoteReceivePubKey.yParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2546
2538
|
"The existing note-receive public key parity does not match the derived note-receive public key.",
|
|
2547
2539
|
);
|
|
2548
2540
|
const l2Identity = {
|
|
2549
2541
|
l2PrivateKey: null,
|
|
2550
2542
|
l2PublicKey: null,
|
|
2551
|
-
l2Address: getAddress(
|
|
2543
|
+
l2Address: getAddress(lifecycleEpoch.l2Address),
|
|
2552
2544
|
};
|
|
2553
|
-
const storageKey = normalizeBytes32Hex(
|
|
2545
|
+
const storageKey = normalizeBytes32Hex(lifecycleEpoch.channelTokenVaultKey);
|
|
2554
2546
|
|
|
2555
|
-
const walletDir =
|
|
2547
|
+
const walletDir = walletEpochPath(walletName, context.workspace.network, lifecycleEpoch.epochId);
|
|
2556
2548
|
const existingWallet = walletConfigExists(walletDir)
|
|
2557
|
-
?
|
|
2549
|
+
? loadWalletFromDir({
|
|
2550
|
+
walletName,
|
|
2551
|
+
networkName: context.workspace.network,
|
|
2552
|
+
walletDir,
|
|
2553
|
+
})
|
|
2558
2554
|
: null;
|
|
2559
2555
|
|
|
2560
2556
|
if (existingWallet) {
|
|
2561
2557
|
existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2558
|
+
applyWalletLifecycleEpoch(existingWallet.wallet, lifecycleEpoch);
|
|
2562
2559
|
persistWalletKeys(existingWallet);
|
|
2560
|
+
persistWallet(existingWallet);
|
|
2561
|
+
persistWalletIndexForContext(existingWallet);
|
|
2563
2562
|
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2564
2563
|
walletContext: existingWallet,
|
|
2565
2564
|
context,
|
|
@@ -2574,13 +2573,18 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2574
2573
|
status: "already-recovered",
|
|
2575
2574
|
wallet: walletName,
|
|
2576
2575
|
walletDir: existingWallet.walletDir,
|
|
2576
|
+
recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
|
|
2577
|
+
channelAutoRecoveryBlockDelta: channelContextResult.autoRecoveryBlockDelta,
|
|
2577
2578
|
workspace: context.workspaceName,
|
|
2578
2579
|
channelName: context.workspace.channelName,
|
|
2579
2580
|
channelId: context.workspace.channelId,
|
|
2580
2581
|
l1Address: signer.address,
|
|
2581
2582
|
l2Address: l2Identity.l2Address,
|
|
2582
2583
|
l2StorageKey: storageKey,
|
|
2583
|
-
leafIndex:
|
|
2584
|
+
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2585
|
+
epochId: lifecycleEpoch.epochId,
|
|
2586
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2587
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2584
2588
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2585
2589
|
l2Nonce: existingWallet.wallet.l2Nonce,
|
|
2586
2590
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
@@ -2590,8 +2594,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2590
2594
|
return;
|
|
2591
2595
|
}
|
|
2592
2596
|
|
|
2593
|
-
fs.rmSync(walletPath(walletName, context.workspace.network), { recursive: true, force: true });
|
|
2594
|
-
|
|
2595
2597
|
const walletContext = ensureWallet({
|
|
2596
2598
|
channelContext: context,
|
|
2597
2599
|
signerAddress: signer.address,
|
|
@@ -2599,8 +2601,9 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2599
2601
|
l2Identity,
|
|
2600
2602
|
walletSecret: noteReceiveKeyMaterial.privateKey,
|
|
2601
2603
|
storageKey,
|
|
2602
|
-
leafIndex:
|
|
2604
|
+
leafIndex: lifecycleEpoch.leafIndex,
|
|
2603
2605
|
noteReceiveKeyMaterial,
|
|
2606
|
+
lifecycleEpoch,
|
|
2604
2607
|
rpcUrl,
|
|
2605
2608
|
});
|
|
2606
2609
|
walletContext.wallet.l2Nonce = 0;
|
|
@@ -2621,13 +2624,18 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2621
2624
|
status: "recovered",
|
|
2622
2625
|
wallet: walletName,
|
|
2623
2626
|
walletDir: walletContext.walletDir,
|
|
2627
|
+
recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
|
|
2628
|
+
channelAutoRecoveryBlockDelta: channelContextResult.autoRecoveryBlockDelta,
|
|
2624
2629
|
workspace: context.workspaceName,
|
|
2625
2630
|
channelName: context.workspace.channelName,
|
|
2626
2631
|
channelId: context.workspace.channelId,
|
|
2627
2632
|
l1Address: signer.address,
|
|
2628
2633
|
l2Address: l2Identity.l2Address,
|
|
2629
2634
|
l2StorageKey: storageKey,
|
|
2630
|
-
leafIndex:
|
|
2635
|
+
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2636
|
+
epochId: lifecycleEpoch.epochId,
|
|
2637
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2638
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2631
2639
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2632
2640
|
l2Nonce: walletContext.wallet.l2Nonce,
|
|
2633
2641
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
@@ -2636,32 +2644,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2636
2644
|
});
|
|
2637
2645
|
}
|
|
2638
2646
|
|
|
2639
|
-
function removeLocalWalletArtifacts(walletName, networkName) {
|
|
2640
|
-
const walletDir = walletPath(walletName, networkName);
|
|
2641
|
-
const walletSecretFile = walletSecretPath(networkName, walletName);
|
|
2642
|
-
const walletSecretDir = path.dirname(walletSecretFile);
|
|
2643
|
-
const removedWalletDir = fs.existsSync(walletDir);
|
|
2644
|
-
const removedWalletSecret = fs.existsSync(walletSecretFile) || fs.existsSync(walletSecretDir);
|
|
2645
|
-
if (removedWalletDir) {
|
|
2646
|
-
fs.rmSync(walletDir, { recursive: true, force: true });
|
|
2647
|
-
}
|
|
2648
|
-
if (removedWalletSecret) {
|
|
2649
|
-
fs.rmSync(walletSecretDir, { recursive: true, force: true });
|
|
2650
|
-
}
|
|
2651
|
-
return {
|
|
2652
|
-
walletDir,
|
|
2653
|
-
walletSecretFile,
|
|
2654
|
-
removed: removedWalletDir || removedWalletSecret,
|
|
2655
|
-
removedWalletDir,
|
|
2656
|
-
removedWalletSecret,
|
|
2657
|
-
};
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
function buildRecoverWalletRemovedNextAction({ channelName, networkName, accountName }) {
|
|
2661
|
-
const account = accountName ? String(accountName) : "<ACCOUNT>";
|
|
2662
|
-
return `channel join --channel-name ${channelName} --network ${networkName} --account ${account} --wallet-secret-path <PATH> --acknowledge-action-impact`;
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
2647
|
async function handleInstallZkEvm({ args }) {
|
|
2666
2648
|
const selectedVersions = await resolvePrivateStateInstallRuntimeVersions(args);
|
|
2667
2649
|
const tokamakCliRuntime = await installTokamakCliRuntimeForPrivateState({
|
|
@@ -2762,6 +2744,73 @@ function uniquePaths(paths) {
|
|
|
2762
2744
|
return [...new Set(paths.map((entry) => path.resolve(entry)))];
|
|
2763
2745
|
}
|
|
2764
2746
|
|
|
2747
|
+
function backupWorkspaceForCleanRebuild({ workspaceDir, networkName, channelName }) {
|
|
2748
|
+
const resolvedWorkspaceDir = path.resolve(workspaceDir);
|
|
2749
|
+
if (!fs.existsSync(resolvedWorkspaceDir)) {
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
expectPathWithinRoot(
|
|
2753
|
+
resolvedWorkspaceDir,
|
|
2754
|
+
workspaceRoot,
|
|
2755
|
+
`Clean rebuild refuses to move a path outside the private-state workspace root: ${resolvedWorkspaceDir}.`,
|
|
2756
|
+
);
|
|
2757
|
+
|
|
2758
|
+
const backupRoot = path.join(
|
|
2759
|
+
privateStateCliDataRoot(),
|
|
2760
|
+
"workspace-rebuild-backups",
|
|
2761
|
+
slugifyPathComponent(networkName),
|
|
2762
|
+
);
|
|
2763
|
+
ensureDir(backupRoot);
|
|
2764
|
+
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
2765
|
+
const backupBasePath = path.join(
|
|
2766
|
+
backupRoot,
|
|
2767
|
+
`${slugifyPathComponent(channelName)}-${timestamp}`,
|
|
2768
|
+
);
|
|
2769
|
+
const backupPath = nextAvailablePath(backupBasePath);
|
|
2770
|
+
fs.renameSync(resolvedWorkspaceDir, backupPath);
|
|
2771
|
+
return {
|
|
2772
|
+
workspaceDir: resolvedWorkspaceDir,
|
|
2773
|
+
backupPath,
|
|
2774
|
+
secretsPreserved: true,
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
function persistChannelWorkspaceFiles({
|
|
2779
|
+
workspaceDir,
|
|
2780
|
+
channelDir,
|
|
2781
|
+
workspace,
|
|
2782
|
+
currentSnapshot,
|
|
2783
|
+
blockInfo,
|
|
2784
|
+
contractCodes,
|
|
2785
|
+
}) {
|
|
2786
|
+
ensureDir(channelDir);
|
|
2787
|
+
ensureDir(channelWorkspaceCurrentPath(workspaceDir));
|
|
2788
|
+
ensureDir(channelWorkspaceOperationsPath(workspaceDir));
|
|
2789
|
+
ensureDir(workspaceWalletsDir(workspaceDir));
|
|
2790
|
+
|
|
2791
|
+
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.json"), currentSnapshot);
|
|
2792
|
+
writeJsonIfChanged(
|
|
2793
|
+
path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.normalized.json"),
|
|
2794
|
+
currentSnapshot,
|
|
2795
|
+
);
|
|
2796
|
+
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "block_info.json"), blockInfo);
|
|
2797
|
+
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "contract_codes.json"), contractCodes);
|
|
2798
|
+
writeJsonIfChanged(channelWorkspaceConfigPath(workspaceDir), workspace);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function nextAvailablePath(basePath) {
|
|
2802
|
+
if (!fs.existsSync(basePath)) {
|
|
2803
|
+
return basePath;
|
|
2804
|
+
}
|
|
2805
|
+
for (let index = 1; index <= 1000; index += 1) {
|
|
2806
|
+
const candidate = `${basePath}-${index}`;
|
|
2807
|
+
if (!fs.existsSync(candidate)) {
|
|
2808
|
+
return candidate;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
throw new Error(`Unable to allocate a clean rebuild backup path for ${basePath}.`);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2765
2814
|
function removeManagedRoot({ label, rootPath }) {
|
|
2766
2815
|
const resolvedPath = path.resolve(rootPath);
|
|
2767
2816
|
try {
|
|
@@ -2982,7 +3031,6 @@ function handleInvestigator() {
|
|
|
2982
3031
|
function resolveInvestigatorIndexPath() {
|
|
2983
3032
|
const candidates = [
|
|
2984
3033
|
path.join(privateStateCliPackageRoot, "investigator", "index.html"),
|
|
2985
|
-
path.resolve(privateStateCliPackageRoot, "..", "investigator", "index.html"),
|
|
2986
3034
|
];
|
|
2987
3035
|
const htmlPath = candidates.find((candidate) => fs.existsSync(candidate));
|
|
2988
3036
|
if (!htmlPath) {
|
|
@@ -3371,9 +3419,13 @@ function handleWalletImportKey({ args, keyKind }) {
|
|
|
3371
3419
|
: walletViewingKeySecretPath(networkName, walletName);
|
|
3372
3420
|
expect(!fs.existsSync(targetPath), `Refusing to overwrite existing ${keyKind} key: ${targetPath}.`);
|
|
3373
3421
|
writeSecretFile(targetPath, JSON.stringify(payload, null, 2));
|
|
3374
|
-
const
|
|
3375
|
-
|
|
3376
|
-
|
|
3422
|
+
const walletRoot = walletRootPath(walletName, networkName);
|
|
3423
|
+
const walletIndex = fs.existsSync(walletRoot)
|
|
3424
|
+
? requireWalletIndex({ walletRoot, walletName, networkName })
|
|
3425
|
+
: null;
|
|
3426
|
+
const selectedEpoch = walletIndex ? selectedWalletEpoch(walletIndex, walletName, networkName) : null;
|
|
3427
|
+
if (selectedEpoch) {
|
|
3428
|
+
const walletDir = walletEpochPathFromRoot(walletRoot, selectedEpoch.epochId);
|
|
3377
3429
|
const metadataPath = keyKind === "spending"
|
|
3378
3430
|
? walletSpendingKeyMetadataPath(walletDir)
|
|
3379
3431
|
: walletViewingKeyMetadataPath(walletDir);
|
|
@@ -3842,7 +3894,13 @@ async function inspectGuideAccount({ account, networkName, network, provider, ar
|
|
|
3842
3894
|
}
|
|
3843
3895
|
|
|
3844
3896
|
async function inspectGuideWallet({ walletName, networkName, provider, artifactsInstalled }) {
|
|
3845
|
-
|
|
3897
|
+
let walletDir = walletRootPath(walletName, networkName);
|
|
3898
|
+
let workspaceError = null;
|
|
3899
|
+
try {
|
|
3900
|
+
walletDir = selectedWalletEpochDir(walletName, networkName);
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
workspaceError = error.message;
|
|
3903
|
+
}
|
|
3846
3904
|
const viewingKeyFile = walletViewingKeySecretPath(networkName, walletName);
|
|
3847
3905
|
const spendingKeyFile = walletSpendingKeySecretPath(networkName, walletName);
|
|
3848
3906
|
const result = {
|
|
@@ -3865,9 +3923,9 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3865
3923
|
unusedNoteBalanceBaseUnits: null,
|
|
3866
3924
|
unusedNoteBalanceTokens: null,
|
|
3867
3925
|
spentNoteCount: null,
|
|
3868
|
-
error:
|
|
3926
|
+
error: workspaceError,
|
|
3869
3927
|
};
|
|
3870
|
-
if (!result.exists) {
|
|
3928
|
+
if (workspaceError || !result.exists) {
|
|
3871
3929
|
return result;
|
|
3872
3930
|
}
|
|
3873
3931
|
|
|
@@ -4109,6 +4167,7 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
4109
4167
|
printJson({
|
|
4110
4168
|
action: "wallet get-meta",
|
|
4111
4169
|
wallet: wallet.walletName,
|
|
4170
|
+
...walletLifecycleMetadata(wallet.wallet),
|
|
4112
4171
|
network: walletMetadata.network,
|
|
4113
4172
|
channelName: walletMetadata.channelName,
|
|
4114
4173
|
l1Address: signer.address,
|
|
@@ -4202,6 +4261,156 @@ async function loadWalletChannelRegistrationState({
|
|
|
4202
4261
|
};
|
|
4203
4262
|
}
|
|
4204
4263
|
|
|
4264
|
+
async function resolveWalletLifecycleEpoch({
|
|
4265
|
+
context,
|
|
4266
|
+
provider,
|
|
4267
|
+
l1Address,
|
|
4268
|
+
registration = null,
|
|
4269
|
+
}) {
|
|
4270
|
+
const epochs = await readWalletLifecycleEpochs({
|
|
4271
|
+
context,
|
|
4272
|
+
provider,
|
|
4273
|
+
l1Address,
|
|
4274
|
+
});
|
|
4275
|
+
if (epochs.length === 0 && registration?.exists) {
|
|
4276
|
+
const block = await provider.getBlock("latest").catch(() => null);
|
|
4277
|
+
return {
|
|
4278
|
+
epochId: `join-registered-${String(registration.joinedAt)}-${String(registration.leafIndex)}`,
|
|
4279
|
+
lifecycleStatus: "active",
|
|
4280
|
+
joinedAtTxHash: null,
|
|
4281
|
+
joinedAtBlockNumber: null,
|
|
4282
|
+
joinedAtLogIndex: null,
|
|
4283
|
+
joinedAtBlockTimestamp: Number(registration.joinedAt),
|
|
4284
|
+
joinedAtBlockTimestampIso: Number(registration.joinedAt) > 0
|
|
4285
|
+
? new Date(Number(registration.joinedAt) * 1000).toISOString()
|
|
4286
|
+
: null,
|
|
4287
|
+
exitedAtTxHash: null,
|
|
4288
|
+
exitedAtBlockNumber: null,
|
|
4289
|
+
exitedAtLogIndex: null,
|
|
4290
|
+
exitedAtBlockTimestamp: null,
|
|
4291
|
+
exitedAtBlockTimestampIso: null,
|
|
4292
|
+
l2Address: getAddress(registration.l2Address),
|
|
4293
|
+
channelTokenVaultKey: normalizeBytes32Hex(registration.channelTokenVaultKey),
|
|
4294
|
+
leafIndex: registration.leafIndex,
|
|
4295
|
+
noteReceivePubKey: {
|
|
4296
|
+
x: normalizeBytes32Hex(registration.noteReceivePubKey.x),
|
|
4297
|
+
yParity: Number(registration.noteReceivePubKey.yParity),
|
|
4298
|
+
},
|
|
4299
|
+
observedAtBlockNumber: block?.number ?? null,
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
if (registration?.exists) {
|
|
4303
|
+
const active = [...epochs].reverse().find((epoch) => (
|
|
4304
|
+
epoch.lifecycleStatus === "active"
|
|
4305
|
+
&& ethers.toBigInt(getAddress(epoch.l2Address)) === ethers.toBigInt(getAddress(registration.l2Address))
|
|
4306
|
+
&& ethers.toBigInt(normalizeBytes32Hex(epoch.channelTokenVaultKey))
|
|
4307
|
+
=== ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
4308
|
+
));
|
|
4309
|
+
if (active) {
|
|
4310
|
+
return active;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
return epochs[epochs.length - 1] ?? null;
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
async function readWalletLifecycleEpochs({ context, provider, l1Address }) {
|
|
4317
|
+
const fromBlock = Number(context.workspace.genesisBlockNumber ?? 0);
|
|
4318
|
+
const [registeredLogs, exitedLogs] = await Promise.all([
|
|
4319
|
+
context.channelManager.queryFilter(
|
|
4320
|
+
context.channelManager.filters.ChannelTokenVaultIdentityRegistered(l1Address),
|
|
4321
|
+
fromBlock,
|
|
4322
|
+
"latest",
|
|
4323
|
+
),
|
|
4324
|
+
context.channelManager.queryFilter(
|
|
4325
|
+
context.channelManager.filters.ChannelTokenVaultIdentityExited(l1Address),
|
|
4326
|
+
fromBlock,
|
|
4327
|
+
"latest",
|
|
4328
|
+
),
|
|
4329
|
+
]);
|
|
4330
|
+
const exits = await Promise.all(exitedLogs.map((log) => walletExitFromLog({ log, provider })));
|
|
4331
|
+
const epochs = [];
|
|
4332
|
+
for (const log of registeredLogs.sort(compareLogsByPosition)) {
|
|
4333
|
+
const registered = await walletEpochFromRegisteredLog({ log, provider });
|
|
4334
|
+
const exit = exits.find((entry) => (
|
|
4335
|
+
compareLogPosition(entry, registered) > 0
|
|
4336
|
+
&& ethers.toBigInt(entry.leafIndex) === ethers.toBigInt(registered.leafIndex)
|
|
4337
|
+
&& !epochs.some((epoch) => epoch.exitedAtTxHash === entry.exitedAtTxHash)
|
|
4338
|
+
));
|
|
4339
|
+
if (exit) {
|
|
4340
|
+
epochs.push({
|
|
4341
|
+
...registered,
|
|
4342
|
+
lifecycleStatus: "exited",
|
|
4343
|
+
exitedAtTxHash: exit.exitedAtTxHash,
|
|
4344
|
+
exitedAtBlockNumber: exit.exitedAtBlockNumber,
|
|
4345
|
+
exitedAtLogIndex: exit.exitedAtLogIndex,
|
|
4346
|
+
exitedAtBlockTimestamp: exit.exitedAtBlockTimestamp,
|
|
4347
|
+
exitedAtBlockTimestampIso: exit.exitedAtBlockTimestampIso,
|
|
4348
|
+
});
|
|
4349
|
+
} else {
|
|
4350
|
+
epochs.push(registered);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
return epochs.sort(compareWalletEpochs);
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
async function walletEpochFromRegisteredLog({ log, provider }) {
|
|
4357
|
+
const block = await provider.getBlock(log.blockNumber).catch(() => null);
|
|
4358
|
+
const args = log.args;
|
|
4359
|
+
return {
|
|
4360
|
+
epochId: walletEpochIdFromLog(log),
|
|
4361
|
+
lifecycleStatus: "active",
|
|
4362
|
+
joinedAtTxHash: log.transactionHash,
|
|
4363
|
+
joinedAtBlockNumber: log.blockNumber,
|
|
4364
|
+
joinedAtLogIndex: log.index ?? log.logIndex ?? null,
|
|
4365
|
+
joinedAtBlockTimestamp: block?.timestamp ?? Number(args.joinedAt ?? 0) ?? null,
|
|
4366
|
+
joinedAtBlockTimestampIso: block?.timestamp
|
|
4367
|
+
? new Date(Number(block.timestamp) * 1000).toISOString()
|
|
4368
|
+
: Number(args.joinedAt ?? 0) > 0
|
|
4369
|
+
? new Date(Number(args.joinedAt) * 1000).toISOString()
|
|
4370
|
+
: null,
|
|
4371
|
+
exitedAtTxHash: null,
|
|
4372
|
+
exitedAtBlockNumber: null,
|
|
4373
|
+
exitedAtLogIndex: null,
|
|
4374
|
+
exitedAtBlockTimestamp: null,
|
|
4375
|
+
exitedAtBlockTimestampIso: null,
|
|
4376
|
+
l2Address: getAddress(args.l2Address),
|
|
4377
|
+
channelTokenVaultKey: normalizeBytes32Hex(args.channelTokenVaultKey),
|
|
4378
|
+
leafIndex: args.leafIndex,
|
|
4379
|
+
noteReceivePubKey: {
|
|
4380
|
+
x: normalizeBytes32Hex(args.noteReceivePubKeyX),
|
|
4381
|
+
yParity: Number(args.noteReceivePubKeyYParity),
|
|
4382
|
+
},
|
|
4383
|
+
};
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
async function walletExitFromLog({ log, provider }) {
|
|
4387
|
+
const block = await provider.getBlock(log.blockNumber).catch(() => null);
|
|
4388
|
+
return {
|
|
4389
|
+
exitedAtTxHash: log.transactionHash,
|
|
4390
|
+
exitedAtBlockNumber: log.blockNumber,
|
|
4391
|
+
exitedAtLogIndex: log.index ?? log.logIndex ?? null,
|
|
4392
|
+
exitedAtBlockTimestamp: block?.timestamp ?? null,
|
|
4393
|
+
exitedAtBlockTimestampIso: block?.timestamp ? new Date(Number(block.timestamp) * 1000).toISOString() : null,
|
|
4394
|
+
leafIndex: log.args.leafIndex,
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
function walletEpochIdFromLog(log) {
|
|
4399
|
+
return `join-${String(log.transactionHash).toLowerCase()}-${Number(log.index ?? log.logIndex ?? 0)}`;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
function compareLogPosition(left, right) {
|
|
4403
|
+
return Number(left.exitedAtBlockNumber ?? left.blockNumber ?? 0) - Number(right.joinedAtBlockNumber ?? right.blockNumber ?? 0)
|
|
4404
|
+
|| Number((left.exitedAtLogIndex ?? left.index ?? left.logIndex) ?? 0)
|
|
4405
|
+
- Number((right.joinedAtLogIndex ?? right.index ?? right.logIndex) ?? 0);
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
function compareWalletEpochs(left, right) {
|
|
4409
|
+
return Number(left.joinedAtBlockNumber ?? 0) - Number(right.joinedAtBlockNumber ?? 0)
|
|
4410
|
+
|| Number(left.joinedAtLogIndex ?? 0) - Number(right.joinedAtLogIndex ?? 0)
|
|
4411
|
+
|| String(left.epochId).localeCompare(String(right.epochId));
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4205
4414
|
async function handleWalletGetChannelFund({ args, provider }) {
|
|
4206
4415
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
4207
4416
|
const {
|
|
@@ -4311,6 +4520,14 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4311
4520
|
{ nonce: nextNonce++ },
|
|
4312
4521
|
),
|
|
4313
4522
|
);
|
|
4523
|
+
const registered = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
4524
|
+
const lifecycleEpoch = await resolveWalletLifecycleEpoch({
|
|
4525
|
+
context,
|
|
4526
|
+
provider,
|
|
4527
|
+
l1Address: signer.address,
|
|
4528
|
+
registration: registered,
|
|
4529
|
+
});
|
|
4530
|
+
expect(lifecycleEpoch, "Unable to resolve the channel join epoch from emitted registration logs.");
|
|
4314
4531
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
4315
4532
|
context,
|
|
4316
4533
|
provider,
|
|
@@ -4327,6 +4544,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4327
4544
|
storageKey,
|
|
4328
4545
|
leafIndex,
|
|
4329
4546
|
noteReceiveKeyMaterial,
|
|
4547
|
+
lifecycleEpoch,
|
|
4330
4548
|
rpcUrl,
|
|
4331
4549
|
});
|
|
4332
4550
|
|
|
@@ -4343,6 +4561,10 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4343
4561
|
l2Address: l2Identity.l2Address,
|
|
4344
4562
|
l2StorageKey: storageKey,
|
|
4345
4563
|
leafIndex: leafIndex.toString(),
|
|
4564
|
+
epochId: lifecycleEpoch.epochId,
|
|
4565
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
4566
|
+
joinedAtTxHash: lifecycleEpoch.joinedAtTxHash,
|
|
4567
|
+
joinedAtBlockNumber: lifecycleEpoch.joinedAtBlockNumber,
|
|
4346
4568
|
joinTollBaseUnits: joinToll.toString(),
|
|
4347
4569
|
joinTollTokens: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4348
4570
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
@@ -4359,6 +4581,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4359
4581
|
|
|
4360
4582
|
async function handleExitChannel({ args, provider }) {
|
|
4361
4583
|
const { wallet: walletContext, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
4584
|
+
requireActiveWalletLifecycle(walletContext, "channel exit");
|
|
4362
4585
|
const { signer, context, channelFund, contextResult } = await loadWalletChannelFundState({
|
|
4363
4586
|
walletContext,
|
|
4364
4587
|
provider,
|
|
@@ -4378,7 +4601,11 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4378
4601
|
const receipt = await waitForReceipt(
|
|
4379
4602
|
await context.bridgeTokenVault.connect(ownerSigner).exitChannel(ethers.toBigInt(context.workspace.channelId)),
|
|
4380
4603
|
);
|
|
4381
|
-
const
|
|
4604
|
+
const lifecycleEpoch = await markWalletEpochExited({
|
|
4605
|
+
walletContext,
|
|
4606
|
+
receipt,
|
|
4607
|
+
provider,
|
|
4608
|
+
});
|
|
4382
4609
|
|
|
4383
4610
|
printJson({
|
|
4384
4611
|
action: "channel exit",
|
|
@@ -4396,8 +4623,12 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4396
4623
|
gasUsed: receiptGasUsed(receipt),
|
|
4397
4624
|
txUrl: explorerTxUrl(network, receipt.hash),
|
|
4398
4625
|
receipt: sanitizeReceipt(receipt),
|
|
4399
|
-
|
|
4400
|
-
|
|
4626
|
+
epochId: lifecycleEpoch.epochId,
|
|
4627
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
4628
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
4629
|
+
exitedAtBlockNumber: lifecycleEpoch.exitedAtBlockNumber,
|
|
4630
|
+
exitedAtBlockTimestampIso: lifecycleEpoch.exitedAtBlockTimestampIso,
|
|
4631
|
+
archivedWalletDir: walletContext.walletDir,
|
|
4401
4632
|
});
|
|
4402
4633
|
}
|
|
4403
4634
|
|
|
@@ -4409,6 +4640,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4409
4640
|
: "wallet withdraw-channel";
|
|
4410
4641
|
emitProgress(operationName, "loading");
|
|
4411
4642
|
const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
|
|
4643
|
+
requireActiveWalletLifecycle(walletContext, operationName);
|
|
4412
4644
|
const contextResult = await loadFreshWalletChannelContext({
|
|
4413
4645
|
walletContext,
|
|
4414
4646
|
provider,
|
|
@@ -4634,6 +4866,7 @@ function resolveFunctionMetadataProofForExecution({
|
|
|
4634
4866
|
|
|
4635
4867
|
async function handleMintNotes({ args, provider }) {
|
|
4636
4868
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4869
|
+
requireActiveWalletLifecycle(wallet, "wallet mint-notes");
|
|
4637
4870
|
requireWalletSpendingCapability(wallet);
|
|
4638
4871
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
4639
4872
|
const amountInputs = parseAmountVector(requireArg(args.amounts, "--amounts"), {
|
|
@@ -4722,6 +4955,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4722
4955
|
|
|
4723
4956
|
async function handleRedeemNotes({ args, provider }) {
|
|
4724
4957
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4958
|
+
requireActiveWalletLifecycle(wallet, "wallet redeem-notes");
|
|
4725
4959
|
requireWalletViewingCapability(wallet);
|
|
4726
4960
|
requireWalletSpendingCapability(wallet);
|
|
4727
4961
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
@@ -4890,24 +5124,39 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4890
5124
|
const outputPath = path.resolve(String(requireArg(args.exportEvidence, "--export-evidence")));
|
|
4891
5125
|
ensureDir(path.dirname(outputPath));
|
|
4892
5126
|
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
for (const
|
|
4899
|
-
|
|
5127
|
+
const evidenceWalletContexts = loadWalletEpochContextsForEvidence({
|
|
5128
|
+
baseWalletContext: walletContext,
|
|
5129
|
+
networkName: walletMetadata.network,
|
|
5130
|
+
});
|
|
5131
|
+
const noteInputs = [];
|
|
5132
|
+
for (const candidateWalletContext of evidenceWalletContexts) {
|
|
5133
|
+
const notes = candidateWalletContext === walletContext
|
|
5134
|
+
? [
|
|
5135
|
+
...unusedTrackedNotes.map(normalizeTrackedNote),
|
|
5136
|
+
...spentTrackedNotes.map(normalizeTrackedNote),
|
|
5137
|
+
]
|
|
5138
|
+
: [
|
|
5139
|
+
...Object.values(candidateWalletContext.wallet.notes.unused).map(normalizeTrackedNote),
|
|
5140
|
+
...Object.values(candidateWalletContext.wallet.notes.spent).map(normalizeTrackedNote),
|
|
5141
|
+
];
|
|
5142
|
+
for (const note of notes) {
|
|
5143
|
+
validateEvidenceNotePlaintext(note, candidateWalletContext.wallet);
|
|
5144
|
+
noteInputs.push({ note, walletContext: candidateWalletContext });
|
|
5145
|
+
}
|
|
4900
5146
|
}
|
|
5147
|
+
noteInputs.sort((left, right) =>
|
|
5148
|
+
String(left.walletContext.wallet.walletEpochId ?? "").localeCompare(String(right.walletContext.wallet.walletEpochId ?? ""))
|
|
5149
|
+
|| left.note.commitment.localeCompare(right.note.commitment));
|
|
4901
5150
|
|
|
4902
5151
|
const txHashes = uniqueNonNull([
|
|
4903
|
-
...
|
|
4904
|
-
...
|
|
5152
|
+
...noteInputs.map(({ note }) => note.createdAtTxHash),
|
|
5153
|
+
...noteInputs.map(({ note }) => note.spentAtTxHash),
|
|
4905
5154
|
]);
|
|
4906
5155
|
const transactionEvidence = await buildTransactionEvidenceMap({ provider, txHashes });
|
|
4907
5156
|
const blockTimestampCache = buildBlockTimestampCache(transactionEvidence);
|
|
4908
|
-
const noteRecords =
|
|
5157
|
+
const noteRecords = noteInputs.map(({ note, walletContext: noteWalletContext }) => buildEvidenceNoteRecord({
|
|
4909
5158
|
note,
|
|
4910
|
-
walletContext,
|
|
5159
|
+
walletContext: noteWalletContext,
|
|
4911
5160
|
walletMetadata,
|
|
4912
5161
|
context,
|
|
4913
5162
|
transactionEvidence,
|
|
@@ -4917,6 +5166,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4917
5166
|
const manifest = buildEvidenceManifest({
|
|
4918
5167
|
outputPath,
|
|
4919
5168
|
walletContext,
|
|
5169
|
+
walletContexts: evidenceWalletContexts,
|
|
4920
5170
|
walletMetadata,
|
|
4921
5171
|
context,
|
|
4922
5172
|
noteRecords,
|
|
@@ -4932,7 +5182,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4932
5182
|
addEvidenceJson(archive, "indexes/by-block-range.json", indexes.byBlockRange);
|
|
4933
5183
|
addEvidenceJson(archive, "indexes/by-counterparty.json", indexes.byCounterparty);
|
|
4934
5184
|
for (const record of noteRecords) {
|
|
4935
|
-
addEvidenceJson(archive,
|
|
5185
|
+
addEvidenceJson(archive, evidenceNotePath(record), record);
|
|
4936
5186
|
}
|
|
4937
5187
|
for (const [txHash, txRecord] of Object.entries(transactionEvidence)) {
|
|
4938
5188
|
addEvidenceJson(archive, `transactions/${txHash}.json`, txRecord.transaction);
|
|
@@ -4941,7 +5191,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4941
5191
|
}
|
|
4942
5192
|
|
|
4943
5193
|
assertEvidenceBundleDoesNotContainSecrets({
|
|
4944
|
-
|
|
5194
|
+
wallets: evidenceWalletContexts.map((entry) => entry.wallet),
|
|
4945
5195
|
payload: {
|
|
4946
5196
|
manifest,
|
|
4947
5197
|
indexes,
|
|
@@ -4958,6 +5208,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4958
5208
|
format: WALLET_EVIDENCE_BUNDLE_FORMAT,
|
|
4959
5209
|
formatVersion: WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION,
|
|
4960
5210
|
noteCount: noteRecords.length,
|
|
5211
|
+
walletEpochCount: evidenceWalletContexts.length,
|
|
4961
5212
|
transactionCount: txHashes.length,
|
|
4962
5213
|
containsNotePlaintext: true,
|
|
4963
5214
|
containsSpendingKey: false,
|
|
@@ -4967,6 +5218,33 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4967
5218
|
};
|
|
4968
5219
|
}
|
|
4969
5220
|
|
|
5221
|
+
function loadWalletEpochContextsForEvidence({ baseWalletContext, networkName }) {
|
|
5222
|
+
const walletRoot = walletRootPath(baseWalletContext.walletName, networkName);
|
|
5223
|
+
const index = requireWalletIndex({
|
|
5224
|
+
walletRoot,
|
|
5225
|
+
walletName: baseWalletContext.walletName,
|
|
5226
|
+
networkName,
|
|
5227
|
+
});
|
|
5228
|
+
const contexts = [];
|
|
5229
|
+
for (const epoch of index.epochs) {
|
|
5230
|
+
const walletDir = walletEpochPathFromRoot(walletRoot, epoch.epochId);
|
|
5231
|
+
if (!walletConfigExists(walletDir)) {
|
|
5232
|
+
continue;
|
|
5233
|
+
}
|
|
5234
|
+
const context = loadWalletFromDir({
|
|
5235
|
+
walletName: baseWalletContext.walletName,
|
|
5236
|
+
networkName,
|
|
5237
|
+
walletDir,
|
|
5238
|
+
});
|
|
5239
|
+
contexts.push(context);
|
|
5240
|
+
}
|
|
5241
|
+
expect(
|
|
5242
|
+
contexts.length > 0,
|
|
5243
|
+
`Wallet ${baseWalletContext.walletName} on ${networkName} has no readable wallet epochs. Run wallet recover-workspace and then wallet get-notes --export-evidence again.`,
|
|
5244
|
+
);
|
|
5245
|
+
return contexts;
|
|
5246
|
+
}
|
|
5247
|
+
|
|
4970
5248
|
function validateEvidenceNotePlaintext(note, wallet) {
|
|
4971
5249
|
expect(
|
|
4972
5250
|
note.owner && note.value !== null && note.salt && note.encryptedNoteValue,
|
|
@@ -5084,6 +5362,7 @@ function buildEvidenceNoteRecord({
|
|
|
5084
5362
|
channelName: walletMetadata.channelName,
|
|
5085
5363
|
channelId: walletContext.wallet.channelId,
|
|
5086
5364
|
wallet: walletContext.walletName,
|
|
5365
|
+
...walletLifecycleMetadata(walletContext.wallet),
|
|
5087
5366
|
walletL1Address: walletContext.wallet.l1Address,
|
|
5088
5367
|
walletL2Address: walletContext.wallet.l2Address,
|
|
5089
5368
|
controller: context.workspace.controller,
|
|
@@ -5198,7 +5477,7 @@ function buildEvidenceIndexes(noteRecords) {
|
|
|
5198
5477
|
},
|
|
5199
5478
|
};
|
|
5200
5479
|
for (const record of noteRecords) {
|
|
5201
|
-
const pathName =
|
|
5480
|
+
const pathName = evidenceNotePath(record);
|
|
5202
5481
|
indexes.byCommitment[record.derived.commitment] = pathName;
|
|
5203
5482
|
indexes.byNullifier[record.derived.nullifier] = pathName;
|
|
5204
5483
|
pushIndexEntry(indexes.byCreationTx, record.creation.txHash, pathName);
|
|
@@ -5227,6 +5506,21 @@ function buildEvidenceIndexes(noteRecords) {
|
|
|
5227
5506
|
return indexes;
|
|
5228
5507
|
}
|
|
5229
5508
|
|
|
5509
|
+
function evidenceNotePath(record) {
|
|
5510
|
+
expect(
|
|
5511
|
+
record.walletScope?.canonicalWalletName && record.walletScope?.epochId,
|
|
5512
|
+
"Evidence note path requires the current epoch-aware wallet scope.",
|
|
5513
|
+
);
|
|
5514
|
+
return [
|
|
5515
|
+
"wallets",
|
|
5516
|
+
slugifyPathComponent(record.walletScope.canonicalWalletName),
|
|
5517
|
+
"epochs",
|
|
5518
|
+
slugifyPathComponent(record.walletScope.epochId),
|
|
5519
|
+
"notes",
|
|
5520
|
+
`${record.derived.commitment}.json`,
|
|
5521
|
+
].join("/");
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5230
5524
|
function pushIndexEntry(index, key, value) {
|
|
5231
5525
|
if (!key) {
|
|
5232
5526
|
return;
|
|
@@ -5240,6 +5534,7 @@ function pushIndexEntry(index, key, value) {
|
|
|
5240
5534
|
function buildEvidenceManifest({
|
|
5241
5535
|
outputPath,
|
|
5242
5536
|
walletContext,
|
|
5537
|
+
walletContexts = [walletContext],
|
|
5243
5538
|
walletMetadata,
|
|
5244
5539
|
context,
|
|
5245
5540
|
noteRecords,
|
|
@@ -5258,10 +5553,17 @@ function buildEvidenceManifest({
|
|
|
5258
5553
|
wallet: walletContext.walletName,
|
|
5259
5554
|
walletL1Address: walletContext.wallet.l1Address,
|
|
5260
5555
|
walletL2Address: walletContext.wallet.l2Address,
|
|
5556
|
+
wallets: walletContexts.map((entry) => ({
|
|
5557
|
+
wallet: entry.walletName,
|
|
5558
|
+
...walletLifecycleMetadata(entry.wallet),
|
|
5559
|
+
walletL1Address: entry.wallet.l1Address,
|
|
5560
|
+
walletL2Address: entry.wallet.l2Address,
|
|
5561
|
+
})),
|
|
5261
5562
|
controller: context.workspace.controller,
|
|
5262
5563
|
channelManager: context.workspace.channelManager,
|
|
5263
5564
|
bridgeTokenVault: context.workspace.bridgeTokenVault,
|
|
5264
5565
|
containsAllLocallyKnownNotes: true,
|
|
5566
|
+
containsAllLocalWalletEpochs: true,
|
|
5265
5567
|
containsNotePlaintext: true,
|
|
5266
5568
|
noteCount: noteRecords.length,
|
|
5267
5569
|
transactionCount: txHashes.length,
|
|
@@ -5281,12 +5583,13 @@ function addEvidenceJson(archive, archivePath, value) {
|
|
|
5281
5583
|
archive.addFile(archivePath, Buffer.from(`${JSON.stringify(normalizeCliOutput(value), null, 2)}\n`, "utf8"));
|
|
5282
5584
|
}
|
|
5283
5585
|
|
|
5284
|
-
function assertEvidenceBundleDoesNotContainSecrets({ wallet, payload }) {
|
|
5586
|
+
function assertEvidenceBundleDoesNotContainSecrets({ wallet = null, wallets = null, payload }) {
|
|
5285
5587
|
const serialized = JSON.stringify(payload);
|
|
5286
|
-
const
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5588
|
+
const walletList = wallets ?? (wallet ? [wallet] : []);
|
|
5589
|
+
const forbiddenValues = walletList.flatMap((entry) => [
|
|
5590
|
+
entry.l2PrivateKey,
|
|
5591
|
+
entry.noteReceivePrivateKey,
|
|
5592
|
+
]).filter((value) => typeof value === "string" && value.length > 0);
|
|
5290
5593
|
for (const value of forbiddenValues) {
|
|
5291
5594
|
expect(
|
|
5292
5595
|
!serialized.includes(value),
|
|
@@ -5301,6 +5604,7 @@ function uniqueNonNull(values) {
|
|
|
5301
5604
|
|
|
5302
5605
|
async function handleTransferNotes({ args, provider }) {
|
|
5303
5606
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
5607
|
+
requireActiveWalletLifecycle(wallet, "wallet transfer-notes");
|
|
5304
5608
|
requireWalletViewingCapability(wallet);
|
|
5305
5609
|
requireWalletSpendingCapability(wallet);
|
|
5306
5610
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
@@ -5497,10 +5801,20 @@ function ensureWallet({
|
|
|
5497
5801
|
storageKey,
|
|
5498
5802
|
leafIndex,
|
|
5499
5803
|
noteReceiveKeyMaterial,
|
|
5804
|
+
lifecycleEpoch,
|
|
5500
5805
|
rpcUrl,
|
|
5501
5806
|
}) {
|
|
5502
5807
|
const walletName = walletNameForChannelAndAddress(channelContext.workspace.channelName, signerAddress);
|
|
5503
|
-
const
|
|
5808
|
+
const walletRoot = walletRootPath(walletName, channelContext.workspace.network);
|
|
5809
|
+
if (fs.existsSync(walletRoot)) {
|
|
5810
|
+
requireWalletIndex({
|
|
5811
|
+
walletRoot,
|
|
5812
|
+
walletName,
|
|
5813
|
+
networkName: channelContext.workspace.network,
|
|
5814
|
+
});
|
|
5815
|
+
}
|
|
5816
|
+
expect(lifecycleEpoch, "Current wallet workspace creation requires an on-chain wallet lifecycle epoch.");
|
|
5817
|
+
const walletDir = walletEpochPath(walletName, channelContext.workspace.network, lifecycleEpoch.epochId);
|
|
5504
5818
|
expect(!walletConfigExists(walletDir), `Wallet ${walletName} already exists on ${channelContext.workspace.network}.`);
|
|
5505
5819
|
ensureDir(walletDir);
|
|
5506
5820
|
ensureDir(path.join(walletDir, "operations"));
|
|
@@ -5508,6 +5822,19 @@ function ensureWallet({
|
|
|
5508
5822
|
const wallet = normalizeWallet({
|
|
5509
5823
|
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
5510
5824
|
name: walletName,
|
|
5825
|
+
canonicalWalletName: walletName,
|
|
5826
|
+
walletEpochId: lifecycleEpoch.epochId,
|
|
5827
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
5828
|
+
joinedAtTxHash: lifecycleEpoch.joinedAtTxHash,
|
|
5829
|
+
joinedAtBlockNumber: lifecycleEpoch.joinedAtBlockNumber,
|
|
5830
|
+
joinedAtLogIndex: lifecycleEpoch.joinedAtLogIndex,
|
|
5831
|
+
joinedAtBlockTimestamp: lifecycleEpoch.joinedAtBlockTimestamp,
|
|
5832
|
+
joinedAtBlockTimestampIso: lifecycleEpoch.joinedAtBlockTimestampIso,
|
|
5833
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
5834
|
+
exitedAtBlockNumber: lifecycleEpoch.exitedAtBlockNumber,
|
|
5835
|
+
exitedAtLogIndex: lifecycleEpoch.exitedAtLogIndex,
|
|
5836
|
+
exitedAtBlockTimestamp: lifecycleEpoch.exitedAtBlockTimestamp,
|
|
5837
|
+
exitedAtBlockTimestampIso: lifecycleEpoch.exitedAtBlockTimestampIso,
|
|
5511
5838
|
network: channelContext.workspace.network,
|
|
5512
5839
|
rpcUrl,
|
|
5513
5840
|
chainId: channelContext.workspace.chainId,
|
|
@@ -5553,18 +5880,57 @@ function ensureWallet({
|
|
|
5553
5880
|
};
|
|
5554
5881
|
persistWalletKeys(context);
|
|
5555
5882
|
persistWallet(context);
|
|
5556
|
-
|
|
5883
|
+
persistWalletIndexForContext(context);
|
|
5557
5884
|
return context;
|
|
5558
5885
|
}
|
|
5559
5886
|
|
|
5887
|
+
function applyWalletLifecycleEpoch(wallet, epoch) {
|
|
5888
|
+
wallet.canonicalWalletName = wallet.name;
|
|
5889
|
+
wallet.walletEpochId = epoch.epochId;
|
|
5890
|
+
wallet.lifecycleStatus = epoch.lifecycleStatus;
|
|
5891
|
+
wallet.joinedAtTxHash = epoch.joinedAtTxHash;
|
|
5892
|
+
wallet.joinedAtBlockNumber = epoch.joinedAtBlockNumber;
|
|
5893
|
+
wallet.joinedAtLogIndex = epoch.joinedAtLogIndex;
|
|
5894
|
+
wallet.joinedAtBlockTimestamp = epoch.joinedAtBlockTimestamp;
|
|
5895
|
+
wallet.joinedAtBlockTimestampIso = epoch.joinedAtBlockTimestampIso;
|
|
5896
|
+
wallet.exitedAtTxHash = epoch.exitedAtTxHash;
|
|
5897
|
+
wallet.exitedAtBlockNumber = epoch.exitedAtBlockNumber;
|
|
5898
|
+
wallet.exitedAtLogIndex = epoch.exitedAtLogIndex;
|
|
5899
|
+
wallet.exitedAtBlockTimestamp = epoch.exitedAtBlockTimestamp;
|
|
5900
|
+
wallet.exitedAtBlockTimestampIso = epoch.exitedAtBlockTimestampIso;
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
function walletLifecycleMetadata(wallet) {
|
|
5904
|
+
expect(wallet.walletEpochId, "Current wallet workspace metadata is missing walletEpochId.");
|
|
5905
|
+
return {
|
|
5906
|
+
canonicalWalletName: wallet.canonicalWalletName ?? wallet.name,
|
|
5907
|
+
epochId: wallet.walletEpochId,
|
|
5908
|
+
lifecycleStatus: wallet.lifecycleStatus ?? "active",
|
|
5909
|
+
joinedAtTxHash: wallet.joinedAtTxHash ?? null,
|
|
5910
|
+
joinedAtBlockNumber: wallet.joinedAtBlockNumber ?? null,
|
|
5911
|
+
joinedAtLogIndex: wallet.joinedAtLogIndex ?? null,
|
|
5912
|
+
joinedAtBlockTimestamp: wallet.joinedAtBlockTimestamp ?? null,
|
|
5913
|
+
joinedAtBlockTimestampIso: wallet.joinedAtBlockTimestampIso ?? null,
|
|
5914
|
+
exitedAtTxHash: wallet.exitedAtTxHash ?? null,
|
|
5915
|
+
exitedAtBlockNumber: wallet.exitedAtBlockNumber ?? null,
|
|
5916
|
+
exitedAtLogIndex: wallet.exitedAtLogIndex ?? null,
|
|
5917
|
+
exitedAtBlockTimestamp: wallet.exitedAtBlockTimestamp ?? null,
|
|
5918
|
+
exitedAtBlockTimestampIso: wallet.exitedAtBlockTimestampIso ?? null,
|
|
5919
|
+
};
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5560
5922
|
function normalizeWallet(wallet) {
|
|
5561
5923
|
assertWalletHasCurrentFormat(wallet, wallet.name ?? "unknown");
|
|
5924
|
+
expect(wallet.walletEpochId, "Current wallet metadata requires walletEpochId. Run wallet recover-workspace to rebuild this wallet.");
|
|
5562
5925
|
const unusedNotes = Object.values(wallet.notes.unused).map(normalizeTrackedNote);
|
|
5563
5926
|
unusedNotes.sort(compareNotesByValueDesc);
|
|
5564
5927
|
const spentNotes = Object.values(wallet.notes.spent).map(normalizeTrackedNote);
|
|
5565
5928
|
|
|
5566
5929
|
return {
|
|
5567
5930
|
...wallet,
|
|
5931
|
+
canonicalWalletName: wallet.canonicalWalletName ?? wallet.name,
|
|
5932
|
+
walletEpochId: wallet.walletEpochId,
|
|
5933
|
+
lifecycleStatus: wallet.lifecycleStatus ?? "active",
|
|
5568
5934
|
canonicalAssetDecimals: Number(wallet.canonicalAssetDecimals),
|
|
5569
5935
|
l2Nonce: Number(wallet.l2Nonce),
|
|
5570
5936
|
l2PrivateKey: wallet.l2PrivateKey ? ethers.hexlify(wallet.l2PrivateKey) : null,
|
|
@@ -5824,10 +6190,16 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
5824
6190
|
};
|
|
5825
6191
|
|
|
5826
6192
|
if (scanStartBlock > latestBlock) {
|
|
6193
|
+
const reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6194
|
+
walletContext,
|
|
6195
|
+
currentSnapshot: context.currentSnapshot,
|
|
6196
|
+
controllerAddress: context.workspace.controller,
|
|
6197
|
+
});
|
|
5827
6198
|
walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
|
|
5828
6199
|
persistWallet(walletContext);
|
|
5829
6200
|
return {
|
|
5830
6201
|
importedNotes: [],
|
|
6202
|
+
reconciledState,
|
|
5831
6203
|
scannedLogs: 0,
|
|
5832
6204
|
scanRange,
|
|
5833
6205
|
};
|
|
@@ -5838,18 +6210,67 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
5838
6210
|
);
|
|
5839
6211
|
const commitmentExistsSlot = ethers.toBigInt(findStorageSlot(storageLayoutManifest, "PrivateStateController", "commitmentExists"));
|
|
5840
6212
|
const nullifierUsedSlot = ethers.toBigInt(findStorageSlot(storageLayoutManifest, "PrivateStateController", "nullifierUsed"));
|
|
5841
|
-
const
|
|
6213
|
+
const importedNotes = [];
|
|
6214
|
+
let reconciledState = null;
|
|
6215
|
+
let scannedLogs = 0;
|
|
6216
|
+
|
|
6217
|
+
await fetchLogsChunked(provider, {
|
|
5842
6218
|
address: context.workspace.channelManager,
|
|
5843
6219
|
topics: [NOTE_VALUE_ENCRYPTED_TOPIC],
|
|
5844
6220
|
fromBlock: scanStartBlock,
|
|
5845
6221
|
toBlock: latestBlock,
|
|
6222
|
+
collectLogs: false,
|
|
5846
6223
|
onProgress: progressAction
|
|
5847
6224
|
? createRpcLogScanProgress({ action: progressAction, label: "note-delivery events" })
|
|
5848
6225
|
: null,
|
|
6226
|
+
onChunk: async ({ logs, chunkToBlock }) => {
|
|
6227
|
+
scannedLogs += logs.length;
|
|
6228
|
+
const importedCandidates = await recoverDeliveredNoteCandidatesFromLogs({
|
|
6229
|
+
logs,
|
|
6230
|
+
walletContext,
|
|
6231
|
+
context,
|
|
6232
|
+
noteReceivePrivateKey,
|
|
6233
|
+
commitmentExistsSlot,
|
|
6234
|
+
nullifierUsedSlot,
|
|
6235
|
+
});
|
|
6236
|
+
if (importedCandidates.length > 0) {
|
|
6237
|
+
importedNotes.push(...mergeTrackedNotesIntoWallet(walletContext, importedCandidates));
|
|
6238
|
+
reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6239
|
+
walletContext,
|
|
6240
|
+
currentSnapshot: context.currentSnapshot,
|
|
6241
|
+
controllerAddress: context.workspace.controller,
|
|
6242
|
+
});
|
|
6243
|
+
}
|
|
6244
|
+
walletContext.wallet.noteReceiveLastScannedBlock = Number(chunkToBlock) + 1;
|
|
6245
|
+
persistWallet(walletContext);
|
|
6246
|
+
},
|
|
5849
6247
|
});
|
|
5850
6248
|
|
|
6249
|
+
reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6250
|
+
walletContext,
|
|
6251
|
+
currentSnapshot: context.currentSnapshot,
|
|
6252
|
+
controllerAddress: context.workspace.controller,
|
|
6253
|
+
});
|
|
6254
|
+
walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
|
|
6255
|
+
persistWallet(walletContext);
|
|
6256
|
+
return {
|
|
6257
|
+
importedNotes,
|
|
6258
|
+
reconciledState,
|
|
6259
|
+
scannedLogs,
|
|
6260
|
+
scanRange,
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
async function recoverDeliveredNoteCandidatesFromLogs({
|
|
6265
|
+
logs,
|
|
6266
|
+
walletContext,
|
|
6267
|
+
context,
|
|
6268
|
+
noteReceivePrivateKey,
|
|
6269
|
+
commitmentExistsSlot,
|
|
6270
|
+
nullifierUsedSlot,
|
|
6271
|
+
}) {
|
|
5851
6272
|
const importedCandidates = [];
|
|
5852
|
-
for (const log of
|
|
6273
|
+
for (const log of logs) {
|
|
5853
6274
|
const encryptedNoteValue = extractEncryptedNoteValueFromBridgeLog(log);
|
|
5854
6275
|
if (!encryptedNoteValue) {
|
|
5855
6276
|
continue;
|
|
@@ -5914,21 +6335,7 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
5914
6335
|
}
|
|
5915
6336
|
importedCandidates.push(trackedNote);
|
|
5916
6337
|
}
|
|
5917
|
-
|
|
5918
|
-
const importedNotes = mergeTrackedNotesIntoWallet(walletContext, importedCandidates);
|
|
5919
|
-
const reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
5920
|
-
walletContext,
|
|
5921
|
-
currentSnapshot: context.currentSnapshot,
|
|
5922
|
-
controllerAddress: context.workspace.controller,
|
|
5923
|
-
});
|
|
5924
|
-
walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
|
|
5925
|
-
persistWallet(walletContext);
|
|
5926
|
-
return {
|
|
5927
|
-
importedNotes,
|
|
5928
|
-
reconciledState,
|
|
5929
|
-
scannedLogs: observedLogs.length,
|
|
5930
|
-
scanRange,
|
|
5931
|
-
};
|
|
6338
|
+
return importedCandidates;
|
|
5932
6339
|
}
|
|
5933
6340
|
|
|
5934
6341
|
function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, latestBlock }) {
|
|
@@ -5942,7 +6349,7 @@ function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, l
|
|
|
5942
6349
|
throw new Error([
|
|
5943
6350
|
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
5944
6351
|
`Expected noteReceiveLastScannedBlock to be an integer between ${genesisBlockNumber} and ${Number(latestBlock) + 1}.`,
|
|
5945
|
-
"Run wallet recover-workspace --from-genesis to
|
|
6352
|
+
"Run wallet recover-workspace --from-genesis to restart received-note scanning from channel genesis.",
|
|
5946
6353
|
].join(" "));
|
|
5947
6354
|
}
|
|
5948
6355
|
return nextBlock;
|
|
@@ -5988,7 +6395,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
5988
6395
|
throw new Error([
|
|
5989
6396
|
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
5990
6397
|
"Automatic wallet recovery uses only the saved note recovery index and will not replay from genesis.",
|
|
5991
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if
|
|
6398
|
+
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
5992
6399
|
`Details: ${indexError.message}`,
|
|
5993
6400
|
].join(" "));
|
|
5994
6401
|
}
|
|
@@ -6026,7 +6433,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6026
6433
|
throw new Error([
|
|
6027
6434
|
`Wallet workspace is not current for wallet ${walletContext.walletName}.`,
|
|
6028
6435
|
"Automatic wallet recovery uses only the saved note recovery index and will not replay from genesis.",
|
|
6029
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if
|
|
6436
|
+
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
6030
6437
|
`Details: ${recoveryError.message}`,
|
|
6031
6438
|
].join(" "));
|
|
6032
6439
|
}
|
|
@@ -6042,7 +6449,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6042
6449
|
throw new Error([
|
|
6043
6450
|
`Wallet workspace is still stale after recovery-index sync for wallet ${walletContext.walletName}.`,
|
|
6044
6451
|
"Automatic wallet recovery will not replay from genesis.",
|
|
6045
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if
|
|
6452
|
+
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
6046
6453
|
`Details: ${postRecoveryError.message}`,
|
|
6047
6454
|
].join(" "));
|
|
6048
6455
|
}
|
|
@@ -6662,7 +7069,7 @@ async function recoverChannelWorkspaceFromIndexOnly({
|
|
|
6662
7069
|
throw new Error([
|
|
6663
7070
|
`Channel workspace is not current for ${channelName} on ${networkName}.`,
|
|
6664
7071
|
"Automatic channel workspace recovery uses only the saved recovery index and will not replay from genesis.",
|
|
6665
|
-
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
7072
|
+
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName} first.`,
|
|
6666
7073
|
`Details: ${recoveryError.message}`,
|
|
6667
7074
|
cause ? `Initial freshness failure: ${cause.message}` : null,
|
|
6668
7075
|
].filter(Boolean).join(" "));
|
|
@@ -6675,7 +7082,7 @@ async function recoverChannelWorkspaceFromIndexOnly({
|
|
|
6675
7082
|
throw new Error([
|
|
6676
7083
|
`Channel workspace is still stale after recovery-index sync for ${channelName} on ${networkName}.`,
|
|
6677
7084
|
"Automatic channel workspace recovery will not replay from genesis.",
|
|
6678
|
-
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
7085
|
+
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName} first.`,
|
|
6679
7086
|
`Details: ${postRecoveryError.message}`,
|
|
6680
7087
|
cause ? `Initial freshness failure: ${cause.message}` : null,
|
|
6681
7088
|
].filter(Boolean).join(" "));
|
|
@@ -6699,7 +7106,7 @@ async function requireChannelWorkspaceRecoveryIndexForAutoRefresh({
|
|
|
6699
7106
|
throw new Error([
|
|
6700
7107
|
message,
|
|
6701
7108
|
"Automatic channel workspace recovery uses only the saved recovery index and will not replay from genesis.",
|
|
6702
|
-
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
7109
|
+
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName} first.`,
|
|
6703
7110
|
cause ? `Initial freshness failure: ${cause.message}` : null,
|
|
6704
7111
|
].filter(Boolean).join(" "));
|
|
6705
7112
|
};
|
|
@@ -7062,7 +7469,17 @@ async function loadJoinChannelContext({ args, network, provider }) {
|
|
|
7062
7469
|
function loadWallet(walletName, networkName) {
|
|
7063
7470
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7064
7471
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
7065
|
-
const walletDir =
|
|
7472
|
+
const walletDir = selectedWalletEpochDir(normalizedWalletName, normalizedNetworkName);
|
|
7473
|
+
return loadWalletFromDir({
|
|
7474
|
+
walletName: normalizedWalletName,
|
|
7475
|
+
networkName: normalizedNetworkName,
|
|
7476
|
+
walletDir,
|
|
7477
|
+
});
|
|
7478
|
+
}
|
|
7479
|
+
|
|
7480
|
+
function loadWalletFromDir({ walletName, networkName, walletDir }) {
|
|
7481
|
+
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7482
|
+
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
7066
7483
|
if (!walletConfigExists(walletDir)) {
|
|
7067
7484
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
7068
7485
|
}
|
|
@@ -7077,11 +7494,11 @@ function loadWallet(walletName, networkName) {
|
|
|
7077
7494
|
walletName: normalizedWalletName,
|
|
7078
7495
|
keyKind: "viewing",
|
|
7079
7496
|
});
|
|
7080
|
-
if (spendingKey) {
|
|
7497
|
+
if (spendingKey && walletSpendingKeyMatchesWallet(spendingKey.metadata, rawWallet)) {
|
|
7081
7498
|
rawWallet.l2PrivateKey = spendingKey.privateKey;
|
|
7082
7499
|
rawWallet.l2PublicKey = spendingKey.metadata?.l2PublicKey ?? rawWallet.l2PublicKey;
|
|
7083
7500
|
}
|
|
7084
|
-
if (viewingKey) {
|
|
7501
|
+
if (viewingKey && walletViewingKeyMatchesWallet(viewingKey.metadata, rawWallet)) {
|
|
7085
7502
|
rawWallet.noteReceivePrivateKey = viewingKey.privateKey;
|
|
7086
7503
|
}
|
|
7087
7504
|
assertWalletHasRequiredKeys(rawWallet, normalizedWalletName);
|
|
@@ -7104,6 +7521,18 @@ function loadWallet(walletName, networkName) {
|
|
|
7104
7521
|
return context;
|
|
7105
7522
|
}
|
|
7106
7523
|
|
|
7524
|
+
function walletSpendingKeyMatchesWallet(metadata, wallet) {
|
|
7525
|
+
return metadata?.l2Address
|
|
7526
|
+
&& ethers.toBigInt(getAddress(metadata.l2Address)) === ethers.toBigInt(getAddress(wallet.l2Address));
|
|
7527
|
+
}
|
|
7528
|
+
|
|
7529
|
+
function walletViewingKeyMatchesWallet(metadata, wallet) {
|
|
7530
|
+
return metadata?.noteReceivePubKey?.x
|
|
7531
|
+
&& ethers.toBigInt(normalizeBytes32Hex(metadata.noteReceivePubKey.x))
|
|
7532
|
+
=== ethers.toBigInt(normalizeBytes32Hex(wallet.noteReceivePubKeyX))
|
|
7533
|
+
&& Number(metadata.noteReceivePubKey.yParity) === Number(wallet.noteReceivePubKeyYParity);
|
|
7534
|
+
}
|
|
7535
|
+
|
|
7107
7536
|
function loadUnlockedWalletWithMetadata(args) {
|
|
7108
7537
|
const networkName = requireNetworkName(args);
|
|
7109
7538
|
const wallet = loadWallet(requireWalletName(args), networkName);
|
|
@@ -7274,6 +7703,16 @@ function requireWalletViewingCapability(walletContext) {
|
|
|
7274
7703
|
);
|
|
7275
7704
|
}
|
|
7276
7705
|
|
|
7706
|
+
function requireActiveWalletLifecycle(walletContext, commandName) {
|
|
7707
|
+
expect(
|
|
7708
|
+
walletContext.wallet.lifecycleStatus !== "exited",
|
|
7709
|
+
[
|
|
7710
|
+
`${commandName} cannot operate on exited wallet epoch ${walletContext.wallet.walletEpochId ?? "unknown"}.`,
|
|
7711
|
+
"Exited wallet epochs are read-only. Use wallet get-notes or wallet get-notes --export-evidence for historical disclosure.",
|
|
7712
|
+
].join(" "),
|
|
7713
|
+
);
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7277
7716
|
function walletOperationSealSecret(walletContext) {
|
|
7278
7717
|
const secret = walletContext.wallet.l2PrivateKey
|
|
7279
7718
|
?? walletContext.wallet.noteReceivePrivateKey
|
|
@@ -7328,7 +7767,7 @@ function loadBridgeResources({ chainId }) {
|
|
|
7328
7767
|
function loadWalletMetadata(walletName, networkName) {
|
|
7329
7768
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7330
7769
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
7331
|
-
const walletDir =
|
|
7770
|
+
const walletDir = selectedWalletEpochDir(normalizedWalletName, normalizedNetworkName);
|
|
7332
7771
|
if (!walletConfigExists(walletDir)) {
|
|
7333
7772
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
7334
7773
|
}
|
|
@@ -7677,9 +8116,9 @@ async function buildGrothTransition({ operationDir, workspace, stateManager, vau
|
|
|
7677
8116
|
update: {
|
|
7678
8117
|
currentRootVector: normalizedRootVector(currentSnapshot.stateRoots),
|
|
7679
8118
|
updatedRoot: bigintToHex32(updatedRoot),
|
|
7680
|
-
currentUserKey:
|
|
8119
|
+
currentUserKey: normalizeBytes32Hex(keyHex),
|
|
7681
8120
|
currentUserValue: currentValue,
|
|
7682
|
-
updatedUserKey:
|
|
8121
|
+
updatedUserKey: normalizeBytes32Hex(keyHex),
|
|
7683
8122
|
updatedUserValue: nextValue,
|
|
7684
8123
|
},
|
|
7685
8124
|
nextSnapshot,
|
|
@@ -7870,10 +8309,6 @@ function normalizeBytes12Hex(value) {
|
|
|
7870
8309
|
return normalizeBytesHex(value, 12);
|
|
7871
8310
|
}
|
|
7872
8311
|
|
|
7873
|
-
function normalizeBytes16Hex(value) {
|
|
7874
|
-
return normalizeBytesHex(value, 16);
|
|
7875
|
-
}
|
|
7876
|
-
|
|
7877
8312
|
function hashTokamakPublicInputs(values) {
|
|
7878
8313
|
return keccak256(abiCoder.encode(["uint256[]"], [values]));
|
|
7879
8314
|
}
|
|
@@ -8001,6 +8436,67 @@ async function fetchChannelRecoveryLogs({
|
|
|
8001
8436
|
};
|
|
8002
8437
|
}
|
|
8003
8438
|
|
|
8439
|
+
async function fetchChannelRecoveryEventGroupsChunked({
|
|
8440
|
+
provider,
|
|
8441
|
+
bridgeAbiManifest,
|
|
8442
|
+
channelInfo,
|
|
8443
|
+
channelManager = null,
|
|
8444
|
+
fromBlock,
|
|
8445
|
+
toBlock,
|
|
8446
|
+
progressAction = null,
|
|
8447
|
+
onChunk,
|
|
8448
|
+
}) {
|
|
8449
|
+
const resolvedChannelManager = channelManager ?? new Contract(
|
|
8450
|
+
channelInfo.manager,
|
|
8451
|
+
bridgeAbiManifest.contracts.channelManager.abi,
|
|
8452
|
+
provider,
|
|
8453
|
+
);
|
|
8454
|
+
const currentRootVectorObservedTopic =
|
|
8455
|
+
normalizeBytes32Hex(resolvedChannelManager.interface.getEvent("CurrentRootVectorObserved").topicHash);
|
|
8456
|
+
const bridgeTokenVault = new Contract(
|
|
8457
|
+
channelInfo.bridgeTokenVault,
|
|
8458
|
+
bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
8459
|
+
provider,
|
|
8460
|
+
);
|
|
8461
|
+
const bridgeVaultTopic = bridgeTokenVault.interface.getEvent("StorageWriteObserved").topicHash;
|
|
8462
|
+
|
|
8463
|
+
await fetchLogsChunked(provider, {
|
|
8464
|
+
address: channelInfo.manager,
|
|
8465
|
+
topics: [[
|
|
8466
|
+
currentRootVectorObservedTopic,
|
|
8467
|
+
CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC,
|
|
8468
|
+
VAULT_STORAGE_WRITE_OBSERVED_TOPIC,
|
|
8469
|
+
]],
|
|
8470
|
+
fromBlock,
|
|
8471
|
+
toBlock,
|
|
8472
|
+
collectLogs: false,
|
|
8473
|
+
onProgress: progressAction
|
|
8474
|
+
? createRpcLogScanProgress({ action: progressAction, label: "channel-recovery chunks" })
|
|
8475
|
+
: null,
|
|
8476
|
+
onChunk: async ({ logs: channelManagerLogs, chunkFromBlock, chunkToBlock }) => {
|
|
8477
|
+
await throttleLogRequest();
|
|
8478
|
+
const bridgeVaultLogs = await provider.getLogs({
|
|
8479
|
+
address: channelInfo.bridgeTokenVault,
|
|
8480
|
+
topics: [bridgeVaultTopic],
|
|
8481
|
+
fromBlock: chunkFromBlock,
|
|
8482
|
+
toBlock: chunkToBlock,
|
|
8483
|
+
});
|
|
8484
|
+
const groupedValues = normalizeWorkspaceMirrorDeltaEventGroups({
|
|
8485
|
+
logs: [...channelManagerLogs, ...bridgeVaultLogs],
|
|
8486
|
+
channelInfo,
|
|
8487
|
+
bridgeAbiManifest,
|
|
8488
|
+
fromBlock: chunkFromBlock,
|
|
8489
|
+
toBlock: chunkToBlock,
|
|
8490
|
+
});
|
|
8491
|
+
await onChunk?.({
|
|
8492
|
+
groupedValues,
|
|
8493
|
+
chunkFromBlock,
|
|
8494
|
+
chunkToBlock,
|
|
8495
|
+
});
|
|
8496
|
+
},
|
|
8497
|
+
});
|
|
8498
|
+
}
|
|
8499
|
+
|
|
8004
8500
|
async function reconstructChannelSnapshot({
|
|
8005
8501
|
provider,
|
|
8006
8502
|
bridgeAbiManifest,
|
|
@@ -8018,6 +8514,7 @@ async function reconstructChannelSnapshot({
|
|
|
8018
8514
|
fromBlock = genesisBlockNumber,
|
|
8019
8515
|
toBlock = null,
|
|
8020
8516
|
progressAction = null,
|
|
8517
|
+
onCheckpoint = null,
|
|
8021
8518
|
}) {
|
|
8022
8519
|
let startingSnapshot = baseSnapshot;
|
|
8023
8520
|
if (!startingSnapshot) {
|
|
@@ -8033,11 +8530,20 @@ async function reconstructChannelSnapshot({
|
|
|
8033
8530
|
|
|
8034
8531
|
const latestBlock = toBlock === null ? await provider.getBlockNumber() : Number(toBlock);
|
|
8035
8532
|
const scanFromBlock = Math.max(Number(genesisBlockNumber), Number(fromBlock));
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8533
|
+
let currentSnapshot = startingSnapshot;
|
|
8534
|
+
if (onCheckpoint && scanFromBlock <= latestBlock) {
|
|
8535
|
+
await onCheckpoint({
|
|
8536
|
+
currentSnapshot,
|
|
8537
|
+
scanRange: {
|
|
8538
|
+
fromBlock: scanFromBlock,
|
|
8539
|
+
toBlock: scanFromBlock - 1,
|
|
8540
|
+
mode: baseSnapshot ? "recovery-index-initial" : "genesis-initial",
|
|
8541
|
+
},
|
|
8542
|
+
});
|
|
8543
|
+
}
|
|
8544
|
+
const stateManager = await buildStateManager(currentSnapshot, contractCodes);
|
|
8545
|
+
|
|
8546
|
+
await fetchChannelRecoveryEventGroupsChunked({
|
|
8041
8547
|
provider,
|
|
8042
8548
|
bridgeAbiManifest,
|
|
8043
8549
|
channelInfo,
|
|
@@ -8045,37 +8551,27 @@ async function reconstructChannelSnapshot({
|
|
|
8045
8551
|
fromBlock: scanFromBlock,
|
|
8046
8552
|
toBlock: latestBlock,
|
|
8047
8553
|
progressAction,
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
};
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
const groupedValues = [...groupedEvents.values()].sort((left, right) => compareLogsByPosition(left[0], right[0]));
|
|
8071
|
-
const currentSnapshot = await applyChannelRecoveryEventGroups({
|
|
8072
|
-
startingSnapshot,
|
|
8073
|
-
groupedValues,
|
|
8074
|
-
contractCodes,
|
|
8075
|
-
channelInfo,
|
|
8076
|
-
controllerAddress,
|
|
8077
|
-
l2AccountingVaultAddress,
|
|
8078
|
-
liquidBalancesSlot,
|
|
8554
|
+
onChunk: async ({ groupedValues, chunkFromBlock, chunkToBlock }) => {
|
|
8555
|
+
currentSnapshot = await applyChannelRecoveryEventGroupsToStateManager({
|
|
8556
|
+
stateManager,
|
|
8557
|
+
fallbackSnapshot: currentSnapshot,
|
|
8558
|
+
groupedValues,
|
|
8559
|
+
channelInfo,
|
|
8560
|
+
controllerAddress,
|
|
8561
|
+
l2AccountingVaultAddress,
|
|
8562
|
+
liquidBalancesSlot,
|
|
8563
|
+
});
|
|
8564
|
+
await onCheckpoint?.({
|
|
8565
|
+
currentSnapshot,
|
|
8566
|
+
scanRange: {
|
|
8567
|
+
fromBlock: scanFromBlock,
|
|
8568
|
+
toBlock: chunkToBlock,
|
|
8569
|
+
chunkFromBlock,
|
|
8570
|
+
chunkToBlock,
|
|
8571
|
+
mode: baseSnapshot ? "recovery-index" : "genesis",
|
|
8572
|
+
},
|
|
8573
|
+
});
|
|
8574
|
+
},
|
|
8079
8575
|
});
|
|
8080
8576
|
|
|
8081
8577
|
expect(
|
|
@@ -8103,9 +8599,28 @@ async function applyChannelRecoveryEventGroups({
|
|
|
8103
8599
|
l2AccountingVaultAddress,
|
|
8104
8600
|
liquidBalancesSlot,
|
|
8105
8601
|
}) {
|
|
8106
|
-
|
|
8107
|
-
|
|
8602
|
+
const stateManager = await buildStateManager(startingSnapshot, contractCodes);
|
|
8603
|
+
return applyChannelRecoveryEventGroupsToStateManager({
|
|
8604
|
+
stateManager,
|
|
8605
|
+
fallbackSnapshot: startingSnapshot,
|
|
8606
|
+
groupedValues,
|
|
8607
|
+
channelInfo,
|
|
8608
|
+
controllerAddress,
|
|
8609
|
+
l2AccountingVaultAddress,
|
|
8610
|
+
liquidBalancesSlot,
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8108
8613
|
|
|
8614
|
+
async function applyChannelRecoveryEventGroupsToStateManager({
|
|
8615
|
+
stateManager,
|
|
8616
|
+
fallbackSnapshot,
|
|
8617
|
+
groupedValues,
|
|
8618
|
+
channelInfo,
|
|
8619
|
+
controllerAddress,
|
|
8620
|
+
l2AccountingVaultAddress,
|
|
8621
|
+
liquidBalancesSlot,
|
|
8622
|
+
}) {
|
|
8623
|
+
let currentSnapshot = fallbackSnapshot;
|
|
8109
8624
|
for (const group of groupedValues) {
|
|
8110
8625
|
const orderedGroup = [...group].sort(compareLogsByPosition);
|
|
8111
8626
|
const rootEvent = orderedGroup.find(
|
|
@@ -8352,11 +8867,14 @@ async function fetchLogsChunked(provider, {
|
|
|
8352
8867
|
fromBlock,
|
|
8353
8868
|
toBlock,
|
|
8354
8869
|
initialChunkSize = DEFAULT_LOG_CHUNK_SIZE,
|
|
8870
|
+
collectLogs = true,
|
|
8355
8871
|
onProgress = null,
|
|
8872
|
+
onChunk = null,
|
|
8356
8873
|
}) {
|
|
8357
8874
|
const normalizedFromBlock = Number(fromBlock);
|
|
8358
8875
|
const resolvedToBlock = toBlock === "latest" ? await provider.getBlockNumber() : Number(toBlock);
|
|
8359
8876
|
const aggregatedLogs = [];
|
|
8877
|
+
let logsFound = 0;
|
|
8360
8878
|
|
|
8361
8879
|
if (normalizedFromBlock > resolvedToBlock) {
|
|
8362
8880
|
onProgress?.({
|
|
@@ -8384,27 +8902,15 @@ async function fetchLogsChunked(provider, {
|
|
|
8384
8902
|
let cursor = normalizedFromBlock;
|
|
8385
8903
|
while (cursor <= resolvedToBlock) {
|
|
8386
8904
|
const chunkToBlock = Math.min(resolvedToBlock, cursor + chunkSize - 1);
|
|
8905
|
+
let logs;
|
|
8387
8906
|
try {
|
|
8388
8907
|
await throttleLogRequest();
|
|
8389
|
-
|
|
8908
|
+
logs = await provider.getLogs({
|
|
8390
8909
|
address,
|
|
8391
8910
|
topics,
|
|
8392
8911
|
fromBlock: cursor,
|
|
8393
8912
|
toBlock: chunkToBlock,
|
|
8394
8913
|
});
|
|
8395
|
-
aggregatedLogs.push(...logs);
|
|
8396
|
-
onProgress?.({
|
|
8397
|
-
status: "progress",
|
|
8398
|
-
fromBlock: normalizedFromBlock,
|
|
8399
|
-
toBlock: resolvedToBlock,
|
|
8400
|
-
chunkFromBlock: cursor,
|
|
8401
|
-
chunkToBlock,
|
|
8402
|
-
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
8403
|
-
totalBlocks,
|
|
8404
|
-
logsFound: aggregatedLogs.length,
|
|
8405
|
-
chunkLogs: logs.length,
|
|
8406
|
-
});
|
|
8407
|
-
cursor = chunkToBlock + 1;
|
|
8408
8914
|
} catch (error) {
|
|
8409
8915
|
if (isRateLimitError(error)) {
|
|
8410
8916
|
throw new Error(
|
|
@@ -8417,7 +8923,36 @@ async function fetchLogsChunked(provider, {
|
|
|
8417
8923
|
throw error;
|
|
8418
8924
|
}
|
|
8419
8925
|
chunkSize = suggestedChunkSize;
|
|
8926
|
+
continue;
|
|
8927
|
+
}
|
|
8928
|
+
logsFound += logs.length;
|
|
8929
|
+
if (collectLogs) {
|
|
8930
|
+
aggregatedLogs.push(...logs);
|
|
8420
8931
|
}
|
|
8932
|
+
onProgress?.({
|
|
8933
|
+
status: "progress",
|
|
8934
|
+
fromBlock: normalizedFromBlock,
|
|
8935
|
+
toBlock: resolvedToBlock,
|
|
8936
|
+
chunkFromBlock: cursor,
|
|
8937
|
+
chunkToBlock,
|
|
8938
|
+
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
8939
|
+
totalBlocks,
|
|
8940
|
+
logsFound,
|
|
8941
|
+
chunkLogs: logs.length,
|
|
8942
|
+
});
|
|
8943
|
+
await onChunk?.({
|
|
8944
|
+
status: "progress",
|
|
8945
|
+
fromBlock: normalizedFromBlock,
|
|
8946
|
+
toBlock: resolvedToBlock,
|
|
8947
|
+
chunkFromBlock: cursor,
|
|
8948
|
+
chunkToBlock,
|
|
8949
|
+
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
8950
|
+
totalBlocks,
|
|
8951
|
+
logsFound,
|
|
8952
|
+
chunkLogs: logs.length,
|
|
8953
|
+
logs,
|
|
8954
|
+
});
|
|
8955
|
+
cursor = chunkToBlock + 1;
|
|
8421
8956
|
}
|
|
8422
8957
|
|
|
8423
8958
|
onProgress?.({
|
|
@@ -8426,7 +8961,7 @@ async function fetchLogsChunked(provider, {
|
|
|
8426
8961
|
toBlock: resolvedToBlock,
|
|
8427
8962
|
scannedBlocks: totalBlocks,
|
|
8428
8963
|
totalBlocks,
|
|
8429
|
-
logsFound
|
|
8964
|
+
logsFound,
|
|
8430
8965
|
});
|
|
8431
8966
|
|
|
8432
8967
|
return aggregatedLogs;
|
|
@@ -8462,7 +8997,7 @@ function assertAutoRecoveryBlockBudget({
|
|
|
8462
8997
|
`Automatic recovery for ${label} would exceed the ${AUTO_RECOVERY_BLOCK_BUDGET}-block pre-command budget.`,
|
|
8463
8998
|
`Recovery delta is ${blockDelta} blocks from ${normalizedFromBlock} to ${normalizedToBlock}.`,
|
|
8464
8999
|
`Remaining automatic recovery budget is ${normalizedBudget} blocks.`,
|
|
8465
|
-
`Run ${recoveryCommand} first
|
|
9000
|
+
`Run ${recoveryCommand} first.`,
|
|
8466
9001
|
].join(" "));
|
|
8467
9002
|
}
|
|
8468
9003
|
|
|
@@ -8912,16 +9447,18 @@ function prepareJoinWalletSecretForName({
|
|
|
8912
9447
|
walletName,
|
|
8913
9448
|
}) {
|
|
8914
9449
|
const { channelName } = parseWalletName(walletName);
|
|
8915
|
-
const
|
|
9450
|
+
const walletRoot = walletRootPath(walletName, networkName);
|
|
9451
|
+
const walletIndex = fs.existsSync(walletRoot)
|
|
9452
|
+
? requireWalletIndex({ walletRoot, walletName, networkName })
|
|
9453
|
+
: null;
|
|
9454
|
+
const activeEpoch = walletIndex ? activeWalletEpoch(walletIndex) : null;
|
|
8916
9455
|
expect(
|
|
8917
|
-
!
|
|
9456
|
+
!activeEpoch,
|
|
8918
9457
|
[
|
|
8919
9458
|
`Wallet ${walletName} already exists on ${networkName}.`,
|
|
8920
|
-
"channel join
|
|
8921
|
-
"If this wallet was previously exited on-chain, run",
|
|
8922
|
-
`wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}`,
|
|
8923
|
-
"once to remove the stale local wallet, then retry channel join.",
|
|
9459
|
+
"channel join creates a new active wallet epoch.",
|
|
8924
9460
|
"Use normal wallet commands for an existing active local wallet.",
|
|
9461
|
+
`For exited history, keep using wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}.`,
|
|
8925
9462
|
].join(" "),
|
|
8926
9463
|
);
|
|
8927
9464
|
const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
|
|
@@ -8932,7 +9469,7 @@ function channelWorkspacePath(networkName, name) {
|
|
|
8932
9469
|
return workspaceDirForName(workspaceRoot, networkName, name);
|
|
8933
9470
|
}
|
|
8934
9471
|
|
|
8935
|
-
function
|
|
9472
|
+
function walletRootPath(name, networkName) {
|
|
8936
9473
|
const walletName = String(name);
|
|
8937
9474
|
const { channelName } = parseWalletName(walletName);
|
|
8938
9475
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
@@ -8940,6 +9477,91 @@ function walletPath(name, networkName) {
|
|
|
8940
9477
|
return walletDirForName(workspaceWalletsDir(workspaceDir), walletName);
|
|
8941
9478
|
}
|
|
8942
9479
|
|
|
9480
|
+
function selectedWalletEpochDir(name, networkName) {
|
|
9481
|
+
const root = walletRootPath(name, networkName);
|
|
9482
|
+
const walletName = requireWalletName({ wallet: name });
|
|
9483
|
+
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
9484
|
+
expect(
|
|
9485
|
+
fs.existsSync(root),
|
|
9486
|
+
cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${walletName} on ${normalizedNetworkName}.`),
|
|
9487
|
+
);
|
|
9488
|
+
const index = requireWalletIndex({ walletRoot: root, walletName, networkName: normalizedNetworkName });
|
|
9489
|
+
const selected = selectedWalletEpoch(index, walletName, normalizedNetworkName);
|
|
9490
|
+
return walletEpochPathFromRoot(root, selected.epochId);
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9493
|
+
function walletEpochPath(walletName, networkName, epochId) {
|
|
9494
|
+
return walletEpochPathFromRoot(walletRootPath(walletName, networkName), epochId);
|
|
9495
|
+
}
|
|
9496
|
+
|
|
9497
|
+
function walletEpochPathFromRoot(walletRoot, epochId) {
|
|
9498
|
+
return path.join(walletRoot, "epochs", slugifyPathComponent(epochId));
|
|
9499
|
+
}
|
|
9500
|
+
|
|
9501
|
+
function walletIndexMetadataPath(walletRoot) {
|
|
9502
|
+
return path.join(walletRoot, "wallet-index.metadata.json");
|
|
9503
|
+
}
|
|
9504
|
+
|
|
9505
|
+
function readWalletIndexIfExists(walletRoot) {
|
|
9506
|
+
const indexPath = walletIndexMetadataPath(walletRoot);
|
|
9507
|
+
if (!fs.existsSync(indexPath)) {
|
|
9508
|
+
return null;
|
|
9509
|
+
}
|
|
9510
|
+
return normalizeWalletIndex(readJson(indexPath));
|
|
9511
|
+
}
|
|
9512
|
+
|
|
9513
|
+
function requireWalletIndex({ walletRoot, walletName, networkName }) {
|
|
9514
|
+
const index = readWalletIndexIfExists(walletRoot);
|
|
9515
|
+
expect(index, currentWalletIndexRequiredMessage({ walletName, networkName, walletRoot }));
|
|
9516
|
+
return index;
|
|
9517
|
+
}
|
|
9518
|
+
|
|
9519
|
+
function selectedWalletEpoch(index, walletName, networkName) {
|
|
9520
|
+
const selected = activeWalletEpoch(index) ?? latestWalletEpoch(index);
|
|
9521
|
+
expect(
|
|
9522
|
+
selected,
|
|
9523
|
+
`Wallet ${walletName} on ${networkName} has no epoch entries. Run wallet recover-workspace to rebuild the workspace in the current format.`,
|
|
9524
|
+
);
|
|
9525
|
+
return selected;
|
|
9526
|
+
}
|
|
9527
|
+
|
|
9528
|
+
function currentWalletIndexRequiredMessage({ walletName, networkName, walletRoot }) {
|
|
9529
|
+
const channelName = parseWalletName(walletName).channelName;
|
|
9530
|
+
return [
|
|
9531
|
+
`Current wallet index is required for ${walletName} on ${networkName}: ${walletRoot}.`,
|
|
9532
|
+
`Run wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account <ACCOUNT> to rebuild the workspace.`,
|
|
9533
|
+
].join(" ");
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9536
|
+
function activeWalletEpoch(index) {
|
|
9537
|
+
const activeEpochId = index?.activeEpochId ?? null;
|
|
9538
|
+
return activeEpochId
|
|
9539
|
+
? (index.epochs ?? []).find((epoch) => epoch.epochId === activeEpochId && epoch.lifecycleStatus === "active") ?? null
|
|
9540
|
+
: null;
|
|
9541
|
+
}
|
|
9542
|
+
|
|
9543
|
+
function latestWalletEpoch(index) {
|
|
9544
|
+
const epochs = [...(index?.epochs ?? [])];
|
|
9545
|
+
epochs.sort((left, right) =>
|
|
9546
|
+
Number(right.joinedAtBlockNumber ?? 0) - Number(left.joinedAtBlockNumber ?? 0)
|
|
9547
|
+
|| String(right.epochId).localeCompare(String(left.epochId)));
|
|
9548
|
+
return epochs[0] ?? null;
|
|
9549
|
+
}
|
|
9550
|
+
|
|
9551
|
+
function normalizeWalletIndex(index) {
|
|
9552
|
+
expect(index?.format === WALLET_INDEX_FORMAT, "Invalid wallet index format.");
|
|
9553
|
+
expect(Number(index.formatVersion) === WALLET_INDEX_FORMAT_VERSION, "Unsupported wallet index format version.");
|
|
9554
|
+
expect(Array.isArray(index.epochs), "Wallet index is missing epochs[].");
|
|
9555
|
+
return {
|
|
9556
|
+
...index,
|
|
9557
|
+
epochs: index.epochs.map((epoch) => ({
|
|
9558
|
+
...epoch,
|
|
9559
|
+
epochId: String(epoch.epochId),
|
|
9560
|
+
lifecycleStatus: epoch.lifecycleStatus === "active" ? "active" : "exited",
|
|
9561
|
+
})),
|
|
9562
|
+
};
|
|
9563
|
+
}
|
|
9564
|
+
|
|
8943
9565
|
function accountPrivateKeyPath(networkName, accountName) {
|
|
8944
9566
|
return path.join(
|
|
8945
9567
|
secretRoot,
|
|
@@ -9013,7 +9635,8 @@ function resolveWalletPathCandidates(walletName) {
|
|
|
9013
9635
|
"wallets",
|
|
9014
9636
|
walletSlug,
|
|
9015
9637
|
);
|
|
9016
|
-
if (
|
|
9638
|
+
if (fs.existsSync(candidate)) {
|
|
9639
|
+
requireWalletIndex({ walletRoot: candidate, walletName, networkName: entry.name });
|
|
9017
9640
|
candidates.push(candidate);
|
|
9018
9641
|
}
|
|
9019
9642
|
}
|
|
@@ -9044,12 +9667,24 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
|
|
|
9044
9667
|
if (!walletEntry.isDirectory()) {
|
|
9045
9668
|
continue;
|
|
9046
9669
|
}
|
|
9047
|
-
const
|
|
9670
|
+
const walletRoot = path.join(walletsDir, walletEntry.name);
|
|
9671
|
+
const walletIndex = requireWalletIndex({
|
|
9672
|
+
walletRoot,
|
|
9673
|
+
walletName: walletEntry.name,
|
|
9674
|
+
networkName: networkEntry.name,
|
|
9675
|
+
});
|
|
9676
|
+
const selectedEpoch = selectedWalletEpoch(walletIndex, walletEntry.name, networkEntry.name);
|
|
9677
|
+
const walletDir = walletEpochPathFromRoot(walletRoot, selectedEpoch.epochId);
|
|
9048
9678
|
wallets.push({
|
|
9049
9679
|
wallet: walletEntry.name,
|
|
9050
9680
|
network: networkEntry.name,
|
|
9051
9681
|
channelName: channelEntry.name,
|
|
9052
9682
|
walletDir,
|
|
9683
|
+
walletRoot,
|
|
9684
|
+
activeEpochId: walletIndex?.activeEpochId ?? null,
|
|
9685
|
+
selectedEpochId: selectedEpoch?.epochId ?? null,
|
|
9686
|
+
lifecycleStatus: selectedEpoch.lifecycleStatus,
|
|
9687
|
+
epochs: walletIndex.epochs,
|
|
9053
9688
|
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9054
9689
|
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
9055
9690
|
hasEncryptedWallet: false,
|
|
@@ -9077,7 +9712,7 @@ function privateStateCliDataRoot() {
|
|
|
9077
9712
|
|
|
9078
9713
|
function resolveExportWalletInfo({ networkName, walletName }) {
|
|
9079
9714
|
resolveCliNetwork(networkName);
|
|
9080
|
-
const walletDir =
|
|
9715
|
+
const walletDir = selectedWalletEpochDir(walletName, networkName);
|
|
9081
9716
|
return {
|
|
9082
9717
|
wallet: walletName,
|
|
9083
9718
|
network: networkName,
|
|
@@ -9092,7 +9727,7 @@ function resolveExportWalletInfo({ networkName, walletName }) {
|
|
|
9092
9727
|
function normalizeExportWalletInfo(walletInfo) {
|
|
9093
9728
|
const wallet = requireWalletName({ wallet: walletInfo.wallet });
|
|
9094
9729
|
const network = requireNetworkName({ network: walletInfo.network });
|
|
9095
|
-
const walletDir = walletInfo.walletDir ??
|
|
9730
|
+
const walletDir = walletInfo.walletDir ?? selectedWalletEpochDir(wallet, network);
|
|
9096
9731
|
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
9097
9732
|
const metadata = readJsonIfExists(metadataPath);
|
|
9098
9733
|
const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
|
|
@@ -9119,6 +9754,13 @@ function walletBackupExportFilePaths(walletInfo) {
|
|
|
9119
9754
|
const walletFiles = [
|
|
9120
9755
|
walletNotesMetadataPath(walletInfo.walletDir),
|
|
9121
9756
|
];
|
|
9757
|
+
const walletRoot = walletRootPath(walletInfo.wallet, walletInfo.network);
|
|
9758
|
+
requireWalletIndex({
|
|
9759
|
+
walletRoot,
|
|
9760
|
+
walletName: walletInfo.wallet,
|
|
9761
|
+
networkName: walletInfo.network,
|
|
9762
|
+
});
|
|
9763
|
+
walletFiles.push(walletIndexMetadataPath(walletRoot));
|
|
9122
9764
|
for (const metadataPath of [
|
|
9123
9765
|
walletViewingKeyMetadataPath(walletInfo.walletDir),
|
|
9124
9766
|
walletSpendingKeyMetadataPath(walletInfo.walletDir),
|
|
@@ -9172,9 +9814,18 @@ function validateWalletExportManifest(manifest) {
|
|
|
9172
9814
|
validateWalletArchivePath(filePath);
|
|
9173
9815
|
}
|
|
9174
9816
|
for (const wallet of manifest.wallets) {
|
|
9175
|
-
requireNetworkName({ network: wallet.network });
|
|
9176
|
-
requireWalletName({ wallet: wallet.wallet });
|
|
9817
|
+
const networkName = requireNetworkName({ network: wallet.network });
|
|
9818
|
+
const walletName = requireWalletName({ wallet: wallet.wallet });
|
|
9177
9819
|
requireArg(wallet.channelName, "wallets[].channelName");
|
|
9820
|
+
const walletRoot = walletRootPath(walletName, networkName);
|
|
9821
|
+
const expectedIndexPath = archivePathForLocalCliFile(walletIndexMetadataPath(walletRoot));
|
|
9822
|
+
expect(
|
|
9823
|
+
uniqueFiles.has(expectedIndexPath),
|
|
9824
|
+
[
|
|
9825
|
+
"Wallet import ZIP must include the current wallet index metadata.",
|
|
9826
|
+
"Run wallet recover-workspace with the current CLI, then export a new backup.",
|
|
9827
|
+
].join(" "),
|
|
9828
|
+
);
|
|
9178
9829
|
}
|
|
9179
9830
|
}
|
|
9180
9831
|
|
|
@@ -9276,21 +9927,8 @@ function assertAllowedCommandKeys(args, commandName, allowedKeys, acceptedUsage)
|
|
|
9276
9927
|
);
|
|
9277
9928
|
}
|
|
9278
9929
|
|
|
9279
|
-
function assertWalletSecretArgs(args, commandName, extraOptionKeys = [], acceptedUsage = "--wallet and --network") {
|
|
9280
|
-
if (COMMAND_ARG_SCHEMAS[commandName]) {
|
|
9281
|
-
assertAllowedCommandSchema(args, commandName);
|
|
9282
|
-
return;
|
|
9283
|
-
}
|
|
9284
|
-
assertAllowedCommandKeys(
|
|
9285
|
-
args,
|
|
9286
|
-
commandName,
|
|
9287
|
-
new Set(["command", "positional", "wallet", "network", ...extraOptionKeys]),
|
|
9288
|
-
acceptedUsage,
|
|
9289
|
-
);
|
|
9290
|
-
}
|
|
9291
|
-
|
|
9292
9930
|
function assertWalletChannelMoveArgs(args, commandName) {
|
|
9293
|
-
|
|
9931
|
+
assertAllowedCommandSchema(args, commandName);
|
|
9294
9932
|
assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
|
|
9295
9933
|
}
|
|
9296
9934
|
|
|
@@ -9404,7 +10042,7 @@ function assertActionImpactArg(args, commandName) {
|
|
|
9404
10042
|
}
|
|
9405
10043
|
|
|
9406
10044
|
function assertWalletGetNotesArgs(args) {
|
|
9407
|
-
|
|
10045
|
+
assertAllowedCommandSchema(args, "wallet-get-notes");
|
|
9408
10046
|
if (args.exportEvidence !== undefined) {
|
|
9409
10047
|
requireArg(args.exportEvidence, "--export-evidence");
|
|
9410
10048
|
if (args.acknowledgeFullNotePlaintextExport !== true) {
|
|
@@ -9459,12 +10097,8 @@ function assertAccountGetBridgeFundArgs(args) {
|
|
|
9459
10097
|
assertAllowedCommandSchema(args, "account-get-bridge-fund");
|
|
9460
10098
|
}
|
|
9461
10099
|
|
|
9462
|
-
function assertExplicitSignerCommandArgs(args, commandName) {
|
|
9463
|
-
assertAllowedCommandSchema(args, commandName);
|
|
9464
|
-
}
|
|
9465
|
-
|
|
9466
10100
|
function assertRecoverWalletArgs(args) {
|
|
9467
|
-
|
|
10101
|
+
assertAllowedCommandSchema(args, "wallet-recover-workspace");
|
|
9468
10102
|
if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
|
|
9469
10103
|
throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
|
|
9470
10104
|
}
|
|
@@ -9476,7 +10110,7 @@ function assertJoinChannelArgs(args) {
|
|
|
9476
10110
|
}
|
|
9477
10111
|
|
|
9478
10112
|
function assertWalletGetMetaArgs(args) {
|
|
9479
|
-
|
|
10113
|
+
assertAllowedCommandSchema(args, "wallet-get-meta");
|
|
9480
10114
|
}
|
|
9481
10115
|
|
|
9482
10116
|
function assertAccountGetL1AddressArgs(args) {
|
|
@@ -9521,17 +10155,17 @@ function assertWithdrawBridgeArgs(args) {
|
|
|
9521
10155
|
}
|
|
9522
10156
|
|
|
9523
10157
|
function assertWalletGetChannelFundArgs(args) {
|
|
9524
|
-
|
|
10158
|
+
assertAllowedCommandSchema(args, "wallet-get-channel-fund");
|
|
9525
10159
|
}
|
|
9526
10160
|
|
|
9527
10161
|
function assertExitChannelArgs(args) {
|
|
9528
|
-
|
|
10162
|
+
assertAllowedCommandSchema(args, "channel-exit");
|
|
9529
10163
|
}
|
|
9530
10164
|
|
|
9531
10165
|
function createWalletOperationDir(walletName, networkName, suffix) {
|
|
9532
10166
|
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
9533
10167
|
const operationDir = path.join(
|
|
9534
|
-
|
|
10168
|
+
selectedWalletEpochDir(walletName, networkName),
|
|
9535
10169
|
"operations",
|
|
9536
10170
|
`${timestamp}-${slugifyPathComponent(suffix)}`,
|
|
9537
10171
|
);
|
|
@@ -9576,8 +10210,73 @@ function persistWalletKeys(context) {
|
|
|
9576
10210
|
}
|
|
9577
10211
|
}
|
|
9578
10212
|
|
|
9579
|
-
function
|
|
9580
|
-
|
|
10213
|
+
function persistWalletIndexForContext(context) {
|
|
10214
|
+
const walletRoot = walletRootPath(context.walletName, context.wallet.network);
|
|
10215
|
+
ensureDir(walletRoot);
|
|
10216
|
+
const currentIndex = readWalletIndexIfExists(walletRoot) ?? {
|
|
10217
|
+
format: WALLET_INDEX_FORMAT,
|
|
10218
|
+
formatVersion: WALLET_INDEX_FORMAT_VERSION,
|
|
10219
|
+
canonicalWalletName: context.walletName,
|
|
10220
|
+
network: context.wallet.network,
|
|
10221
|
+
channelName: context.wallet.channelName,
|
|
10222
|
+
channelId: context.wallet.channelId,
|
|
10223
|
+
l1Address: context.wallet.l1Address,
|
|
10224
|
+
activeEpochId: null,
|
|
10225
|
+
epochs: [],
|
|
10226
|
+
};
|
|
10227
|
+
const epoch = walletEpochSummaryFromWallet(context.wallet);
|
|
10228
|
+
const epochs = [
|
|
10229
|
+
...currentIndex.epochs.filter((entry) => entry.epochId !== epoch.epochId),
|
|
10230
|
+
epoch,
|
|
10231
|
+
].sort((left, right) =>
|
|
10232
|
+
Number(left.joinedAtBlockNumber ?? 0) - Number(right.joinedAtBlockNumber ?? 0)
|
|
10233
|
+
|| String(left.epochId).localeCompare(String(right.epochId)));
|
|
10234
|
+
const activeEpoch = epochs.find((entry) => entry.lifecycleStatus === "active") ?? null;
|
|
10235
|
+
const nextIndex = {
|
|
10236
|
+
...currentIndex,
|
|
10237
|
+
canonicalWalletName: context.walletName,
|
|
10238
|
+
network: context.wallet.network,
|
|
10239
|
+
channelName: context.wallet.channelName,
|
|
10240
|
+
channelId: context.wallet.channelId,
|
|
10241
|
+
l1Address: context.wallet.l1Address,
|
|
10242
|
+
activeEpochId: activeEpoch?.epochId ?? null,
|
|
10243
|
+
epochs,
|
|
10244
|
+
};
|
|
10245
|
+
writeJson(walletIndexMetadataPath(walletRoot), nextIndex);
|
|
10246
|
+
}
|
|
10247
|
+
|
|
10248
|
+
async function markWalletEpochExited({ walletContext, receipt, provider }) {
|
|
10249
|
+
const block = receipt?.blockNumber === null || receipt?.blockNumber === undefined
|
|
10250
|
+
? null
|
|
10251
|
+
: await provider.getBlock(receipt.blockNumber).catch(() => null);
|
|
10252
|
+
const exitedAtBlockTimestamp = block?.timestamp ?? null;
|
|
10253
|
+
walletContext.wallet.lifecycleStatus = "exited";
|
|
10254
|
+
walletContext.wallet.exitedAtTxHash = receipt?.hash ?? null;
|
|
10255
|
+
walletContext.wallet.exitedAtBlockNumber = receipt?.blockNumber ?? null;
|
|
10256
|
+
walletContext.wallet.exitedAtLogIndex = firstReceiptLogIndex(receipt);
|
|
10257
|
+
walletContext.wallet.exitedAtBlockTimestamp = exitedAtBlockTimestamp;
|
|
10258
|
+
walletContext.wallet.exitedAtBlockTimestampIso = exitedAtBlockTimestamp === null
|
|
10259
|
+
? null
|
|
10260
|
+
: new Date(Number(exitedAtBlockTimestamp) * 1000).toISOString();
|
|
10261
|
+
persistWallet(walletContext);
|
|
10262
|
+
persistWalletIndexForContext(walletContext);
|
|
10263
|
+
return walletEpochSummaryFromWallet(walletContext.wallet);
|
|
10264
|
+
}
|
|
10265
|
+
|
|
10266
|
+
function firstReceiptLogIndex(receipt) {
|
|
10267
|
+
const first = receipt?.logs?.[0] ?? null;
|
|
10268
|
+
return first?.index ?? first?.logIndex ?? null;
|
|
10269
|
+
}
|
|
10270
|
+
|
|
10271
|
+
function walletEpochSummaryFromWallet(wallet) {
|
|
10272
|
+
const lifecycle = walletLifecycleMetadata(wallet);
|
|
10273
|
+
return {
|
|
10274
|
+
...lifecycle,
|
|
10275
|
+
walletDirName: slugifyPathComponent(lifecycle.epochId),
|
|
10276
|
+
l2Address: wallet.l2Address,
|
|
10277
|
+
l2StorageKey: wallet.l2StorageKey,
|
|
10278
|
+
leafIndex: wallet.leafIndex,
|
|
10279
|
+
};
|
|
9581
10280
|
}
|
|
9582
10281
|
|
|
9583
10282
|
function sanitizeWalletForNotesMetadata(wallet) {
|
|
@@ -9633,6 +10332,7 @@ function buildWalletSpendingKeyMetadata(wallet) {
|
|
|
9633
10332
|
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
9634
10333
|
network: wallet.network,
|
|
9635
10334
|
wallet: wallet.name,
|
|
10335
|
+
...walletLifecycleMetadata(wallet),
|
|
9636
10336
|
channelName: wallet.channelName,
|
|
9637
10337
|
channelId: wallet.channelId,
|
|
9638
10338
|
l1Address: wallet.l1Address,
|
|
@@ -9650,6 +10350,7 @@ function buildWalletViewingKeyMetadata(wallet) {
|
|
|
9650
10350
|
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
9651
10351
|
network: wallet.network,
|
|
9652
10352
|
wallet: wallet.name,
|
|
10353
|
+
...walletLifecycleMetadata(wallet),
|
|
9653
10354
|
channelName: wallet.channelName,
|
|
9654
10355
|
channelId: wallet.channelId,
|
|
9655
10356
|
l1Address: wallet.l1Address,
|
|
@@ -10028,9 +10729,9 @@ function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
|
10028
10729
|
version: WALLET_ENCRYPTION_VERSION,
|
|
10029
10730
|
algorithm: WALLET_ENCRYPTION_ALGORITHM,
|
|
10030
10731
|
kdf: "scrypt",
|
|
10031
|
-
salt:
|
|
10732
|
+
salt: normalizeBytesHex(salt, 16),
|
|
10032
10733
|
iv: normalizeBytes12Hex(iv),
|
|
10033
|
-
tag:
|
|
10734
|
+
tag: normalizeBytesHex(tag, 16),
|
|
10034
10735
|
ciphertext: ethers.hexlify(ciphertext),
|
|
10035
10736
|
};
|
|
10036
10737
|
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
@@ -10528,8 +11229,7 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
10528
11229
|
}
|
|
10529
11230
|
|
|
10530
11231
|
if (message.includes("Workspace recovery index is missing or unusable")) {
|
|
10531
|
-
hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
10532
|
-
hints.push(`private-state-cli wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
|
|
11232
|
+
hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
|
|
10533
11233
|
}
|
|
10534
11234
|
|
|
10535
11235
|
if (message.includes("Wallet note recovery index is missing or unusable")) {
|