@tokamak-private-dapps/private-state-cli 2.0.0 → 2.1.0
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 +15 -0
- package/README.md +18 -3
- package/investigator/README.md +4 -0
- package/investigator/app.js +20 -3
- package/lib/private-state-cli-command-registry.mjs +9 -2
- package/package.json +1 -1
- package/private-state-bridge-cli.mjs +629 -145
|
@@ -137,9 +137,11 @@ const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
|
|
|
137
137
|
const TOKAMAK_ZKEVM_CLI_PACKAGE_NAME = "@tokamak-zk-evm/cli";
|
|
138
138
|
const WALLET_BACKUP_EXPORT_FORMAT = "tokamak-private-state-wallet-backup-export";
|
|
139
139
|
const WALLET_KEY_EXPORT_FORMAT = "tokamak-private-state-wallet-key-export";
|
|
140
|
+
const WALLET_INDEX_FORMAT = "tokamak-private-state-wallet-index";
|
|
140
141
|
const WALLET_EVIDENCE_BUNDLE_FORMAT = "tokamak-private-state-raw-evidence-bundle";
|
|
141
142
|
const WALLET_EXPORT_FORMAT_VERSION = 2;
|
|
142
|
-
const
|
|
143
|
+
const WALLET_INDEX_FORMAT_VERSION = 1;
|
|
144
|
+
const WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION = 2;
|
|
143
145
|
const WALLET_WORKSPACE_FORMAT_VERSION = 2;
|
|
144
146
|
const CHANNEL_WORKSPACE_MIRROR_PROTOCOL_VERSION = 2;
|
|
145
147
|
const CHANNEL_WORKSPACE_MIRROR_MANIFEST_PATH_PREFIX =
|
|
@@ -2502,64 +2504,51 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2502
2504
|
account: signer.address,
|
|
2503
2505
|
});
|
|
2504
2506
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
status: "stale-wallet-removed",
|
|
2512
|
-
wallet: walletName,
|
|
2513
|
-
removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
|
|
2514
|
-
removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
|
|
2515
|
-
workspace: context.workspaceName,
|
|
2516
|
-
channelName: context.workspace.channelName,
|
|
2517
|
-
channelId: context.workspace.channelId,
|
|
2518
|
-
l1Address: signer.address,
|
|
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
|
-
}
|
|
2507
|
+
const lifecycleEpoch = await resolveWalletLifecycleEpoch({
|
|
2508
|
+
context,
|
|
2509
|
+
provider,
|
|
2510
|
+
l1Address: signer.address,
|
|
2511
|
+
registration,
|
|
2512
|
+
});
|
|
2539
2513
|
expect(
|
|
2540
|
-
|
|
2514
|
+
lifecycleEpoch,
|
|
2515
|
+
cliError(
|
|
2516
|
+
CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
|
|
2517
|
+
`No channelTokenVault registration history exists for ${signer.address}. Run channel join first.`,
|
|
2518
|
+
),
|
|
2519
|
+
);
|
|
2520
|
+
const registeredNoteReceivePubKey = lifecycleEpoch.noteReceivePubKey;
|
|
2521
|
+
expect(
|
|
2522
|
+
ethers.toBigInt(normalizeBytes32Hex(registeredNoteReceivePubKey.x))
|
|
2541
2523
|
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
2542
2524
|
"The existing note-receive public key X does not match the derived note-receive public key.",
|
|
2543
2525
|
);
|
|
2544
2526
|
expect(
|
|
2545
|
-
Number(
|
|
2527
|
+
Number(registeredNoteReceivePubKey.yParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2546
2528
|
"The existing note-receive public key parity does not match the derived note-receive public key.",
|
|
2547
2529
|
);
|
|
2548
2530
|
const l2Identity = {
|
|
2549
2531
|
l2PrivateKey: null,
|
|
2550
2532
|
l2PublicKey: null,
|
|
2551
|
-
l2Address: getAddress(
|
|
2533
|
+
l2Address: getAddress(lifecycleEpoch.l2Address),
|
|
2552
2534
|
};
|
|
2553
|
-
const storageKey = normalizeBytes32Hex(
|
|
2535
|
+
const storageKey = normalizeBytes32Hex(lifecycleEpoch.channelTokenVaultKey);
|
|
2554
2536
|
|
|
2555
|
-
const walletDir =
|
|
2537
|
+
const walletDir = walletEpochPath(walletName, context.workspace.network, lifecycleEpoch.epochId);
|
|
2556
2538
|
const existingWallet = walletConfigExists(walletDir)
|
|
2557
|
-
?
|
|
2539
|
+
? loadWalletFromDir({
|
|
2540
|
+
walletName,
|
|
2541
|
+
networkName: context.workspace.network,
|
|
2542
|
+
walletDir,
|
|
2543
|
+
})
|
|
2558
2544
|
: null;
|
|
2559
2545
|
|
|
2560
2546
|
if (existingWallet) {
|
|
2561
2547
|
existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2548
|
+
applyWalletLifecycleEpoch(existingWallet.wallet, lifecycleEpoch);
|
|
2562
2549
|
persistWalletKeys(existingWallet);
|
|
2550
|
+
persistWallet(existingWallet);
|
|
2551
|
+
persistWalletIndexForContext(existingWallet);
|
|
2563
2552
|
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2564
2553
|
walletContext: existingWallet,
|
|
2565
2554
|
context,
|
|
@@ -2580,7 +2569,10 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2580
2569
|
l1Address: signer.address,
|
|
2581
2570
|
l2Address: l2Identity.l2Address,
|
|
2582
2571
|
l2StorageKey: storageKey,
|
|
2583
|
-
leafIndex:
|
|
2572
|
+
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2573
|
+
epochId: lifecycleEpoch.epochId,
|
|
2574
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2575
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2584
2576
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2585
2577
|
l2Nonce: existingWallet.wallet.l2Nonce,
|
|
2586
2578
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
@@ -2590,8 +2582,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2590
2582
|
return;
|
|
2591
2583
|
}
|
|
2592
2584
|
|
|
2593
|
-
fs.rmSync(walletPath(walletName, context.workspace.network), { recursive: true, force: true });
|
|
2594
|
-
|
|
2595
2585
|
const walletContext = ensureWallet({
|
|
2596
2586
|
channelContext: context,
|
|
2597
2587
|
signerAddress: signer.address,
|
|
@@ -2599,8 +2589,9 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2599
2589
|
l2Identity,
|
|
2600
2590
|
walletSecret: noteReceiveKeyMaterial.privateKey,
|
|
2601
2591
|
storageKey,
|
|
2602
|
-
leafIndex:
|
|
2592
|
+
leafIndex: lifecycleEpoch.leafIndex,
|
|
2603
2593
|
noteReceiveKeyMaterial,
|
|
2594
|
+
lifecycleEpoch,
|
|
2604
2595
|
rpcUrl,
|
|
2605
2596
|
});
|
|
2606
2597
|
walletContext.wallet.l2Nonce = 0;
|
|
@@ -2627,7 +2618,10 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2627
2618
|
l1Address: signer.address,
|
|
2628
2619
|
l2Address: l2Identity.l2Address,
|
|
2629
2620
|
l2StorageKey: storageKey,
|
|
2630
|
-
leafIndex:
|
|
2621
|
+
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2622
|
+
epochId: lifecycleEpoch.epochId,
|
|
2623
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2624
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2631
2625
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2632
2626
|
l2Nonce: walletContext.wallet.l2Nonce,
|
|
2633
2627
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
@@ -2636,32 +2630,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2636
2630
|
});
|
|
2637
2631
|
}
|
|
2638
2632
|
|
|
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
2633
|
async function handleInstallZkEvm({ args }) {
|
|
2666
2634
|
const selectedVersions = await resolvePrivateStateInstallRuntimeVersions(args);
|
|
2667
2635
|
const tokamakCliRuntime = await installTokamakCliRuntimeForPrivateState({
|
|
@@ -3371,9 +3339,13 @@ function handleWalletImportKey({ args, keyKind }) {
|
|
|
3371
3339
|
: walletViewingKeySecretPath(networkName, walletName);
|
|
3372
3340
|
expect(!fs.existsSync(targetPath), `Refusing to overwrite existing ${keyKind} key: ${targetPath}.`);
|
|
3373
3341
|
writeSecretFile(targetPath, JSON.stringify(payload, null, 2));
|
|
3374
|
-
const
|
|
3375
|
-
|
|
3376
|
-
|
|
3342
|
+
const walletRoot = walletRootPath(walletName, networkName);
|
|
3343
|
+
const walletIndex = fs.existsSync(walletRoot)
|
|
3344
|
+
? requireWalletIndex({ walletRoot, walletName, networkName })
|
|
3345
|
+
: null;
|
|
3346
|
+
const selectedEpoch = walletIndex ? selectedWalletEpoch(walletIndex, walletName, networkName) : null;
|
|
3347
|
+
if (selectedEpoch) {
|
|
3348
|
+
const walletDir = walletEpochPathFromRoot(walletRoot, selectedEpoch.epochId);
|
|
3377
3349
|
const metadataPath = keyKind === "spending"
|
|
3378
3350
|
? walletSpendingKeyMetadataPath(walletDir)
|
|
3379
3351
|
: walletViewingKeyMetadataPath(walletDir);
|
|
@@ -3842,7 +3814,13 @@ async function inspectGuideAccount({ account, networkName, network, provider, ar
|
|
|
3842
3814
|
}
|
|
3843
3815
|
|
|
3844
3816
|
async function inspectGuideWallet({ walletName, networkName, provider, artifactsInstalled }) {
|
|
3845
|
-
|
|
3817
|
+
let walletDir = walletRootPath(walletName, networkName);
|
|
3818
|
+
let workspaceError = null;
|
|
3819
|
+
try {
|
|
3820
|
+
walletDir = selectedWalletEpochDir(walletName, networkName);
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
workspaceError = error.message;
|
|
3823
|
+
}
|
|
3846
3824
|
const viewingKeyFile = walletViewingKeySecretPath(networkName, walletName);
|
|
3847
3825
|
const spendingKeyFile = walletSpendingKeySecretPath(networkName, walletName);
|
|
3848
3826
|
const result = {
|
|
@@ -3865,9 +3843,9 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3865
3843
|
unusedNoteBalanceBaseUnits: null,
|
|
3866
3844
|
unusedNoteBalanceTokens: null,
|
|
3867
3845
|
spentNoteCount: null,
|
|
3868
|
-
error:
|
|
3846
|
+
error: workspaceError,
|
|
3869
3847
|
};
|
|
3870
|
-
if (!result.exists) {
|
|
3848
|
+
if (workspaceError || !result.exists) {
|
|
3871
3849
|
return result;
|
|
3872
3850
|
}
|
|
3873
3851
|
|
|
@@ -4109,6 +4087,7 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
4109
4087
|
printJson({
|
|
4110
4088
|
action: "wallet get-meta",
|
|
4111
4089
|
wallet: wallet.walletName,
|
|
4090
|
+
...walletLifecycleMetadata(wallet.wallet),
|
|
4112
4091
|
network: walletMetadata.network,
|
|
4113
4092
|
channelName: walletMetadata.channelName,
|
|
4114
4093
|
l1Address: signer.address,
|
|
@@ -4202,6 +4181,156 @@ async function loadWalletChannelRegistrationState({
|
|
|
4202
4181
|
};
|
|
4203
4182
|
}
|
|
4204
4183
|
|
|
4184
|
+
async function resolveWalletLifecycleEpoch({
|
|
4185
|
+
context,
|
|
4186
|
+
provider,
|
|
4187
|
+
l1Address,
|
|
4188
|
+
registration = null,
|
|
4189
|
+
}) {
|
|
4190
|
+
const epochs = await readWalletLifecycleEpochs({
|
|
4191
|
+
context,
|
|
4192
|
+
provider,
|
|
4193
|
+
l1Address,
|
|
4194
|
+
});
|
|
4195
|
+
if (epochs.length === 0 && registration?.exists) {
|
|
4196
|
+
const block = await provider.getBlock("latest").catch(() => null);
|
|
4197
|
+
return {
|
|
4198
|
+
epochId: `join-registered-${String(registration.joinedAt)}-${String(registration.leafIndex)}`,
|
|
4199
|
+
lifecycleStatus: "active",
|
|
4200
|
+
joinedAtTxHash: null,
|
|
4201
|
+
joinedAtBlockNumber: null,
|
|
4202
|
+
joinedAtLogIndex: null,
|
|
4203
|
+
joinedAtBlockTimestamp: Number(registration.joinedAt),
|
|
4204
|
+
joinedAtBlockTimestampIso: Number(registration.joinedAt) > 0
|
|
4205
|
+
? new Date(Number(registration.joinedAt) * 1000).toISOString()
|
|
4206
|
+
: null,
|
|
4207
|
+
exitedAtTxHash: null,
|
|
4208
|
+
exitedAtBlockNumber: null,
|
|
4209
|
+
exitedAtLogIndex: null,
|
|
4210
|
+
exitedAtBlockTimestamp: null,
|
|
4211
|
+
exitedAtBlockTimestampIso: null,
|
|
4212
|
+
l2Address: getAddress(registration.l2Address),
|
|
4213
|
+
channelTokenVaultKey: normalizeBytes32Hex(registration.channelTokenVaultKey),
|
|
4214
|
+
leafIndex: registration.leafIndex,
|
|
4215
|
+
noteReceivePubKey: {
|
|
4216
|
+
x: normalizeBytes32Hex(registration.noteReceivePubKey.x),
|
|
4217
|
+
yParity: Number(registration.noteReceivePubKey.yParity),
|
|
4218
|
+
},
|
|
4219
|
+
observedAtBlockNumber: block?.number ?? null,
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
if (registration?.exists) {
|
|
4223
|
+
const active = [...epochs].reverse().find((epoch) => (
|
|
4224
|
+
epoch.lifecycleStatus === "active"
|
|
4225
|
+
&& ethers.toBigInt(getAddress(epoch.l2Address)) === ethers.toBigInt(getAddress(registration.l2Address))
|
|
4226
|
+
&& ethers.toBigInt(normalizeBytes32Hex(epoch.channelTokenVaultKey))
|
|
4227
|
+
=== ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
4228
|
+
));
|
|
4229
|
+
if (active) {
|
|
4230
|
+
return active;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
return epochs[epochs.length - 1] ?? null;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
async function readWalletLifecycleEpochs({ context, provider, l1Address }) {
|
|
4237
|
+
const fromBlock = Number(context.workspace.genesisBlockNumber ?? 0);
|
|
4238
|
+
const [registeredLogs, exitedLogs] = await Promise.all([
|
|
4239
|
+
context.channelManager.queryFilter(
|
|
4240
|
+
context.channelManager.filters.ChannelTokenVaultIdentityRegistered(l1Address),
|
|
4241
|
+
fromBlock,
|
|
4242
|
+
"latest",
|
|
4243
|
+
),
|
|
4244
|
+
context.channelManager.queryFilter(
|
|
4245
|
+
context.channelManager.filters.ChannelTokenVaultIdentityExited(l1Address),
|
|
4246
|
+
fromBlock,
|
|
4247
|
+
"latest",
|
|
4248
|
+
),
|
|
4249
|
+
]);
|
|
4250
|
+
const exits = await Promise.all(exitedLogs.map((log) => walletExitFromLog({ log, provider })));
|
|
4251
|
+
const epochs = [];
|
|
4252
|
+
for (const log of registeredLogs.sort(compareLogsByPosition)) {
|
|
4253
|
+
const registered = await walletEpochFromRegisteredLog({ log, provider });
|
|
4254
|
+
const exit = exits.find((entry) => (
|
|
4255
|
+
compareLogPosition(entry, registered) > 0
|
|
4256
|
+
&& ethers.toBigInt(entry.leafIndex) === ethers.toBigInt(registered.leafIndex)
|
|
4257
|
+
&& !epochs.some((epoch) => epoch.exitedAtTxHash === entry.exitedAtTxHash)
|
|
4258
|
+
));
|
|
4259
|
+
if (exit) {
|
|
4260
|
+
epochs.push({
|
|
4261
|
+
...registered,
|
|
4262
|
+
lifecycleStatus: "exited",
|
|
4263
|
+
exitedAtTxHash: exit.exitedAtTxHash,
|
|
4264
|
+
exitedAtBlockNumber: exit.exitedAtBlockNumber,
|
|
4265
|
+
exitedAtLogIndex: exit.exitedAtLogIndex,
|
|
4266
|
+
exitedAtBlockTimestamp: exit.exitedAtBlockTimestamp,
|
|
4267
|
+
exitedAtBlockTimestampIso: exit.exitedAtBlockTimestampIso,
|
|
4268
|
+
});
|
|
4269
|
+
} else {
|
|
4270
|
+
epochs.push(registered);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
return epochs.sort(compareWalletEpochs);
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
async function walletEpochFromRegisteredLog({ log, provider }) {
|
|
4277
|
+
const block = await provider.getBlock(log.blockNumber).catch(() => null);
|
|
4278
|
+
const args = log.args;
|
|
4279
|
+
return {
|
|
4280
|
+
epochId: walletEpochIdFromLog(log),
|
|
4281
|
+
lifecycleStatus: "active",
|
|
4282
|
+
joinedAtTxHash: log.transactionHash,
|
|
4283
|
+
joinedAtBlockNumber: log.blockNumber,
|
|
4284
|
+
joinedAtLogIndex: log.index ?? log.logIndex ?? null,
|
|
4285
|
+
joinedAtBlockTimestamp: block?.timestamp ?? Number(args.joinedAt ?? 0) ?? null,
|
|
4286
|
+
joinedAtBlockTimestampIso: block?.timestamp
|
|
4287
|
+
? new Date(Number(block.timestamp) * 1000).toISOString()
|
|
4288
|
+
: Number(args.joinedAt ?? 0) > 0
|
|
4289
|
+
? new Date(Number(args.joinedAt) * 1000).toISOString()
|
|
4290
|
+
: null,
|
|
4291
|
+
exitedAtTxHash: null,
|
|
4292
|
+
exitedAtBlockNumber: null,
|
|
4293
|
+
exitedAtLogIndex: null,
|
|
4294
|
+
exitedAtBlockTimestamp: null,
|
|
4295
|
+
exitedAtBlockTimestampIso: null,
|
|
4296
|
+
l2Address: getAddress(args.l2Address),
|
|
4297
|
+
channelTokenVaultKey: normalizeBytes32Hex(args.channelTokenVaultKey),
|
|
4298
|
+
leafIndex: args.leafIndex,
|
|
4299
|
+
noteReceivePubKey: {
|
|
4300
|
+
x: normalizeBytes32Hex(args.noteReceivePubKeyX),
|
|
4301
|
+
yParity: Number(args.noteReceivePubKeyYParity),
|
|
4302
|
+
},
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
async function walletExitFromLog({ log, provider }) {
|
|
4307
|
+
const block = await provider.getBlock(log.blockNumber).catch(() => null);
|
|
4308
|
+
return {
|
|
4309
|
+
exitedAtTxHash: log.transactionHash,
|
|
4310
|
+
exitedAtBlockNumber: log.blockNumber,
|
|
4311
|
+
exitedAtLogIndex: log.index ?? log.logIndex ?? null,
|
|
4312
|
+
exitedAtBlockTimestamp: block?.timestamp ?? null,
|
|
4313
|
+
exitedAtBlockTimestampIso: block?.timestamp ? new Date(Number(block.timestamp) * 1000).toISOString() : null,
|
|
4314
|
+
leafIndex: log.args.leafIndex,
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
function walletEpochIdFromLog(log) {
|
|
4319
|
+
return `join-${String(log.transactionHash).toLowerCase()}-${Number(log.index ?? log.logIndex ?? 0)}`;
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
function compareLogPosition(left, right) {
|
|
4323
|
+
return Number(left.exitedAtBlockNumber ?? left.blockNumber ?? 0) - Number(right.joinedAtBlockNumber ?? right.blockNumber ?? 0)
|
|
4324
|
+
|| Number((left.exitedAtLogIndex ?? left.index ?? left.logIndex) ?? 0)
|
|
4325
|
+
- Number((right.joinedAtLogIndex ?? right.index ?? right.logIndex) ?? 0);
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
function compareWalletEpochs(left, right) {
|
|
4329
|
+
return Number(left.joinedAtBlockNumber ?? 0) - Number(right.joinedAtBlockNumber ?? 0)
|
|
4330
|
+
|| Number(left.joinedAtLogIndex ?? 0) - Number(right.joinedAtLogIndex ?? 0)
|
|
4331
|
+
|| String(left.epochId).localeCompare(String(right.epochId));
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4205
4334
|
async function handleWalletGetChannelFund({ args, provider }) {
|
|
4206
4335
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
4207
4336
|
const {
|
|
@@ -4311,6 +4440,14 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4311
4440
|
{ nonce: nextNonce++ },
|
|
4312
4441
|
),
|
|
4313
4442
|
);
|
|
4443
|
+
const registered = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
4444
|
+
const lifecycleEpoch = await resolveWalletLifecycleEpoch({
|
|
4445
|
+
context,
|
|
4446
|
+
provider,
|
|
4447
|
+
l1Address: signer.address,
|
|
4448
|
+
registration: registered,
|
|
4449
|
+
});
|
|
4450
|
+
expect(lifecycleEpoch, "Unable to resolve the channel join epoch from emitted registration logs.");
|
|
4314
4451
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
4315
4452
|
context,
|
|
4316
4453
|
provider,
|
|
@@ -4327,6 +4464,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4327
4464
|
storageKey,
|
|
4328
4465
|
leafIndex,
|
|
4329
4466
|
noteReceiveKeyMaterial,
|
|
4467
|
+
lifecycleEpoch,
|
|
4330
4468
|
rpcUrl,
|
|
4331
4469
|
});
|
|
4332
4470
|
|
|
@@ -4343,6 +4481,10 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4343
4481
|
l2Address: l2Identity.l2Address,
|
|
4344
4482
|
l2StorageKey: storageKey,
|
|
4345
4483
|
leafIndex: leafIndex.toString(),
|
|
4484
|
+
epochId: lifecycleEpoch.epochId,
|
|
4485
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
4486
|
+
joinedAtTxHash: lifecycleEpoch.joinedAtTxHash,
|
|
4487
|
+
joinedAtBlockNumber: lifecycleEpoch.joinedAtBlockNumber,
|
|
4346
4488
|
joinTollBaseUnits: joinToll.toString(),
|
|
4347
4489
|
joinTollTokens: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4348
4490
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
@@ -4359,6 +4501,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4359
4501
|
|
|
4360
4502
|
async function handleExitChannel({ args, provider }) {
|
|
4361
4503
|
const { wallet: walletContext, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
4504
|
+
requireActiveWalletLifecycle(walletContext, "channel exit");
|
|
4362
4505
|
const { signer, context, channelFund, contextResult } = await loadWalletChannelFundState({
|
|
4363
4506
|
walletContext,
|
|
4364
4507
|
provider,
|
|
@@ -4378,7 +4521,11 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4378
4521
|
const receipt = await waitForReceipt(
|
|
4379
4522
|
await context.bridgeTokenVault.connect(ownerSigner).exitChannel(ethers.toBigInt(context.workspace.channelId)),
|
|
4380
4523
|
);
|
|
4381
|
-
const
|
|
4524
|
+
const lifecycleEpoch = await markWalletEpochExited({
|
|
4525
|
+
walletContext,
|
|
4526
|
+
receipt,
|
|
4527
|
+
provider,
|
|
4528
|
+
});
|
|
4382
4529
|
|
|
4383
4530
|
printJson({
|
|
4384
4531
|
action: "channel exit",
|
|
@@ -4396,8 +4543,12 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4396
4543
|
gasUsed: receiptGasUsed(receipt),
|
|
4397
4544
|
txUrl: explorerTxUrl(network, receipt.hash),
|
|
4398
4545
|
receipt: sanitizeReceipt(receipt),
|
|
4399
|
-
|
|
4400
|
-
|
|
4546
|
+
epochId: lifecycleEpoch.epochId,
|
|
4547
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
4548
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
4549
|
+
exitedAtBlockNumber: lifecycleEpoch.exitedAtBlockNumber,
|
|
4550
|
+
exitedAtBlockTimestampIso: lifecycleEpoch.exitedAtBlockTimestampIso,
|
|
4551
|
+
archivedWalletDir: walletContext.walletDir,
|
|
4401
4552
|
});
|
|
4402
4553
|
}
|
|
4403
4554
|
|
|
@@ -4409,6 +4560,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4409
4560
|
: "wallet withdraw-channel";
|
|
4410
4561
|
emitProgress(operationName, "loading");
|
|
4411
4562
|
const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
|
|
4563
|
+
requireActiveWalletLifecycle(walletContext, operationName);
|
|
4412
4564
|
const contextResult = await loadFreshWalletChannelContext({
|
|
4413
4565
|
walletContext,
|
|
4414
4566
|
provider,
|
|
@@ -4634,6 +4786,7 @@ function resolveFunctionMetadataProofForExecution({
|
|
|
4634
4786
|
|
|
4635
4787
|
async function handleMintNotes({ args, provider }) {
|
|
4636
4788
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4789
|
+
requireActiveWalletLifecycle(wallet, "wallet mint-notes");
|
|
4637
4790
|
requireWalletSpendingCapability(wallet);
|
|
4638
4791
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
4639
4792
|
const amountInputs = parseAmountVector(requireArg(args.amounts, "--amounts"), {
|
|
@@ -4722,6 +4875,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4722
4875
|
|
|
4723
4876
|
async function handleRedeemNotes({ args, provider }) {
|
|
4724
4877
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4878
|
+
requireActiveWalletLifecycle(wallet, "wallet redeem-notes");
|
|
4725
4879
|
requireWalletViewingCapability(wallet);
|
|
4726
4880
|
requireWalletSpendingCapability(wallet);
|
|
4727
4881
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
@@ -4890,24 +5044,39 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4890
5044
|
const outputPath = path.resolve(String(requireArg(args.exportEvidence, "--export-evidence")));
|
|
4891
5045
|
ensureDir(path.dirname(outputPath));
|
|
4892
5046
|
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
for (const
|
|
4899
|
-
|
|
5047
|
+
const evidenceWalletContexts = loadWalletEpochContextsForEvidence({
|
|
5048
|
+
baseWalletContext: walletContext,
|
|
5049
|
+
networkName: walletMetadata.network,
|
|
5050
|
+
});
|
|
5051
|
+
const noteInputs = [];
|
|
5052
|
+
for (const candidateWalletContext of evidenceWalletContexts) {
|
|
5053
|
+
const notes = candidateWalletContext === walletContext
|
|
5054
|
+
? [
|
|
5055
|
+
...unusedTrackedNotes.map(normalizeTrackedNote),
|
|
5056
|
+
...spentTrackedNotes.map(normalizeTrackedNote),
|
|
5057
|
+
]
|
|
5058
|
+
: [
|
|
5059
|
+
...Object.values(candidateWalletContext.wallet.notes.unused).map(normalizeTrackedNote),
|
|
5060
|
+
...Object.values(candidateWalletContext.wallet.notes.spent).map(normalizeTrackedNote),
|
|
5061
|
+
];
|
|
5062
|
+
for (const note of notes) {
|
|
5063
|
+
validateEvidenceNotePlaintext(note, candidateWalletContext.wallet);
|
|
5064
|
+
noteInputs.push({ note, walletContext: candidateWalletContext });
|
|
5065
|
+
}
|
|
4900
5066
|
}
|
|
5067
|
+
noteInputs.sort((left, right) =>
|
|
5068
|
+
String(left.walletContext.wallet.walletEpochId ?? "").localeCompare(String(right.walletContext.wallet.walletEpochId ?? ""))
|
|
5069
|
+
|| left.note.commitment.localeCompare(right.note.commitment));
|
|
4901
5070
|
|
|
4902
5071
|
const txHashes = uniqueNonNull([
|
|
4903
|
-
...
|
|
4904
|
-
...
|
|
5072
|
+
...noteInputs.map(({ note }) => note.createdAtTxHash),
|
|
5073
|
+
...noteInputs.map(({ note }) => note.spentAtTxHash),
|
|
4905
5074
|
]);
|
|
4906
5075
|
const transactionEvidence = await buildTransactionEvidenceMap({ provider, txHashes });
|
|
4907
5076
|
const blockTimestampCache = buildBlockTimestampCache(transactionEvidence);
|
|
4908
|
-
const noteRecords =
|
|
5077
|
+
const noteRecords = noteInputs.map(({ note, walletContext: noteWalletContext }) => buildEvidenceNoteRecord({
|
|
4909
5078
|
note,
|
|
4910
|
-
walletContext,
|
|
5079
|
+
walletContext: noteWalletContext,
|
|
4911
5080
|
walletMetadata,
|
|
4912
5081
|
context,
|
|
4913
5082
|
transactionEvidence,
|
|
@@ -4917,6 +5086,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4917
5086
|
const manifest = buildEvidenceManifest({
|
|
4918
5087
|
outputPath,
|
|
4919
5088
|
walletContext,
|
|
5089
|
+
walletContexts: evidenceWalletContexts,
|
|
4920
5090
|
walletMetadata,
|
|
4921
5091
|
context,
|
|
4922
5092
|
noteRecords,
|
|
@@ -4932,7 +5102,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4932
5102
|
addEvidenceJson(archive, "indexes/by-block-range.json", indexes.byBlockRange);
|
|
4933
5103
|
addEvidenceJson(archive, "indexes/by-counterparty.json", indexes.byCounterparty);
|
|
4934
5104
|
for (const record of noteRecords) {
|
|
4935
|
-
addEvidenceJson(archive,
|
|
5105
|
+
addEvidenceJson(archive, evidenceNotePath(record), record);
|
|
4936
5106
|
}
|
|
4937
5107
|
for (const [txHash, txRecord] of Object.entries(transactionEvidence)) {
|
|
4938
5108
|
addEvidenceJson(archive, `transactions/${txHash}.json`, txRecord.transaction);
|
|
@@ -4941,7 +5111,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4941
5111
|
}
|
|
4942
5112
|
|
|
4943
5113
|
assertEvidenceBundleDoesNotContainSecrets({
|
|
4944
|
-
|
|
5114
|
+
wallets: evidenceWalletContexts.map((entry) => entry.wallet),
|
|
4945
5115
|
payload: {
|
|
4946
5116
|
manifest,
|
|
4947
5117
|
indexes,
|
|
@@ -4958,6 +5128,7 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4958
5128
|
format: WALLET_EVIDENCE_BUNDLE_FORMAT,
|
|
4959
5129
|
formatVersion: WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION,
|
|
4960
5130
|
noteCount: noteRecords.length,
|
|
5131
|
+
walletEpochCount: evidenceWalletContexts.length,
|
|
4961
5132
|
transactionCount: txHashes.length,
|
|
4962
5133
|
containsNotePlaintext: true,
|
|
4963
5134
|
containsSpendingKey: false,
|
|
@@ -4967,6 +5138,33 @@ async function exportWalletGetNotesEvidenceBundle({
|
|
|
4967
5138
|
};
|
|
4968
5139
|
}
|
|
4969
5140
|
|
|
5141
|
+
function loadWalletEpochContextsForEvidence({ baseWalletContext, networkName }) {
|
|
5142
|
+
const walletRoot = walletRootPath(baseWalletContext.walletName, networkName);
|
|
5143
|
+
const index = requireWalletIndex({
|
|
5144
|
+
walletRoot,
|
|
5145
|
+
walletName: baseWalletContext.walletName,
|
|
5146
|
+
networkName,
|
|
5147
|
+
});
|
|
5148
|
+
const contexts = [];
|
|
5149
|
+
for (const epoch of index.epochs) {
|
|
5150
|
+
const walletDir = walletEpochPathFromRoot(walletRoot, epoch.epochId);
|
|
5151
|
+
if (!walletConfigExists(walletDir)) {
|
|
5152
|
+
continue;
|
|
5153
|
+
}
|
|
5154
|
+
const context = loadWalletFromDir({
|
|
5155
|
+
walletName: baseWalletContext.walletName,
|
|
5156
|
+
networkName,
|
|
5157
|
+
walletDir,
|
|
5158
|
+
});
|
|
5159
|
+
contexts.push(context);
|
|
5160
|
+
}
|
|
5161
|
+
expect(
|
|
5162
|
+
contexts.length > 0,
|
|
5163
|
+
`Wallet ${baseWalletContext.walletName} on ${networkName} has no readable wallet epochs. Run wallet recover-workspace and then wallet get-notes --export-evidence again.`,
|
|
5164
|
+
);
|
|
5165
|
+
return contexts;
|
|
5166
|
+
}
|
|
5167
|
+
|
|
4970
5168
|
function validateEvidenceNotePlaintext(note, wallet) {
|
|
4971
5169
|
expect(
|
|
4972
5170
|
note.owner && note.value !== null && note.salt && note.encryptedNoteValue,
|
|
@@ -5084,6 +5282,7 @@ function buildEvidenceNoteRecord({
|
|
|
5084
5282
|
channelName: walletMetadata.channelName,
|
|
5085
5283
|
channelId: walletContext.wallet.channelId,
|
|
5086
5284
|
wallet: walletContext.walletName,
|
|
5285
|
+
...walletLifecycleMetadata(walletContext.wallet),
|
|
5087
5286
|
walletL1Address: walletContext.wallet.l1Address,
|
|
5088
5287
|
walletL2Address: walletContext.wallet.l2Address,
|
|
5089
5288
|
controller: context.workspace.controller,
|
|
@@ -5198,7 +5397,7 @@ function buildEvidenceIndexes(noteRecords) {
|
|
|
5198
5397
|
},
|
|
5199
5398
|
};
|
|
5200
5399
|
for (const record of noteRecords) {
|
|
5201
|
-
const pathName =
|
|
5400
|
+
const pathName = evidenceNotePath(record);
|
|
5202
5401
|
indexes.byCommitment[record.derived.commitment] = pathName;
|
|
5203
5402
|
indexes.byNullifier[record.derived.nullifier] = pathName;
|
|
5204
5403
|
pushIndexEntry(indexes.byCreationTx, record.creation.txHash, pathName);
|
|
@@ -5227,6 +5426,21 @@ function buildEvidenceIndexes(noteRecords) {
|
|
|
5227
5426
|
return indexes;
|
|
5228
5427
|
}
|
|
5229
5428
|
|
|
5429
|
+
function evidenceNotePath(record) {
|
|
5430
|
+
expect(
|
|
5431
|
+
record.walletScope?.canonicalWalletName && record.walletScope?.epochId,
|
|
5432
|
+
"Evidence note path requires the current epoch-aware wallet scope.",
|
|
5433
|
+
);
|
|
5434
|
+
return [
|
|
5435
|
+
"wallets",
|
|
5436
|
+
slugifyPathComponent(record.walletScope.canonicalWalletName),
|
|
5437
|
+
"epochs",
|
|
5438
|
+
slugifyPathComponent(record.walletScope.epochId),
|
|
5439
|
+
"notes",
|
|
5440
|
+
`${record.derived.commitment}.json`,
|
|
5441
|
+
].join("/");
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5230
5444
|
function pushIndexEntry(index, key, value) {
|
|
5231
5445
|
if (!key) {
|
|
5232
5446
|
return;
|
|
@@ -5240,6 +5454,7 @@ function pushIndexEntry(index, key, value) {
|
|
|
5240
5454
|
function buildEvidenceManifest({
|
|
5241
5455
|
outputPath,
|
|
5242
5456
|
walletContext,
|
|
5457
|
+
walletContexts = [walletContext],
|
|
5243
5458
|
walletMetadata,
|
|
5244
5459
|
context,
|
|
5245
5460
|
noteRecords,
|
|
@@ -5258,10 +5473,17 @@ function buildEvidenceManifest({
|
|
|
5258
5473
|
wallet: walletContext.walletName,
|
|
5259
5474
|
walletL1Address: walletContext.wallet.l1Address,
|
|
5260
5475
|
walletL2Address: walletContext.wallet.l2Address,
|
|
5476
|
+
wallets: walletContexts.map((entry) => ({
|
|
5477
|
+
wallet: entry.walletName,
|
|
5478
|
+
...walletLifecycleMetadata(entry.wallet),
|
|
5479
|
+
walletL1Address: entry.wallet.l1Address,
|
|
5480
|
+
walletL2Address: entry.wallet.l2Address,
|
|
5481
|
+
})),
|
|
5261
5482
|
controller: context.workspace.controller,
|
|
5262
5483
|
channelManager: context.workspace.channelManager,
|
|
5263
5484
|
bridgeTokenVault: context.workspace.bridgeTokenVault,
|
|
5264
5485
|
containsAllLocallyKnownNotes: true,
|
|
5486
|
+
containsAllLocalWalletEpochs: true,
|
|
5265
5487
|
containsNotePlaintext: true,
|
|
5266
5488
|
noteCount: noteRecords.length,
|
|
5267
5489
|
transactionCount: txHashes.length,
|
|
@@ -5281,12 +5503,13 @@ function addEvidenceJson(archive, archivePath, value) {
|
|
|
5281
5503
|
archive.addFile(archivePath, Buffer.from(`${JSON.stringify(normalizeCliOutput(value), null, 2)}\n`, "utf8"));
|
|
5282
5504
|
}
|
|
5283
5505
|
|
|
5284
|
-
function assertEvidenceBundleDoesNotContainSecrets({ wallet, payload }) {
|
|
5506
|
+
function assertEvidenceBundleDoesNotContainSecrets({ wallet = null, wallets = null, payload }) {
|
|
5285
5507
|
const serialized = JSON.stringify(payload);
|
|
5286
|
-
const
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5508
|
+
const walletList = wallets ?? (wallet ? [wallet] : []);
|
|
5509
|
+
const forbiddenValues = walletList.flatMap((entry) => [
|
|
5510
|
+
entry.l2PrivateKey,
|
|
5511
|
+
entry.noteReceivePrivateKey,
|
|
5512
|
+
]).filter((value) => typeof value === "string" && value.length > 0);
|
|
5290
5513
|
for (const value of forbiddenValues) {
|
|
5291
5514
|
expect(
|
|
5292
5515
|
!serialized.includes(value),
|
|
@@ -5301,6 +5524,7 @@ function uniqueNonNull(values) {
|
|
|
5301
5524
|
|
|
5302
5525
|
async function handleTransferNotes({ args, provider }) {
|
|
5303
5526
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
5527
|
+
requireActiveWalletLifecycle(wallet, "wallet transfer-notes");
|
|
5304
5528
|
requireWalletViewingCapability(wallet);
|
|
5305
5529
|
requireWalletSpendingCapability(wallet);
|
|
5306
5530
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
@@ -5497,10 +5721,20 @@ function ensureWallet({
|
|
|
5497
5721
|
storageKey,
|
|
5498
5722
|
leafIndex,
|
|
5499
5723
|
noteReceiveKeyMaterial,
|
|
5724
|
+
lifecycleEpoch,
|
|
5500
5725
|
rpcUrl,
|
|
5501
5726
|
}) {
|
|
5502
5727
|
const walletName = walletNameForChannelAndAddress(channelContext.workspace.channelName, signerAddress);
|
|
5503
|
-
const
|
|
5728
|
+
const walletRoot = walletRootPath(walletName, channelContext.workspace.network);
|
|
5729
|
+
if (fs.existsSync(walletRoot)) {
|
|
5730
|
+
requireWalletIndex({
|
|
5731
|
+
walletRoot,
|
|
5732
|
+
walletName,
|
|
5733
|
+
networkName: channelContext.workspace.network,
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
expect(lifecycleEpoch, "Current wallet workspace creation requires an on-chain wallet lifecycle epoch.");
|
|
5737
|
+
const walletDir = walletEpochPath(walletName, channelContext.workspace.network, lifecycleEpoch.epochId);
|
|
5504
5738
|
expect(!walletConfigExists(walletDir), `Wallet ${walletName} already exists on ${channelContext.workspace.network}.`);
|
|
5505
5739
|
ensureDir(walletDir);
|
|
5506
5740
|
ensureDir(path.join(walletDir, "operations"));
|
|
@@ -5508,6 +5742,19 @@ function ensureWallet({
|
|
|
5508
5742
|
const wallet = normalizeWallet({
|
|
5509
5743
|
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
5510
5744
|
name: walletName,
|
|
5745
|
+
canonicalWalletName: walletName,
|
|
5746
|
+
walletEpochId: lifecycleEpoch.epochId,
|
|
5747
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
5748
|
+
joinedAtTxHash: lifecycleEpoch.joinedAtTxHash,
|
|
5749
|
+
joinedAtBlockNumber: lifecycleEpoch.joinedAtBlockNumber,
|
|
5750
|
+
joinedAtLogIndex: lifecycleEpoch.joinedAtLogIndex,
|
|
5751
|
+
joinedAtBlockTimestamp: lifecycleEpoch.joinedAtBlockTimestamp,
|
|
5752
|
+
joinedAtBlockTimestampIso: lifecycleEpoch.joinedAtBlockTimestampIso,
|
|
5753
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
5754
|
+
exitedAtBlockNumber: lifecycleEpoch.exitedAtBlockNumber,
|
|
5755
|
+
exitedAtLogIndex: lifecycleEpoch.exitedAtLogIndex,
|
|
5756
|
+
exitedAtBlockTimestamp: lifecycleEpoch.exitedAtBlockTimestamp,
|
|
5757
|
+
exitedAtBlockTimestampIso: lifecycleEpoch.exitedAtBlockTimestampIso,
|
|
5511
5758
|
network: channelContext.workspace.network,
|
|
5512
5759
|
rpcUrl,
|
|
5513
5760
|
chainId: channelContext.workspace.chainId,
|
|
@@ -5553,18 +5800,57 @@ function ensureWallet({
|
|
|
5553
5800
|
};
|
|
5554
5801
|
persistWalletKeys(context);
|
|
5555
5802
|
persistWallet(context);
|
|
5556
|
-
|
|
5803
|
+
persistWalletIndexForContext(context);
|
|
5557
5804
|
return context;
|
|
5558
5805
|
}
|
|
5559
5806
|
|
|
5807
|
+
function applyWalletLifecycleEpoch(wallet, epoch) {
|
|
5808
|
+
wallet.canonicalWalletName = wallet.name;
|
|
5809
|
+
wallet.walletEpochId = epoch.epochId;
|
|
5810
|
+
wallet.lifecycleStatus = epoch.lifecycleStatus;
|
|
5811
|
+
wallet.joinedAtTxHash = epoch.joinedAtTxHash;
|
|
5812
|
+
wallet.joinedAtBlockNumber = epoch.joinedAtBlockNumber;
|
|
5813
|
+
wallet.joinedAtLogIndex = epoch.joinedAtLogIndex;
|
|
5814
|
+
wallet.joinedAtBlockTimestamp = epoch.joinedAtBlockTimestamp;
|
|
5815
|
+
wallet.joinedAtBlockTimestampIso = epoch.joinedAtBlockTimestampIso;
|
|
5816
|
+
wallet.exitedAtTxHash = epoch.exitedAtTxHash;
|
|
5817
|
+
wallet.exitedAtBlockNumber = epoch.exitedAtBlockNumber;
|
|
5818
|
+
wallet.exitedAtLogIndex = epoch.exitedAtLogIndex;
|
|
5819
|
+
wallet.exitedAtBlockTimestamp = epoch.exitedAtBlockTimestamp;
|
|
5820
|
+
wallet.exitedAtBlockTimestampIso = epoch.exitedAtBlockTimestampIso;
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
function walletLifecycleMetadata(wallet) {
|
|
5824
|
+
expect(wallet.walletEpochId, "Current wallet workspace metadata is missing walletEpochId.");
|
|
5825
|
+
return {
|
|
5826
|
+
canonicalWalletName: wallet.canonicalWalletName ?? wallet.name,
|
|
5827
|
+
epochId: wallet.walletEpochId,
|
|
5828
|
+
lifecycleStatus: wallet.lifecycleStatus ?? "active",
|
|
5829
|
+
joinedAtTxHash: wallet.joinedAtTxHash ?? null,
|
|
5830
|
+
joinedAtBlockNumber: wallet.joinedAtBlockNumber ?? null,
|
|
5831
|
+
joinedAtLogIndex: wallet.joinedAtLogIndex ?? null,
|
|
5832
|
+
joinedAtBlockTimestamp: wallet.joinedAtBlockTimestamp ?? null,
|
|
5833
|
+
joinedAtBlockTimestampIso: wallet.joinedAtBlockTimestampIso ?? null,
|
|
5834
|
+
exitedAtTxHash: wallet.exitedAtTxHash ?? null,
|
|
5835
|
+
exitedAtBlockNumber: wallet.exitedAtBlockNumber ?? null,
|
|
5836
|
+
exitedAtLogIndex: wallet.exitedAtLogIndex ?? null,
|
|
5837
|
+
exitedAtBlockTimestamp: wallet.exitedAtBlockTimestamp ?? null,
|
|
5838
|
+
exitedAtBlockTimestampIso: wallet.exitedAtBlockTimestampIso ?? null,
|
|
5839
|
+
};
|
|
5840
|
+
}
|
|
5841
|
+
|
|
5560
5842
|
function normalizeWallet(wallet) {
|
|
5561
5843
|
assertWalletHasCurrentFormat(wallet, wallet.name ?? "unknown");
|
|
5844
|
+
expect(wallet.walletEpochId, "Current wallet metadata requires walletEpochId. Run wallet recover-workspace to rebuild this wallet.");
|
|
5562
5845
|
const unusedNotes = Object.values(wallet.notes.unused).map(normalizeTrackedNote);
|
|
5563
5846
|
unusedNotes.sort(compareNotesByValueDesc);
|
|
5564
5847
|
const spentNotes = Object.values(wallet.notes.spent).map(normalizeTrackedNote);
|
|
5565
5848
|
|
|
5566
5849
|
return {
|
|
5567
5850
|
...wallet,
|
|
5851
|
+
canonicalWalletName: wallet.canonicalWalletName ?? wallet.name,
|
|
5852
|
+
walletEpochId: wallet.walletEpochId,
|
|
5853
|
+
lifecycleStatus: wallet.lifecycleStatus ?? "active",
|
|
5568
5854
|
canonicalAssetDecimals: Number(wallet.canonicalAssetDecimals),
|
|
5569
5855
|
l2Nonce: Number(wallet.l2Nonce),
|
|
5570
5856
|
l2PrivateKey: wallet.l2PrivateKey ? ethers.hexlify(wallet.l2PrivateKey) : null,
|
|
@@ -7062,7 +7348,17 @@ async function loadJoinChannelContext({ args, network, provider }) {
|
|
|
7062
7348
|
function loadWallet(walletName, networkName) {
|
|
7063
7349
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7064
7350
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
7065
|
-
const walletDir =
|
|
7351
|
+
const walletDir = selectedWalletEpochDir(normalizedWalletName, normalizedNetworkName);
|
|
7352
|
+
return loadWalletFromDir({
|
|
7353
|
+
walletName: normalizedWalletName,
|
|
7354
|
+
networkName: normalizedNetworkName,
|
|
7355
|
+
walletDir,
|
|
7356
|
+
});
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
function loadWalletFromDir({ walletName, networkName, walletDir }) {
|
|
7360
|
+
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7361
|
+
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
7066
7362
|
if (!walletConfigExists(walletDir)) {
|
|
7067
7363
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
7068
7364
|
}
|
|
@@ -7077,11 +7373,11 @@ function loadWallet(walletName, networkName) {
|
|
|
7077
7373
|
walletName: normalizedWalletName,
|
|
7078
7374
|
keyKind: "viewing",
|
|
7079
7375
|
});
|
|
7080
|
-
if (spendingKey) {
|
|
7376
|
+
if (spendingKey && walletSpendingKeyMatchesWallet(spendingKey.metadata, rawWallet)) {
|
|
7081
7377
|
rawWallet.l2PrivateKey = spendingKey.privateKey;
|
|
7082
7378
|
rawWallet.l2PublicKey = spendingKey.metadata?.l2PublicKey ?? rawWallet.l2PublicKey;
|
|
7083
7379
|
}
|
|
7084
|
-
if (viewingKey) {
|
|
7380
|
+
if (viewingKey && walletViewingKeyMatchesWallet(viewingKey.metadata, rawWallet)) {
|
|
7085
7381
|
rawWallet.noteReceivePrivateKey = viewingKey.privateKey;
|
|
7086
7382
|
}
|
|
7087
7383
|
assertWalletHasRequiredKeys(rawWallet, normalizedWalletName);
|
|
@@ -7104,6 +7400,18 @@ function loadWallet(walletName, networkName) {
|
|
|
7104
7400
|
return context;
|
|
7105
7401
|
}
|
|
7106
7402
|
|
|
7403
|
+
function walletSpendingKeyMatchesWallet(metadata, wallet) {
|
|
7404
|
+
return metadata?.l2Address
|
|
7405
|
+
&& ethers.toBigInt(getAddress(metadata.l2Address)) === ethers.toBigInt(getAddress(wallet.l2Address));
|
|
7406
|
+
}
|
|
7407
|
+
|
|
7408
|
+
function walletViewingKeyMatchesWallet(metadata, wallet) {
|
|
7409
|
+
return metadata?.noteReceivePubKey?.x
|
|
7410
|
+
&& ethers.toBigInt(normalizeBytes32Hex(metadata.noteReceivePubKey.x))
|
|
7411
|
+
=== ethers.toBigInt(normalizeBytes32Hex(wallet.noteReceivePubKeyX))
|
|
7412
|
+
&& Number(metadata.noteReceivePubKey.yParity) === Number(wallet.noteReceivePubKeyYParity);
|
|
7413
|
+
}
|
|
7414
|
+
|
|
7107
7415
|
function loadUnlockedWalletWithMetadata(args) {
|
|
7108
7416
|
const networkName = requireNetworkName(args);
|
|
7109
7417
|
const wallet = loadWallet(requireWalletName(args), networkName);
|
|
@@ -7274,6 +7582,16 @@ function requireWalletViewingCapability(walletContext) {
|
|
|
7274
7582
|
);
|
|
7275
7583
|
}
|
|
7276
7584
|
|
|
7585
|
+
function requireActiveWalletLifecycle(walletContext, commandName) {
|
|
7586
|
+
expect(
|
|
7587
|
+
walletContext.wallet.lifecycleStatus !== "exited",
|
|
7588
|
+
[
|
|
7589
|
+
`${commandName} cannot operate on exited wallet epoch ${walletContext.wallet.walletEpochId ?? "unknown"}.`,
|
|
7590
|
+
"Exited wallet epochs are read-only. Use wallet get-notes or wallet get-notes --export-evidence for historical disclosure.",
|
|
7591
|
+
].join(" "),
|
|
7592
|
+
);
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7277
7595
|
function walletOperationSealSecret(walletContext) {
|
|
7278
7596
|
const secret = walletContext.wallet.l2PrivateKey
|
|
7279
7597
|
?? walletContext.wallet.noteReceivePrivateKey
|
|
@@ -7328,7 +7646,7 @@ function loadBridgeResources({ chainId }) {
|
|
|
7328
7646
|
function loadWalletMetadata(walletName, networkName) {
|
|
7329
7647
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7330
7648
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
7331
|
-
const walletDir =
|
|
7649
|
+
const walletDir = selectedWalletEpochDir(normalizedWalletName, normalizedNetworkName);
|
|
7332
7650
|
if (!walletConfigExists(walletDir)) {
|
|
7333
7651
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
7334
7652
|
}
|
|
@@ -8912,16 +9230,18 @@ function prepareJoinWalletSecretForName({
|
|
|
8912
9230
|
walletName,
|
|
8913
9231
|
}) {
|
|
8914
9232
|
const { channelName } = parseWalletName(walletName);
|
|
8915
|
-
const
|
|
9233
|
+
const walletRoot = walletRootPath(walletName, networkName);
|
|
9234
|
+
const walletIndex = fs.existsSync(walletRoot)
|
|
9235
|
+
? requireWalletIndex({ walletRoot, walletName, networkName })
|
|
9236
|
+
: null;
|
|
9237
|
+
const activeEpoch = walletIndex ? activeWalletEpoch(walletIndex) : null;
|
|
8916
9238
|
expect(
|
|
8917
|
-
!
|
|
9239
|
+
!activeEpoch,
|
|
8918
9240
|
[
|
|
8919
9241
|
`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.",
|
|
9242
|
+
"channel join creates a new active wallet epoch.",
|
|
8924
9243
|
"Use normal wallet commands for an existing active local wallet.",
|
|
9244
|
+
`For exited history, keep using wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}.`,
|
|
8925
9245
|
].join(" "),
|
|
8926
9246
|
);
|
|
8927
9247
|
const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
|
|
@@ -8932,7 +9252,7 @@ function channelWorkspacePath(networkName, name) {
|
|
|
8932
9252
|
return workspaceDirForName(workspaceRoot, networkName, name);
|
|
8933
9253
|
}
|
|
8934
9254
|
|
|
8935
|
-
function
|
|
9255
|
+
function walletRootPath(name, networkName) {
|
|
8936
9256
|
const walletName = String(name);
|
|
8937
9257
|
const { channelName } = parseWalletName(walletName);
|
|
8938
9258
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
@@ -8940,6 +9260,91 @@ function walletPath(name, networkName) {
|
|
|
8940
9260
|
return walletDirForName(workspaceWalletsDir(workspaceDir), walletName);
|
|
8941
9261
|
}
|
|
8942
9262
|
|
|
9263
|
+
function selectedWalletEpochDir(name, networkName) {
|
|
9264
|
+
const root = walletRootPath(name, networkName);
|
|
9265
|
+
const walletName = requireWalletName({ wallet: name });
|
|
9266
|
+
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
9267
|
+
expect(
|
|
9268
|
+
fs.existsSync(root),
|
|
9269
|
+
cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${walletName} on ${normalizedNetworkName}.`),
|
|
9270
|
+
);
|
|
9271
|
+
const index = requireWalletIndex({ walletRoot: root, walletName, networkName: normalizedNetworkName });
|
|
9272
|
+
const selected = selectedWalletEpoch(index, walletName, normalizedNetworkName);
|
|
9273
|
+
return walletEpochPathFromRoot(root, selected.epochId);
|
|
9274
|
+
}
|
|
9275
|
+
|
|
9276
|
+
function walletEpochPath(walletName, networkName, epochId) {
|
|
9277
|
+
return walletEpochPathFromRoot(walletRootPath(walletName, networkName), epochId);
|
|
9278
|
+
}
|
|
9279
|
+
|
|
9280
|
+
function walletEpochPathFromRoot(walletRoot, epochId) {
|
|
9281
|
+
return path.join(walletRoot, "epochs", slugifyPathComponent(epochId));
|
|
9282
|
+
}
|
|
9283
|
+
|
|
9284
|
+
function walletIndexMetadataPath(walletRoot) {
|
|
9285
|
+
return path.join(walletRoot, "wallet-index.metadata.json");
|
|
9286
|
+
}
|
|
9287
|
+
|
|
9288
|
+
function readWalletIndexIfExists(walletRoot) {
|
|
9289
|
+
const indexPath = walletIndexMetadataPath(walletRoot);
|
|
9290
|
+
if (!fs.existsSync(indexPath)) {
|
|
9291
|
+
return null;
|
|
9292
|
+
}
|
|
9293
|
+
return normalizeWalletIndex(readJson(indexPath));
|
|
9294
|
+
}
|
|
9295
|
+
|
|
9296
|
+
function requireWalletIndex({ walletRoot, walletName, networkName }) {
|
|
9297
|
+
const index = readWalletIndexIfExists(walletRoot);
|
|
9298
|
+
expect(index, currentWalletIndexRequiredMessage({ walletName, networkName, walletRoot }));
|
|
9299
|
+
return index;
|
|
9300
|
+
}
|
|
9301
|
+
|
|
9302
|
+
function selectedWalletEpoch(index, walletName, networkName) {
|
|
9303
|
+
const selected = activeWalletEpoch(index) ?? latestWalletEpoch(index);
|
|
9304
|
+
expect(
|
|
9305
|
+
selected,
|
|
9306
|
+
`Wallet ${walletName} on ${networkName} has no epoch entries. Run wallet recover-workspace to rebuild the workspace in the current format.`,
|
|
9307
|
+
);
|
|
9308
|
+
return selected;
|
|
9309
|
+
}
|
|
9310
|
+
|
|
9311
|
+
function currentWalletIndexRequiredMessage({ walletName, networkName, walletRoot }) {
|
|
9312
|
+
const channelName = parseWalletName(walletName).channelName;
|
|
9313
|
+
return [
|
|
9314
|
+
`Current wallet index is required for ${walletName} on ${networkName}: ${walletRoot}.`,
|
|
9315
|
+
`Run wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account <ACCOUNT> to rebuild the workspace.`,
|
|
9316
|
+
].join(" ");
|
|
9317
|
+
}
|
|
9318
|
+
|
|
9319
|
+
function activeWalletEpoch(index) {
|
|
9320
|
+
const activeEpochId = index?.activeEpochId ?? null;
|
|
9321
|
+
return activeEpochId
|
|
9322
|
+
? (index.epochs ?? []).find((epoch) => epoch.epochId === activeEpochId && epoch.lifecycleStatus === "active") ?? null
|
|
9323
|
+
: null;
|
|
9324
|
+
}
|
|
9325
|
+
|
|
9326
|
+
function latestWalletEpoch(index) {
|
|
9327
|
+
const epochs = [...(index?.epochs ?? [])];
|
|
9328
|
+
epochs.sort((left, right) =>
|
|
9329
|
+
Number(right.joinedAtBlockNumber ?? 0) - Number(left.joinedAtBlockNumber ?? 0)
|
|
9330
|
+
|| String(right.epochId).localeCompare(String(left.epochId)));
|
|
9331
|
+
return epochs[0] ?? null;
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
function normalizeWalletIndex(index) {
|
|
9335
|
+
expect(index?.format === WALLET_INDEX_FORMAT, "Invalid wallet index format.");
|
|
9336
|
+
expect(Number(index.formatVersion) === WALLET_INDEX_FORMAT_VERSION, "Unsupported wallet index format version.");
|
|
9337
|
+
expect(Array.isArray(index.epochs), "Wallet index is missing epochs[].");
|
|
9338
|
+
return {
|
|
9339
|
+
...index,
|
|
9340
|
+
epochs: index.epochs.map((epoch) => ({
|
|
9341
|
+
...epoch,
|
|
9342
|
+
epochId: String(epoch.epochId),
|
|
9343
|
+
lifecycleStatus: epoch.lifecycleStatus === "active" ? "active" : "exited",
|
|
9344
|
+
})),
|
|
9345
|
+
};
|
|
9346
|
+
}
|
|
9347
|
+
|
|
8943
9348
|
function accountPrivateKeyPath(networkName, accountName) {
|
|
8944
9349
|
return path.join(
|
|
8945
9350
|
secretRoot,
|
|
@@ -9013,7 +9418,8 @@ function resolveWalletPathCandidates(walletName) {
|
|
|
9013
9418
|
"wallets",
|
|
9014
9419
|
walletSlug,
|
|
9015
9420
|
);
|
|
9016
|
-
if (
|
|
9421
|
+
if (fs.existsSync(candidate)) {
|
|
9422
|
+
requireWalletIndex({ walletRoot: candidate, walletName, networkName: entry.name });
|
|
9017
9423
|
candidates.push(candidate);
|
|
9018
9424
|
}
|
|
9019
9425
|
}
|
|
@@ -9044,12 +9450,24 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
|
|
|
9044
9450
|
if (!walletEntry.isDirectory()) {
|
|
9045
9451
|
continue;
|
|
9046
9452
|
}
|
|
9047
|
-
const
|
|
9453
|
+
const walletRoot = path.join(walletsDir, walletEntry.name);
|
|
9454
|
+
const walletIndex = requireWalletIndex({
|
|
9455
|
+
walletRoot,
|
|
9456
|
+
walletName: walletEntry.name,
|
|
9457
|
+
networkName: networkEntry.name,
|
|
9458
|
+
});
|
|
9459
|
+
const selectedEpoch = selectedWalletEpoch(walletIndex, walletEntry.name, networkEntry.name);
|
|
9460
|
+
const walletDir = walletEpochPathFromRoot(walletRoot, selectedEpoch.epochId);
|
|
9048
9461
|
wallets.push({
|
|
9049
9462
|
wallet: walletEntry.name,
|
|
9050
9463
|
network: networkEntry.name,
|
|
9051
9464
|
channelName: channelEntry.name,
|
|
9052
9465
|
walletDir,
|
|
9466
|
+
walletRoot,
|
|
9467
|
+
activeEpochId: walletIndex?.activeEpochId ?? null,
|
|
9468
|
+
selectedEpochId: selectedEpoch?.epochId ?? null,
|
|
9469
|
+
lifecycleStatus: selectedEpoch.lifecycleStatus,
|
|
9470
|
+
epochs: walletIndex.epochs,
|
|
9053
9471
|
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9054
9472
|
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
9055
9473
|
hasEncryptedWallet: false,
|
|
@@ -9077,7 +9495,7 @@ function privateStateCliDataRoot() {
|
|
|
9077
9495
|
|
|
9078
9496
|
function resolveExportWalletInfo({ networkName, walletName }) {
|
|
9079
9497
|
resolveCliNetwork(networkName);
|
|
9080
|
-
const walletDir =
|
|
9498
|
+
const walletDir = selectedWalletEpochDir(walletName, networkName);
|
|
9081
9499
|
return {
|
|
9082
9500
|
wallet: walletName,
|
|
9083
9501
|
network: networkName,
|
|
@@ -9092,7 +9510,7 @@ function resolveExportWalletInfo({ networkName, walletName }) {
|
|
|
9092
9510
|
function normalizeExportWalletInfo(walletInfo) {
|
|
9093
9511
|
const wallet = requireWalletName({ wallet: walletInfo.wallet });
|
|
9094
9512
|
const network = requireNetworkName({ network: walletInfo.network });
|
|
9095
|
-
const walletDir = walletInfo.walletDir ??
|
|
9513
|
+
const walletDir = walletInfo.walletDir ?? selectedWalletEpochDir(wallet, network);
|
|
9096
9514
|
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
9097
9515
|
const metadata = readJsonIfExists(metadataPath);
|
|
9098
9516
|
const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
|
|
@@ -9119,6 +9537,13 @@ function walletBackupExportFilePaths(walletInfo) {
|
|
|
9119
9537
|
const walletFiles = [
|
|
9120
9538
|
walletNotesMetadataPath(walletInfo.walletDir),
|
|
9121
9539
|
];
|
|
9540
|
+
const walletRoot = walletRootPath(walletInfo.wallet, walletInfo.network);
|
|
9541
|
+
requireWalletIndex({
|
|
9542
|
+
walletRoot,
|
|
9543
|
+
walletName: walletInfo.wallet,
|
|
9544
|
+
networkName: walletInfo.network,
|
|
9545
|
+
});
|
|
9546
|
+
walletFiles.push(walletIndexMetadataPath(walletRoot));
|
|
9122
9547
|
for (const metadataPath of [
|
|
9123
9548
|
walletViewingKeyMetadataPath(walletInfo.walletDir),
|
|
9124
9549
|
walletSpendingKeyMetadataPath(walletInfo.walletDir),
|
|
@@ -9172,9 +9597,18 @@ function validateWalletExportManifest(manifest) {
|
|
|
9172
9597
|
validateWalletArchivePath(filePath);
|
|
9173
9598
|
}
|
|
9174
9599
|
for (const wallet of manifest.wallets) {
|
|
9175
|
-
requireNetworkName({ network: wallet.network });
|
|
9176
|
-
requireWalletName({ wallet: wallet.wallet });
|
|
9600
|
+
const networkName = requireNetworkName({ network: wallet.network });
|
|
9601
|
+
const walletName = requireWalletName({ wallet: wallet.wallet });
|
|
9177
9602
|
requireArg(wallet.channelName, "wallets[].channelName");
|
|
9603
|
+
const walletRoot = walletRootPath(walletName, networkName);
|
|
9604
|
+
const expectedIndexPath = archivePathForLocalCliFile(walletIndexMetadataPath(walletRoot));
|
|
9605
|
+
expect(
|
|
9606
|
+
uniqueFiles.has(expectedIndexPath),
|
|
9607
|
+
[
|
|
9608
|
+
"Wallet import ZIP must include the current wallet index metadata.",
|
|
9609
|
+
"Run wallet recover-workspace with the current CLI, then export a new backup.",
|
|
9610
|
+
].join(" "),
|
|
9611
|
+
);
|
|
9178
9612
|
}
|
|
9179
9613
|
}
|
|
9180
9614
|
|
|
@@ -9276,21 +9710,8 @@ function assertAllowedCommandKeys(args, commandName, allowedKeys, acceptedUsage)
|
|
|
9276
9710
|
);
|
|
9277
9711
|
}
|
|
9278
9712
|
|
|
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
9713
|
function assertWalletChannelMoveArgs(args, commandName) {
|
|
9293
|
-
|
|
9714
|
+
assertAllowedCommandSchema(args, commandName);
|
|
9294
9715
|
assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
|
|
9295
9716
|
}
|
|
9296
9717
|
|
|
@@ -9404,7 +9825,7 @@ function assertActionImpactArg(args, commandName) {
|
|
|
9404
9825
|
}
|
|
9405
9826
|
|
|
9406
9827
|
function assertWalletGetNotesArgs(args) {
|
|
9407
|
-
|
|
9828
|
+
assertAllowedCommandSchema(args, "wallet-get-notes");
|
|
9408
9829
|
if (args.exportEvidence !== undefined) {
|
|
9409
9830
|
requireArg(args.exportEvidence, "--export-evidence");
|
|
9410
9831
|
if (args.acknowledgeFullNotePlaintextExport !== true) {
|
|
@@ -9459,12 +9880,8 @@ function assertAccountGetBridgeFundArgs(args) {
|
|
|
9459
9880
|
assertAllowedCommandSchema(args, "account-get-bridge-fund");
|
|
9460
9881
|
}
|
|
9461
9882
|
|
|
9462
|
-
function assertExplicitSignerCommandArgs(args, commandName) {
|
|
9463
|
-
assertAllowedCommandSchema(args, commandName);
|
|
9464
|
-
}
|
|
9465
|
-
|
|
9466
9883
|
function assertRecoverWalletArgs(args) {
|
|
9467
|
-
|
|
9884
|
+
assertAllowedCommandSchema(args, "wallet-recover-workspace");
|
|
9468
9885
|
if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
|
|
9469
9886
|
throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
|
|
9470
9887
|
}
|
|
@@ -9476,7 +9893,7 @@ function assertJoinChannelArgs(args) {
|
|
|
9476
9893
|
}
|
|
9477
9894
|
|
|
9478
9895
|
function assertWalletGetMetaArgs(args) {
|
|
9479
|
-
|
|
9896
|
+
assertAllowedCommandSchema(args, "wallet-get-meta");
|
|
9480
9897
|
}
|
|
9481
9898
|
|
|
9482
9899
|
function assertAccountGetL1AddressArgs(args) {
|
|
@@ -9521,17 +9938,17 @@ function assertWithdrawBridgeArgs(args) {
|
|
|
9521
9938
|
}
|
|
9522
9939
|
|
|
9523
9940
|
function assertWalletGetChannelFundArgs(args) {
|
|
9524
|
-
|
|
9941
|
+
assertAllowedCommandSchema(args, "wallet-get-channel-fund");
|
|
9525
9942
|
}
|
|
9526
9943
|
|
|
9527
9944
|
function assertExitChannelArgs(args) {
|
|
9528
|
-
|
|
9945
|
+
assertAllowedCommandSchema(args, "channel-exit");
|
|
9529
9946
|
}
|
|
9530
9947
|
|
|
9531
9948
|
function createWalletOperationDir(walletName, networkName, suffix) {
|
|
9532
9949
|
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
9533
9950
|
const operationDir = path.join(
|
|
9534
|
-
|
|
9951
|
+
selectedWalletEpochDir(walletName, networkName),
|
|
9535
9952
|
"operations",
|
|
9536
9953
|
`${timestamp}-${slugifyPathComponent(suffix)}`,
|
|
9537
9954
|
);
|
|
@@ -9576,8 +9993,73 @@ function persistWalletKeys(context) {
|
|
|
9576
9993
|
}
|
|
9577
9994
|
}
|
|
9578
9995
|
|
|
9579
|
-
function
|
|
9580
|
-
|
|
9996
|
+
function persistWalletIndexForContext(context) {
|
|
9997
|
+
const walletRoot = walletRootPath(context.walletName, context.wallet.network);
|
|
9998
|
+
ensureDir(walletRoot);
|
|
9999
|
+
const currentIndex = readWalletIndexIfExists(walletRoot) ?? {
|
|
10000
|
+
format: WALLET_INDEX_FORMAT,
|
|
10001
|
+
formatVersion: WALLET_INDEX_FORMAT_VERSION,
|
|
10002
|
+
canonicalWalletName: context.walletName,
|
|
10003
|
+
network: context.wallet.network,
|
|
10004
|
+
channelName: context.wallet.channelName,
|
|
10005
|
+
channelId: context.wallet.channelId,
|
|
10006
|
+
l1Address: context.wallet.l1Address,
|
|
10007
|
+
activeEpochId: null,
|
|
10008
|
+
epochs: [],
|
|
10009
|
+
};
|
|
10010
|
+
const epoch = walletEpochSummaryFromWallet(context.wallet);
|
|
10011
|
+
const epochs = [
|
|
10012
|
+
...currentIndex.epochs.filter((entry) => entry.epochId !== epoch.epochId),
|
|
10013
|
+
epoch,
|
|
10014
|
+
].sort((left, right) =>
|
|
10015
|
+
Number(left.joinedAtBlockNumber ?? 0) - Number(right.joinedAtBlockNumber ?? 0)
|
|
10016
|
+
|| String(left.epochId).localeCompare(String(right.epochId)));
|
|
10017
|
+
const activeEpoch = epochs.find((entry) => entry.lifecycleStatus === "active") ?? null;
|
|
10018
|
+
const nextIndex = {
|
|
10019
|
+
...currentIndex,
|
|
10020
|
+
canonicalWalletName: context.walletName,
|
|
10021
|
+
network: context.wallet.network,
|
|
10022
|
+
channelName: context.wallet.channelName,
|
|
10023
|
+
channelId: context.wallet.channelId,
|
|
10024
|
+
l1Address: context.wallet.l1Address,
|
|
10025
|
+
activeEpochId: activeEpoch?.epochId ?? null,
|
|
10026
|
+
epochs,
|
|
10027
|
+
};
|
|
10028
|
+
writeJson(walletIndexMetadataPath(walletRoot), nextIndex);
|
|
10029
|
+
}
|
|
10030
|
+
|
|
10031
|
+
async function markWalletEpochExited({ walletContext, receipt, provider }) {
|
|
10032
|
+
const block = receipt?.blockNumber === null || receipt?.blockNumber === undefined
|
|
10033
|
+
? null
|
|
10034
|
+
: await provider.getBlock(receipt.blockNumber).catch(() => null);
|
|
10035
|
+
const exitedAtBlockTimestamp = block?.timestamp ?? null;
|
|
10036
|
+
walletContext.wallet.lifecycleStatus = "exited";
|
|
10037
|
+
walletContext.wallet.exitedAtTxHash = receipt?.hash ?? null;
|
|
10038
|
+
walletContext.wallet.exitedAtBlockNumber = receipt?.blockNumber ?? null;
|
|
10039
|
+
walletContext.wallet.exitedAtLogIndex = firstReceiptLogIndex(receipt);
|
|
10040
|
+
walletContext.wallet.exitedAtBlockTimestamp = exitedAtBlockTimestamp;
|
|
10041
|
+
walletContext.wallet.exitedAtBlockTimestampIso = exitedAtBlockTimestamp === null
|
|
10042
|
+
? null
|
|
10043
|
+
: new Date(Number(exitedAtBlockTimestamp) * 1000).toISOString();
|
|
10044
|
+
persistWallet(walletContext);
|
|
10045
|
+
persistWalletIndexForContext(walletContext);
|
|
10046
|
+
return walletEpochSummaryFromWallet(walletContext.wallet);
|
|
10047
|
+
}
|
|
10048
|
+
|
|
10049
|
+
function firstReceiptLogIndex(receipt) {
|
|
10050
|
+
const first = receipt?.logs?.[0] ?? null;
|
|
10051
|
+
return first?.index ?? first?.logIndex ?? null;
|
|
10052
|
+
}
|
|
10053
|
+
|
|
10054
|
+
function walletEpochSummaryFromWallet(wallet) {
|
|
10055
|
+
const lifecycle = walletLifecycleMetadata(wallet);
|
|
10056
|
+
return {
|
|
10057
|
+
...lifecycle,
|
|
10058
|
+
walletDirName: slugifyPathComponent(lifecycle.epochId),
|
|
10059
|
+
l2Address: wallet.l2Address,
|
|
10060
|
+
l2StorageKey: wallet.l2StorageKey,
|
|
10061
|
+
leafIndex: wallet.leafIndex,
|
|
10062
|
+
};
|
|
9581
10063
|
}
|
|
9582
10064
|
|
|
9583
10065
|
function sanitizeWalletForNotesMetadata(wallet) {
|
|
@@ -9633,6 +10115,7 @@ function buildWalletSpendingKeyMetadata(wallet) {
|
|
|
9633
10115
|
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
9634
10116
|
network: wallet.network,
|
|
9635
10117
|
wallet: wallet.name,
|
|
10118
|
+
...walletLifecycleMetadata(wallet),
|
|
9636
10119
|
channelName: wallet.channelName,
|
|
9637
10120
|
channelId: wallet.channelId,
|
|
9638
10121
|
l1Address: wallet.l1Address,
|
|
@@ -9650,6 +10133,7 @@ function buildWalletViewingKeyMetadata(wallet) {
|
|
|
9650
10133
|
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
9651
10134
|
network: wallet.network,
|
|
9652
10135
|
wallet: wallet.name,
|
|
10136
|
+
...walletLifecycleMetadata(wallet),
|
|
9653
10137
|
channelName: wallet.channelName,
|
|
9654
10138
|
channelId: wallet.channelId,
|
|
9655
10139
|
l1Address: wallet.l1Address,
|