@tokamak-private-dapps/private-state-cli 1.2.1 → 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 +53 -0
- package/README.md +194 -50
- package/cli-assistant.html +14 -9
- package/investigator/README.md +41 -0
- package/investigator/app.js +674 -0
- package/investigator/index.html +161 -0
- package/investigator/styles.css +249 -0
- package/lib/private-state-cli-command-registry.mjs +174 -54
- package/lib/private-state-cli-shared.mjs +0 -4
- package/package.json +2 -1
- package/private-state-bridge-cli.mjs +2205 -587
|
@@ -7,11 +7,11 @@ import process from "node:process";
|
|
|
7
7
|
import readline from "node:readline/promises";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
10
11
|
import AdmZip from "adm-zip";
|
|
11
12
|
import {
|
|
12
13
|
createHash,
|
|
13
14
|
createCipheriv,
|
|
14
|
-
createDecipheriv,
|
|
15
15
|
randomBytes,
|
|
16
16
|
scryptSync,
|
|
17
17
|
} from "node:crypto";
|
|
@@ -61,7 +61,6 @@ import {
|
|
|
61
61
|
workspaceDirForName,
|
|
62
62
|
workspaceWalletsDir,
|
|
63
63
|
walletDirForName,
|
|
64
|
-
walletMetadataPathForDir,
|
|
65
64
|
walletNameForChannelAndAddress,
|
|
66
65
|
} from "./lib/private-state-cli-shared.mjs";
|
|
67
66
|
import {
|
|
@@ -131,11 +130,19 @@ const secretRoot = path.resolve(os.homedir(), "tokamak-private-channels", "secre
|
|
|
131
130
|
const flatDeploymentArtifactPathsByChainId = new Map();
|
|
132
131
|
const PRIVATE_STATE_UNINSTALL_CONFIRMATION =
|
|
133
132
|
"I understand that the wallet secrets deleted due to this decision cannot be recovered";
|
|
133
|
+
const ACTION_IMPACT_CONFIRMATION =
|
|
134
|
+
"I understand the public and private impact of this action";
|
|
134
135
|
const PRIVATE_STATE_CLI_PACKAGE_NAME = privateStateCliPackageJson.name;
|
|
135
136
|
const GROTH16_PACKAGE_NAME = "@tokamak-private-dapps/groth16";
|
|
136
137
|
const TOKAMAK_ZKEVM_CLI_PACKAGE_NAME = "@tokamak-zk-evm/cli";
|
|
137
|
-
const
|
|
138
|
-
const
|
|
138
|
+
const WALLET_BACKUP_EXPORT_FORMAT = "tokamak-private-state-wallet-backup-export";
|
|
139
|
+
const WALLET_KEY_EXPORT_FORMAT = "tokamak-private-state-wallet-key-export";
|
|
140
|
+
const WALLET_INDEX_FORMAT = "tokamak-private-state-wallet-index";
|
|
141
|
+
const WALLET_EVIDENCE_BUNDLE_FORMAT = "tokamak-private-state-raw-evidence-bundle";
|
|
142
|
+
const WALLET_EXPORT_FORMAT_VERSION = 2;
|
|
143
|
+
const WALLET_INDEX_FORMAT_VERSION = 1;
|
|
144
|
+
const WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION = 2;
|
|
145
|
+
const WALLET_WORKSPACE_FORMAT_VERSION = 2;
|
|
139
146
|
const CHANNEL_WORKSPACE_MIRROR_PROTOCOL_VERSION = 2;
|
|
140
147
|
const CHANNEL_WORKSPACE_MIRROR_MANIFEST_PATH_PREFIX =
|
|
141
148
|
".well-known/tokamak-private-state/channel-workspace";
|
|
@@ -151,8 +158,6 @@ let activeCliArgs = {};
|
|
|
151
158
|
const CLI_ERROR_CODES = Object.freeze({
|
|
152
159
|
MISSING_RPC_URL: "MISSING_RPC_URL",
|
|
153
160
|
UNKNOWN_WALLET: "UNKNOWN_WALLET",
|
|
154
|
-
MISSING_WALLET_SECRET: "MISSING_WALLET_SECRET",
|
|
155
|
-
WALLET_DECRYPT_FAILED: "WALLET_DECRYPT_FAILED",
|
|
156
161
|
MISSING_DEPLOYMENT_ARTIFACTS: "MISSING_DEPLOYMENT_ARTIFACTS",
|
|
157
162
|
MISSING_CHANNEL_REGISTRATION: "MISSING_CHANNEL_REGISTRATION",
|
|
158
163
|
STALE_WORKSPACE: "STALE_WORKSPACE",
|
|
@@ -247,6 +252,206 @@ function printImmutableChannelPolicyWarning({
|
|
|
247
252
|
console.error(details.join("\n"));
|
|
248
253
|
}
|
|
249
254
|
|
|
255
|
+
const ACTION_IMPACT_SUMMARIES = Object.freeze({
|
|
256
|
+
"account-deposit-bridge": {
|
|
257
|
+
display: "account deposit-bridge",
|
|
258
|
+
l1PublicEvent: "Yes. ERC-20 approval and bridge vault funding transactions are public L1 events.",
|
|
259
|
+
privateNoteState: "No. This action only moves canonical tokens into the shared bridge vault.",
|
|
260
|
+
publicFields: ({ l1Address, amountInput, bridgeTokenVault }) => [
|
|
261
|
+
`L1 account: ${l1Address}`,
|
|
262
|
+
`Bridge token vault: ${bridgeTokenVault}`,
|
|
263
|
+
`Amount: ${amountInput}`,
|
|
264
|
+
"Approval and funding transaction hashes, block numbers, and event logs.",
|
|
265
|
+
],
|
|
266
|
+
notPublic: [
|
|
267
|
+
"No private note owner, value, salt, counterparty, or note provenance is created by this action.",
|
|
268
|
+
],
|
|
269
|
+
noteProvenance: "Not applicable for this bridge-edge action.",
|
|
270
|
+
cexWarning: "Do not use a centralized-exchange controlled address as a self-custody bridge source.",
|
|
271
|
+
policy: "No channel policy is accepted by this action.",
|
|
272
|
+
},
|
|
273
|
+
"account-withdraw-bridge": {
|
|
274
|
+
display: "account withdraw-bridge",
|
|
275
|
+
l1PublicEvent: "Yes. The bridge withdrawal transaction and claim event are public L1 data.",
|
|
276
|
+
privateNoteState: "No. This action claims shared bridge-vault balance to the local L1 account.",
|
|
277
|
+
publicFields: ({ l1Address, amountInput, bridgeTokenVault }) => [
|
|
278
|
+
`L1 recipient/account: ${l1Address}`,
|
|
279
|
+
`Bridge token vault: ${bridgeTokenVault}`,
|
|
280
|
+
`Amount: ${amountInput}`,
|
|
281
|
+
"Withdrawal transaction hash, block number, and event log.",
|
|
282
|
+
],
|
|
283
|
+
notPublic: [
|
|
284
|
+
"The private note path that produced any prior channel balance is not reconstructed from this event alone.",
|
|
285
|
+
],
|
|
286
|
+
noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this withdrawal alone.",
|
|
287
|
+
cexWarning: "Do not use a centralized-exchange deposit address as the direct bridge withdrawal target unless the user has explicitly accepted the compliance implications. Prefer a self-custody L1 wallet.",
|
|
288
|
+
policy: "No channel policy is accepted by this action.",
|
|
289
|
+
},
|
|
290
|
+
"channel-join": {
|
|
291
|
+
display: "channel join",
|
|
292
|
+
l1PublicEvent: "Yes. Channel join and token-vault registration transactions are public L1 data.",
|
|
293
|
+
privateNoteState: "No. This action registers identity and note-receive metadata; it does not create or spend notes.",
|
|
294
|
+
publicFields: ({ l1Address, l2Address, noteReceivePubKey, joinToll, channelName, channelId }) => [
|
|
295
|
+
`Channel: ${channelName} (${channelId})`,
|
|
296
|
+
`L1 account: ${l1Address}`,
|
|
297
|
+
`L2 address: ${l2Address}`,
|
|
298
|
+
`Note-receive public key: ${noteReceivePubKey}`,
|
|
299
|
+
`Join toll: ${joinToll}`,
|
|
300
|
+
],
|
|
301
|
+
notPublic: [
|
|
302
|
+
"Wallet secret, L2 spending private key, note-receive private key, and future note plaintext.",
|
|
303
|
+
],
|
|
304
|
+
noteProvenance: "Future note provenance is not made public by joining.",
|
|
305
|
+
policy: "Joining accepts the displayed immutable channel policy snapshot.",
|
|
306
|
+
},
|
|
307
|
+
"wallet-deposit-channel": {
|
|
308
|
+
display: "wallet deposit-channel",
|
|
309
|
+
l1PublicEvent: "Yes. The proof-backed channel accounting transaction is public L1 data.",
|
|
310
|
+
privateNoteState: "No. This action increases liquid channel accounting balance; it does not create notes.",
|
|
311
|
+
publicFields: ({ l1Address, l2Address, amountInput, channelName, channelId }) => [
|
|
312
|
+
`Channel: ${channelName} (${channelId})`,
|
|
313
|
+
`L1 submitter/account: ${l1Address}`,
|
|
314
|
+
`Registered L2 address: ${l2Address}`,
|
|
315
|
+
`Amount: ${amountInput}`,
|
|
316
|
+
"Transaction hash, accepted proof surface, and accounting root update.",
|
|
317
|
+
],
|
|
318
|
+
notPublic: [
|
|
319
|
+
"No note owner, value, salt, counterparty, or note provenance is created by this action.",
|
|
320
|
+
],
|
|
321
|
+
noteProvenance: "Not applicable; this action does not transfer note ownership.",
|
|
322
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
323
|
+
},
|
|
324
|
+
"wallet-withdraw-channel": {
|
|
325
|
+
display: "wallet withdraw-channel",
|
|
326
|
+
l1PublicEvent: "Yes. The proof-backed channel accounting transaction is public L1 data.",
|
|
327
|
+
privateNoteState: "No. This action decreases liquid channel accounting balance; it does not spend notes directly.",
|
|
328
|
+
publicFields: ({ l1Address, l2Address, amountInput, channelName, channelId }) => [
|
|
329
|
+
`Channel: ${channelName} (${channelId})`,
|
|
330
|
+
`L1 submitter/account: ${l1Address}`,
|
|
331
|
+
`Registered L2 address: ${l2Address}`,
|
|
332
|
+
`Amount: ${amountInput}`,
|
|
333
|
+
"Transaction hash, accepted proof surface, and accounting root update.",
|
|
334
|
+
],
|
|
335
|
+
notPublic: [
|
|
336
|
+
"Any prior private note path that produced the liquid balance is not reconstructed from this action alone.",
|
|
337
|
+
],
|
|
338
|
+
noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this withdrawal-channel action alone.",
|
|
339
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
340
|
+
},
|
|
341
|
+
"wallet-mint-notes": {
|
|
342
|
+
display: "wallet mint-notes",
|
|
343
|
+
l1PublicEvent: "Yes. executeChannelTransaction, accepted transition, commitments, encrypted note events, and root updates are public L1 data.",
|
|
344
|
+
privateNoteState: "Yes. This action creates private-state notes tracked by the local wallet.",
|
|
345
|
+
publicFields: ({ l1Address, l2Address, amounts, channelName, channelId }) => [
|
|
346
|
+
`Channel: ${channelName} (${channelId})`,
|
|
347
|
+
`L1 submitter/account: ${l1Address}`,
|
|
348
|
+
`Registered L2 address: ${l2Address}`,
|
|
349
|
+
`Requested note amounts: ${amounts}`,
|
|
350
|
+
"New commitments, encrypted note-delivery events, transaction hash, and root updates.",
|
|
351
|
+
],
|
|
352
|
+
notPublic: [
|
|
353
|
+
"Note owner, value, salt, plaintext note contents, and later note provenance are not public by default.",
|
|
354
|
+
],
|
|
355
|
+
noteProvenance: "Public observers cannot reconstruct later note provenance without user-controlled disclosure.",
|
|
356
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
357
|
+
},
|
|
358
|
+
"wallet-transfer-notes": {
|
|
359
|
+
display: "wallet transfer-notes",
|
|
360
|
+
l1PublicEvent: "Yes. executeChannelTransaction, nullifiers, output commitments, encrypted note events, and root updates are public L1 data.",
|
|
361
|
+
privateNoteState: "Yes. This action spends selected input notes and creates output notes.",
|
|
362
|
+
publicFields: ({ l1Address, l2Address, noteIds, amounts, channelName, channelId }) => [
|
|
363
|
+
`Channel: ${channelName} (${channelId})`,
|
|
364
|
+
`L1 submitter/account: ${l1Address}`,
|
|
365
|
+
`Registered L2 address: ${l2Address}`,
|
|
366
|
+
`Input note commitments: ${noteIds}`,
|
|
367
|
+
`Output amounts supplied to the CLI: ${amounts}`,
|
|
368
|
+
"Input nullifiers, output commitments, encrypted note-delivery events, transaction hash, and root updates.",
|
|
369
|
+
],
|
|
370
|
+
notPublic: [
|
|
371
|
+
"Sender-recipient relationship, recipient note plaintext, and note provenance are not public by default.",
|
|
372
|
+
],
|
|
373
|
+
noteProvenance: "Public observers cannot reconstruct private note counterparty relationships or provenance from public contract state alone.",
|
|
374
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
375
|
+
},
|
|
376
|
+
"wallet-redeem-notes": {
|
|
377
|
+
display: "wallet redeem-notes",
|
|
378
|
+
l1PublicEvent: "Yes. executeChannelTransaction, nullifier usage, accounting update, and root updates are public L1 data.",
|
|
379
|
+
privateNoteState: "Yes. This action consumes selected notes and credits liquid channel accounting balance.",
|
|
380
|
+
publicFields: ({ l1Address, l2Address, noteIds, channelName, channelId }) => [
|
|
381
|
+
`Channel: ${channelName} (${channelId})`,
|
|
382
|
+
`L1 submitter/account: ${l1Address}`,
|
|
383
|
+
`Registered L2 address: ${l2Address}`,
|
|
384
|
+
`Input note commitments: ${noteIds}`,
|
|
385
|
+
"Input nullifiers, accounting update, transaction hash, and root updates.",
|
|
386
|
+
],
|
|
387
|
+
notPublic: [
|
|
388
|
+
"The prior path by which the redeemed note was received is not public by default.",
|
|
389
|
+
],
|
|
390
|
+
noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this redeem action alone.",
|
|
391
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
async function requireActionImpactAcknowledgement(commandId, args, details = {}) {
|
|
396
|
+
const summary = ACTION_IMPACT_SUMMARIES[commandId];
|
|
397
|
+
if (!summary) {
|
|
398
|
+
throw new Error(`Missing action-impact summary for ${commandId}.`);
|
|
399
|
+
}
|
|
400
|
+
printActionImpactSummary(summary, details);
|
|
401
|
+
if (args.acknowledgeActionImpact === true) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (args.acknowledgeActionImpact !== undefined) {
|
|
405
|
+
throw new Error(`${summary.display} option --acknowledge-action-impact does not accept a value.`);
|
|
406
|
+
}
|
|
407
|
+
if (!process.stdin.isTTY) {
|
|
408
|
+
throw new Error(`${summary.display} requires --acknowledge-action-impact after reviewing the action-impact warning.`);
|
|
409
|
+
}
|
|
410
|
+
const prompt = [
|
|
411
|
+
`Type exactly: ${ACTION_IMPACT_CONFIRMATION}`,
|
|
412
|
+
"> ",
|
|
413
|
+
].join("\n");
|
|
414
|
+
const rl = readline.createInterface({
|
|
415
|
+
input: process.stdin,
|
|
416
|
+
output: process.stderr,
|
|
417
|
+
terminal: process.stdin.isTTY && process.stderr.isTTY,
|
|
418
|
+
});
|
|
419
|
+
try {
|
|
420
|
+
const answer = await rl.question(prompt);
|
|
421
|
+
if (answer !== ACTION_IMPACT_CONFIRMATION) {
|
|
422
|
+
throw new Error(`${summary.display} action-impact confirmation did not match. No transaction was submitted.`);
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
rl.close();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function printActionImpactSummary(summary, details) {
|
|
430
|
+
const lines = [
|
|
431
|
+
`ACTION IMPACT SUMMARY: ${summary.display}`,
|
|
432
|
+
`- L1 public event: ${summary.l1PublicEvent}`,
|
|
433
|
+
`- Private note state change: ${summary.privateNoteState}`,
|
|
434
|
+
"- Public addresses and amounts:",
|
|
435
|
+
...normalizeImpactLines(summary.publicFields, details).map((line) => ` - ${line}`),
|
|
436
|
+
"- Not public by default:",
|
|
437
|
+
...normalizeImpactLines(summary.notPublic, details).map((line) => ` - ${line}`),
|
|
438
|
+
`- Note provenance: ${summary.noteProvenance}`,
|
|
439
|
+
`- Illegal-use prohibition: Do not use this command for money laundering, sanctions evasion, terrorist financing, illegal gambling, criminal-proceeds concealment, or regulatory evasion.`,
|
|
440
|
+
`- Secret recovery: Losing wallet secrets, viewing keys, or spending keys can prevent note discovery or note use. The CLI cannot recover lost secrets.`,
|
|
441
|
+
`- Channel policy: ${summary.policy}`,
|
|
442
|
+
];
|
|
443
|
+
if (summary.cexWarning) {
|
|
444
|
+
lines.push(`- CEX address warning: ${summary.cexWarning}`);
|
|
445
|
+
}
|
|
446
|
+
lines.push(`- Confirmation: pass --acknowledge-action-impact or type the exact confirmation phrase when prompted.`);
|
|
447
|
+
console.error(lines.join("\n"));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function normalizeImpactLines(value, details) {
|
|
451
|
+
const resolved = typeof value === "function" ? value(details) : value;
|
|
452
|
+
return Array.isArray(resolved) ? resolved : [resolved];
|
|
453
|
+
}
|
|
454
|
+
|
|
250
455
|
function normalizeDAppPolicySnapshot({
|
|
251
456
|
dappId,
|
|
252
457
|
metadataDigest,
|
|
@@ -386,6 +591,12 @@ async function main() {
|
|
|
386
591
|
return;
|
|
387
592
|
}
|
|
388
593
|
|
|
594
|
+
if (args.command === "investigator") {
|
|
595
|
+
assertInvestigatorArgs(args);
|
|
596
|
+
handleInvestigator();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
389
600
|
if (args.command === "account-get-l1-address") {
|
|
390
601
|
assertAccountGetL1AddressArgs(args);
|
|
391
602
|
handleAccountGetL1Address({ args });
|
|
@@ -404,15 +615,39 @@ async function main() {
|
|
|
404
615
|
return;
|
|
405
616
|
}
|
|
406
617
|
|
|
407
|
-
if (args.command === "wallet-export") {
|
|
408
|
-
|
|
409
|
-
|
|
618
|
+
if (args.command === "wallet-export-backup") {
|
|
619
|
+
assertWalletExportBackupArgs(args);
|
|
620
|
+
handleWalletExportBackup({ args });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (args.command === "wallet-export-viewing-key") {
|
|
625
|
+
assertWalletExportKeyArgs(args, "wallet-export-viewing-key");
|
|
626
|
+
handleWalletExportKey({ args, keyKind: "viewing" });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (args.command === "wallet-export-spending-key") {
|
|
631
|
+
assertWalletExportKeyArgs(args, "wallet-export-spending-key");
|
|
632
|
+
handleWalletExportKey({ args, keyKind: "spending" });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (args.command === "wallet-import-backup") {
|
|
637
|
+
assertWalletImportBackupArgs(args);
|
|
638
|
+
handleWalletImportBackup({ args });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (args.command === "wallet-import-viewing-key") {
|
|
643
|
+
assertWalletImportKeyArgs(args, "wallet-import-viewing-key");
|
|
644
|
+
handleWalletImportKey({ args, keyKind: "viewing" });
|
|
410
645
|
return;
|
|
411
646
|
}
|
|
412
647
|
|
|
413
|
-
if (args.command === "wallet-import") {
|
|
414
|
-
|
|
415
|
-
|
|
648
|
+
if (args.command === "wallet-import-spending-key") {
|
|
649
|
+
assertWalletImportKeyArgs(args, "wallet-import-spending-key");
|
|
650
|
+
handleWalletImportKey({ args, keyKind: "spending" });
|
|
416
651
|
return;
|
|
417
652
|
}
|
|
418
653
|
|
|
@@ -2161,6 +2396,11 @@ async function handleDepositBridge({ args, network, provider }) {
|
|
|
2161
2396
|
const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId: network.chainId });
|
|
2162
2397
|
const amountInput = requireArg(args.amount, "--amount");
|
|
2163
2398
|
const amount = parseTokenAmount(amountInput, Number(bridgeVaultContext.canonicalAssetDecimals));
|
|
2399
|
+
await requireActionImpactAcknowledgement("account-deposit-bridge", args, {
|
|
2400
|
+
l1Address: signer.address,
|
|
2401
|
+
amountInput,
|
|
2402
|
+
bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
|
|
2403
|
+
});
|
|
2164
2404
|
const bridgeTokenVault = new Contract(
|
|
2165
2405
|
bridgeVaultContext.bridgeTokenVaultAddress,
|
|
2166
2406
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
@@ -2223,10 +2463,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2223
2463
|
const channelName = requireArg(args.channelName, "--channel-name");
|
|
2224
2464
|
const signer = requireL1Signer(args, provider);
|
|
2225
2465
|
const walletName = walletNameForChannelAndAddress(channelName, signer.address);
|
|
2226
|
-
const walletSecret = resolveWalletSecretForName({
|
|
2227
|
-
networkName: network.name,
|
|
2228
|
-
walletName,
|
|
2229
|
-
});
|
|
2230
2466
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
2231
2467
|
const initialized = await syncChannelWorkspace({
|
|
2232
2468
|
workspaceName: channelName,
|
|
@@ -2260,11 +2496,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2260
2496
|
provider,
|
|
2261
2497
|
),
|
|
2262
2498
|
};
|
|
2263
|
-
const l2Identity = await deriveParticipantIdentityFromSigner({
|
|
2264
|
-
channelName,
|
|
2265
|
-
walletSecret,
|
|
2266
|
-
signer,
|
|
2267
|
-
});
|
|
2268
2499
|
const noteReceiveKeyMaterial = await deriveNoteReceiveKeyMaterial({
|
|
2269
2500
|
signer,
|
|
2270
2501
|
chainId: network.chainId,
|
|
@@ -2272,82 +2503,52 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2272
2503
|
channelName,
|
|
2273
2504
|
account: signer.address,
|
|
2274
2505
|
});
|
|
2275
|
-
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
2276
|
-
const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
|
|
2277
2506
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
status: "stale-wallet-removed",
|
|
2285
|
-
wallet: walletName,
|
|
2286
|
-
removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
|
|
2287
|
-
removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
|
|
2288
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2289
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2290
|
-
workspace: context.workspaceName,
|
|
2291
|
-
channelName: context.workspace.channelName,
|
|
2292
|
-
channelId: context.workspace.channelId,
|
|
2293
|
-
l1Address: signer.address,
|
|
2294
|
-
l2Address: l2Identity.l2Address,
|
|
2295
|
-
l2StorageKey: storageKey,
|
|
2296
|
-
leafIndex: leafIndex.toString(),
|
|
2297
|
-
reason: "The local wallet existed, but the L1 address is no longer registered in the channel.",
|
|
2298
|
-
nextAction: buildRecoverWalletRemovedNextAction({
|
|
2299
|
-
channelName,
|
|
2300
|
-
networkName: network.name,
|
|
2301
|
-
accountName: args.account,
|
|
2302
|
-
}),
|
|
2303
|
-
});
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
expect(
|
|
2307
|
-
false,
|
|
2308
|
-
cliError(
|
|
2309
|
-
CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION,
|
|
2310
|
-
`No channelTokenVault registration exists for ${signer.address}. Run channel join first.`,
|
|
2311
|
-
),
|
|
2312
|
-
);
|
|
2313
|
-
}
|
|
2314
|
-
expect(
|
|
2315
|
-
ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
|
|
2316
|
-
"The existing channel registration L2 address does not match the derived L2 address.",
|
|
2317
|
-
);
|
|
2318
|
-
expect(
|
|
2319
|
-
ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2320
|
-
=== ethers.toBigInt(normalizeBytes32Hex(storageKey)),
|
|
2321
|
-
"The existing channel registration key does not match the derived channelTokenVault key.",
|
|
2322
|
-
);
|
|
2507
|
+
const lifecycleEpoch = await resolveWalletLifecycleEpoch({
|
|
2508
|
+
context,
|
|
2509
|
+
provider,
|
|
2510
|
+
l1Address: signer.address,
|
|
2511
|
+
registration,
|
|
2512
|
+
});
|
|
2323
2513
|
expect(
|
|
2324
|
-
|
|
2325
|
-
|
|
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
|
+
),
|
|
2326
2519
|
);
|
|
2520
|
+
const registeredNoteReceivePubKey = lifecycleEpoch.noteReceivePubKey;
|
|
2327
2521
|
expect(
|
|
2328
|
-
ethers.toBigInt(normalizeBytes32Hex(
|
|
2522
|
+
ethers.toBigInt(normalizeBytes32Hex(registeredNoteReceivePubKey.x))
|
|
2329
2523
|
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
2330
2524
|
"The existing note-receive public key X does not match the derived note-receive public key.",
|
|
2331
2525
|
);
|
|
2332
2526
|
expect(
|
|
2333
|
-
Number(
|
|
2527
|
+
Number(registeredNoteReceivePubKey.yParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2334
2528
|
"The existing note-receive public key parity does not match the derived note-receive public key.",
|
|
2335
2529
|
);
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2530
|
+
const l2Identity = {
|
|
2531
|
+
l2PrivateKey: null,
|
|
2532
|
+
l2PublicKey: null,
|
|
2533
|
+
l2Address: getAddress(lifecycleEpoch.l2Address),
|
|
2534
|
+
};
|
|
2535
|
+
const storageKey = normalizeBytes32Hex(lifecycleEpoch.channelTokenVaultKey);
|
|
2536
|
+
|
|
2537
|
+
const walletDir = walletEpochPath(walletName, context.workspace.network, lifecycleEpoch.epochId);
|
|
2538
|
+
const existingWallet = walletConfigExists(walletDir)
|
|
2539
|
+
? loadWalletFromDir({
|
|
2540
|
+
walletName,
|
|
2541
|
+
networkName: context.workspace.network,
|
|
2542
|
+
walletDir,
|
|
2543
|
+
})
|
|
2544
|
+
: null;
|
|
2349
2545
|
|
|
2350
2546
|
if (existingWallet) {
|
|
2547
|
+
existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2548
|
+
applyWalletLifecycleEpoch(existingWallet.wallet, lifecycleEpoch);
|
|
2549
|
+
persistWalletKeys(existingWallet);
|
|
2550
|
+
persistWallet(existingWallet);
|
|
2551
|
+
persistWalletIndexForContext(existingWallet);
|
|
2351
2552
|
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2352
2553
|
walletContext: existingWallet,
|
|
2353
2554
|
context,
|
|
@@ -2362,15 +2563,16 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2362
2563
|
status: "already-recovered",
|
|
2363
2564
|
wallet: walletName,
|
|
2364
2565
|
walletDir: existingWallet.walletDir,
|
|
2365
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2366
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2367
2566
|
workspace: context.workspaceName,
|
|
2368
2567
|
channelName: context.workspace.channelName,
|
|
2369
2568
|
channelId: context.workspace.channelId,
|
|
2370
2569
|
l1Address: signer.address,
|
|
2371
2570
|
l2Address: l2Identity.l2Address,
|
|
2372
2571
|
l2StorageKey: storageKey,
|
|
2373
|
-
leafIndex:
|
|
2572
|
+
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2573
|
+
epochId: lifecycleEpoch.epochId,
|
|
2574
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2575
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2374
2576
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2375
2577
|
l2Nonce: existingWallet.wallet.l2Nonce,
|
|
2376
2578
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
@@ -2380,17 +2582,16 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2380
2582
|
return;
|
|
2381
2583
|
}
|
|
2382
2584
|
|
|
2383
|
-
fs.rmSync(walletPath(walletName, context.workspace.network), { recursive: true, force: true });
|
|
2384
|
-
|
|
2385
2585
|
const walletContext = ensureWallet({
|
|
2386
2586
|
channelContext: context,
|
|
2387
2587
|
signerAddress: signer.address,
|
|
2388
2588
|
signerPrivateKey: signer.privateKey,
|
|
2389
2589
|
l2Identity,
|
|
2390
|
-
walletSecret,
|
|
2590
|
+
walletSecret: noteReceiveKeyMaterial.privateKey,
|
|
2391
2591
|
storageKey,
|
|
2392
|
-
leafIndex:
|
|
2592
|
+
leafIndex: lifecycleEpoch.leafIndex,
|
|
2393
2593
|
noteReceiveKeyMaterial,
|
|
2594
|
+
lifecycleEpoch,
|
|
2394
2595
|
rpcUrl,
|
|
2395
2596
|
});
|
|
2396
2597
|
walletContext.wallet.l2Nonce = 0;
|
|
@@ -2411,15 +2612,16 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2411
2612
|
status: "recovered",
|
|
2412
2613
|
wallet: walletName,
|
|
2413
2614
|
walletDir: walletContext.walletDir,
|
|
2414
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2415
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2416
2615
|
workspace: context.workspaceName,
|
|
2417
2616
|
channelName: context.workspace.channelName,
|
|
2418
2617
|
channelId: context.workspace.channelId,
|
|
2419
2618
|
l1Address: signer.address,
|
|
2420
2619
|
l2Address: l2Identity.l2Address,
|
|
2421
2620
|
l2StorageKey: storageKey,
|
|
2422
|
-
leafIndex:
|
|
2621
|
+
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2622
|
+
epochId: lifecycleEpoch.epochId,
|
|
2623
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2624
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2423
2625
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2424
2626
|
l2Nonce: walletContext.wallet.l2Nonce,
|
|
2425
2627
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
@@ -2428,161 +2630,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2428
2630
|
});
|
|
2429
2631
|
}
|
|
2430
2632
|
|
|
2431
|
-
function tryLoadRecoverableWallet({
|
|
2432
|
-
walletName,
|
|
2433
|
-
walletSecret,
|
|
2434
|
-
signerAddress,
|
|
2435
|
-
signerPrivateKey,
|
|
2436
|
-
l2Identity,
|
|
2437
|
-
storageKey,
|
|
2438
|
-
leafIndex,
|
|
2439
|
-
rpcUrl,
|
|
2440
|
-
channelContext,
|
|
2441
|
-
noteReceiveKeyMaterial,
|
|
2442
|
-
}) {
|
|
2443
|
-
const walletDir = walletPath(walletName, channelContext.workspace.network);
|
|
2444
|
-
if (!walletConfigExists(walletDir)) {
|
|
2445
|
-
return null;
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
try {
|
|
2449
|
-
const walletMetadata = loadWalletMetadata(walletName, channelContext.workspace.network);
|
|
2450
|
-
const walletContext = loadWallet(walletName, walletSecret, channelContext.workspace.network);
|
|
2451
|
-
assertWalletMatchesMetadata(walletContext, walletMetadata);
|
|
2452
|
-
assertExistingRecoverableWallet({
|
|
2453
|
-
walletContext,
|
|
2454
|
-
walletMetadata,
|
|
2455
|
-
signerAddress,
|
|
2456
|
-
signerPrivateKey,
|
|
2457
|
-
l2Identity,
|
|
2458
|
-
storageKey,
|
|
2459
|
-
leafIndex,
|
|
2460
|
-
rpcUrl,
|
|
2461
|
-
channelContext,
|
|
2462
|
-
noteReceiveKeyMaterial,
|
|
2463
|
-
});
|
|
2464
|
-
return walletContext;
|
|
2465
|
-
} catch {
|
|
2466
|
-
return null;
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
function assertExistingRecoverableWallet({
|
|
2471
|
-
walletContext,
|
|
2472
|
-
walletMetadata,
|
|
2473
|
-
signerAddress,
|
|
2474
|
-
signerPrivateKey,
|
|
2475
|
-
l2Identity,
|
|
2476
|
-
storageKey,
|
|
2477
|
-
leafIndex,
|
|
2478
|
-
rpcUrl,
|
|
2479
|
-
channelContext,
|
|
2480
|
-
noteReceiveKeyMaterial,
|
|
2481
|
-
}) {
|
|
2482
|
-
const wallet = walletContext.wallet;
|
|
2483
|
-
expect(
|
|
2484
|
-
walletMetadata.network === channelContext.workspace.network,
|
|
2485
|
-
`Wallet ${walletContext.walletName} metadata network does not match the requested network.`,
|
|
2486
|
-
);
|
|
2487
|
-
expect(
|
|
2488
|
-
walletMetadata.channelName === channelContext.workspace.channelName,
|
|
2489
|
-
`Wallet ${walletContext.walletName} metadata channel does not match the requested channel.`,
|
|
2490
|
-
);
|
|
2491
|
-
expect(
|
|
2492
|
-
walletMetadata.rpcUrl === rpcUrl,
|
|
2493
|
-
`Wallet ${walletContext.walletName} metadata rpcUrl does not match the requested runtime RPC URL.`,
|
|
2494
|
-
);
|
|
2495
|
-
expect(
|
|
2496
|
-
normalizePrivateKey(wallet.l1PrivateKey) === normalizePrivateKey(signerPrivateKey),
|
|
2497
|
-
`Wallet ${walletContext.walletName} does not decrypt to the requested L1 private key.`,
|
|
2498
|
-
);
|
|
2499
|
-
expect(
|
|
2500
|
-
ethers.toBigInt(getAddress(wallet.l1Address)) === ethers.toBigInt(getAddress(signerAddress)),
|
|
2501
|
-
`Wallet ${walletContext.walletName} L1 address does not match the requested signer.`,
|
|
2502
|
-
);
|
|
2503
|
-
expect(
|
|
2504
|
-
ethers.toBigInt(getAddress(wallet.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
|
|
2505
|
-
`Wallet ${walletContext.walletName} L2 address does not match the derived channel identity.`,
|
|
2506
|
-
);
|
|
2507
|
-
expect(
|
|
2508
|
-
ethers.toBigInt(normalizeBytes32Hex(wallet.l2StorageKey))
|
|
2509
|
-
=== ethers.toBigInt(normalizeBytes32Hex(storageKey)),
|
|
2510
|
-
`Wallet ${walletContext.walletName} storage key does not match the derived registration key.`,
|
|
2511
|
-
);
|
|
2512
|
-
expect(
|
|
2513
|
-
ethers.toBigInt(wallet.leafIndex) === ethers.toBigInt(leafIndex),
|
|
2514
|
-
`Wallet ${walletContext.walletName} leaf index does not match the derived registration leaf index.`,
|
|
2515
|
-
);
|
|
2516
|
-
expect(
|
|
2517
|
-
ethers.toBigInt(wallet.channelId) === ethers.toBigInt(channelContext.workspace.channelId),
|
|
2518
|
-
`Wallet ${walletContext.walletName} channel ID does not match the requested channel.`,
|
|
2519
|
-
);
|
|
2520
|
-
expect(
|
|
2521
|
-
wallet.channelName === channelContext.workspace.channelName,
|
|
2522
|
-
`Wallet ${walletContext.walletName} channel name does not match the requested channel.`,
|
|
2523
|
-
);
|
|
2524
|
-
expect(
|
|
2525
|
-
wallet.network === channelContext.workspace.network,
|
|
2526
|
-
`Wallet ${walletContext.walletName} network does not match the requested network.`,
|
|
2527
|
-
);
|
|
2528
|
-
expect(
|
|
2529
|
-
wallet.rpcUrl === rpcUrl,
|
|
2530
|
-
`Wallet ${walletContext.walletName} rpcUrl does not match the requested runtime RPC URL.`,
|
|
2531
|
-
);
|
|
2532
|
-
expect(
|
|
2533
|
-
ethers.toBigInt(getAddress(wallet.channelManager)) === ethers.toBigInt(getAddress(channelContext.workspace.channelManager)),
|
|
2534
|
-
`Wallet ${walletContext.walletName} channel manager does not match the recovered workspace.`,
|
|
2535
|
-
);
|
|
2536
|
-
expect(
|
|
2537
|
-
ethers.toBigInt(getAddress(wallet.bridgeTokenVault)) === ethers.toBigInt(getAddress(channelContext.workspace.bridgeTokenVault)),
|
|
2538
|
-
`Wallet ${walletContext.walletName} bridge token vault does not match the recovered workspace.`,
|
|
2539
|
-
);
|
|
2540
|
-
expect(
|
|
2541
|
-
ethers.toBigInt(getAddress(wallet.controller)) === ethers.toBigInt(getAddress(channelContext.workspace.controller)),
|
|
2542
|
-
`Wallet ${walletContext.walletName} controller does not match the recovered workspace.`,
|
|
2543
|
-
);
|
|
2544
|
-
expect(
|
|
2545
|
-
ethers.toBigInt(getAddress(wallet.l2AccountingVault))
|
|
2546
|
-
=== ethers.toBigInt(getAddress(channelContext.workspace.l2AccountingVault)),
|
|
2547
|
-
`Wallet ${walletContext.walletName} L2 accounting vault does not match the recovered workspace.`,
|
|
2548
|
-
);
|
|
2549
|
-
expect(
|
|
2550
|
-
ethers.toBigInt(normalizeBytes32Hex(wallet.noteReceivePubKeyX))
|
|
2551
|
-
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
2552
|
-
`Wallet ${walletContext.walletName} note-receive public key X does not match the derived key.`,
|
|
2553
|
-
);
|
|
2554
|
-
expect(
|
|
2555
|
-
Number(wallet.noteReceivePubKeyYParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2556
|
-
`Wallet ${walletContext.walletName} note-receive public key parity does not match the derived key.`,
|
|
2557
|
-
);
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
function removeLocalWalletArtifacts(walletName, networkName) {
|
|
2561
|
-
const walletDir = walletPath(walletName, networkName);
|
|
2562
|
-
const walletSecretFile = walletSecretPath(networkName, walletName);
|
|
2563
|
-
const walletSecretDir = path.dirname(walletSecretFile);
|
|
2564
|
-
const removedWalletDir = fs.existsSync(walletDir);
|
|
2565
|
-
const removedWalletSecret = fs.existsSync(walletSecretFile) || fs.existsSync(walletSecretDir);
|
|
2566
|
-
if (removedWalletDir) {
|
|
2567
|
-
fs.rmSync(walletDir, { recursive: true, force: true });
|
|
2568
|
-
}
|
|
2569
|
-
if (removedWalletSecret) {
|
|
2570
|
-
fs.rmSync(walletSecretDir, { recursive: true, force: true });
|
|
2571
|
-
}
|
|
2572
|
-
return {
|
|
2573
|
-
walletDir,
|
|
2574
|
-
walletSecretFile,
|
|
2575
|
-
removed: removedWalletDir || removedWalletSecret,
|
|
2576
|
-
removedWalletDir,
|
|
2577
|
-
removedWalletSecret,
|
|
2578
|
-
};
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
function buildRecoverWalletRemovedNextAction({ channelName, networkName, accountName }) {
|
|
2582
|
-
const account = accountName ? String(accountName) : "<ACCOUNT>";
|
|
2583
|
-
return `channel join --channel-name ${channelName} --network ${networkName} --account ${account} --wallet-secret-path <PATH>`;
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
2633
|
async function handleInstallZkEvm({ args }) {
|
|
2587
2634
|
const selectedVersions = await resolvePrivateStateInstallRuntimeVersions(args);
|
|
2588
2635
|
const tokamakCliRuntime = await installTokamakCliRuntimeForPrivateState({
|
|
@@ -2880,6 +2927,68 @@ async function handleDoctor({ args }) {
|
|
|
2880
2927
|
}
|
|
2881
2928
|
}
|
|
2882
2929
|
|
|
2930
|
+
function handleInvestigator() {
|
|
2931
|
+
const htmlPath = resolveInvestigatorIndexPath();
|
|
2932
|
+
const fileUrl = pathToFileURL(htmlPath).href;
|
|
2933
|
+
const browser = openFileInDefaultBrowser(fileUrl);
|
|
2934
|
+
printJson({
|
|
2935
|
+
action: "investigator",
|
|
2936
|
+
htmlPath,
|
|
2937
|
+
fileUrl,
|
|
2938
|
+
browserOpened: browser.opened,
|
|
2939
|
+
browserOpenCommand: browser.command,
|
|
2940
|
+
browserOpenError: browser.error,
|
|
2941
|
+
nextSteps: [
|
|
2942
|
+
"Create a raw evidence ZIP with wallet get-notes --export-evidence and --acknowledge-full-note-plaintext-export.",
|
|
2943
|
+
"Load the raw evidence ZIP in the browser investigator.",
|
|
2944
|
+
"Filter the raw bundle and export a user-consent disclosure ZIP.",
|
|
2945
|
+
"Do not submit the raw evidence ZIP unless full wallet-history disclosure is intended.",
|
|
2946
|
+
],
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function resolveInvestigatorIndexPath() {
|
|
2951
|
+
const candidates = [
|
|
2952
|
+
path.join(privateStateCliPackageRoot, "investigator", "index.html"),
|
|
2953
|
+
path.resolve(privateStateCliPackageRoot, "..", "investigator", "index.html"),
|
|
2954
|
+
];
|
|
2955
|
+
const htmlPath = candidates.find((candidate) => fs.existsSync(candidate));
|
|
2956
|
+
if (!htmlPath) {
|
|
2957
|
+
throw new Error(
|
|
2958
|
+
[
|
|
2959
|
+
"Missing investigator HTML asset.",
|
|
2960
|
+
`Checked: ${candidates.join(", ")}`,
|
|
2961
|
+
"Reinstall the private-state CLI package or run from a complete repository checkout.",
|
|
2962
|
+
].join(" "),
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
return htmlPath;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function openFileInDefaultBrowser(fileUrl) {
|
|
2969
|
+
const opener = defaultBrowserOpenCommand(fileUrl);
|
|
2970
|
+
const result = spawnSync(opener.command, opener.args, {
|
|
2971
|
+
stdio: "ignore",
|
|
2972
|
+
windowsHide: true,
|
|
2973
|
+
});
|
|
2974
|
+
return {
|
|
2975
|
+
command: [opener.command, ...opener.args].join(" "),
|
|
2976
|
+
opened: result.status === 0,
|
|
2977
|
+
status: result.status,
|
|
2978
|
+
error: result.error?.message ?? null,
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
function defaultBrowserOpenCommand(fileUrl) {
|
|
2983
|
+
if (process.platform === "darwin") {
|
|
2984
|
+
return { command: "open", args: [fileUrl] };
|
|
2985
|
+
}
|
|
2986
|
+
if (process.platform === "win32") {
|
|
2987
|
+
return { command: "cmd", args: ["/c", "start", "", fileUrl] };
|
|
2988
|
+
}
|
|
2989
|
+
return { command: "xdg-open", args: [fileUrl] };
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2883
2992
|
async function handleTransactionFees({ network, provider, rpcUrl }) {
|
|
2884
2993
|
const feeAsset = loadTransactionFeeAsset();
|
|
2885
2994
|
const feeData = await provider.getFeeData();
|
|
@@ -3079,24 +3188,19 @@ function handleListLocalWallets({ args }) {
|
|
|
3079
3188
|
});
|
|
3080
3189
|
}
|
|
3081
3190
|
|
|
3082
|
-
function
|
|
3191
|
+
function handleWalletExportBackup({ args }) {
|
|
3083
3192
|
const outputPath = path.resolve(String(requireArg(args.output, "--output")));
|
|
3084
3193
|
expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
|
|
3085
3194
|
ensureDir(path.dirname(outputPath));
|
|
3086
3195
|
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
networkName: requireNetworkName(args),
|
|
3092
|
-
walletName: requireWalletName(args),
|
|
3093
|
-
})];
|
|
3196
|
+
const wallets = [resolveExportWalletInfo({
|
|
3197
|
+
networkName: requireNetworkName(args),
|
|
3198
|
+
walletName: requireWalletName(args),
|
|
3199
|
+
})];
|
|
3094
3200
|
|
|
3095
3201
|
expect(
|
|
3096
3202
|
wallets.length > 0,
|
|
3097
|
-
|
|
3098
|
-
? "No local mainnet wallets are available to export."
|
|
3099
|
-
: "No local wallet is available to export.",
|
|
3203
|
+
"No local wallet is available to export.",
|
|
3100
3204
|
);
|
|
3101
3205
|
|
|
3102
3206
|
const archive = new AdmZip();
|
|
@@ -3109,7 +3213,7 @@ function handleWalletExport({ args }) {
|
|
|
3109
3213
|
channelName: normalized.channelName,
|
|
3110
3214
|
wallet: normalized.wallet,
|
|
3111
3215
|
});
|
|
3112
|
-
for (const filePath of
|
|
3216
|
+
for (const filePath of walletBackupExportFilePaths(normalized)) {
|
|
3113
3217
|
const archivePath = archivePathForLocalCliFile(filePath);
|
|
3114
3218
|
if (!files.has(archivePath)) {
|
|
3115
3219
|
files.set(archivePath, filePath);
|
|
@@ -3118,44 +3222,65 @@ function handleWalletExport({ args }) {
|
|
|
3118
3222
|
}
|
|
3119
3223
|
|
|
3120
3224
|
const manifest = {
|
|
3121
|
-
format:
|
|
3225
|
+
format: WALLET_BACKUP_EXPORT_FORMAT,
|
|
3122
3226
|
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
3123
3227
|
createdAt: new Date().toISOString(),
|
|
3124
3228
|
cliPackage: PRIVATE_STATE_CLI_PACKAGE_NAME,
|
|
3125
3229
|
cliVersion: privateStateCliPackageJson.version,
|
|
3126
|
-
exportMode:
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
]
|
|
3132
|
-
: [
|
|
3133
|
-
"Includes wallet identity, encrypted wallet state, metadata, and wallet-local secret only.",
|
|
3134
|
-
"Run channel recover-workspace after import before wallet commands need channel state.",
|
|
3135
|
-
],
|
|
3230
|
+
exportMode: "backup",
|
|
3231
|
+
notes: [
|
|
3232
|
+
"Includes wallet note-tracking metadata, public key metadata, and channel workspace cache.",
|
|
3233
|
+
"Excludes spending keys, viewing keys, key derivation material, owner, value, and salt.",
|
|
3234
|
+
],
|
|
3136
3235
|
wallets: exportedWallets,
|
|
3137
3236
|
files: [...files.keys()].sort(),
|
|
3138
3237
|
};
|
|
3139
3238
|
|
|
3140
3239
|
archive.addFile("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
|
|
3141
3240
|
for (const archivePath of manifest.files) {
|
|
3142
|
-
|
|
3241
|
+
const filePath = files.get(archivePath);
|
|
3242
|
+
validateBackupExportFile(filePath);
|
|
3243
|
+
archive.addFile(archivePath, fs.readFileSync(filePath));
|
|
3143
3244
|
}
|
|
3144
3245
|
archive.writeZip(outputPath);
|
|
3145
3246
|
protectSecretFile(outputPath, "wallet export ZIP");
|
|
3146
3247
|
|
|
3147
3248
|
printJson({
|
|
3148
|
-
action: "wallet export",
|
|
3249
|
+
action: "wallet export backup",
|
|
3149
3250
|
output: outputPath,
|
|
3150
3251
|
exportMode: manifest.exportMode,
|
|
3151
|
-
includeNotes,
|
|
3152
3252
|
walletCount: exportedWallets.length,
|
|
3153
3253
|
fileCount: manifest.files.length,
|
|
3154
3254
|
wallets: exportedWallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
|
|
3155
3255
|
});
|
|
3156
3256
|
}
|
|
3157
3257
|
|
|
3158
|
-
function
|
|
3258
|
+
function handleWalletExportKey({ args, keyKind }) {
|
|
3259
|
+
const outputPath = path.resolve(String(requireArg(args.output, "--output")));
|
|
3260
|
+
expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
|
|
3261
|
+
ensureDir(path.dirname(outputPath));
|
|
3262
|
+
const networkName = requireNetworkName(args);
|
|
3263
|
+
const walletName = requireWalletName(args);
|
|
3264
|
+
const wallet = loadWallet(walletName, networkName);
|
|
3265
|
+
const secretPath = keyKind === "spending"
|
|
3266
|
+
? walletSpendingKeySecretPath(networkName, walletName)
|
|
3267
|
+
: walletViewingKeySecretPath(networkName, walletName);
|
|
3268
|
+
expect(fs.existsSync(secretPath), `Wallet ${walletName} is missing its ${keyKind} key.`);
|
|
3269
|
+
const payload = JSON.parse(readSecretFile(secretPath, `${keyKind} key`));
|
|
3270
|
+
validateWalletKeyPayload(payload, keyKind);
|
|
3271
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
3272
|
+
protectSecretFile(outputPath, `${keyKind} key export`);
|
|
3273
|
+
printJson({
|
|
3274
|
+
action: `wallet export ${keyKind}-key`,
|
|
3275
|
+
wallet: wallet.walletName,
|
|
3276
|
+
network: networkName,
|
|
3277
|
+
output: outputPath,
|
|
3278
|
+
keyKind,
|
|
3279
|
+
metadata: payload.metadata,
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
function handleWalletImportBackup({ args }) {
|
|
3159
3284
|
const inputPath = path.resolve(String(requireArg(args.input, "--input")));
|
|
3160
3285
|
expect(fs.existsSync(inputPath), `Import ZIP does not exist: ${inputPath}.`);
|
|
3161
3286
|
|
|
@@ -3191,16 +3316,55 @@ function handleWalletImport({ args }) {
|
|
|
3191
3316
|
commitWalletImportFiles({ targetRoot, plannedWrites });
|
|
3192
3317
|
|
|
3193
3318
|
printJson({
|
|
3194
|
-
action: "wallet import",
|
|
3319
|
+
action: "wallet import backup",
|
|
3195
3320
|
input: inputPath,
|
|
3196
3321
|
exportMode: manifest.exportMode,
|
|
3197
|
-
includeNotes: Boolean(manifest.includeNotes),
|
|
3198
3322
|
walletCount: manifest.wallets.length,
|
|
3199
3323
|
fileCount: plannedWrites.length,
|
|
3200
3324
|
wallets: manifest.wallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
|
|
3201
|
-
nextStep:
|
|
3202
|
-
|
|
3203
|
-
|
|
3325
|
+
nextStep: "Import viewing-key and spending-key files separately when the wallet needs those capabilities.",
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
function handleWalletImportKey({ args, keyKind }) {
|
|
3330
|
+
const inputPath = path.resolve(String(requireArg(args.input, "--input")));
|
|
3331
|
+
expect(fs.existsSync(inputPath), `Key import file does not exist: ${inputPath}.`);
|
|
3332
|
+
const payload = JSON.parse(readImportSecretSourceFile(inputPath, "--input"));
|
|
3333
|
+
validateWalletKeyPayload(payload, keyKind);
|
|
3334
|
+
const metadata = payload.metadata;
|
|
3335
|
+
const networkName = requireNetworkName({ network: metadata.network });
|
|
3336
|
+
const walletName = requireWalletName({ wallet: metadata.wallet });
|
|
3337
|
+
const targetPath = keyKind === "spending"
|
|
3338
|
+
? walletSpendingKeySecretPath(networkName, walletName)
|
|
3339
|
+
: walletViewingKeySecretPath(networkName, walletName);
|
|
3340
|
+
expect(!fs.existsSync(targetPath), `Refusing to overwrite existing ${keyKind} key: ${targetPath}.`);
|
|
3341
|
+
writeSecretFile(targetPath, JSON.stringify(payload, null, 2));
|
|
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);
|
|
3349
|
+
const metadataPath = keyKind === "spending"
|
|
3350
|
+
? walletSpendingKeyMetadataPath(walletDir)
|
|
3351
|
+
: walletViewingKeyMetadataPath(walletDir);
|
|
3352
|
+
if (fs.existsSync(metadataPath)) {
|
|
3353
|
+
expect(
|
|
3354
|
+
JSON.stringify(readJson(metadataPath)) === JSON.stringify(normalizeCliOutput(metadata)),
|
|
3355
|
+
`Refusing to overwrite mismatched ${keyKind} key metadata: ${metadataPath}.`,
|
|
3356
|
+
);
|
|
3357
|
+
} else {
|
|
3358
|
+
writeJson(metadataPath, metadata);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
printJson({
|
|
3362
|
+
action: `wallet import ${keyKind}-key`,
|
|
3363
|
+
input: inputPath,
|
|
3364
|
+
network: networkName,
|
|
3365
|
+
wallet: walletName,
|
|
3366
|
+
keyKind,
|
|
3367
|
+
metadata,
|
|
3204
3368
|
});
|
|
3205
3369
|
}
|
|
3206
3370
|
|
|
@@ -3217,17 +3381,58 @@ function readWalletImportArchive(inputPath) {
|
|
|
3217
3381
|
}
|
|
3218
3382
|
}
|
|
3219
3383
|
|
|
3220
|
-
function
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3384
|
+
function validateBackupExportFile(filePath) {
|
|
3385
|
+
if (path.basename(filePath) !== "wallet-notes.metadata.json") {
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const metadata = readJson(filePath);
|
|
3389
|
+
const forbidden = findForbiddenBackupMetadataPaths(metadata);
|
|
3390
|
+
expect(
|
|
3391
|
+
forbidden.length === 0,
|
|
3392
|
+
`wallet export backup refuses to export plaintext note secrets or key material: ${forbidden.join(", ")}.`,
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function findForbiddenBackupMetadataPaths(value, pathParts = []) {
|
|
3397
|
+
const forbiddenNames = new Set([
|
|
3398
|
+
"owner",
|
|
3399
|
+
"value",
|
|
3400
|
+
"salt",
|
|
3401
|
+
"l1PrivateKey",
|
|
3402
|
+
"l2PrivateKey",
|
|
3403
|
+
"noteReceivePrivateKey",
|
|
3404
|
+
"walletSecret",
|
|
3405
|
+
"seedSignature",
|
|
3406
|
+
]);
|
|
3407
|
+
if (Array.isArray(value)) {
|
|
3408
|
+
return value.flatMap((entry, index) => findForbiddenBackupMetadataPaths(entry, [...pathParts, String(index)]));
|
|
3409
|
+
}
|
|
3410
|
+
if (!value || typeof value !== "object") {
|
|
3411
|
+
return [];
|
|
3412
|
+
}
|
|
3413
|
+
const found = [];
|
|
3414
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3415
|
+
const nextPath = [...pathParts, key];
|
|
3416
|
+
if (forbiddenNames.has(key) && entry !== undefined && entry !== null) {
|
|
3417
|
+
found.push(nextPath.join("."));
|
|
3418
|
+
continue;
|
|
3419
|
+
}
|
|
3420
|
+
found.push(...findForbiddenBackupMetadataPaths(entry, nextPath));
|
|
3421
|
+
}
|
|
3422
|
+
return found;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
function commitWalletImportFiles({ targetRoot, plannedWrites }) {
|
|
3426
|
+
const stagingRoot = fs.mkdtempSync(path.join(targetRoot, ".wallet-import-"));
|
|
3427
|
+
const committedPaths = [];
|
|
3428
|
+
try {
|
|
3429
|
+
for (const write of plannedWrites) {
|
|
3430
|
+
write.stagingPath = path.resolve(stagingRoot, write.archivePath);
|
|
3431
|
+
expectPathWithinRoot(write.stagingPath, stagingRoot, `Unsafe staging target for ${write.archivePath}.`);
|
|
3432
|
+
ensureDir(path.dirname(write.stagingPath));
|
|
3433
|
+
fs.writeFileSync(write.stagingPath, write.data);
|
|
3434
|
+
applyImportedWalletFileMode(write.archivePath, write.stagingPath);
|
|
3435
|
+
}
|
|
3231
3436
|
|
|
3232
3437
|
for (const write of plannedWrites) {
|
|
3233
3438
|
expect(!fs.existsSync(write.targetPath), `Refusing to overwrite existing file: ${write.targetPath}.`);
|
|
@@ -3609,15 +3814,25 @@ async function inspectGuideAccount({ account, networkName, network, provider, ar
|
|
|
3609
3814
|
}
|
|
3610
3815
|
|
|
3611
3816
|
async function inspectGuideWallet({ walletName, networkName, provider, artifactsInstalled }) {
|
|
3612
|
-
|
|
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
|
+
}
|
|
3824
|
+
const viewingKeyFile = walletViewingKeySecretPath(networkName, walletName);
|
|
3825
|
+
const spendingKeyFile = walletSpendingKeySecretPath(networkName, walletName);
|
|
3613
3826
|
const result = {
|
|
3614
3827
|
wallet: walletName,
|
|
3615
3828
|
network: networkName,
|
|
3616
3829
|
walletDir,
|
|
3617
3830
|
exists: walletConfigExists(walletDir),
|
|
3618
|
-
metadataExists: fs.existsSync(
|
|
3619
|
-
|
|
3620
|
-
|
|
3831
|
+
metadataExists: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
3832
|
+
viewingKeyFile,
|
|
3833
|
+
viewingKeyFileExists: fs.existsSync(viewingKeyFile),
|
|
3834
|
+
spendingKeyFile,
|
|
3835
|
+
spendingKeyFileExists: fs.existsSync(spendingKeyFile),
|
|
3621
3836
|
channelName: null,
|
|
3622
3837
|
l1Address: null,
|
|
3623
3838
|
l2Address: null,
|
|
@@ -3628,14 +3843,14 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3628
3843
|
unusedNoteBalanceBaseUnits: null,
|
|
3629
3844
|
unusedNoteBalanceTokens: null,
|
|
3630
3845
|
spentNoteCount: null,
|
|
3631
|
-
error:
|
|
3846
|
+
error: workspaceError,
|
|
3632
3847
|
};
|
|
3633
|
-
if (!result.exists) {
|
|
3848
|
+
if (workspaceError || !result.exists) {
|
|
3634
3849
|
return result;
|
|
3635
3850
|
}
|
|
3636
3851
|
|
|
3637
3852
|
try {
|
|
3638
|
-
const walletContext = loadWallet(walletName,
|
|
3853
|
+
const walletContext = loadWallet(walletName, networkName);
|
|
3639
3854
|
const walletMetadata = loadWalletMetadata(walletName, networkName);
|
|
3640
3855
|
assertWalletMatchesMetadata(walletContext, walletMetadata);
|
|
3641
3856
|
result.channelName = walletContext.wallet.channelName;
|
|
@@ -3643,10 +3858,13 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3643
3858
|
result.l2Address = getAddress(walletContext.wallet.l2Address);
|
|
3644
3859
|
result.unusedNoteCount = Object.keys(walletContext.wallet.notes.unused).length;
|
|
3645
3860
|
result.spentNoteCount = Object.keys(walletContext.wallet.notes.spent).length;
|
|
3646
|
-
const
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3861
|
+
const unusedValues = Object.values(walletContext.wallet.notes.unused).map((note) => note.value);
|
|
3862
|
+
if (unusedValues.every((value) => value !== null)) {
|
|
3863
|
+
const unusedNoteBalance = Object.values(walletContext.wallet.notes.unused)
|
|
3864
|
+
.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n);
|
|
3865
|
+
result.unusedNoteBalanceBaseUnits = unusedNoteBalance.toString();
|
|
3866
|
+
result.unusedNoteBalanceTokens = ethers.formatUnits(unusedNoteBalance, Number(walletContext.wallet.canonicalAssetDecimals));
|
|
3867
|
+
}
|
|
3650
3868
|
|
|
3651
3869
|
if (provider && artifactsInstalled && walletChannelWorkspaceIsReady(walletContext)) {
|
|
3652
3870
|
const context = await loadWorkspaceContext(walletContext.wallet.channelName, networkName, provider);
|
|
@@ -3717,7 +3935,7 @@ function applyGuideNextAction(guide) {
|
|
|
3717
3935
|
const channelName = guide.selectors.channelName ?? guide.state.channel?.channelName ?? "<CHANNEL>";
|
|
3718
3936
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3719
3937
|
setGuideNextAction(guide, {
|
|
3720
|
-
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH
|
|
3938
|
+
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH> --acknowledge-action-impact`,
|
|
3721
3939
|
why: "The selected local wallet does not exist. Join the channel to create the wallet and register the channel L2 identity.",
|
|
3722
3940
|
});
|
|
3723
3941
|
return;
|
|
@@ -3726,7 +3944,7 @@ function applyGuideNextAction(guide) {
|
|
|
3726
3944
|
const channelName = guide.state.wallet.channelName ?? guide.selectors.channelName ?? "<CHANNEL>";
|
|
3727
3945
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3728
3946
|
setGuideNextAction(guide, {
|
|
3729
|
-
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH
|
|
3947
|
+
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH> --acknowledge-action-impact`,
|
|
3730
3948
|
why: "The local wallet exists, but the corresponding L1 address is not registered in the channel.",
|
|
3731
3949
|
});
|
|
3732
3950
|
return;
|
|
@@ -3743,32 +3961,32 @@ function applyGuideNextAction(guide) {
|
|
|
3743
3961
|
if (guide.state.wallet?.exists && bridgeBalance === 0n && (channelBalance === null || channelBalance === 0n) && unusedNotes === 0) {
|
|
3744
3962
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3745
3963
|
setGuideNextAction(guide, {
|
|
3746
|
-
command: `account deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account}`,
|
|
3964
|
+
command: `account deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account} --acknowledge-action-impact`,
|
|
3747
3965
|
why: "The wallet is joined, but there is no bridge balance, channel balance, or local unused note to spend.",
|
|
3748
3966
|
});
|
|
3749
3967
|
return;
|
|
3750
3968
|
}
|
|
3751
3969
|
if (guide.state.wallet?.exists && bridgeBalance !== null && bridgeBalance > 0n && channelBalance === 0n) {
|
|
3752
3970
|
setGuideNextAction(guide, {
|
|
3753
|
-
command: `wallet deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS
|
|
3971
|
+
command: `wallet deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS> --acknowledge-action-impact`,
|
|
3754
3972
|
why: "The account has funds in the shared bridge vault, but the wallet has no channel L2 accounting balance.",
|
|
3755
3973
|
});
|
|
3756
3974
|
return;
|
|
3757
3975
|
}
|
|
3758
3976
|
if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
|
|
3759
3977
|
setGuideNextAction(guide, {
|
|
3760
|
-
command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
3978
|
+
command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
3761
3979
|
why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
3762
3980
|
});
|
|
3763
3981
|
return;
|
|
3764
3982
|
}
|
|
3765
3983
|
if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
|
|
3766
3984
|
setGuideNextAction(guide, {
|
|
3767
|
-
command: `wallet transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
3985
|
+
command: `wallet transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID,ID> --recipients <ADDR,ADDR> --amounts <A,B> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
3768
3986
|
why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
3769
3987
|
candidates: [
|
|
3770
3988
|
`wallet get-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
|
|
3771
|
-
`wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> [--tx-submitter <ACCOUNT>]`,
|
|
3989
|
+
`wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
3772
3990
|
],
|
|
3773
3991
|
});
|
|
3774
3992
|
return;
|
|
@@ -3869,6 +4087,7 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
3869
4087
|
printJson({
|
|
3870
4088
|
action: "wallet get-meta",
|
|
3871
4089
|
wallet: wallet.walletName,
|
|
4090
|
+
...walletLifecycleMetadata(wallet.wallet),
|
|
3872
4091
|
network: walletMetadata.network,
|
|
3873
4092
|
channelName: walletMetadata.channelName,
|
|
3874
4093
|
l1Address: signer.address,
|
|
@@ -3879,6 +4098,12 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
3879
4098
|
registeredL2Address: registration.exists ? getAddress(registration.l2Address) : null,
|
|
3880
4099
|
registeredL2StorageKey: registration.exists ? normalizeBytes32Hex(registration.channelTokenVaultKey) : null,
|
|
3881
4100
|
registeredLeafIndex: registration.exists ? registration.leafIndex.toString() : null,
|
|
4101
|
+
registeredNoteReceivePubKey: registration.exists
|
|
4102
|
+
? {
|
|
4103
|
+
x: normalizeBytes32Hex(registration.noteReceivePubKey.x),
|
|
4104
|
+
yParity: Number(registration.noteReceivePubKey.yParity),
|
|
4105
|
+
}
|
|
4106
|
+
: null,
|
|
3882
4107
|
});
|
|
3883
4108
|
}
|
|
3884
4109
|
|
|
@@ -3924,7 +4149,8 @@ async function loadWalletChannelRegistrationState({
|
|
|
3924
4149
|
provider,
|
|
3925
4150
|
requireRegistration = false,
|
|
3926
4151
|
}) {
|
|
3927
|
-
const
|
|
4152
|
+
const signer = requireWalletOwnerSigner(walletContext, provider);
|
|
4153
|
+
const l2Identity = restoreParticipantIdentityFromWallet(walletContext.wallet);
|
|
3928
4154
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
3929
4155
|
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
3930
4156
|
const matchesWallet = registration.exists
|
|
@@ -3955,6 +4181,156 @@ async function loadWalletChannelRegistrationState({
|
|
|
3955
4181
|
};
|
|
3956
4182
|
}
|
|
3957
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
|
+
|
|
3958
4334
|
async function handleWalletGetChannelFund({ args, provider }) {
|
|
3959
4335
|
const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
3960
4336
|
const {
|
|
@@ -4025,13 +4401,9 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4025
4401
|
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4026
4402
|
const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
|
|
4027
4403
|
|
|
4028
|
-
const resolvedLeafIndex = leafIndex;
|
|
4029
4404
|
let approveReceipt = null;
|
|
4030
4405
|
let receipt = null;
|
|
4031
|
-
|
|
4032
|
-
let status = null;
|
|
4033
|
-
|
|
4034
|
-
joinToll = ethers.toBigInt(await context.channelManager.joinToll());
|
|
4406
|
+
const joinToll = ethers.toBigInt(await context.channelManager.joinToll());
|
|
4035
4407
|
const asset = new Contract(
|
|
4036
4408
|
context.workspace.canonicalAsset,
|
|
4037
4409
|
context.bridgeAbiManifest.contracts.erc20.abi,
|
|
@@ -4045,6 +4417,14 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4045
4417
|
channelManager: context.workspace.channelManager,
|
|
4046
4418
|
policySnapshot: context.workspace.policySnapshot,
|
|
4047
4419
|
});
|
|
4420
|
+
await requireActionImpactAcknowledgement("channel-join", args, {
|
|
4421
|
+
l1Address: signer.address,
|
|
4422
|
+
l2Address: l2Identity.l2Address,
|
|
4423
|
+
noteReceivePubKey: JSON.stringify(noteReceiveKeyMaterial.noteReceivePubKey),
|
|
4424
|
+
joinToll: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4425
|
+
channelName: context.workspace.channelName,
|
|
4426
|
+
channelId: context.workspace.channelId,
|
|
4427
|
+
});
|
|
4048
4428
|
if (joinToll !== 0n) {
|
|
4049
4429
|
approveReceipt = await waitForReceipt(
|
|
4050
4430
|
await asset.approve(context.workspace.bridgeTokenVault, joinToll, { nonce: nextNonce++ }),
|
|
@@ -4060,8 +4440,14 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4060
4440
|
{ nonce: nextNonce++ },
|
|
4061
4441
|
),
|
|
4062
4442
|
);
|
|
4063
|
-
|
|
4064
|
-
|
|
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.");
|
|
4065
4451
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
4066
4452
|
context,
|
|
4067
4453
|
provider,
|
|
@@ -4076,8 +4462,9 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4076
4462
|
l2Identity,
|
|
4077
4463
|
walletSecret,
|
|
4078
4464
|
storageKey,
|
|
4079
|
-
leafIndex
|
|
4465
|
+
leafIndex,
|
|
4080
4466
|
noteReceiveKeyMaterial,
|
|
4467
|
+
lifecycleEpoch,
|
|
4081
4468
|
rpcUrl,
|
|
4082
4469
|
});
|
|
4083
4470
|
|
|
@@ -4085,14 +4472,19 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4085
4472
|
action: "channel join",
|
|
4086
4473
|
workspace: context.workspaceName,
|
|
4087
4474
|
wallet: walletContext.walletName,
|
|
4088
|
-
walletSecretSource:
|
|
4089
|
-
|
|
4475
|
+
walletSecretSource: "wallet-secret-path-one-time-derivation",
|
|
4476
|
+
walletSecretStored: false,
|
|
4477
|
+
walletSecretRecoveryWarning: "Keep the wallet secret source backed up. If the spending-key file is lost and this wallet secret source is also lost, the CLI cannot rederive the spending key; notes for this wallet cannot be spent, transferred, or redeemed through the normal note flow.",
|
|
4090
4478
|
channelName: context.workspace.channelName,
|
|
4091
4479
|
channelId: context.workspace.channelId,
|
|
4092
4480
|
l1Address: signer.address,
|
|
4093
4481
|
l2Address: l2Identity.l2Address,
|
|
4094
4482
|
l2StorageKey: storageKey,
|
|
4095
|
-
leafIndex:
|
|
4483
|
+
leafIndex: leafIndex.toString(),
|
|
4484
|
+
epochId: lifecycleEpoch.epochId,
|
|
4485
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
4486
|
+
joinedAtTxHash: lifecycleEpoch.joinedAtTxHash,
|
|
4487
|
+
joinedAtBlockNumber: lifecycleEpoch.joinedAtBlockNumber,
|
|
4096
4488
|
joinTollBaseUnits: joinToll.toString(),
|
|
4097
4489
|
joinTollTokens: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4098
4490
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
@@ -4103,31 +4495,37 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4103
4495
|
txUrl: receipt ? explorerTxUrl(network, receipt.hash) : null,
|
|
4104
4496
|
approveReceipt: approveReceipt ? sanitizeReceipt(approveReceipt) : null,
|
|
4105
4497
|
receipt: receipt ? sanitizeReceipt(receipt) : null,
|
|
4106
|
-
status,
|
|
4498
|
+
status: "joined",
|
|
4107
4499
|
});
|
|
4108
4500
|
}
|
|
4109
4501
|
|
|
4110
4502
|
async function handleExitChannel({ args, provider }) {
|
|
4111
4503
|
const { wallet: walletContext, walletMetadata } = loadUnlockedWalletWithMetadata(args);
|
|
4504
|
+
requireActiveWalletLifecycle(walletContext, "channel exit");
|
|
4112
4505
|
const { signer, context, channelFund, contextResult } = await loadWalletChannelFundState({
|
|
4113
4506
|
walletContext,
|
|
4114
4507
|
provider,
|
|
4115
4508
|
progressAction: "channel exit",
|
|
4116
4509
|
});
|
|
4510
|
+
const ownerSigner = requireWalletOwnerSigner(walletContext, provider);
|
|
4117
4511
|
const network = contextResult.network;
|
|
4118
4512
|
expect(
|
|
4119
4513
|
channelFund === 0n,
|
|
4120
4514
|
[
|
|
4121
|
-
`The current channel fund for ${
|
|
4515
|
+
`The current channel fund for ${ownerSigner.address} is ${channelFund.toString()}.`,
|
|
4122
4516
|
"channel exit requires a zero channel balance.",
|
|
4123
4517
|
"Run wallet withdraw-channel first, then retry channel exit.",
|
|
4124
4518
|
].join(" "),
|
|
4125
4519
|
);
|
|
4126
|
-
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(
|
|
4520
|
+
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(ownerSigner.address);
|
|
4127
4521
|
const receipt = await waitForReceipt(
|
|
4128
|
-
await context.bridgeTokenVault.connect(
|
|
4522
|
+
await context.bridgeTokenVault.connect(ownerSigner).exitChannel(ethers.toBigInt(context.workspace.channelId)),
|
|
4129
4523
|
);
|
|
4130
|
-
const
|
|
4524
|
+
const lifecycleEpoch = await markWalletEpochExited({
|
|
4525
|
+
walletContext,
|
|
4526
|
+
receipt,
|
|
4527
|
+
provider,
|
|
4528
|
+
});
|
|
4131
4529
|
|
|
4132
4530
|
printJson({
|
|
4133
4531
|
action: "channel exit",
|
|
@@ -4135,7 +4533,7 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4135
4533
|
network: walletMetadata.network,
|
|
4136
4534
|
channelName: walletMetadata.channelName,
|
|
4137
4535
|
channelId: context.workspace.channelId,
|
|
4138
|
-
l1Address:
|
|
4536
|
+
l1Address: ownerSigner.address,
|
|
4139
4537
|
currentUserValue: channelFund.toString(),
|
|
4140
4538
|
refundAmountBaseUnits: refundAmount.toString(),
|
|
4141
4539
|
refundAmountTokens: ethers.formatUnits(refundAmount, Number(context.workspace.canonicalAssetDecimals)),
|
|
@@ -4145,8 +4543,12 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4145
4543
|
gasUsed: receiptGasUsed(receipt),
|
|
4146
4544
|
txUrl: explorerTxUrl(network, receipt.hash),
|
|
4147
4545
|
receipt: sanitizeReceipt(receipt),
|
|
4148
|
-
|
|
4149
|
-
|
|
4546
|
+
epochId: lifecycleEpoch.epochId,
|
|
4547
|
+
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
4548
|
+
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
4549
|
+
exitedAtBlockNumber: lifecycleEpoch.exitedAtBlockNumber,
|
|
4550
|
+
exitedAtBlockTimestampIso: lifecycleEpoch.exitedAtBlockTimestampIso,
|
|
4551
|
+
archivedWalletDir: walletContext.walletDir,
|
|
4150
4552
|
});
|
|
4151
4553
|
}
|
|
4152
4554
|
|
|
@@ -4158,6 +4560,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4158
4560
|
: "wallet withdraw-channel";
|
|
4159
4561
|
emitProgress(operationName, "loading");
|
|
4160
4562
|
const { wallet: walletContext } = loadUnlockedWalletWithMetadata(args);
|
|
4563
|
+
requireActiveWalletLifecycle(walletContext, operationName);
|
|
4161
4564
|
const contextResult = await loadFreshWalletChannelContext({
|
|
4162
4565
|
walletContext,
|
|
4163
4566
|
provider,
|
|
@@ -4174,6 +4577,13 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4174
4577
|
const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
|
|
4175
4578
|
const amountInput = requireArg(args.amount, "--amount");
|
|
4176
4579
|
const amount = parseTokenAmount(amountInput, Number(context.workspace.canonicalAssetDecimals));
|
|
4580
|
+
await requireActionImpactAcknowledgement(args.command, args, {
|
|
4581
|
+
l1Address: signer.address,
|
|
4582
|
+
l2Address: l2Identity.l2Address,
|
|
4583
|
+
amountInput,
|
|
4584
|
+
channelName: context.workspace.channelName,
|
|
4585
|
+
channelId: context.workspace.channelId,
|
|
4586
|
+
});
|
|
4177
4587
|
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4178
4588
|
const bridgeTokenVault = new Contract(
|
|
4179
4589
|
context.workspace.bridgeTokenVault,
|
|
@@ -4256,7 +4666,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4256
4666
|
writeJson(path.join(operationDir, `${operationName}-receipt.json`), sanitizeReceipt(receipt));
|
|
4257
4667
|
writeJson(path.join(operationDir, "state_snapshot.json"), transition.nextSnapshot);
|
|
4258
4668
|
writeJson(path.join(operationDir, "state_snapshot.normalized.json"), transition.nextSnapshot);
|
|
4259
|
-
sealWalletOperationDir(operationDir, walletContext
|
|
4669
|
+
sealWalletOperationDir(operationDir, walletOperationSealSecret(walletContext));
|
|
4260
4670
|
|
|
4261
4671
|
context.currentSnapshot = transition.nextSnapshot;
|
|
4262
4672
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -4291,6 +4701,11 @@ async function handleWithdrawBridge({ args, network, provider }) {
|
|
|
4291
4701
|
const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId });
|
|
4292
4702
|
const amountInput = requireArg(args.amount, "--amount");
|
|
4293
4703
|
const amount = parseTokenAmount(amountInput, Number(bridgeVaultContext.canonicalAssetDecimals));
|
|
4704
|
+
await requireActionImpactAcknowledgement("account-withdraw-bridge", args, {
|
|
4705
|
+
l1Address: signer.address,
|
|
4706
|
+
amountInput,
|
|
4707
|
+
bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
|
|
4708
|
+
});
|
|
4294
4709
|
const bridgeTokenVault = new Contract(
|
|
4295
4710
|
bridgeVaultContext.bridgeTokenVaultAddress,
|
|
4296
4711
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
@@ -4371,6 +4786,8 @@ function resolveFunctionMetadataProofForExecution({
|
|
|
4371
4786
|
|
|
4372
4787
|
async function handleMintNotes({ args, provider }) {
|
|
4373
4788
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4789
|
+
requireActiveWalletLifecycle(wallet, "wallet mint-notes");
|
|
4790
|
+
requireWalletSpendingCapability(wallet);
|
|
4374
4791
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
4375
4792
|
const amountInputs = parseAmountVector(requireArg(args.amounts, "--amounts"), {
|
|
4376
4793
|
allowZeroEntries: true,
|
|
@@ -4400,6 +4817,19 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4400
4817
|
`${channelFund.toString()}. Run wallet get-channel-fund to inspect the available balance.`,
|
|
4401
4818
|
].join(" "),
|
|
4402
4819
|
);
|
|
4820
|
+
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
4821
|
+
const { txSubmitter } = resolveTxSubmitterSigner({
|
|
4822
|
+
args,
|
|
4823
|
+
ownerSigner: signer,
|
|
4824
|
+
provider,
|
|
4825
|
+
});
|
|
4826
|
+
await requireActionImpactAcknowledgement("wallet-mint-notes", args, {
|
|
4827
|
+
l1Address: txSubmitter.address,
|
|
4828
|
+
l2Address: l2Identity.l2Address,
|
|
4829
|
+
amounts: baseUnitAmounts.map(({ amountInput }) => amountInput).join(", "),
|
|
4830
|
+
channelName: wallet.wallet.channelName,
|
|
4831
|
+
channelId: wallet.wallet.channelId,
|
|
4832
|
+
});
|
|
4403
4833
|
const templatePayload = buildMintNotesTemplatePayload({
|
|
4404
4834
|
wallet,
|
|
4405
4835
|
baseUnitAmounts: baseUnitAmounts.map(({ amountBaseUnits }) => amountBaseUnits),
|
|
@@ -4432,6 +4862,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4432
4862
|
sourceFunction: templatePayload.method,
|
|
4433
4863
|
sourceTxHash: execution.receipt.hash,
|
|
4434
4864
|
bridgeCommitmentKeys: execution.noteLifecycle.outputCommitmentKeys,
|
|
4865
|
+
sourceBlockNumber: execution.receipt.blockNumber,
|
|
4435
4866
|
}),
|
|
4436
4867
|
gasUsed: receiptGasUsed(execution.receipt),
|
|
4437
4868
|
txUrl: explorerTxUrl(contextResult.network, execution.receipt.hash),
|
|
@@ -4444,6 +4875,9 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4444
4875
|
|
|
4445
4876
|
async function handleRedeemNotes({ args, provider }) {
|
|
4446
4877
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4878
|
+
requireActiveWalletLifecycle(wallet, "wallet redeem-notes");
|
|
4879
|
+
requireWalletViewingCapability(wallet);
|
|
4880
|
+
requireWalletSpendingCapability(wallet);
|
|
4447
4881
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
4448
4882
|
const preparedContextResult = await loadFreshWalletChannelContext({
|
|
4449
4883
|
walletContext: wallet,
|
|
@@ -4458,6 +4892,19 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
4458
4892
|
preConsumedBlockDelta: preparedContextResult.autoRecoveryBlockDelta,
|
|
4459
4893
|
});
|
|
4460
4894
|
const inputNotes = loadWalletUnusedInputNotes(wallet, noteIds);
|
|
4895
|
+
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
4896
|
+
const { txSubmitter } = resolveTxSubmitterSigner({
|
|
4897
|
+
args,
|
|
4898
|
+
ownerSigner: signer,
|
|
4899
|
+
provider,
|
|
4900
|
+
});
|
|
4901
|
+
await requireActionImpactAcknowledgement("wallet-redeem-notes", args, {
|
|
4902
|
+
l1Address: txSubmitter.address,
|
|
4903
|
+
l2Address: l2Identity.l2Address,
|
|
4904
|
+
noteIds: noteIds.join(", "),
|
|
4905
|
+
channelName: wallet.wallet.channelName,
|
|
4906
|
+
channelId: wallet.wallet.channelId,
|
|
4907
|
+
});
|
|
4461
4908
|
const templatePayload = buildRedeemNotesTemplatePayload({
|
|
4462
4909
|
wallet,
|
|
4463
4910
|
inputNotes,
|
|
@@ -4513,13 +4960,20 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4513
4960
|
progressAction: "wallet get-notes",
|
|
4514
4961
|
});
|
|
4515
4962
|
const context = contextResult.context;
|
|
4516
|
-
const noteReceiveFreshness =
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4963
|
+
const noteReceiveFreshness = wallet.wallet.noteReceivePrivateKey
|
|
4964
|
+
? await ensureWalletNoteReceiveStateCurrent({
|
|
4965
|
+
walletContext: wallet,
|
|
4966
|
+
context,
|
|
4967
|
+
provider,
|
|
4968
|
+
progressAction: "wallet get-notes",
|
|
4969
|
+
preConsumedBlockDelta: contextResult.autoRecoveryBlockDelta,
|
|
4970
|
+
})
|
|
4971
|
+
: {
|
|
4972
|
+
nextBlock: wallet.wallet.noteReceiveLastScannedBlock,
|
|
4973
|
+
latestBlock: await provider.getBlockNumber(),
|
|
4974
|
+
recoveredWalletWorkspace: false,
|
|
4975
|
+
recoveredDeliveryState: null,
|
|
4976
|
+
};
|
|
4523
4977
|
|
|
4524
4978
|
const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
|
|
4525
4979
|
.map((commitment) => wallet.wallet.notes.unused[commitment])
|
|
@@ -4539,8 +4993,20 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4539
4993
|
canonicalAssetDecimals,
|
|
4540
4994
|
})));
|
|
4541
4995
|
|
|
4542
|
-
const
|
|
4543
|
-
const
|
|
4996
|
+
const canComputeTotals = [...unusedTrackedNotes, ...spentTrackedNotes].every((note) => note.value !== null);
|
|
4997
|
+
const unusedTotal = canComputeTotals ? unusedTrackedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n) : null;
|
|
4998
|
+
const spentTotal = canComputeTotals ? spentTrackedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n) : null;
|
|
4999
|
+
const evidenceExport = args.exportEvidence
|
|
5000
|
+
? await exportWalletGetNotesEvidenceBundle({
|
|
5001
|
+
args,
|
|
5002
|
+
provider,
|
|
5003
|
+
walletContext: wallet,
|
|
5004
|
+
walletMetadata,
|
|
5005
|
+
context,
|
|
5006
|
+
unusedTrackedNotes,
|
|
5007
|
+
spentTrackedNotes,
|
|
5008
|
+
})
|
|
5009
|
+
: null;
|
|
4544
5010
|
|
|
4545
5011
|
printJson({
|
|
4546
5012
|
action: "wallet get-notes",
|
|
@@ -4550,22 +5016,517 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4550
5016
|
controller: wallet.wallet.controller,
|
|
4551
5017
|
unusedNotes,
|
|
4552
5018
|
spentNotes,
|
|
4553
|
-
unusedTotalBaseUnits: unusedTotal
|
|
4554
|
-
unusedTotalTokens: ethers.formatUnits(unusedTotal, canonicalAssetDecimals),
|
|
4555
|
-
spentTotalBaseUnits: spentTotal
|
|
4556
|
-
spentTotalTokens: ethers.formatUnits(spentTotal, canonicalAssetDecimals),
|
|
5019
|
+
unusedTotalBaseUnits: unusedTotal?.toString() ?? null,
|
|
5020
|
+
unusedTotalTokens: unusedTotal === null ? null : ethers.formatUnits(unusedTotal, canonicalAssetDecimals),
|
|
5021
|
+
spentTotalBaseUnits: spentTotal?.toString() ?? null,
|
|
5022
|
+
spentTotalTokens: spentTotal === null ? null : ethers.formatUnits(spentTotal, canonicalAssetDecimals),
|
|
4557
5023
|
bridgeStatusMismatches: [...unusedNotes, ...spentNotes].filter((note) => !note.walletStatusMatchesBridge).length,
|
|
4558
5024
|
noteReceiveLastScannedBlock: noteReceiveFreshness.nextBlock,
|
|
4559
5025
|
latestBlock: noteReceiveFreshness.latestBlock,
|
|
5026
|
+
viewingKeyAvailable: Boolean(wallet.wallet.noteReceivePrivateKey),
|
|
4560
5027
|
recoveredWalletWorkspace: noteReceiveFreshness.recoveredWalletWorkspace,
|
|
4561
5028
|
recoveredFromLogs: noteReceiveFreshness.recoveredDeliveryState?.importedNotes ?? 0,
|
|
4562
5029
|
scannedDeliveryLogs: noteReceiveFreshness.recoveredDeliveryState?.scannedLogs ?? 0,
|
|
4563
5030
|
noteReceiveScanRange: noteReceiveFreshness.recoveredDeliveryState?.scanRange ?? null,
|
|
5031
|
+
evidenceExport,
|
|
5032
|
+
});
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
async function exportWalletGetNotesEvidenceBundle({
|
|
5036
|
+
args,
|
|
5037
|
+
provider,
|
|
5038
|
+
walletContext,
|
|
5039
|
+
walletMetadata,
|
|
5040
|
+
context,
|
|
5041
|
+
unusedTrackedNotes,
|
|
5042
|
+
spentTrackedNotes,
|
|
5043
|
+
}) {
|
|
5044
|
+
const outputPath = path.resolve(String(requireArg(args.exportEvidence, "--export-evidence")));
|
|
5045
|
+
ensureDir(path.dirname(outputPath));
|
|
5046
|
+
|
|
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
|
+
}
|
|
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));
|
|
5070
|
+
|
|
5071
|
+
const txHashes = uniqueNonNull([
|
|
5072
|
+
...noteInputs.map(({ note }) => note.createdAtTxHash),
|
|
5073
|
+
...noteInputs.map(({ note }) => note.spentAtTxHash),
|
|
5074
|
+
]);
|
|
5075
|
+
const transactionEvidence = await buildTransactionEvidenceMap({ provider, txHashes });
|
|
5076
|
+
const blockTimestampCache = buildBlockTimestampCache(transactionEvidence);
|
|
5077
|
+
const noteRecords = noteInputs.map(({ note, walletContext: noteWalletContext }) => buildEvidenceNoteRecord({
|
|
5078
|
+
note,
|
|
5079
|
+
walletContext: noteWalletContext,
|
|
5080
|
+
walletMetadata,
|
|
5081
|
+
context,
|
|
5082
|
+
transactionEvidence,
|
|
5083
|
+
blockTimestampCache,
|
|
5084
|
+
}));
|
|
5085
|
+
const indexes = buildEvidenceIndexes(noteRecords);
|
|
5086
|
+
const manifest = buildEvidenceManifest({
|
|
5087
|
+
outputPath,
|
|
5088
|
+
walletContext,
|
|
5089
|
+
walletContexts: evidenceWalletContexts,
|
|
5090
|
+
walletMetadata,
|
|
5091
|
+
context,
|
|
5092
|
+
noteRecords,
|
|
5093
|
+
txHashes,
|
|
5094
|
+
});
|
|
5095
|
+
|
|
5096
|
+
const archive = new AdmZip();
|
|
5097
|
+
addEvidenceJson(archive, "manifest.json", manifest);
|
|
5098
|
+
addEvidenceJson(archive, "indexes/by-commitment.json", indexes.byCommitment);
|
|
5099
|
+
addEvidenceJson(archive, "indexes/by-nullifier.json", indexes.byNullifier);
|
|
5100
|
+
addEvidenceJson(archive, "indexes/by-creation-tx.json", indexes.byCreationTx);
|
|
5101
|
+
addEvidenceJson(archive, "indexes/by-spend-tx.json", indexes.bySpendTx);
|
|
5102
|
+
addEvidenceJson(archive, "indexes/by-block-range.json", indexes.byBlockRange);
|
|
5103
|
+
addEvidenceJson(archive, "indexes/by-counterparty.json", indexes.byCounterparty);
|
|
5104
|
+
for (const record of noteRecords) {
|
|
5105
|
+
addEvidenceJson(archive, evidenceNotePath(record), record);
|
|
5106
|
+
}
|
|
5107
|
+
for (const [txHash, txRecord] of Object.entries(transactionEvidence)) {
|
|
5108
|
+
addEvidenceJson(archive, `transactions/${txHash}.json`, txRecord.transaction);
|
|
5109
|
+
addEvidenceJson(archive, `receipts/${txHash}.json`, txRecord.receipt);
|
|
5110
|
+
addEvidenceJson(archive, `events/${txHash}.json`, txRecord.events);
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
assertEvidenceBundleDoesNotContainSecrets({
|
|
5114
|
+
wallets: evidenceWalletContexts.map((entry) => entry.wallet),
|
|
5115
|
+
payload: {
|
|
5116
|
+
manifest,
|
|
5117
|
+
indexes,
|
|
5118
|
+
noteRecords,
|
|
5119
|
+
transactionEvidence,
|
|
5120
|
+
},
|
|
5121
|
+
});
|
|
5122
|
+
fs.rmSync(outputPath, { force: true });
|
|
5123
|
+
archive.writeZip(outputPath);
|
|
5124
|
+
protectSecretFile(outputPath, "wallet evidence export ZIP");
|
|
5125
|
+
|
|
5126
|
+
return {
|
|
5127
|
+
output: outputPath,
|
|
5128
|
+
format: WALLET_EVIDENCE_BUNDLE_FORMAT,
|
|
5129
|
+
formatVersion: WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION,
|
|
5130
|
+
noteCount: noteRecords.length,
|
|
5131
|
+
walletEpochCount: evidenceWalletContexts.length,
|
|
5132
|
+
transactionCount: txHashes.length,
|
|
5133
|
+
containsNotePlaintext: true,
|
|
5134
|
+
containsSpendingKey: false,
|
|
5135
|
+
containsViewingKey: false,
|
|
5136
|
+
containsWalletSecret: false,
|
|
5137
|
+
warning: "Local full-note evidence bundle. Do not submit as-is unless full wallet-history disclosure is intended.",
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
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
|
+
|
|
5168
|
+
function validateEvidenceNotePlaintext(note, wallet) {
|
|
5169
|
+
expect(
|
|
5170
|
+
note.owner && note.value !== null && note.salt && note.encryptedNoteValue,
|
|
5171
|
+
[
|
|
5172
|
+
`Cannot export evidence for note ${note.commitment} because plaintext note data is incomplete.`,
|
|
5173
|
+
"Import the wallet viewing key and run wallet recover-workspace before exporting evidence.",
|
|
5174
|
+
].join(" "),
|
|
5175
|
+
);
|
|
5176
|
+
expect(
|
|
5177
|
+
getAddress(note.owner) === getAddress(wallet.l2Address),
|
|
5178
|
+
`Cannot export evidence for note ${note.commitment}: owner does not match wallet L2 address.`,
|
|
5179
|
+
);
|
|
5180
|
+
const recomputedSalt = computeEncryptedNoteSalt(note.encryptedNoteValue);
|
|
5181
|
+
expect(
|
|
5182
|
+
ethers.toBigInt(recomputedSalt) === ethers.toBigInt(note.salt),
|
|
5183
|
+
`Cannot export evidence for note ${note.commitment}: encrypted note salt mismatch.`,
|
|
5184
|
+
);
|
|
5185
|
+
const plaintext = normalizePlaintextNote(note);
|
|
5186
|
+
expect(
|
|
5187
|
+
ethers.toBigInt(computeNoteCommitment(plaintext)) === ethers.toBigInt(note.commitment),
|
|
5188
|
+
`Cannot export evidence for note ${note.commitment}: commitment mismatch.`,
|
|
5189
|
+
);
|
|
5190
|
+
expect(
|
|
5191
|
+
ethers.toBigInt(computeNullifier(plaintext)) === ethers.toBigInt(note.nullifier),
|
|
5192
|
+
`Cannot export evidence for note ${note.commitment}: nullifier mismatch.`,
|
|
5193
|
+
);
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
async function buildTransactionEvidenceMap({ provider, txHashes }) {
|
|
5197
|
+
const entries = {};
|
|
5198
|
+
for (const txHash of txHashes) {
|
|
5199
|
+
const [transaction, receipt] = await Promise.all([
|
|
5200
|
+
provider.getTransaction(txHash).catch(() => null),
|
|
5201
|
+
provider.getTransactionReceipt(txHash).catch(() => null),
|
|
5202
|
+
]);
|
|
5203
|
+
const blockNumber = receipt?.blockNumber ?? transaction?.blockNumber ?? null;
|
|
5204
|
+
const block = blockNumber === null ? null : await provider.getBlock(blockNumber).catch(() => null);
|
|
5205
|
+
entries[txHash] = {
|
|
5206
|
+
transaction: sanitizeTransactionEvidence(transaction, block),
|
|
5207
|
+
receipt: receipt ? sanitizeReceipt(receipt) : null,
|
|
5208
|
+
events: receipt ? sanitizeReceiptEvents(receipt) : [],
|
|
5209
|
+
};
|
|
5210
|
+
}
|
|
5211
|
+
return entries;
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
function sanitizeTransactionEvidence(transaction, block) {
|
|
5215
|
+
if (!transaction) {
|
|
5216
|
+
return null;
|
|
5217
|
+
}
|
|
5218
|
+
return normalizeCliOutput(serializeBigInts({
|
|
5219
|
+
hash: transaction.hash,
|
|
5220
|
+
from: transaction.from,
|
|
5221
|
+
to: transaction.to,
|
|
5222
|
+
nonce: transaction.nonce,
|
|
5223
|
+
data: transaction.data,
|
|
5224
|
+
value: transaction.value,
|
|
5225
|
+
chainId: transaction.chainId,
|
|
5226
|
+
blockHash: transaction.blockHash,
|
|
5227
|
+
blockNumber: transaction.blockNumber,
|
|
5228
|
+
blockTimestamp: block?.timestamp ?? null,
|
|
5229
|
+
blockTimestampIso: block?.timestamp ? new Date(Number(block.timestamp) * 1000).toISOString() : null,
|
|
5230
|
+
}));
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
function sanitizeReceiptEvents(receipt) {
|
|
5234
|
+
return (receipt.logs ?? []).map((log) => normalizeCliOutput(serializeBigInts({
|
|
5235
|
+
address: log.address,
|
|
5236
|
+
blockHash: log.blockHash,
|
|
5237
|
+
blockNumber: log.blockNumber,
|
|
5238
|
+
transactionHash: log.transactionHash,
|
|
5239
|
+
transactionIndex: log.transactionIndex,
|
|
5240
|
+
logIndex: log.index ?? log.logIndex ?? null,
|
|
5241
|
+
topics: log.topics,
|
|
5242
|
+
data: log.data,
|
|
5243
|
+
})));
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
function buildBlockTimestampCache(transactionEvidence) {
|
|
5247
|
+
const cache = {};
|
|
5248
|
+
for (const txRecord of Object.values(transactionEvidence)) {
|
|
5249
|
+
const tx = txRecord.transaction;
|
|
5250
|
+
if (tx?.blockNumber !== null && tx?.blockNumber !== undefined) {
|
|
5251
|
+
cache[Number(tx.blockNumber)] = {
|
|
5252
|
+
timestamp: tx.blockTimestamp ?? null,
|
|
5253
|
+
iso: tx.blockTimestampIso ?? null,
|
|
5254
|
+
};
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return cache;
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
function buildEvidenceNoteRecord({
|
|
5261
|
+
note,
|
|
5262
|
+
walletContext,
|
|
5263
|
+
walletMetadata,
|
|
5264
|
+
context,
|
|
5265
|
+
transactionEvidence,
|
|
5266
|
+
blockTimestampCache,
|
|
5267
|
+
}) {
|
|
5268
|
+
const creationBlockNumber = note.createdAtBlockNumber
|
|
5269
|
+
?? transactionEvidence[note.createdAtTxHash]?.transaction?.blockNumber
|
|
5270
|
+
?? null;
|
|
5271
|
+
const spentBlockNumber = note.spentAtBlockNumber
|
|
5272
|
+
?? transactionEvidence[note.spentAtTxHash]?.transaction?.blockNumber
|
|
5273
|
+
?? null;
|
|
5274
|
+
const scheme = note.encryptedNoteValue ? unpackEncryptedNoteValue(note.encryptedNoteValue).scheme : null;
|
|
5275
|
+
return normalizeCliOutput({
|
|
5276
|
+
recordType: "note-evidence",
|
|
5277
|
+
recordVersion: 1,
|
|
5278
|
+
noteId: note.commitment,
|
|
5279
|
+
walletScope: {
|
|
5280
|
+
network: walletMetadata.network,
|
|
5281
|
+
chainId: walletContext.wallet.chainId,
|
|
5282
|
+
channelName: walletMetadata.channelName,
|
|
5283
|
+
channelId: walletContext.wallet.channelId,
|
|
5284
|
+
wallet: walletContext.walletName,
|
|
5285
|
+
...walletLifecycleMetadata(walletContext.wallet),
|
|
5286
|
+
walletL1Address: walletContext.wallet.l1Address,
|
|
5287
|
+
walletL2Address: walletContext.wallet.l2Address,
|
|
5288
|
+
controller: context.workspace.controller,
|
|
5289
|
+
},
|
|
5290
|
+
plaintext: {
|
|
5291
|
+
owner: note.owner,
|
|
5292
|
+
value: note.value,
|
|
5293
|
+
salt: note.salt,
|
|
5294
|
+
},
|
|
5295
|
+
derived: {
|
|
5296
|
+
commitment: note.commitment,
|
|
5297
|
+
nullifier: note.nullifier,
|
|
5298
|
+
commitmentStorageKey: note.bridgeCommitmentKey,
|
|
5299
|
+
nullifierStorageKey: note.bridgeNullifierKey,
|
|
5300
|
+
},
|
|
5301
|
+
encryptedDelivery: {
|
|
5302
|
+
encryptedNoteValue: note.encryptedNoteValue,
|
|
5303
|
+
saltDerivation: "poseidon(encryptedNoteValue)",
|
|
5304
|
+
scheme: encryptedNoteSchemeLabel(scheme),
|
|
5305
|
+
event: {
|
|
5306
|
+
txHash: note.createdAtTxHash,
|
|
5307
|
+
blockNumber: creationBlockNumber,
|
|
5308
|
+
blockTimestamp: blockTimestampCache[Number(creationBlockNumber)]?.timestamp ?? null,
|
|
5309
|
+
blockTimestampIso: blockTimestampCache[Number(creationBlockNumber)]?.iso ?? null,
|
|
5310
|
+
logIndex: note.createdAtLogIndex,
|
|
5311
|
+
contract: context.workspace.channelManager,
|
|
5312
|
+
},
|
|
5313
|
+
},
|
|
5314
|
+
creation: {
|
|
5315
|
+
txHash: note.createdAtTxHash,
|
|
5316
|
+
blockNumber: creationBlockNumber,
|
|
5317
|
+
blockTimestamp: blockTimestampCache[Number(creationBlockNumber)]?.timestamp ?? null,
|
|
5318
|
+
blockTimestampIso: blockTimestampCache[Number(creationBlockNumber)]?.iso ?? null,
|
|
5319
|
+
function: note.createdByFunction,
|
|
5320
|
+
outputIndex: note.createdOutputIndex,
|
|
5321
|
+
acceptedTransition: acceptedTransitionReference(note.createdAtTxHash),
|
|
5322
|
+
},
|
|
5323
|
+
spend: {
|
|
5324
|
+
status: note.status,
|
|
5325
|
+
txHash: note.spentAtTxHash,
|
|
5326
|
+
blockNumber: spentBlockNumber,
|
|
5327
|
+
blockTimestamp: blockTimestampCache[Number(spentBlockNumber)]?.timestamp ?? null,
|
|
5328
|
+
blockTimestampIso: blockTimestampCache[Number(spentBlockNumber)]?.iso ?? null,
|
|
5329
|
+
function: note.spentByFunction,
|
|
5330
|
+
inputIndex: note.spentInputIndex,
|
|
5331
|
+
acceptedTransition: note.spentAtTxHash ? acceptedTransitionReference(note.spentAtTxHash) : null,
|
|
5332
|
+
},
|
|
5333
|
+
relationshipHints: {
|
|
5334
|
+
direction: note.counterpartyDirection ?? inferEvidenceDirection(note, scheme),
|
|
5335
|
+
counterpartyL2Address: note.counterpartyL2Address,
|
|
5336
|
+
counterpartyL1Address: null,
|
|
5337
|
+
confidence: note.counterpartyConfidence ?? (note.counterpartyL2Address ? "direct-local-metadata" : "unavailable"),
|
|
5338
|
+
},
|
|
5339
|
+
verificationClaims: {
|
|
5340
|
+
commitmentRecomputesFromPlaintext: true,
|
|
5341
|
+
nullifierRecomputesFromPlaintext: true,
|
|
5342
|
+
ownerMatchesWalletL2Address: true,
|
|
5343
|
+
spendingKeyIncluded: false,
|
|
5344
|
+
viewingKeyIncluded: false,
|
|
5345
|
+
walletSecretIncluded: false,
|
|
5346
|
+
fullWalletHistoryRequiredForFinalDisclosure: false,
|
|
5347
|
+
},
|
|
5348
|
+
});
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
function acceptedTransitionReference(txHash) {
|
|
5352
|
+
if (!txHash) {
|
|
5353
|
+
return null;
|
|
5354
|
+
}
|
|
5355
|
+
return {
|
|
5356
|
+
txHash,
|
|
5357
|
+
transactionPath: `transactions/${txHash}.json`,
|
|
5358
|
+
receiptPath: `receipts/${txHash}.json`,
|
|
5359
|
+
eventsPath: `events/${txHash}.json`,
|
|
5360
|
+
proofCalldataLocation: "transactions[].data",
|
|
5361
|
+
localProofArtifactIncluded: false,
|
|
5362
|
+
};
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5365
|
+
function encryptedNoteSchemeLabel(scheme) {
|
|
5366
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT) {
|
|
5367
|
+
return "self-mint";
|
|
5368
|
+
}
|
|
5369
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_TRANSFER) {
|
|
5370
|
+
return "transfer";
|
|
5371
|
+
}
|
|
5372
|
+
return "unknown";
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5375
|
+
function inferEvidenceDirection(note, scheme) {
|
|
5376
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT) {
|
|
5377
|
+
return "self-mint";
|
|
5378
|
+
}
|
|
5379
|
+
if (note.spentByFunction?.startsWith("transferNotes")) {
|
|
5380
|
+
return "sent";
|
|
5381
|
+
}
|
|
5382
|
+
if (note.createdByFunction?.startsWith("transferNotes")) {
|
|
5383
|
+
return "received";
|
|
5384
|
+
}
|
|
5385
|
+
return "unknown";
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5388
|
+
function buildEvidenceIndexes(noteRecords) {
|
|
5389
|
+
const indexes = {
|
|
5390
|
+
byCommitment: {},
|
|
5391
|
+
byNullifier: {},
|
|
5392
|
+
byCreationTx: {},
|
|
5393
|
+
bySpendTx: {},
|
|
5394
|
+
byBlockRange: [],
|
|
5395
|
+
byCounterparty: {
|
|
5396
|
+
unavailable: [],
|
|
5397
|
+
},
|
|
5398
|
+
};
|
|
5399
|
+
for (const record of noteRecords) {
|
|
5400
|
+
const pathName = evidenceNotePath(record);
|
|
5401
|
+
indexes.byCommitment[record.derived.commitment] = pathName;
|
|
5402
|
+
indexes.byNullifier[record.derived.nullifier] = pathName;
|
|
5403
|
+
pushIndexEntry(indexes.byCreationTx, record.creation.txHash, pathName);
|
|
5404
|
+
pushIndexEntry(indexes.bySpendTx, record.spend.txHash, pathName);
|
|
5405
|
+
indexes.byBlockRange.push({
|
|
5406
|
+
commitment: record.derived.commitment,
|
|
5407
|
+
createdAtBlockNumber: record.creation.blockNumber,
|
|
5408
|
+
spentAtBlockNumber: record.spend.blockNumber,
|
|
5409
|
+
path: pathName,
|
|
5410
|
+
});
|
|
5411
|
+
const counterparty = record.relationshipHints.counterpartyL2Address;
|
|
5412
|
+
if (counterparty) {
|
|
5413
|
+
if (!indexes.byCounterparty[counterparty]) {
|
|
5414
|
+
indexes.byCounterparty[counterparty] = { sent: [], received: [], both: [] };
|
|
5415
|
+
}
|
|
5416
|
+
const direction = record.relationshipHints.direction === "received" ? "received" : "sent";
|
|
5417
|
+
indexes.byCounterparty[counterparty][direction].push(pathName);
|
|
5418
|
+
indexes.byCounterparty[counterparty].both.push(pathName);
|
|
5419
|
+
} else {
|
|
5420
|
+
indexes.byCounterparty.unavailable.push(pathName);
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
indexes.byBlockRange.sort((left, right) =>
|
|
5424
|
+
Number(left.createdAtBlockNumber ?? Number.MAX_SAFE_INTEGER)
|
|
5425
|
+
- Number(right.createdAtBlockNumber ?? Number.MAX_SAFE_INTEGER));
|
|
5426
|
+
return indexes;
|
|
5427
|
+
}
|
|
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
|
+
|
|
5444
|
+
function pushIndexEntry(index, key, value) {
|
|
5445
|
+
if (!key) {
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
if (!index[key]) {
|
|
5449
|
+
index[key] = [];
|
|
5450
|
+
}
|
|
5451
|
+
index[key].push(value);
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
function buildEvidenceManifest({
|
|
5455
|
+
outputPath,
|
|
5456
|
+
walletContext,
|
|
5457
|
+
walletContexts = [walletContext],
|
|
5458
|
+
walletMetadata,
|
|
5459
|
+
context,
|
|
5460
|
+
noteRecords,
|
|
5461
|
+
txHashes,
|
|
5462
|
+
}) {
|
|
5463
|
+
return normalizeCliOutput({
|
|
5464
|
+
format: WALLET_EVIDENCE_BUNDLE_FORMAT,
|
|
5465
|
+
formatVersion: WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION,
|
|
5466
|
+
bundleType: "local-full-note-evidence",
|
|
5467
|
+
generatedAt: new Date().toISOString(),
|
|
5468
|
+
outputFileName: path.basename(outputPath),
|
|
5469
|
+
network: walletMetadata.network,
|
|
5470
|
+
chainId: walletContext.wallet.chainId,
|
|
5471
|
+
channelName: walletMetadata.channelName,
|
|
5472
|
+
channelId: walletContext.wallet.channelId,
|
|
5473
|
+
wallet: walletContext.walletName,
|
|
5474
|
+
walletL1Address: walletContext.wallet.l1Address,
|
|
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
|
+
})),
|
|
5482
|
+
controller: context.workspace.controller,
|
|
5483
|
+
channelManager: context.workspace.channelManager,
|
|
5484
|
+
bridgeTokenVault: context.workspace.bridgeTokenVault,
|
|
5485
|
+
containsAllLocallyKnownNotes: true,
|
|
5486
|
+
containsAllLocalWalletEpochs: true,
|
|
5487
|
+
containsNotePlaintext: true,
|
|
5488
|
+
noteCount: noteRecords.length,
|
|
5489
|
+
transactionCount: txHashes.length,
|
|
5490
|
+
intendedUse: "Input for private-state-cli investigator; not a default exchange submission package.",
|
|
5491
|
+
warning: "DO_NOT_SUBMIT_AS_IS unless full wallet-history disclosure is intended.",
|
|
5492
|
+
excludedSecrets: {
|
|
5493
|
+
spendingKey: true,
|
|
5494
|
+
viewingKey: true,
|
|
5495
|
+
walletSecret: true,
|
|
5496
|
+
accountPrivateKey: true,
|
|
5497
|
+
keyFiles: true,
|
|
5498
|
+
},
|
|
4564
5499
|
});
|
|
4565
5500
|
}
|
|
4566
5501
|
|
|
5502
|
+
function addEvidenceJson(archive, archivePath, value) {
|
|
5503
|
+
archive.addFile(archivePath, Buffer.from(`${JSON.stringify(normalizeCliOutput(value), null, 2)}\n`, "utf8"));
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
function assertEvidenceBundleDoesNotContainSecrets({ wallet = null, wallets = null, payload }) {
|
|
5507
|
+
const serialized = JSON.stringify(payload);
|
|
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);
|
|
5513
|
+
for (const value of forbiddenValues) {
|
|
5514
|
+
expect(
|
|
5515
|
+
!serialized.includes(value),
|
|
5516
|
+
"Evidence export refused to write authority-bearing wallet secret material.",
|
|
5517
|
+
);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
function uniqueNonNull(values) {
|
|
5522
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
5523
|
+
}
|
|
5524
|
+
|
|
4567
5525
|
async function handleTransferNotes({ args, provider }) {
|
|
4568
5526
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
5527
|
+
requireActiveWalletLifecycle(wallet, "wallet transfer-notes");
|
|
5528
|
+
requireWalletViewingCapability(wallet);
|
|
5529
|
+
requireWalletSpendingCapability(wallet);
|
|
4569
5530
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
4570
5531
|
const preparedContextResult = await loadFreshWalletChannelContext({
|
|
4571
5532
|
walletContext: wallet,
|
|
@@ -4603,6 +5564,19 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4603
5564
|
"The sum of --amounts must equal the sum of the selected input note values.",
|
|
4604
5565
|
);
|
|
4605
5566
|
|
|
5567
|
+
const { txSubmitter } = resolveTxSubmitterSigner({
|
|
5568
|
+
args,
|
|
5569
|
+
ownerSigner: signer,
|
|
5570
|
+
provider,
|
|
5571
|
+
});
|
|
5572
|
+
await requireActionImpactAcknowledgement("wallet-transfer-notes", args, {
|
|
5573
|
+
l1Address: txSubmitter.address,
|
|
5574
|
+
l2Address: wallet.wallet.l2Address,
|
|
5575
|
+
noteIds: noteIds.join(", "),
|
|
5576
|
+
amounts: amountInputs.join(", "),
|
|
5577
|
+
channelName: context.workspace.channelName,
|
|
5578
|
+
channelId: context.workspace.channelId,
|
|
5579
|
+
});
|
|
4606
5580
|
const templatePayload = await buildTransferNotesTemplatePayload({
|
|
4607
5581
|
context,
|
|
4608
5582
|
signer,
|
|
@@ -4623,6 +5597,9 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4623
5597
|
sourceFunction: templatePayload.method,
|
|
4624
5598
|
sourceTxHash: execution.receipt.hash,
|
|
4625
5599
|
bridgeCommitmentKeys: execution.noteLifecycle.outputCommitmentKeys,
|
|
5600
|
+
sourceBlockNumber: execution.receipt.blockNumber,
|
|
5601
|
+
counterpartyL2Addresses: templatePayload.recipientAddresses,
|
|
5602
|
+
counterpartyDirection: "sent",
|
|
4626
5603
|
});
|
|
4627
5604
|
|
|
4628
5605
|
printJson({
|
|
@@ -4744,16 +5721,40 @@ function ensureWallet({
|
|
|
4744
5721
|
storageKey,
|
|
4745
5722
|
leafIndex,
|
|
4746
5723
|
noteReceiveKeyMaterial,
|
|
5724
|
+
lifecycleEpoch,
|
|
4747
5725
|
rpcUrl,
|
|
4748
5726
|
}) {
|
|
4749
5727
|
const walletName = walletNameForChannelAndAddress(channelContext.workspace.channelName, signerAddress);
|
|
4750
|
-
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);
|
|
4751
5738
|
expect(!walletConfigExists(walletDir), `Wallet ${walletName} already exists on ${channelContext.workspace.network}.`);
|
|
4752
5739
|
ensureDir(walletDir);
|
|
4753
5740
|
ensureDir(path.join(walletDir, "operations"));
|
|
4754
5741
|
|
|
4755
5742
|
const wallet = normalizeWallet({
|
|
5743
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
4756
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,
|
|
4757
5758
|
network: channelContext.workspace.network,
|
|
4758
5759
|
rpcUrl,
|
|
4759
5760
|
chainId: channelContext.workspace.chainId,
|
|
@@ -4769,10 +5770,8 @@ function ensureWallet({
|
|
|
4769
5770
|
l2AccountingVault: channelContext.workspace.l2AccountingVault,
|
|
4770
5771
|
liquidBalancesSlot: channelContext.workspace.liquidBalancesSlot,
|
|
4771
5772
|
l1Address: signerAddress,
|
|
4772
|
-
l1PrivateKey: normalizePrivateKey(signerPrivateKey),
|
|
4773
5773
|
l2Address: l2Identity.l2Address,
|
|
4774
|
-
|
|
4775
|
-
l2PublicKey: ethers.hexlify(l2Identity.l2PublicKey),
|
|
5774
|
+
l2PublicKey: l2Identity.l2PublicKey ? ethers.hexlify(l2Identity.l2PublicKey) : null,
|
|
4776
5775
|
l2DerivationMode: CHANNEL_BOUND_L2_DERIVATION_MODE,
|
|
4777
5776
|
l2DerivationChannelName: channelContext.workspace.channelName,
|
|
4778
5777
|
l2StorageKey: storageKey,
|
|
@@ -4788,31 +5787,75 @@ function ensureWallet({
|
|
|
4788
5787
|
spent: {},
|
|
4789
5788
|
},
|
|
4790
5789
|
});
|
|
5790
|
+
if (l2Identity.l2PrivateKey) {
|
|
5791
|
+
wallet.l2PrivateKey = ethers.hexlify(l2Identity.l2PrivateKey);
|
|
5792
|
+
}
|
|
5793
|
+
wallet.noteReceivePrivateKey = normalizePrivateKey(noteReceiveKeyMaterial.privateKey);
|
|
4791
5794
|
|
|
4792
5795
|
const context = {
|
|
4793
5796
|
walletName,
|
|
4794
5797
|
walletDir,
|
|
4795
5798
|
wallet,
|
|
4796
|
-
walletSecret,
|
|
5799
|
+
walletSecret: wallet.l2PrivateKey,
|
|
4797
5800
|
};
|
|
5801
|
+
persistWalletKeys(context);
|
|
4798
5802
|
persistWallet(context);
|
|
4799
|
-
|
|
5803
|
+
persistWalletIndexForContext(context);
|
|
4800
5804
|
return context;
|
|
4801
5805
|
}
|
|
4802
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
|
+
|
|
4803
5842
|
function normalizeWallet(wallet) {
|
|
4804
5843
|
assertWalletHasCurrentFormat(wallet, wallet.name ?? "unknown");
|
|
5844
|
+
expect(wallet.walletEpochId, "Current wallet metadata requires walletEpochId. Run wallet recover-workspace to rebuild this wallet.");
|
|
4805
5845
|
const unusedNotes = Object.values(wallet.notes.unused).map(normalizeTrackedNote);
|
|
4806
5846
|
unusedNotes.sort(compareNotesByValueDesc);
|
|
4807
5847
|
const spentNotes = Object.values(wallet.notes.spent).map(normalizeTrackedNote);
|
|
4808
5848
|
|
|
4809
5849
|
return {
|
|
4810
5850
|
...wallet,
|
|
5851
|
+
canonicalWalletName: wallet.canonicalWalletName ?? wallet.name,
|
|
5852
|
+
walletEpochId: wallet.walletEpochId,
|
|
5853
|
+
lifecycleStatus: wallet.lifecycleStatus ?? "active",
|
|
4811
5854
|
canonicalAssetDecimals: Number(wallet.canonicalAssetDecimals),
|
|
4812
5855
|
l2Nonce: Number(wallet.l2Nonce),
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5856
|
+
l2PrivateKey: wallet.l2PrivateKey ? ethers.hexlify(wallet.l2PrivateKey) : null,
|
|
5857
|
+
l2PublicKey: wallet.l2PublicKey ? ethers.hexlify(wallet.l2PublicKey) : null,
|
|
5858
|
+
noteReceivePrivateKey: wallet.noteReceivePrivateKey ? normalizePrivateKey(wallet.noteReceivePrivateKey) : null,
|
|
4816
5859
|
noteReceiveDerivationVersion: Number(wallet.noteReceiveDerivationVersion),
|
|
4817
5860
|
noteReceiveTypedDataMethod: wallet.noteReceiveTypedDataMethod,
|
|
4818
5861
|
noteReceivePubKeyX: normalizeBytes32Hex(wallet.noteReceivePubKeyX),
|
|
@@ -4822,18 +5865,18 @@ function normalizeWallet(wallet) {
|
|
|
4822
5865
|
unused: Object.fromEntries(unusedNotes.map((note) => [note.commitment, note])),
|
|
4823
5866
|
spent: Object.fromEntries(spentNotes.map((note) => [note.nullifier, note])),
|
|
4824
5867
|
unusedOrder: unusedNotes.map((note) => note.commitment),
|
|
4825
|
-
unusedBalance: unusedNotes.
|
|
5868
|
+
unusedBalance: unusedNotes.every((note) => note.value !== null)
|
|
5869
|
+
? unusedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n).toString()
|
|
5870
|
+
: null,
|
|
4826
5871
|
},
|
|
4827
5872
|
};
|
|
4828
5873
|
}
|
|
4829
5874
|
|
|
4830
5875
|
function assertWalletHasCurrentFormat(wallet, walletName) {
|
|
4831
5876
|
const requiredKeys = [
|
|
5877
|
+
"walletFormatVersion",
|
|
4832
5878
|
"canonicalAssetDecimals",
|
|
4833
5879
|
"l2Nonce",
|
|
4834
|
-
"l1PrivateKey",
|
|
4835
|
-
"l2PrivateKey",
|
|
4836
|
-
"l2PublicKey",
|
|
4837
5880
|
"noteReceiveDerivationVersion",
|
|
4838
5881
|
"noteReceiveTypedDataMethod",
|
|
4839
5882
|
"noteReceivePubKeyX",
|
|
@@ -4849,18 +5892,48 @@ function assertWalletHasCurrentFormat(wallet, walletName) {
|
|
|
4849
5892
|
wallet.notes && typeof wallet.notes.unused === "object" && typeof wallet.notes.spent === "object",
|
|
4850
5893
|
`Wallet ${walletName} was not created with the current CLI notes format.`,
|
|
4851
5894
|
);
|
|
5895
|
+
expect(
|
|
5896
|
+
Number(wallet.walletFormatVersion) === WALLET_WORKSPACE_FORMAT_VERSION,
|
|
5897
|
+
[
|
|
5898
|
+
`Wallet ${walletName} uses unsupported wallet workspace format ${wallet.walletFormatVersion ?? "missing"}.`,
|
|
5899
|
+
"Rebuild the wallet metadata with wallet recover-workspace.",
|
|
5900
|
+
].join(" "),
|
|
5901
|
+
);
|
|
4852
5902
|
}
|
|
4853
5903
|
|
|
4854
5904
|
function normalizeTrackedNote(note) {
|
|
4855
5905
|
return {
|
|
4856
|
-
owner: getAddress(note.owner),
|
|
4857
|
-
value: ethers.toBigInt(note.value).toString(),
|
|
4858
|
-
salt: normalizeBytes32Hex(note.salt),
|
|
5906
|
+
owner: note.owner ? getAddress(note.owner) : null,
|
|
5907
|
+
value: note.value !== undefined && note.value !== null ? ethers.toBigInt(note.value).toString() : null,
|
|
5908
|
+
salt: note.salt ? normalizeBytes32Hex(note.salt) : null,
|
|
4859
5909
|
commitment: normalizeBytes32Hex(note.commitment),
|
|
4860
5910
|
nullifier: normalizeBytes32Hex(note.nullifier),
|
|
5911
|
+
encryptedNoteValue: note.encryptedNoteValue ? normalizeEncryptedNoteValueWords(note.encryptedNoteValue) : null,
|
|
4861
5912
|
status: note.status,
|
|
4862
5913
|
sourceFunction: note.sourceFunction ?? null,
|
|
4863
5914
|
sourceTxHash: note.sourceTxHash ?? null,
|
|
5915
|
+
createdAtTxHash: note.createdAtTxHash ?? (note.status === "unused" ? note.sourceTxHash ?? null : null),
|
|
5916
|
+
createdAtBlockNumber: note.createdAtBlockNumber !== undefined && note.createdAtBlockNumber !== null
|
|
5917
|
+
? Number(note.createdAtBlockNumber)
|
|
5918
|
+
: null,
|
|
5919
|
+
createdAtLogIndex: note.createdAtLogIndex !== undefined && note.createdAtLogIndex !== null
|
|
5920
|
+
? Number(note.createdAtLogIndex)
|
|
5921
|
+
: null,
|
|
5922
|
+
createdByFunction: note.createdByFunction ?? (note.status === "unused" ? note.sourceFunction ?? null : null),
|
|
5923
|
+
createdOutputIndex: note.createdOutputIndex !== undefined && note.createdOutputIndex !== null
|
|
5924
|
+
? Number(note.createdOutputIndex)
|
|
5925
|
+
: null,
|
|
5926
|
+
spentAtTxHash: note.spentAtTxHash ?? (note.status === "spent" ? note.sourceTxHash ?? null : null),
|
|
5927
|
+
spentAtBlockNumber: note.spentAtBlockNumber !== undefined && note.spentAtBlockNumber !== null
|
|
5928
|
+
? Number(note.spentAtBlockNumber)
|
|
5929
|
+
: null,
|
|
5930
|
+
spentByFunction: note.spentByFunction ?? (note.status === "spent" ? note.sourceFunction ?? null : null),
|
|
5931
|
+
spentInputIndex: note.spentInputIndex !== undefined && note.spentInputIndex !== null
|
|
5932
|
+
? Number(note.spentInputIndex)
|
|
5933
|
+
: null,
|
|
5934
|
+
counterpartyL2Address: note.counterpartyL2Address ? getAddress(note.counterpartyL2Address) : null,
|
|
5935
|
+
counterpartyDirection: note.counterpartyDirection ?? null,
|
|
5936
|
+
counterpartyConfidence: note.counterpartyConfidence ?? null,
|
|
4864
5937
|
bridgeCommitmentKey: note.bridgeCommitmentKey ? normalizeBytes32Hex(note.bridgeCommitmentKey) : null,
|
|
4865
5938
|
bridgeNullifierKey: note.bridgeNullifierKey ? normalizeBytes32Hex(note.bridgeNullifierKey) : null,
|
|
4866
5939
|
};
|
|
@@ -4886,15 +5959,28 @@ async function buildWalletNoteBridgeStatus({
|
|
|
4886
5959
|
return {
|
|
4887
5960
|
owner: note.owner,
|
|
4888
5961
|
valueBaseUnits: note.value,
|
|
4889
|
-
valueTokens: ethers.formatUnits(ethers.toBigInt(note.value), canonicalAssetDecimals),
|
|
5962
|
+
valueTokens: note.value === null ? null : ethers.formatUnits(ethers.toBigInt(note.value), canonicalAssetDecimals),
|
|
4890
5963
|
commitment: note.commitment,
|
|
4891
5964
|
nullifier: note.nullifier,
|
|
5965
|
+
encryptedNoteValue: note.encryptedNoteValue,
|
|
4892
5966
|
walletStatus: note.status,
|
|
4893
5967
|
bridgeCommitmentExists: commitmentExists,
|
|
4894
5968
|
bridgeNullifierUsed: nullifierUsed,
|
|
4895
5969
|
walletStatusMatchesBridge: commitmentExists && nullifierUsed === expectedNullifierUsed,
|
|
4896
5970
|
sourceFunction: note.sourceFunction ?? null,
|
|
4897
5971
|
sourceTxHash: note.sourceTxHash ?? null,
|
|
5972
|
+
createdAtTxHash: note.createdAtTxHash ?? null,
|
|
5973
|
+
createdAtBlockNumber: note.createdAtBlockNumber ?? null,
|
|
5974
|
+
createdAtLogIndex: note.createdAtLogIndex ?? null,
|
|
5975
|
+
createdByFunction: note.createdByFunction ?? null,
|
|
5976
|
+
createdOutputIndex: note.createdOutputIndex ?? null,
|
|
5977
|
+
spentAtTxHash: note.spentAtTxHash ?? null,
|
|
5978
|
+
spentAtBlockNumber: note.spentAtBlockNumber ?? null,
|
|
5979
|
+
spentByFunction: note.spentByFunction ?? null,
|
|
5980
|
+
spentInputIndex: note.spentInputIndex ?? null,
|
|
5981
|
+
counterpartyL2Address: note.counterpartyL2Address ?? null,
|
|
5982
|
+
counterpartyDirection: note.counterpartyDirection ?? null,
|
|
5983
|
+
counterpartyConfidence: note.counterpartyConfidence ?? null,
|
|
4898
5984
|
};
|
|
4899
5985
|
}
|
|
4900
5986
|
|
|
@@ -4912,8 +5998,8 @@ async function readBooleanStorageValueFromSnapshot({ snapshot, storageAddress, s
|
|
|
4912
5998
|
}
|
|
4913
5999
|
|
|
4914
6000
|
function compareNotesByValueDesc(left, right) {
|
|
4915
|
-
const leftValue = ethers.toBigInt(left.value);
|
|
4916
|
-
const rightValue = ethers.toBigInt(right.value);
|
|
6001
|
+
const leftValue = left.value === null || left.value === undefined ? 0n : ethers.toBigInt(left.value);
|
|
6002
|
+
const rightValue = right.value === null || right.value === undefined ? 0n : ethers.toBigInt(right.value);
|
|
4917
6003
|
if (leftValue === rightValue) {
|
|
4918
6004
|
return left.commitment.localeCompare(right.commitment);
|
|
4919
6005
|
}
|
|
@@ -4922,13 +6008,28 @@ function compareNotesByValueDesc(left, right) {
|
|
|
4922
6008
|
|
|
4923
6009
|
function buildTrackedNote(note, sourceFunction, sourceTxHash, bridgeKeys = {}) {
|
|
4924
6010
|
const normalizedNote = normalizePlaintextNote(note);
|
|
6011
|
+
const createdTxHash = bridgeKeys.createdAtTxHash ?? sourceTxHash ?? null;
|
|
6012
|
+
const createdFunction = bridgeKeys.createdByFunction ?? sourceFunction ?? null;
|
|
4925
6013
|
return {
|
|
4926
6014
|
...normalizedNote,
|
|
4927
6015
|
commitment: normalizeBytes32Hex(computeNoteCommitment(normalizedNote)),
|
|
4928
6016
|
nullifier: normalizeBytes32Hex(computeNullifier(normalizedNote)),
|
|
6017
|
+
encryptedNoteValue: note.encryptedNoteValue ? normalizeEncryptedNoteValueWords(note.encryptedNoteValue) : null,
|
|
4929
6018
|
status: "unused",
|
|
4930
6019
|
sourceFunction,
|
|
4931
6020
|
sourceTxHash,
|
|
6021
|
+
createdAtTxHash: createdTxHash,
|
|
6022
|
+
createdAtBlockNumber: bridgeKeys.createdAtBlockNumber ?? null,
|
|
6023
|
+
createdAtLogIndex: bridgeKeys.createdAtLogIndex ?? null,
|
|
6024
|
+
createdByFunction: createdFunction,
|
|
6025
|
+
createdOutputIndex: bridgeKeys.createdOutputIndex ?? null,
|
|
6026
|
+
spentAtTxHash: bridgeKeys.spentAtTxHash ?? null,
|
|
6027
|
+
spentAtBlockNumber: bridgeKeys.spentAtBlockNumber ?? null,
|
|
6028
|
+
spentByFunction: bridgeKeys.spentByFunction ?? null,
|
|
6029
|
+
spentInputIndex: bridgeKeys.spentInputIndex ?? null,
|
|
6030
|
+
counterpartyL2Address: bridgeKeys.counterpartyL2Address ? getAddress(bridgeKeys.counterpartyL2Address) : null,
|
|
6031
|
+
counterpartyDirection: bridgeKeys.counterpartyDirection ?? null,
|
|
6032
|
+
counterpartyConfidence: bridgeKeys.counterpartyConfidence ?? null,
|
|
4932
6033
|
bridgeCommitmentKey: bridgeKeys.bridgeCommitmentKey
|
|
4933
6034
|
? normalizeBytes32Hex(bridgeKeys.bridgeCommitmentKey)
|
|
4934
6035
|
: null,
|
|
@@ -4943,9 +6044,19 @@ function buildLifecycleTrackedOutputs({
|
|
|
4943
6044
|
sourceFunction,
|
|
4944
6045
|
sourceTxHash,
|
|
4945
6046
|
bridgeCommitmentKeys,
|
|
6047
|
+
sourceBlockNumber = null,
|
|
6048
|
+
counterpartyL2Addresses = [],
|
|
6049
|
+
counterpartyDirection = null,
|
|
4946
6050
|
}) {
|
|
4947
6051
|
return (outputNotes ?? []).map((note, index) => buildTrackedNote(note, sourceFunction, sourceTxHash, {
|
|
4948
6052
|
bridgeCommitmentKey: bridgeCommitmentKeys?.[index] ?? null,
|
|
6053
|
+
createdAtTxHash: sourceTxHash,
|
|
6054
|
+
createdAtBlockNumber: sourceBlockNumber,
|
|
6055
|
+
createdByFunction: sourceFunction,
|
|
6056
|
+
createdOutputIndex: index,
|
|
6057
|
+
counterpartyL2Address: counterpartyL2Addresses?.[index] ?? null,
|
|
6058
|
+
counterpartyDirection,
|
|
6059
|
+
counterpartyConfidence: counterpartyL2Addresses?.[index] ? "direct-local-metadata" : null,
|
|
4949
6060
|
}));
|
|
4950
6061
|
}
|
|
4951
6062
|
|
|
@@ -4958,13 +6069,11 @@ async function recoverWalletReceivedNotes({
|
|
|
4958
6069
|
progressAction = null,
|
|
4959
6070
|
fromGenesis = false,
|
|
4960
6071
|
}) {
|
|
4961
|
-
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ??
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
account: signer.address,
|
|
4967
|
-
});
|
|
6072
|
+
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ?? {
|
|
6073
|
+
privateKey: walletContext.wallet.noteReceivePrivateKey,
|
|
6074
|
+
noteReceivePubKey: walletNoteReceivePubKey(walletContext),
|
|
6075
|
+
};
|
|
6076
|
+
requireWalletViewingCapability(walletContext);
|
|
4968
6077
|
const recoveredDeliveryState = await recoverDeliveredNotesFromEventLogs({
|
|
4969
6078
|
walletContext,
|
|
4970
6079
|
context,
|
|
@@ -5064,12 +6173,22 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
5064
6173
|
owner: walletContext.wallet.l2Address,
|
|
5065
6174
|
value: recoveredValue,
|
|
5066
6175
|
salt: computeEncryptedNoteSalt(encryptedNoteValue),
|
|
6176
|
+
encryptedNoteValue,
|
|
5067
6177
|
});
|
|
5068
6178
|
const commitment = normalizeBytes32Hex(computeNoteCommitment(plaintextNote));
|
|
5069
6179
|
const nullifier = normalizeBytes32Hex(computeNullifier(plaintextNote));
|
|
5070
|
-
const trackedNote = buildTrackedNote(
|
|
6180
|
+
const trackedNote = buildTrackedNote({
|
|
6181
|
+
...plaintextNote,
|
|
6182
|
+
encryptedNoteValue,
|
|
6183
|
+
}, sourceFunction, log.transactionHash, {
|
|
5071
6184
|
bridgeCommitmentKey: derivePrivateStateControllerMappingStorageKey(commitment, commitmentExistsSlot),
|
|
5072
6185
|
bridgeNullifierKey: derivePrivateStateControllerMappingStorageKey(nullifier, nullifierUsedSlot),
|
|
6186
|
+
createdAtTxHash: log.transactionHash,
|
|
6187
|
+
createdAtBlockNumber: log.blockNumber !== undefined ? Number(log.blockNumber) : null,
|
|
6188
|
+
createdAtLogIndex: log.index ?? log.logIndex ?? null,
|
|
6189
|
+
createdByFunction: sourceFunction,
|
|
6190
|
+
counterpartyDirection: scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT ? "self-mint" : "received",
|
|
6191
|
+
counterpartyConfidence: scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT ? "direct-local-metadata" : "unavailable",
|
|
5073
6192
|
});
|
|
5074
6193
|
const commitmentExists = await readBooleanStorageValueFromSnapshot({
|
|
5075
6194
|
snapshot: context.currentSnapshot,
|
|
@@ -5420,7 +6539,7 @@ function snapshotRootForAddress(snapshot, storageAddress) {
|
|
|
5420
6539
|
return snapshot.stateRoots[addressIndex];
|
|
5421
6540
|
}
|
|
5422
6541
|
|
|
5423
|
-
function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, sourceTxHash) {
|
|
6542
|
+
function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, sourceTxHash, sourceBlockNumber = null) {
|
|
5424
6543
|
for (const [index, inputNote] of lifecycle.inputs.entries()) {
|
|
5425
6544
|
const trackedInput = buildTrackedNote(inputNote, sourceFunction, sourceTxHash);
|
|
5426
6545
|
const existingUnusedNote = walletContext.wallet.notes.unused[trackedInput.commitment];
|
|
@@ -5434,12 +6553,26 @@ function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, so
|
|
|
5434
6553
|
sourceFunction,
|
|
5435
6554
|
sourceTxHash,
|
|
5436
6555
|
bridgeNullifierKey: lifecycle.inputNullifierKeys?.[index] ?? existingUnusedNote.bridgeNullifierKey ?? null,
|
|
6556
|
+
spentAtTxHash: sourceTxHash,
|
|
6557
|
+
spentAtBlockNumber: sourceBlockNumber,
|
|
6558
|
+
spentByFunction: sourceFunction,
|
|
6559
|
+
spentInputIndex: index,
|
|
6560
|
+
counterpartyL2Address: lifecycle.spendCounterpartyL2Address ?? existingUnusedNote.counterpartyL2Address ?? null,
|
|
6561
|
+
counterpartyDirection: lifecycle.spendCounterpartyDirection ?? existingUnusedNote.counterpartyDirection ?? null,
|
|
6562
|
+
counterpartyConfidence: lifecycle.spendCounterpartyConfidence ?? existingUnusedNote.counterpartyConfidence ?? null,
|
|
5437
6563
|
};
|
|
5438
6564
|
}
|
|
5439
6565
|
|
|
5440
6566
|
for (const [index, outputNote] of lifecycle.outputs.entries()) {
|
|
5441
6567
|
const trackedOutput = buildTrackedNote(outputNote, sourceFunction, sourceTxHash, {
|
|
5442
6568
|
bridgeCommitmentKey: lifecycle.outputCommitmentKeys?.[index] ?? null,
|
|
6569
|
+
createdAtTxHash: sourceTxHash,
|
|
6570
|
+
createdAtBlockNumber: sourceBlockNumber,
|
|
6571
|
+
createdByFunction: sourceFunction,
|
|
6572
|
+
createdOutputIndex: index,
|
|
6573
|
+
counterpartyL2Address: lifecycle.outputCounterpartyL2Addresses?.[index] ?? null,
|
|
6574
|
+
counterpartyDirection: lifecycle.outputCounterpartyDirection ?? null,
|
|
6575
|
+
counterpartyConfidence: lifecycle.outputCounterpartyL2Addresses?.[index] ? "direct-local-metadata" : null,
|
|
5443
6576
|
});
|
|
5444
6577
|
if (trackedOutput.owner !== walletContext.wallet.l2Address) {
|
|
5445
6578
|
continue;
|
|
@@ -5524,6 +6657,7 @@ function buildMintEncryptedOutputs({ wallet, values }) {
|
|
|
5524
6657
|
owner: wallet.wallet.l2Address,
|
|
5525
6658
|
value: ethers.toBigInt(value).toString(),
|
|
5526
6659
|
salt: computeEncryptedNoteSalt(encryptedNoteValue),
|
|
6660
|
+
encryptedNoteValue,
|
|
5527
6661
|
});
|
|
5528
6662
|
}
|
|
5529
6663
|
return {
|
|
@@ -5581,6 +6715,7 @@ async function buildTransferNotesTemplatePayload({
|
|
|
5581
6715
|
owner: recipient,
|
|
5582
6716
|
value: ethers.toBigInt(outputAmounts[index]).toString(),
|
|
5583
6717
|
salt,
|
|
6718
|
+
encryptedNoteValue,
|
|
5584
6719
|
});
|
|
5585
6720
|
}
|
|
5586
6721
|
return {
|
|
@@ -5589,6 +6724,7 @@ async function buildTransferNotesTemplatePayload({
|
|
|
5589
6724
|
args: [transferOutputs, inputNotes],
|
|
5590
6725
|
lifecycleInputs: inputNotes,
|
|
5591
6726
|
lifecycleOutputs,
|
|
6727
|
+
recipientAddresses,
|
|
5592
6728
|
};
|
|
5593
6729
|
}
|
|
5594
6730
|
|
|
@@ -5609,6 +6745,10 @@ function loadWalletUnusedInputNotes(walletContext, noteIds) {
|
|
|
5609
6745
|
return noteIds.map((noteId) => {
|
|
5610
6746
|
const trackedNote = walletContext.wallet.notes.unused[noteId];
|
|
5611
6747
|
expect(trackedNote, `Unknown unused note commitment: ${noteId}.`);
|
|
6748
|
+
expect(
|
|
6749
|
+
trackedNote.owner && trackedNote.value !== null && trackedNote.salt,
|
|
6750
|
+
`Note ${noteId} is encrypted-only. Import the wallet viewing key before spending it.`,
|
|
6751
|
+
);
|
|
5612
6752
|
return normalizePlaintextNote(trackedNote);
|
|
5613
6753
|
});
|
|
5614
6754
|
}
|
|
@@ -5957,6 +7097,7 @@ async function executeWalletDirectTemplateCommand({
|
|
|
5957
7097
|
}) {
|
|
5958
7098
|
emitProgress(operationName, "loading");
|
|
5959
7099
|
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
7100
|
+
requireWalletSpendingCapability(wallet);
|
|
5960
7101
|
const {
|
|
5961
7102
|
txSubmitter,
|
|
5962
7103
|
source: txSubmitterSource,
|
|
@@ -6098,7 +7239,16 @@ async function executeWalletTemplateSend({
|
|
|
6098
7239
|
|
|
6099
7240
|
emitProgress(operationName, "persisting");
|
|
6100
7241
|
wallet.wallet.l2Nonce = nonce + 1;
|
|
6101
|
-
|
|
7242
|
+
if (functionName.startsWith("transferNotes")) {
|
|
7243
|
+
noteLifecycle.spendCounterpartyL2Address = templatePayload.recipientAddresses?.[0] ?? null;
|
|
7244
|
+
noteLifecycle.spendCounterpartyDirection = "sent";
|
|
7245
|
+
noteLifecycle.spendCounterpartyConfidence = noteLifecycle.spendCounterpartyL2Address
|
|
7246
|
+
? "direct-local-metadata"
|
|
7247
|
+
: null;
|
|
7248
|
+
noteLifecycle.outputCounterpartyL2Addresses = templatePayload.recipientAddresses ?? [];
|
|
7249
|
+
noteLifecycle.outputCounterpartyDirection = "sent";
|
|
7250
|
+
}
|
|
7251
|
+
applyNoteLifecycleToWallet(wallet, noteLifecycle, functionName, receipt.hash, receipt.blockNumber);
|
|
6102
7252
|
context.currentSnapshot = nextSnapshot;
|
|
6103
7253
|
persistWallet(wallet);
|
|
6104
7254
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -6107,7 +7257,7 @@ async function executeWalletTemplateSend({
|
|
|
6107
7257
|
receipt,
|
|
6108
7258
|
progressAction: operationName,
|
|
6109
7259
|
});
|
|
6110
|
-
sealWalletOperationDir(operationDir, wallet
|
|
7260
|
+
sealWalletOperationDir(operationDir, walletOperationSealSecret(wallet));
|
|
6111
7261
|
|
|
6112
7262
|
return {
|
|
6113
7263
|
wallet,
|
|
@@ -6195,34 +7345,76 @@ async function loadJoinChannelContext({ args, network, provider }) {
|
|
|
6195
7345
|
};
|
|
6196
7346
|
}
|
|
6197
7347
|
|
|
6198
|
-
function loadWallet(walletName,
|
|
7348
|
+
function loadWallet(walletName, networkName) {
|
|
7349
|
+
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
7350
|
+
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
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 }) {
|
|
6199
7360
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
6200
7361
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
6201
|
-
const walletDir = walletPath(normalizedWalletName, normalizedNetworkName);
|
|
6202
7362
|
if (!walletConfigExists(walletDir)) {
|
|
6203
7363
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
6204
7364
|
}
|
|
6205
|
-
const rawWallet =
|
|
7365
|
+
const rawWallet = readJson(walletNotesMetadataPath(walletDir));
|
|
7366
|
+
const spendingKey = readWalletKeySecretIfExists({
|
|
7367
|
+
networkName: normalizedNetworkName,
|
|
7368
|
+
walletName: normalizedWalletName,
|
|
7369
|
+
keyKind: "spending",
|
|
7370
|
+
});
|
|
7371
|
+
const viewingKey = readWalletKeySecretIfExists({
|
|
7372
|
+
networkName: normalizedNetworkName,
|
|
7373
|
+
walletName: normalizedWalletName,
|
|
7374
|
+
keyKind: "viewing",
|
|
7375
|
+
});
|
|
7376
|
+
if (spendingKey && walletSpendingKeyMatchesWallet(spendingKey.metadata, rawWallet)) {
|
|
7377
|
+
rawWallet.l2PrivateKey = spendingKey.privateKey;
|
|
7378
|
+
rawWallet.l2PublicKey = spendingKey.metadata?.l2PublicKey ?? rawWallet.l2PublicKey;
|
|
7379
|
+
}
|
|
7380
|
+
if (viewingKey && walletViewingKeyMatchesWallet(viewingKey.metadata, rawWallet)) {
|
|
7381
|
+
rawWallet.noteReceivePrivateKey = viewingKey.privateKey;
|
|
7382
|
+
}
|
|
6206
7383
|
assertWalletHasRequiredKeys(rawWallet, normalizedWalletName);
|
|
6207
7384
|
const wallet = normalizeWallet(rawWallet);
|
|
6208
7385
|
assertWalletUsesChannelBoundDerivation(wallet, normalizedWalletName);
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
7386
|
+
if (wallet.l2PrivateKey) {
|
|
7387
|
+
const restoredIdentity = restoreParticipantIdentityFromWallet(wallet);
|
|
7388
|
+
expect(
|
|
7389
|
+
wallet.l2Address === restoredIdentity.l2Address,
|
|
7390
|
+
`Wallet ${normalizedWalletName} is internally inconsistent: stored keys do not match the stored L2 address.`,
|
|
7391
|
+
);
|
|
7392
|
+
}
|
|
7393
|
+
hydrateWalletNotesWithViewingKey(wallet);
|
|
6214
7394
|
const context = {
|
|
6215
7395
|
walletName: normalizedWalletName,
|
|
6216
7396
|
walletDir,
|
|
6217
7397
|
wallet,
|
|
6218
|
-
walletSecret,
|
|
7398
|
+
walletSecret: wallet.l2PrivateKey ?? wallet.noteReceivePrivateKey ?? null,
|
|
6219
7399
|
};
|
|
6220
7400
|
return context;
|
|
6221
7401
|
}
|
|
6222
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
|
+
|
|
6223
7415
|
function loadUnlockedWalletWithMetadata(args) {
|
|
6224
7416
|
const networkName = requireNetworkName(args);
|
|
6225
|
-
const wallet = loadWallet(requireWalletName(args),
|
|
7417
|
+
const wallet = loadWallet(requireWalletName(args), networkName);
|
|
6226
7418
|
const walletMetadata = loadWalletMetadata(wallet.walletName, networkName);
|
|
6227
7419
|
assertWalletMatchesMetadata(wallet, walletMetadata);
|
|
6228
7420
|
expect(
|
|
@@ -6238,19 +7430,71 @@ function loadUnlockedWalletWithMetadata(args) {
|
|
|
6238
7430
|
};
|
|
6239
7431
|
}
|
|
6240
7432
|
|
|
7433
|
+
function readWalletKeySecretIfExists({ networkName, walletName, keyKind }) {
|
|
7434
|
+
const secretPath = keyKind === "spending"
|
|
7435
|
+
? walletSpendingKeySecretPath(networkName, walletName)
|
|
7436
|
+
: walletViewingKeySecretPath(networkName, walletName);
|
|
7437
|
+
if (!fs.existsSync(secretPath)) {
|
|
7438
|
+
return null;
|
|
7439
|
+
}
|
|
7440
|
+
const payload = JSON.parse(readSecretFile(secretPath, `${keyKind} key`));
|
|
7441
|
+
validateWalletKeyPayload(payload, keyKind);
|
|
7442
|
+
return payload;
|
|
7443
|
+
}
|
|
7444
|
+
|
|
7445
|
+
function validateWalletKeyPayload(payload, keyKind) {
|
|
7446
|
+
expect(payload?.format === WALLET_KEY_EXPORT_FORMAT, `Invalid ${keyKind} key file format.`);
|
|
7447
|
+
expect(Number(payload.formatVersion) === WALLET_EXPORT_FORMAT_VERSION, `Unsupported ${keyKind} key file version.`);
|
|
7448
|
+
expect(payload.keyKind === keyKind, `Expected ${keyKind} key file, received ${payload.keyKind}.`);
|
|
7449
|
+
expect(typeof payload.privateKey === "string" && payload.privateKey.length > 0, `Missing ${keyKind} private key.`);
|
|
7450
|
+
expect(payload.metadata && typeof payload.metadata === "object", `Missing ${keyKind} key metadata.`);
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
function hydrateWalletNotesWithViewingKey(wallet) {
|
|
7454
|
+
if (!wallet.noteReceivePrivateKey) {
|
|
7455
|
+
return;
|
|
7456
|
+
}
|
|
7457
|
+
const noteGroups = [wallet.notes?.unused ?? {}, wallet.notes?.spent ?? {}];
|
|
7458
|
+
for (const notes of noteGroups) {
|
|
7459
|
+
for (const note of Object.values(notes)) {
|
|
7460
|
+
if (!note.encryptedNoteValue || note.value !== null) {
|
|
7461
|
+
continue;
|
|
7462
|
+
}
|
|
7463
|
+
try {
|
|
7464
|
+
const { scheme } = unpackEncryptedNoteValue(note.encryptedNoteValue);
|
|
7465
|
+
let value;
|
|
7466
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_TRANSFER) {
|
|
7467
|
+
value = decryptEncryptedNoteValue({
|
|
7468
|
+
encryptedValue: note.encryptedNoteValue,
|
|
7469
|
+
noteReceivePrivateKey: wallet.noteReceivePrivateKey,
|
|
7470
|
+
chainId: wallet.chainId,
|
|
7471
|
+
channelId: wallet.channelId,
|
|
7472
|
+
owner: wallet.l2Address,
|
|
7473
|
+
});
|
|
7474
|
+
} else if (scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT) {
|
|
7475
|
+
value = decryptMintEncryptedNoteValue({
|
|
7476
|
+
encryptedValue: note.encryptedNoteValue,
|
|
7477
|
+
noteReceivePrivateKey: wallet.noteReceivePrivateKey,
|
|
7478
|
+
chainId: wallet.chainId,
|
|
7479
|
+
channelId: wallet.channelId,
|
|
7480
|
+
owner: wallet.l2Address,
|
|
7481
|
+
});
|
|
7482
|
+
} else {
|
|
7483
|
+
continue;
|
|
7484
|
+
}
|
|
7485
|
+
note.owner = wallet.l2Address;
|
|
7486
|
+
note.value = ethers.toBigInt(value).toString();
|
|
7487
|
+
note.salt = computeEncryptedNoteSalt(note.encryptedNoteValue);
|
|
7488
|
+
} catch {
|
|
7489
|
+
// Keep encrypted-only note records readable even when the local viewing key cannot decrypt them.
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
wallet.notes = normalizeWallet(wallet).notes;
|
|
7494
|
+
}
|
|
7495
|
+
|
|
6241
7496
|
function assertWalletHasRequiredKeys(wallet, walletName) {
|
|
6242
|
-
expect(
|
|
6243
|
-
typeof wallet.l1PrivateKey === "string" && wallet.l1PrivateKey.length > 0,
|
|
6244
|
-
`Wallet ${walletName} is missing the stored L1 private key.`,
|
|
6245
|
-
);
|
|
6246
|
-
expect(
|
|
6247
|
-
typeof wallet.l2PrivateKey === "string" && wallet.l2PrivateKey.length > 0,
|
|
6248
|
-
`Wallet ${walletName} is missing the stored L2 private key.`,
|
|
6249
|
-
);
|
|
6250
|
-
expect(
|
|
6251
|
-
typeof wallet.l2PublicKey === "string" && wallet.l2PublicKey.length > 0,
|
|
6252
|
-
`Wallet ${walletName} is missing the stored L2 public key.`,
|
|
6253
|
-
);
|
|
7497
|
+
expect(wallet.walletFormatVersion !== undefined, `Wallet ${walletName} is missing walletFormatVersion.`);
|
|
6254
7498
|
}
|
|
6255
7499
|
|
|
6256
7500
|
function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
|
|
@@ -6271,6 +7515,13 @@ function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
|
|
|
6271
7515
|
}
|
|
6272
7516
|
|
|
6273
7517
|
function restoreParticipantIdentityFromWallet(wallet) {
|
|
7518
|
+
if (!wallet.l2PrivateKey) {
|
|
7519
|
+
return {
|
|
7520
|
+
l2PrivateKey: null,
|
|
7521
|
+
l2PublicKey: wallet.l2PublicKey ? Uint8Array.from(ethers.getBytes(wallet.l2PublicKey)) : null,
|
|
7522
|
+
l2Address: getAddress(wallet.l2Address),
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
6274
7525
|
const l2PrivateKey = Uint8Array.from(ethers.getBytes(wallet.l2PrivateKey));
|
|
6275
7526
|
const l2PublicKey = Uint8Array.from(ethers.getBytes(wallet.l2PublicKey));
|
|
6276
7527
|
const l2Address = getAddress(fromEdwardsToAddress(l2PublicKey).toString());
|
|
@@ -6282,7 +7533,14 @@ function restoreParticipantIdentityFromWallet(wallet) {
|
|
|
6282
7533
|
}
|
|
6283
7534
|
|
|
6284
7535
|
function restoreWalletSigner(walletContext, provider) {
|
|
6285
|
-
|
|
7536
|
+
const privateKey = findAccountPrivateKeyForAddress(walletContext.wallet.network, walletContext.wallet.l1Address);
|
|
7537
|
+
if (privateKey) {
|
|
7538
|
+
return new Wallet(privateKey, provider);
|
|
7539
|
+
}
|
|
7540
|
+
return {
|
|
7541
|
+
address: getAddress(walletContext.wallet.l1Address),
|
|
7542
|
+
provider,
|
|
7543
|
+
};
|
|
6286
7544
|
}
|
|
6287
7545
|
|
|
6288
7546
|
function restoreWalletParticipant(walletContext, provider) {
|
|
@@ -6292,6 +7550,85 @@ function restoreWalletParticipant(walletContext, provider) {
|
|
|
6292
7550
|
};
|
|
6293
7551
|
}
|
|
6294
7552
|
|
|
7553
|
+
function requireWalletOwnerSigner(walletContext, provider) {
|
|
7554
|
+
const signer = restoreWalletSigner(walletContext, provider);
|
|
7555
|
+
expect(
|
|
7556
|
+
typeof signer.privateKey === "string",
|
|
7557
|
+
[
|
|
7558
|
+
`Missing local account secret for wallet owner ${walletContext.wallet.l1Address}.`,
|
|
7559
|
+
"Import the matching account secret or use a command-specific transaction submitter where supported.",
|
|
7560
|
+
].join(" "),
|
|
7561
|
+
);
|
|
7562
|
+
return signer;
|
|
7563
|
+
}
|
|
7564
|
+
|
|
7565
|
+
function requireWalletSpendingCapability(walletContext) {
|
|
7566
|
+
expect(
|
|
7567
|
+
walletContext.wallet.l2PrivateKey,
|
|
7568
|
+
[
|
|
7569
|
+
`Wallet ${walletContext.walletName} is missing its spending key.`,
|
|
7570
|
+
"Import it with wallet import spending-key before commands that spend notes or change L2 channel accounting state.",
|
|
7571
|
+
].join(" "),
|
|
7572
|
+
);
|
|
7573
|
+
}
|
|
7574
|
+
|
|
7575
|
+
function requireWalletViewingCapability(walletContext) {
|
|
7576
|
+
expect(
|
|
7577
|
+
walletContext.wallet.noteReceivePrivateKey,
|
|
7578
|
+
[
|
|
7579
|
+
`Wallet ${walletContext.walletName} is missing its viewing key.`,
|
|
7580
|
+
"Import it with wallet import viewing-key before commands that decrypt or refresh received notes.",
|
|
7581
|
+
].join(" "),
|
|
7582
|
+
);
|
|
7583
|
+
}
|
|
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
|
+
|
|
7595
|
+
function walletOperationSealSecret(walletContext) {
|
|
7596
|
+
const secret = walletContext.wallet.l2PrivateKey
|
|
7597
|
+
?? walletContext.wallet.noteReceivePrivateKey
|
|
7598
|
+
?? findAccountPrivateKeyForAddress(walletContext.wallet.network, walletContext.wallet.l1Address);
|
|
7599
|
+
expect(
|
|
7600
|
+
secret,
|
|
7601
|
+
`Wallet ${walletContext.walletName} needs a local key to seal operation artifacts.`,
|
|
7602
|
+
);
|
|
7603
|
+
return secret;
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
function findAccountPrivateKeyForAddress(networkName, l1Address) {
|
|
7607
|
+
const accountsRoot = path.join(secretRoot, requireNetworkName({ network: networkName }), "accounts");
|
|
7608
|
+
if (!fs.existsSync(accountsRoot)) {
|
|
7609
|
+
return null;
|
|
7610
|
+
}
|
|
7611
|
+
for (const entry of fs.readdirSync(accountsRoot, { withFileTypes: true })) {
|
|
7612
|
+
if (!entry.isDirectory()) {
|
|
7613
|
+
continue;
|
|
7614
|
+
}
|
|
7615
|
+
const privateKeyPath = path.join(accountsRoot, entry.name, "private-key");
|
|
7616
|
+
if (!fs.existsSync(privateKeyPath)) {
|
|
7617
|
+
continue;
|
|
7618
|
+
}
|
|
7619
|
+
try {
|
|
7620
|
+
const privateKey = normalizePrivateKey(readSecretFile(privateKeyPath, "--account"));
|
|
7621
|
+
const signer = new Wallet(privateKey);
|
|
7622
|
+
if (ethers.toBigInt(getAddress(signer.address)) === ethers.toBigInt(getAddress(l1Address))) {
|
|
7623
|
+
return privateKey;
|
|
7624
|
+
}
|
|
7625
|
+
} catch {
|
|
7626
|
+
continue;
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
return null;
|
|
7630
|
+
}
|
|
7631
|
+
|
|
6295
7632
|
function loadBridgeResources({ chainId }) {
|
|
6296
7633
|
const bridgeDeploymentPath = defaultBridgeDeploymentPath(chainId);
|
|
6297
7634
|
const bridgeDeployment = readJson(bridgeDeploymentPath);
|
|
@@ -6309,11 +7646,11 @@ function loadBridgeResources({ chainId }) {
|
|
|
6309
7646
|
function loadWalletMetadata(walletName, networkName) {
|
|
6310
7647
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
6311
7648
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
6312
|
-
const walletDir =
|
|
7649
|
+
const walletDir = selectedWalletEpochDir(normalizedWalletName, normalizedNetworkName);
|
|
6313
7650
|
if (!walletConfigExists(walletDir)) {
|
|
6314
7651
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
6315
7652
|
}
|
|
6316
|
-
const metadataPath =
|
|
7653
|
+
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
6317
7654
|
if (!fs.existsSync(metadataPath)) {
|
|
6318
7655
|
throw new Error(`Wallet ${normalizedWalletName} is missing unencrypted metadata at ${metadataPath}.`);
|
|
6319
7656
|
}
|
|
@@ -6342,14 +7679,14 @@ function assertWalletMatchesMetadata(walletContext, walletMetadata) {
|
|
|
6342
7679
|
walletContext.wallet.network === walletMetadata.network,
|
|
6343
7680
|
[
|
|
6344
7681
|
`Wallet ${walletContext.walletName} metadata network (${walletMetadata.network}) does not match`,
|
|
6345
|
-
`the
|
|
7682
|
+
`the wallet note metadata network (${walletContext.wallet.network}).`,
|
|
6346
7683
|
].join(" "),
|
|
6347
7684
|
);
|
|
6348
7685
|
expect(
|
|
6349
7686
|
walletContext.wallet.channelName === walletMetadata.channelName,
|
|
6350
7687
|
[
|
|
6351
7688
|
`Wallet ${walletContext.walletName} metadata channelName (${walletMetadata.channelName}) does not match`,
|
|
6352
|
-
`the
|
|
7689
|
+
`the wallet note metadata channel (${walletContext.wallet.channelName}).`,
|
|
6353
7690
|
].join(" "),
|
|
6354
7691
|
);
|
|
6355
7692
|
}
|
|
@@ -7535,6 +8872,7 @@ const OUTPUT_BYTES32_SCALAR_KEYS = new Set([
|
|
|
7535
8872
|
"commitment",
|
|
7536
8873
|
"currentRootVectorHash",
|
|
7537
8874
|
"currentUserKey",
|
|
8875
|
+
"createdAtTxHash",
|
|
7538
8876
|
"emittedRootVectorHash",
|
|
7539
8877
|
"ephemeralPubKeyX",
|
|
7540
8878
|
"hash",
|
|
@@ -7548,6 +8886,7 @@ const OUTPUT_BYTES32_SCALAR_KEYS = new Set([
|
|
|
7548
8886
|
"rootVectorHash",
|
|
7549
8887
|
"salt",
|
|
7550
8888
|
"sourceTxHash",
|
|
8889
|
+
"spentAtTxHash",
|
|
7551
8890
|
"topic0",
|
|
7552
8891
|
"transactionHash",
|
|
7553
8892
|
"txHash",
|
|
@@ -7744,6 +9083,13 @@ function parseArgs(argv) {
|
|
|
7744
9083
|
&& parsed.positional[1]
|
|
7745
9084
|
) {
|
|
7746
9085
|
parsed.command = `${parsed.command}-${parsed.positional[1]}`;
|
|
9086
|
+
if (
|
|
9087
|
+
parsed.positional[0] === "wallet"
|
|
9088
|
+
&& (parsed.positional[1] === "export" || parsed.positional[1] === "import")
|
|
9089
|
+
&& parsed.positional[2]
|
|
9090
|
+
) {
|
|
9091
|
+
parsed.command = `${parsed.command}-${parsed.positional[2]}`;
|
|
9092
|
+
}
|
|
7747
9093
|
parsed.positional = [parsed.command];
|
|
7748
9094
|
}
|
|
7749
9095
|
return parsed;
|
|
@@ -7773,18 +9119,6 @@ function parseTokenAmount(value, decimals) {
|
|
|
7773
9119
|
}
|
|
7774
9120
|
}
|
|
7775
9121
|
|
|
7776
|
-
function requireWalletSecret(args) {
|
|
7777
|
-
if (args.wallet !== undefined && args.network !== undefined) {
|
|
7778
|
-
return resolveWalletSecretForName({
|
|
7779
|
-
networkName: requireNetworkName(args),
|
|
7780
|
-
walletName: requireWalletName(args),
|
|
7781
|
-
});
|
|
7782
|
-
}
|
|
7783
|
-
throw new Error(
|
|
7784
|
-
"Missing --wallet and --network. Wallet commands use the wallet-local default secret file.",
|
|
7785
|
-
);
|
|
7786
|
-
}
|
|
7787
|
-
|
|
7788
9122
|
function requireArg(value, label) {
|
|
7789
9123
|
if (value === undefined || value === null || value === "") {
|
|
7790
9124
|
throw new Error(`Missing ${label}.`);
|
|
@@ -7849,6 +9183,13 @@ function requireL1Signer(args, provider) {
|
|
|
7849
9183
|
|
|
7850
9184
|
function resolveTxSubmitterSigner({ args, ownerSigner, provider }) {
|
|
7851
9185
|
if (args.txSubmitter === undefined) {
|
|
9186
|
+
expect(
|
|
9187
|
+
typeof ownerSigner.privateKey === "string",
|
|
9188
|
+
[
|
|
9189
|
+
`Missing local account secret for wallet owner ${ownerSigner.address}.`,
|
|
9190
|
+
"Pass --tx-submitter <ACCOUNT> or import the matching local account secret.",
|
|
9191
|
+
].join(" "),
|
|
9192
|
+
);
|
|
7852
9193
|
return {
|
|
7853
9194
|
txSubmitter: ownerSigner,
|
|
7854
9195
|
source: "wallet-owner",
|
|
@@ -7883,68 +9224,35 @@ function resolveStandalonePrivateKeySource(args) {
|
|
|
7883
9224
|
));
|
|
7884
9225
|
}
|
|
7885
9226
|
|
|
7886
|
-
function resolveWalletSecretForName({ networkName, walletName }) {
|
|
7887
|
-
return resolveWalletDefaultSecret(networkName, walletName);
|
|
7888
|
-
}
|
|
7889
|
-
|
|
7890
|
-
function resolvedWalletSecretSource(args) {
|
|
7891
|
-
if (args.walletSecretPath !== undefined) return "wallet-secret-path";
|
|
7892
|
-
return "wallet-default";
|
|
7893
|
-
}
|
|
7894
|
-
|
|
7895
|
-
function resolvedWalletSecretFile(networkName, walletName) {
|
|
7896
|
-
return walletSecretPath(networkName, walletName);
|
|
7897
|
-
}
|
|
7898
|
-
|
|
7899
|
-
function resolveWalletDefaultSecret(networkName, walletName) {
|
|
7900
|
-
const secretPath = walletSecretPath(networkName, walletName);
|
|
7901
|
-
if (!fs.existsSync(secretPath)) {
|
|
7902
|
-
throw cliError(
|
|
7903
|
-
CLI_ERROR_CODES.MISSING_WALLET_SECRET,
|
|
7904
|
-
[
|
|
7905
|
-
`Missing wallet default secret file: ${secretPath}.`,
|
|
7906
|
-
"Run channel join with --wallet-secret-path before wallet commands.",
|
|
7907
|
-
].join(" "),
|
|
7908
|
-
);
|
|
7909
|
-
}
|
|
7910
|
-
return readSecretFile(secretPath, "wallet default secret file");
|
|
7911
|
-
}
|
|
7912
|
-
|
|
7913
9227
|
function prepareJoinWalletSecretForName({
|
|
7914
9228
|
args,
|
|
7915
9229
|
networkName,
|
|
7916
9230
|
walletName,
|
|
7917
9231
|
}) {
|
|
7918
|
-
const secretPath = walletSecretPath(networkName, walletName);
|
|
7919
9232
|
const { channelName } = parseWalletName(walletName);
|
|
7920
|
-
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;
|
|
7921
9238
|
expect(
|
|
7922
|
-
!
|
|
9239
|
+
!activeEpoch,
|
|
7923
9240
|
[
|
|
7924
9241
|
`Wallet ${walletName} already exists on ${networkName}.`,
|
|
7925
|
-
"channel join
|
|
7926
|
-
"If this wallet was previously exited on-chain, run",
|
|
7927
|
-
`wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}`,
|
|
7928
|
-
"once to remove the stale local wallet, then retry channel join.",
|
|
9242
|
+
"channel join creates a new active wallet epoch.",
|
|
7929
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>"}.`,
|
|
7930
9245
|
].join(" "),
|
|
7931
9246
|
);
|
|
7932
9247
|
const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
|
|
7933
|
-
|
|
7934
|
-
const walletSecret = sourcePath === canonicalPath
|
|
7935
|
-
? readSecretFile(sourcePath, "--wallet-secret-path")
|
|
7936
|
-
: readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
|
|
7937
|
-
if (sourcePath !== canonicalPath) {
|
|
7938
|
-
writeSecretFile(canonicalPath, walletSecret);
|
|
7939
|
-
}
|
|
7940
|
-
return walletSecret;
|
|
9248
|
+
return readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
|
|
7941
9249
|
}
|
|
7942
9250
|
|
|
7943
9251
|
function channelWorkspacePath(networkName, name) {
|
|
7944
9252
|
return workspaceDirForName(workspaceRoot, networkName, name);
|
|
7945
9253
|
}
|
|
7946
9254
|
|
|
7947
|
-
function
|
|
9255
|
+
function walletRootPath(name, networkName) {
|
|
7948
9256
|
const walletName = String(name);
|
|
7949
9257
|
const { channelName } = parseWalletName(walletName);
|
|
7950
9258
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
@@ -7952,6 +9260,91 @@ function walletPath(name, networkName) {
|
|
|
7952
9260
|
return walletDirForName(workspaceWalletsDir(workspaceDir), walletName);
|
|
7953
9261
|
}
|
|
7954
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
|
+
|
|
7955
9348
|
function accountPrivateKeyPath(networkName, accountName) {
|
|
7956
9349
|
return path.join(
|
|
7957
9350
|
secretRoot,
|
|
@@ -7986,6 +9379,24 @@ function walletSecretPath(networkName, walletName) {
|
|
|
7986
9379
|
);
|
|
7987
9380
|
}
|
|
7988
9381
|
|
|
9382
|
+
function walletViewingKeySecretPath(networkName, walletName) {
|
|
9383
|
+
return walletKeySecretPath(networkName, walletName, "viewing");
|
|
9384
|
+
}
|
|
9385
|
+
|
|
9386
|
+
function walletSpendingKeySecretPath(networkName, walletName) {
|
|
9387
|
+
return walletKeySecretPath(networkName, walletName, "spending");
|
|
9388
|
+
}
|
|
9389
|
+
|
|
9390
|
+
function walletKeySecretPath(networkName, walletName, keyKind) {
|
|
9391
|
+
return path.join(
|
|
9392
|
+
secretRoot,
|
|
9393
|
+
requireNetworkName({ network: networkName }),
|
|
9394
|
+
"wallets",
|
|
9395
|
+
slugifyPathComponent(walletName),
|
|
9396
|
+
`${keyKind}.key`,
|
|
9397
|
+
);
|
|
9398
|
+
}
|
|
9399
|
+
|
|
7989
9400
|
function resolveWalletPathCandidates(walletName) {
|
|
7990
9401
|
if (!fs.existsSync(workspaceRoot)) {
|
|
7991
9402
|
return [];
|
|
@@ -8007,7 +9418,8 @@ function resolveWalletPathCandidates(walletName) {
|
|
|
8007
9418
|
"wallets",
|
|
8008
9419
|
walletSlug,
|
|
8009
9420
|
);
|
|
8010
|
-
if (
|
|
9421
|
+
if (fs.existsSync(candidate)) {
|
|
9422
|
+
requireWalletIndex({ walletRoot: candidate, walletName, networkName: entry.name });
|
|
8011
9423
|
candidates.push(candidate);
|
|
8012
9424
|
}
|
|
8013
9425
|
}
|
|
@@ -8038,15 +9450,30 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
|
|
|
8038
9450
|
if (!walletEntry.isDirectory()) {
|
|
8039
9451
|
continue;
|
|
8040
9452
|
}
|
|
8041
|
-
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);
|
|
8042
9461
|
wallets.push({
|
|
8043
9462
|
wallet: walletEntry.name,
|
|
8044
9463
|
network: networkEntry.name,
|
|
8045
9464
|
channelName: channelEntry.name,
|
|
8046
9465
|
walletDir,
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
9466
|
+
walletRoot,
|
|
9467
|
+
activeEpochId: walletIndex?.activeEpochId ?? null,
|
|
9468
|
+
selectedEpochId: selectedEpoch?.epochId ?? null,
|
|
9469
|
+
lifecycleStatus: selectedEpoch.lifecycleStatus,
|
|
9470
|
+
epochs: walletIndex.epochs,
|
|
9471
|
+
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9472
|
+
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
9473
|
+
hasEncryptedWallet: false,
|
|
9474
|
+
hasBackupMetadata: walletConfigExists(walletDir),
|
|
9475
|
+
hasViewingKey: fs.existsSync(walletViewingKeySecretPath(networkEntry.name, walletEntry.name)),
|
|
9476
|
+
hasSpendingKey: fs.existsSync(walletSpendingKeySecretPath(networkEntry.name, walletEntry.name)),
|
|
8050
9477
|
});
|
|
8051
9478
|
}
|
|
8052
9479
|
}
|
|
@@ -8068,14 +9495,14 @@ function privateStateCliDataRoot() {
|
|
|
8068
9495
|
|
|
8069
9496
|
function resolveExportWalletInfo({ networkName, walletName }) {
|
|
8070
9497
|
resolveCliNetwork(networkName);
|
|
8071
|
-
const walletDir =
|
|
9498
|
+
const walletDir = selectedWalletEpochDir(walletName, networkName);
|
|
8072
9499
|
return {
|
|
8073
9500
|
wallet: walletName,
|
|
8074
9501
|
network: networkName,
|
|
8075
9502
|
channelName: parseWalletName(walletName).channelName,
|
|
8076
9503
|
walletDir,
|
|
8077
|
-
metadataPath:
|
|
8078
|
-
hasMetadata: fs.existsSync(
|
|
9504
|
+
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9505
|
+
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
8079
9506
|
hasEncryptedWallet: walletConfigExists(walletDir),
|
|
8080
9507
|
};
|
|
8081
9508
|
}
|
|
@@ -8083,16 +9510,12 @@ function resolveExportWalletInfo({ networkName, walletName }) {
|
|
|
8083
9510
|
function normalizeExportWalletInfo(walletInfo) {
|
|
8084
9511
|
const wallet = requireWalletName({ wallet: walletInfo.wallet });
|
|
8085
9512
|
const network = requireNetworkName({ network: walletInfo.network });
|
|
8086
|
-
const walletDir = walletInfo.walletDir ??
|
|
8087
|
-
const metadataPath =
|
|
8088
|
-
const encryptedWalletPath = walletConfigPath(walletDir);
|
|
9513
|
+
const walletDir = walletInfo.walletDir ?? selectedWalletEpochDir(wallet, network);
|
|
9514
|
+
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
8089
9515
|
const metadata = readJsonIfExists(metadataPath);
|
|
8090
9516
|
const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
|
|
8091
|
-
const walletSecret = walletSecretPath(network, wallet);
|
|
8092
9517
|
|
|
8093
|
-
expect(fs.existsSync(encryptedWalletPath), `Wallet export cannot find encrypted wallet file: ${encryptedWalletPath}.`);
|
|
8094
9518
|
expect(fs.existsSync(metadataPath), `Wallet export cannot find wallet metadata file: ${metadataPath}.`);
|
|
8095
|
-
expect(fs.existsSync(walletSecret), `Wallet export cannot find wallet-local secret file: ${walletSecret}.`);
|
|
8096
9519
|
expect(
|
|
8097
9520
|
metadata.network === network,
|
|
8098
9521
|
`Wallet export metadata network ${metadata.network} does not match ${network}.`,
|
|
@@ -8107,18 +9530,27 @@ function normalizeExportWalletInfo(walletInfo) {
|
|
|
8107
9530
|
channelName,
|
|
8108
9531
|
wallet,
|
|
8109
9532
|
walletDir,
|
|
8110
|
-
walletSecretPath: walletSecret,
|
|
8111
9533
|
};
|
|
8112
9534
|
}
|
|
8113
9535
|
|
|
8114
|
-
function
|
|
9536
|
+
function walletBackupExportFilePaths(walletInfo) {
|
|
8115
9537
|
const walletFiles = [
|
|
8116
|
-
walletInfo.
|
|
8117
|
-
walletConfigPath(walletInfo.walletDir),
|
|
8118
|
-
walletMetadataPath(walletInfo.walletDir),
|
|
9538
|
+
walletNotesMetadataPath(walletInfo.walletDir),
|
|
8119
9539
|
];
|
|
8120
|
-
|
|
8121
|
-
|
|
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));
|
|
9547
|
+
for (const metadataPath of [
|
|
9548
|
+
walletViewingKeyMetadataPath(walletInfo.walletDir),
|
|
9549
|
+
walletSpendingKeyMetadataPath(walletInfo.walletDir),
|
|
9550
|
+
]) {
|
|
9551
|
+
if (fs.existsSync(metadataPath)) {
|
|
9552
|
+
walletFiles.push(metadataPath);
|
|
9553
|
+
}
|
|
8122
9554
|
}
|
|
8123
9555
|
|
|
8124
9556
|
const workspaceDir = channelWorkspacePath(walletInfo.network, walletInfo.channelName);
|
|
@@ -8134,8 +9566,8 @@ function walletExportFilePaths(walletInfo, { includeNotes }) {
|
|
|
8134
9566
|
expect(
|
|
8135
9567
|
fs.existsSync(filePath),
|
|
8136
9568
|
[
|
|
8137
|
-
`wallet export
|
|
8138
|
-
"Run channel recover-workspace first
|
|
9569
|
+
`wallet export backup requires channel workspace cache file: ${filePath}.`,
|
|
9570
|
+
"Run channel recover-workspace first.",
|
|
8139
9571
|
].join(" "),
|
|
8140
9572
|
);
|
|
8141
9573
|
}
|
|
@@ -8150,14 +9582,13 @@ function archivePathForLocalCliFile(filePath) {
|
|
|
8150
9582
|
}
|
|
8151
9583
|
|
|
8152
9584
|
function validateWalletExportManifest(manifest) {
|
|
8153
|
-
expect(manifest?.format ===
|
|
9585
|
+
expect(manifest?.format === WALLET_BACKUP_EXPORT_FORMAT, "Wallet import ZIP has an unsupported format.");
|
|
8154
9586
|
expect(
|
|
8155
9587
|
Number(manifest.formatVersion) === WALLET_EXPORT_FORMAT_VERSION,
|
|
8156
9588
|
`Wallet import ZIP format version ${manifest?.formatVersion} is not supported.`,
|
|
8157
9589
|
);
|
|
8158
9590
|
expect(Array.isArray(manifest.files), "Wallet import ZIP manifest is missing files[].");
|
|
8159
9591
|
expect(Array.isArray(manifest.wallets), "Wallet import ZIP manifest is missing wallets[].");
|
|
8160
|
-
expect(typeof manifest.includeNotes === "boolean", "Wallet import ZIP manifest is missing includeNotes.");
|
|
8161
9592
|
expect(manifest.wallets.length > 0, "Wallet import ZIP manifest does not list any wallets.");
|
|
8162
9593
|
const uniqueFiles = new Set(manifest.files);
|
|
8163
9594
|
expect(uniqueFiles.size === manifest.files.length, "Wallet import ZIP manifest contains duplicate file paths.");
|
|
@@ -8166,9 +9597,18 @@ function validateWalletExportManifest(manifest) {
|
|
|
8166
9597
|
validateWalletArchivePath(filePath);
|
|
8167
9598
|
}
|
|
8168
9599
|
for (const wallet of manifest.wallets) {
|
|
8169
|
-
requireNetworkName({ network: wallet.network });
|
|
8170
|
-
requireWalletName({ wallet: wallet.wallet });
|
|
9600
|
+
const networkName = requireNetworkName({ network: wallet.network });
|
|
9601
|
+
const walletName = requireWalletName({ wallet: wallet.wallet });
|
|
8171
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
|
+
);
|
|
8172
9612
|
}
|
|
8173
9613
|
}
|
|
8174
9614
|
|
|
@@ -8179,8 +9619,8 @@ function validateWalletArchivePath(archivePath) {
|
|
|
8179
9619
|
expect(!path.posix.isAbsolute(archivePath), `Wallet import ZIP path must be relative: ${archivePath}.`);
|
|
8180
9620
|
expect(path.posix.normalize(archivePath) === archivePath, `Wallet import ZIP path is not normalized: ${archivePath}.`);
|
|
8181
9621
|
expect(
|
|
8182
|
-
archivePath.startsWith("
|
|
8183
|
-
`Wallet import ZIP path must start with
|
|
9622
|
+
archivePath.startsWith("workspace/"),
|
|
9623
|
+
`Wallet backup import ZIP path must start with workspace/: ${archivePath}.`,
|
|
8184
9624
|
);
|
|
8185
9625
|
}
|
|
8186
9626
|
|
|
@@ -8191,9 +9631,9 @@ function expectPathWithinRoot(targetPath, rootPath, message) {
|
|
|
8191
9631
|
|
|
8192
9632
|
function applyImportedWalletFileMode(archivePath, targetPath) {
|
|
8193
9633
|
if (
|
|
8194
|
-
archivePath.
|
|
8195
|
-
|| archivePath.endsWith("/wallet.json")
|
|
8196
|
-
|| archivePath.endsWith("/wallet.metadata.json")
|
|
9634
|
+
archivePath.endsWith("/wallet-notes.metadata.json")
|
|
9635
|
+
|| archivePath.endsWith("/wallet-viewing-key.metadata.json")
|
|
9636
|
+
|| archivePath.endsWith("/wallet-spending-key.metadata.json")
|
|
8197
9637
|
) {
|
|
8198
9638
|
protectSecretFile(targetPath, `imported wallet file ${archivePath}`);
|
|
8199
9639
|
}
|
|
@@ -8215,16 +9655,20 @@ function channelWorkspaceOperationsPath(workspaceDir) {
|
|
|
8215
9655
|
return path.join(channelDataPath(workspaceDir), "operations");
|
|
8216
9656
|
}
|
|
8217
9657
|
|
|
8218
|
-
function
|
|
8219
|
-
return path.join(walletDir, "wallet.json");
|
|
9658
|
+
function walletNotesMetadataPath(walletDir) {
|
|
9659
|
+
return path.join(walletDir, "wallet-notes.metadata.json");
|
|
9660
|
+
}
|
|
9661
|
+
|
|
9662
|
+
function walletViewingKeyMetadataPath(walletDir) {
|
|
9663
|
+
return path.join(walletDir, "wallet-viewing-key.metadata.json");
|
|
8220
9664
|
}
|
|
8221
9665
|
|
|
8222
|
-
function
|
|
8223
|
-
return
|
|
9666
|
+
function walletSpendingKeyMetadataPath(walletDir) {
|
|
9667
|
+
return path.join(walletDir, "wallet-spending-key.metadata.json");
|
|
8224
9668
|
}
|
|
8225
9669
|
|
|
8226
9670
|
function walletConfigExists(walletDir) {
|
|
8227
|
-
return fs.existsSync(
|
|
9671
|
+
return fs.existsSync(walletNotesMetadataPath(walletDir));
|
|
8228
9672
|
}
|
|
8229
9673
|
|
|
8230
9674
|
const COMMAND_ARG_SCHEMAS = Object.freeze(
|
|
@@ -8266,21 +9710,9 @@ function assertAllowedCommandKeys(args, commandName, allowedKeys, acceptedUsage)
|
|
|
8266
9710
|
);
|
|
8267
9711
|
}
|
|
8268
9712
|
|
|
8269
|
-
function assertWalletSecretArgs(args, commandName, extraOptionKeys = [], acceptedUsage = "--wallet and --network") {
|
|
8270
|
-
if (COMMAND_ARG_SCHEMAS[commandName]) {
|
|
8271
|
-
assertAllowedCommandSchema(args, commandName);
|
|
8272
|
-
return;
|
|
8273
|
-
}
|
|
8274
|
-
assertAllowedCommandKeys(
|
|
8275
|
-
args,
|
|
8276
|
-
commandName,
|
|
8277
|
-
new Set(["command", "positional", "wallet", "network", ...extraOptionKeys]),
|
|
8278
|
-
acceptedUsage,
|
|
8279
|
-
);
|
|
8280
|
-
}
|
|
8281
|
-
|
|
8282
9713
|
function assertWalletChannelMoveArgs(args, commandName) {
|
|
8283
|
-
|
|
9714
|
+
assertAllowedCommandSchema(args, commandName);
|
|
9715
|
+
assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
|
|
8284
9716
|
}
|
|
8285
9717
|
|
|
8286
9718
|
function assertInstallZkEvmArgs(args) {
|
|
@@ -8332,12 +9764,17 @@ function assertTransactionFeesArgs(args) {
|
|
|
8332
9764
|
assertAllowedCommandSchema(args, "help-transaction-fees");
|
|
8333
9765
|
}
|
|
8334
9766
|
|
|
9767
|
+
function assertInvestigatorArgs(args) {
|
|
9768
|
+
assertAllowedCommandSchema(args, "investigator");
|
|
9769
|
+
}
|
|
9770
|
+
|
|
8335
9771
|
function assertAccountImportArgs(args) {
|
|
8336
9772
|
assertAllowedCommandSchema(args, "account-import");
|
|
8337
9773
|
}
|
|
8338
9774
|
|
|
8339
9775
|
function assertMintNotesArgs(args) {
|
|
8340
9776
|
assertAllowedCommandSchema(args, "wallet-mint-notes");
|
|
9777
|
+
assertActionImpactArg(args, "wallet mint-notes");
|
|
8341
9778
|
assertTxSubmitterArg(args);
|
|
8342
9779
|
parseAmountVector(args.amounts, {
|
|
8343
9780
|
allowZeroEntries: true,
|
|
@@ -8347,12 +9784,14 @@ function assertMintNotesArgs(args) {
|
|
|
8347
9784
|
|
|
8348
9785
|
function assertRedeemNotesArgs(args) {
|
|
8349
9786
|
assertAllowedCommandSchema(args, "wallet-redeem-notes");
|
|
9787
|
+
assertActionImpactArg(args, "wallet redeem-notes");
|
|
8350
9788
|
assertTxSubmitterArg(args);
|
|
8351
9789
|
selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
|
|
8352
9790
|
}
|
|
8353
9791
|
|
|
8354
9792
|
function assertTransferNotesArgs(args) {
|
|
8355
9793
|
assertAllowedCommandSchema(args, "wallet-transfer-notes");
|
|
9794
|
+
assertActionImpactArg(args, "wallet transfer-notes");
|
|
8356
9795
|
assertTxSubmitterArg(args);
|
|
8357
9796
|
const noteIds = parseNoteIdVector(args.noteIds);
|
|
8358
9797
|
const recipients = parseRecipientVector(args.recipients);
|
|
@@ -8373,8 +9812,34 @@ function assertTxSubmitterArg(args) {
|
|
|
8373
9812
|
}
|
|
8374
9813
|
}
|
|
8375
9814
|
|
|
9815
|
+
function assertActionImpactArg(args, commandName) {
|
|
9816
|
+
if (
|
|
9817
|
+
args.acknowledgeActionImpact !== undefined
|
|
9818
|
+
&& args.acknowledgeActionImpact !== true
|
|
9819
|
+
) {
|
|
9820
|
+
throw new Error(`${commandName} option --acknowledge-action-impact does not accept a value.`);
|
|
9821
|
+
}
|
|
9822
|
+
if (args.acknowledgeActionImpact !== true && !process.stdin.isTTY) {
|
|
9823
|
+
throw new Error(`${commandName} requires --acknowledge-action-impact after reviewing the action-impact warning.`);
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9826
|
+
|
|
8376
9827
|
function assertWalletGetNotesArgs(args) {
|
|
8377
|
-
|
|
9828
|
+
assertAllowedCommandSchema(args, "wallet-get-notes");
|
|
9829
|
+
if (args.exportEvidence !== undefined) {
|
|
9830
|
+
requireArg(args.exportEvidence, "--export-evidence");
|
|
9831
|
+
if (args.acknowledgeFullNotePlaintextExport !== true) {
|
|
9832
|
+
throw new Error(
|
|
9833
|
+
"wallet get-notes --export-evidence requires --acknowledge-full-note-plaintext-export.",
|
|
9834
|
+
);
|
|
9835
|
+
}
|
|
9836
|
+
}
|
|
9837
|
+
if (
|
|
9838
|
+
args.acknowledgeFullNotePlaintextExport !== undefined
|
|
9839
|
+
&& args.acknowledgeFullNotePlaintextExport !== true
|
|
9840
|
+
) {
|
|
9841
|
+
throw new Error("wallet get-notes option --acknowledge-full-note-plaintext-export does not accept a value.");
|
|
9842
|
+
}
|
|
8378
9843
|
}
|
|
8379
9844
|
|
|
8380
9845
|
function assertCreateChannelArgs(args) {
|
|
@@ -8408,18 +9873,15 @@ function assertPublishWorkspaceMirrorArgs(args) {
|
|
|
8408
9873
|
|
|
8409
9874
|
function assertDepositBridgeArgs(args) {
|
|
8410
9875
|
assertAllowedCommandSchema(args, "account-deposit-bridge");
|
|
9876
|
+
assertActionImpactArg(args, "account deposit-bridge");
|
|
8411
9877
|
}
|
|
8412
9878
|
|
|
8413
9879
|
function assertAccountGetBridgeFundArgs(args) {
|
|
8414
9880
|
assertAllowedCommandSchema(args, "account-get-bridge-fund");
|
|
8415
9881
|
}
|
|
8416
9882
|
|
|
8417
|
-
function assertExplicitSignerCommandArgs(args, commandName) {
|
|
8418
|
-
assertAllowedCommandSchema(args, commandName);
|
|
8419
|
-
}
|
|
8420
|
-
|
|
8421
9883
|
function assertRecoverWalletArgs(args) {
|
|
8422
|
-
|
|
9884
|
+
assertAllowedCommandSchema(args, "wallet-recover-workspace");
|
|
8423
9885
|
if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
|
|
8424
9886
|
throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
|
|
8425
9887
|
}
|
|
@@ -8427,10 +9889,11 @@ function assertRecoverWalletArgs(args) {
|
|
|
8427
9889
|
|
|
8428
9890
|
function assertJoinChannelArgs(args) {
|
|
8429
9891
|
assertAllowedCommandSchema(args, "channel-join");
|
|
9892
|
+
assertActionImpactArg(args, "channel join");
|
|
8430
9893
|
}
|
|
8431
9894
|
|
|
8432
9895
|
function assertWalletGetMetaArgs(args) {
|
|
8433
|
-
|
|
9896
|
+
assertAllowedCommandSchema(args, "wallet-get-meta");
|
|
8434
9897
|
}
|
|
8435
9898
|
|
|
8436
9899
|
function assertAccountGetL1AddressArgs(args) {
|
|
@@ -8447,48 +9910,45 @@ function assertListLocalWalletsArgs(args) {
|
|
|
8447
9910
|
assertAllowedCommandSchema(args, "wallet-list");
|
|
8448
9911
|
}
|
|
8449
9912
|
|
|
8450
|
-
function
|
|
8451
|
-
assertAllowedCommandSchema(args, "wallet-export");
|
|
8452
|
-
assertFlagOption(args, "all", "wallet export");
|
|
8453
|
-
assertFlagOption(args, "includeNotes", "wallet export");
|
|
9913
|
+
function assertWalletExportBackupArgs(args) {
|
|
9914
|
+
assertAllowedCommandSchema(args, "wallet-export-backup");
|
|
8454
9915
|
requireArg(args.output, "--output");
|
|
8455
|
-
if (args.all === true) {
|
|
8456
|
-
expect(
|
|
8457
|
-
args.network === undefined && args.wallet === undefined,
|
|
8458
|
-
"wallet export --all exports every local mainnet wallet and does not accept --network or --wallet.",
|
|
8459
|
-
);
|
|
8460
|
-
return;
|
|
8461
|
-
}
|
|
8462
9916
|
requireNetworkName(args);
|
|
8463
9917
|
requireWalletName(args);
|
|
8464
9918
|
}
|
|
8465
9919
|
|
|
8466
|
-
function
|
|
8467
|
-
assertAllowedCommandSchema(args,
|
|
9920
|
+
function assertWalletExportKeyArgs(args, commandName) {
|
|
9921
|
+
assertAllowedCommandSchema(args, commandName);
|
|
9922
|
+
requireArg(args.output, "--output");
|
|
9923
|
+
requireNetworkName(args);
|
|
9924
|
+
requireWalletName(args);
|
|
8468
9925
|
}
|
|
8469
9926
|
|
|
8470
|
-
function
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
9927
|
+
function assertWalletImportBackupArgs(args) {
|
|
9928
|
+
assertAllowedCommandSchema(args, "wallet-import-backup");
|
|
9929
|
+
}
|
|
9930
|
+
|
|
9931
|
+
function assertWalletImportKeyArgs(args, commandName) {
|
|
9932
|
+
assertAllowedCommandSchema(args, commandName);
|
|
8474
9933
|
}
|
|
8475
9934
|
|
|
8476
9935
|
function assertWithdrawBridgeArgs(args) {
|
|
8477
9936
|
assertAllowedCommandSchema(args, "account-withdraw-bridge");
|
|
9937
|
+
assertActionImpactArg(args, "account withdraw-bridge");
|
|
8478
9938
|
}
|
|
8479
9939
|
|
|
8480
9940
|
function assertWalletGetChannelFundArgs(args) {
|
|
8481
|
-
|
|
9941
|
+
assertAllowedCommandSchema(args, "wallet-get-channel-fund");
|
|
8482
9942
|
}
|
|
8483
9943
|
|
|
8484
9944
|
function assertExitChannelArgs(args) {
|
|
8485
|
-
|
|
9945
|
+
assertAllowedCommandSchema(args, "channel-exit");
|
|
8486
9946
|
}
|
|
8487
9947
|
|
|
8488
9948
|
function createWalletOperationDir(walletName, networkName, suffix) {
|
|
8489
9949
|
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
8490
9950
|
const operationDir = path.join(
|
|
8491
|
-
|
|
9951
|
+
selectedWalletEpochDir(walletName, networkName),
|
|
8492
9952
|
"operations",
|
|
8493
9953
|
`${timestamp}-${slugifyPathComponent(suffix)}`,
|
|
8494
9954
|
);
|
|
@@ -8497,14 +9957,193 @@ function createWalletOperationDir(walletName, networkName, suffix) {
|
|
|
8497
9957
|
}
|
|
8498
9958
|
|
|
8499
9959
|
function persistWallet(context) {
|
|
8500
|
-
|
|
9960
|
+
writeJson(walletNotesMetadataPath(context.walletDir), sanitizeWalletForNotesMetadata(context.wallet));
|
|
9961
|
+
if (context.wallet?.l2PrivateKey || context.wallet?.l2PublicKey || context.wallet?.l2Address) {
|
|
9962
|
+
writeJson(walletSpendingKeyMetadataPath(context.walletDir), buildWalletSpendingKeyMetadata(context.wallet));
|
|
9963
|
+
}
|
|
9964
|
+
if (context.wallet?.noteReceivePubKeyX || context.wallet?.noteReceivePubKeyYParity !== undefined) {
|
|
9965
|
+
writeJson(walletViewingKeyMetadataPath(context.walletDir), buildWalletViewingKeyMetadata(context.wallet));
|
|
9966
|
+
}
|
|
9967
|
+
}
|
|
9968
|
+
|
|
9969
|
+
function persistWalletKeys(context) {
|
|
9970
|
+
if (context.wallet?.l2PrivateKey) {
|
|
9971
|
+
writeSecretFile(
|
|
9972
|
+
walletSpendingKeySecretPath(context.wallet.network, context.walletName),
|
|
9973
|
+
JSON.stringify({
|
|
9974
|
+
format: WALLET_KEY_EXPORT_FORMAT,
|
|
9975
|
+
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
9976
|
+
keyKind: "spending",
|
|
9977
|
+
metadata: buildWalletSpendingKeyMetadata(context.wallet),
|
|
9978
|
+
privateKey: normalizePrivateKey(context.wallet.l2PrivateKey),
|
|
9979
|
+
}, null, 2),
|
|
9980
|
+
);
|
|
9981
|
+
}
|
|
9982
|
+
if (context.wallet?.noteReceivePrivateKey) {
|
|
9983
|
+
writeSecretFile(
|
|
9984
|
+
walletViewingKeySecretPath(context.wallet.network, context.walletName),
|
|
9985
|
+
JSON.stringify({
|
|
9986
|
+
format: WALLET_KEY_EXPORT_FORMAT,
|
|
9987
|
+
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
9988
|
+
keyKind: "viewing",
|
|
9989
|
+
metadata: buildWalletViewingKeyMetadata(context.wallet),
|
|
9990
|
+
privateKey: normalizePrivateKey(context.wallet.noteReceivePrivateKey),
|
|
9991
|
+
}, null, 2),
|
|
9992
|
+
);
|
|
9993
|
+
}
|
|
8501
9994
|
}
|
|
8502
9995
|
|
|
8503
|
-
function
|
|
8504
|
-
|
|
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,
|
|
8505
10021
|
network: context.wallet.network,
|
|
8506
|
-
rpcUrl: context.wallet.rpcUrl,
|
|
8507
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
|
+
};
|
|
10063
|
+
}
|
|
10064
|
+
|
|
10065
|
+
function sanitizeWalletForNotesMetadata(wallet) {
|
|
10066
|
+
const normalized = normalizeWallet({
|
|
10067
|
+
...wallet,
|
|
10068
|
+
l2PrivateKey: null,
|
|
10069
|
+
noteReceivePrivateKey: null,
|
|
10070
|
+
});
|
|
10071
|
+
const { l2PrivateKey: _l2PrivateKey, noteReceivePrivateKey: _noteReceivePrivateKey, ...publicWallet } = normalized;
|
|
10072
|
+
return {
|
|
10073
|
+
...publicWallet,
|
|
10074
|
+
notes: {
|
|
10075
|
+
unused: sanitizeTrackedNoteMap(normalized.notes.unused),
|
|
10076
|
+
spent: sanitizeTrackedNoteMap(normalized.notes.spent),
|
|
10077
|
+
unusedOrder: normalized.notes.unusedOrder,
|
|
10078
|
+
unusedBalance: null,
|
|
10079
|
+
},
|
|
10080
|
+
};
|
|
10081
|
+
}
|
|
10082
|
+
|
|
10083
|
+
function sanitizeTrackedNoteMap(notes) {
|
|
10084
|
+
return Object.fromEntries(Object.entries(notes ?? {}).map(([key, note]) => [key, sanitizeTrackedNoteForPersistence(note)]));
|
|
10085
|
+
}
|
|
10086
|
+
|
|
10087
|
+
function sanitizeTrackedNoteForPersistence(note) {
|
|
10088
|
+
const normalized = normalizeTrackedNote(note);
|
|
10089
|
+
return {
|
|
10090
|
+
commitment: normalized.commitment,
|
|
10091
|
+
nullifier: normalized.nullifier,
|
|
10092
|
+
encryptedNoteValue: normalized.encryptedNoteValue,
|
|
10093
|
+
status: normalized.status,
|
|
10094
|
+
sourceFunction: normalized.sourceFunction,
|
|
10095
|
+
sourceTxHash: normalized.sourceTxHash,
|
|
10096
|
+
createdAtTxHash: normalized.createdAtTxHash,
|
|
10097
|
+
createdAtBlockNumber: normalized.createdAtBlockNumber,
|
|
10098
|
+
createdAtLogIndex: normalized.createdAtLogIndex,
|
|
10099
|
+
createdByFunction: normalized.createdByFunction,
|
|
10100
|
+
createdOutputIndex: normalized.createdOutputIndex,
|
|
10101
|
+
spentAtTxHash: normalized.spentAtTxHash,
|
|
10102
|
+
spentAtBlockNumber: normalized.spentAtBlockNumber,
|
|
10103
|
+
spentByFunction: normalized.spentByFunction,
|
|
10104
|
+
spentInputIndex: normalized.spentInputIndex,
|
|
10105
|
+
counterpartyL2Address: normalized.counterpartyL2Address,
|
|
10106
|
+
counterpartyDirection: normalized.counterpartyDirection,
|
|
10107
|
+
counterpartyConfidence: normalized.counterpartyConfidence,
|
|
10108
|
+
bridgeCommitmentKey: normalized.bridgeCommitmentKey,
|
|
10109
|
+
bridgeNullifierKey: normalized.bridgeNullifierKey,
|
|
10110
|
+
};
|
|
10111
|
+
}
|
|
10112
|
+
|
|
10113
|
+
function buildWalletSpendingKeyMetadata(wallet) {
|
|
10114
|
+
return normalizeCliOutput({
|
|
10115
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
10116
|
+
network: wallet.network,
|
|
10117
|
+
wallet: wallet.name,
|
|
10118
|
+
...walletLifecycleMetadata(wallet),
|
|
10119
|
+
channelName: wallet.channelName,
|
|
10120
|
+
channelId: wallet.channelId,
|
|
10121
|
+
l1Address: wallet.l1Address,
|
|
10122
|
+
l2Address: wallet.l2Address,
|
|
10123
|
+
l2PublicKey: wallet.l2PublicKey,
|
|
10124
|
+
l2DerivationMode: wallet.l2DerivationMode,
|
|
10125
|
+
l2DerivationChannelName: wallet.l2DerivationChannelName,
|
|
10126
|
+
l2StorageKey: wallet.l2StorageKey,
|
|
10127
|
+
leafIndex: wallet.leafIndex,
|
|
10128
|
+
});
|
|
10129
|
+
}
|
|
10130
|
+
|
|
10131
|
+
function buildWalletViewingKeyMetadata(wallet) {
|
|
10132
|
+
return normalizeCliOutput({
|
|
10133
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
10134
|
+
network: wallet.network,
|
|
10135
|
+
wallet: wallet.name,
|
|
10136
|
+
...walletLifecycleMetadata(wallet),
|
|
10137
|
+
channelName: wallet.channelName,
|
|
10138
|
+
channelId: wallet.channelId,
|
|
10139
|
+
l1Address: wallet.l1Address,
|
|
10140
|
+
l2Address: wallet.l2Address,
|
|
10141
|
+
noteReceiveDerivationVersion: wallet.noteReceiveDerivationVersion,
|
|
10142
|
+
noteReceiveTypedDataMethod: wallet.noteReceiveTypedDataMethod,
|
|
10143
|
+
noteReceivePubKey: {
|
|
10144
|
+
x: wallet.noteReceivePubKeyX,
|
|
10145
|
+
yParity: wallet.noteReceivePubKeyYParity,
|
|
10146
|
+
},
|
|
8508
10147
|
});
|
|
8509
10148
|
}
|
|
8510
10149
|
|
|
@@ -8524,10 +10163,10 @@ Secret source options:
|
|
|
8524
10163
|
A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
|
|
8525
10164
|
Create one before joining a channel, for example:
|
|
8526
10165
|
openssl rand -hex 32 > ./wallet-secret.txt
|
|
8527
|
-
private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt
|
|
10166
|
+
private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt --acknowledge-action-impact
|
|
8528
10167
|
Bridge-facing commands accept optional --rpc-url. When provided, it is saved to
|
|
8529
10168
|
~/tokamak-private-channels/secrets/<network>/.env as RPC_URL. When omitted, the CLI reads RPC_URL from that file.
|
|
8530
|
-
Wallet commands use
|
|
10169
|
+
Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
|
|
8531
10170
|
Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
|
|
8532
10171
|
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
8533
10172
|
|
|
@@ -8861,23 +10500,6 @@ function getUsableWorkspaceRecoveryIndex({
|
|
|
8861
10500
|
};
|
|
8862
10501
|
}
|
|
8863
10502
|
|
|
8864
|
-
function writeEncryptedWalletJson(filePath, value, walletSecret) {
|
|
8865
|
-
const normalizedValue = normalizeCliOutput(value);
|
|
8866
|
-
writeEncryptedWalletFile(filePath, Buffer.from(`${JSON.stringify(normalizedValue, null, 2)}\n`, "utf8"), walletSecret);
|
|
8867
|
-
}
|
|
8868
|
-
|
|
8869
|
-
function readEncryptedWalletJson(filePath, walletSecret) {
|
|
8870
|
-
try {
|
|
8871
|
-
return JSON.parse(readEncryptedWalletFile(filePath, walletSecret).toString("utf8"));
|
|
8872
|
-
} catch (error) {
|
|
8873
|
-
throw cliError(
|
|
8874
|
-
CLI_ERROR_CODES.WALLET_DECRYPT_FAILED,
|
|
8875
|
-
`Unable to decrypt wallet data at ${filePath}. Check the wallet-local default secret file.`,
|
|
8876
|
-
{ cause: error },
|
|
8877
|
-
);
|
|
8878
|
-
}
|
|
8879
|
-
}
|
|
8880
|
-
|
|
8881
10503
|
function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
8882
10504
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8883
10505
|
const salt = randomBytes(16);
|
|
@@ -8898,23 +10520,6 @@ function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
|
8898
10520
|
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
8899
10521
|
}
|
|
8900
10522
|
|
|
8901
|
-
function readEncryptedWalletFile(filePath, walletSecret) {
|
|
8902
|
-
const envelope = readJson(filePath);
|
|
8903
|
-
expect(
|
|
8904
|
-
envelope.version === WALLET_ENCRYPTION_VERSION
|
|
8905
|
-
&& envelope.algorithm === WALLET_ENCRYPTION_ALGORITHM
|
|
8906
|
-
&& envelope.kdf === "scrypt",
|
|
8907
|
-
`Unsupported wallet encryption envelope at ${filePath}.`,
|
|
8908
|
-
);
|
|
8909
|
-
const encryptionKey = deriveWalletEncryptionKey(walletSecret, Buffer.from(ethers.getBytes(envelope.salt)));
|
|
8910
|
-
const decipher = createDecipheriv("aes-256-gcm", encryptionKey, Buffer.from(ethers.getBytes(envelope.iv)));
|
|
8911
|
-
decipher.setAuthTag(Buffer.from(ethers.getBytes(envelope.tag)));
|
|
8912
|
-
return Buffer.concat([
|
|
8913
|
-
decipher.update(Buffer.from(ethers.getBytes(envelope.ciphertext))),
|
|
8914
|
-
decipher.final(),
|
|
8915
|
-
]);
|
|
8916
|
-
}
|
|
8917
|
-
|
|
8918
10523
|
function deriveWalletEncryptionKey(walletSecret, salt) {
|
|
8919
10524
|
return scryptSync(String(walletSecret), salt, 32);
|
|
8920
10525
|
}
|
|
@@ -8963,6 +10568,7 @@ function loadWalletCommandRuntime(args) {
|
|
|
8963
10568
|
|
|
8964
10569
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
8965
10570
|
guide: printGuideHumanResult,
|
|
10571
|
+
investigator: printInvestigatorHumanResult,
|
|
8966
10572
|
"transaction-fees": printTransactionFeesHumanResult,
|
|
8967
10573
|
update: printUpdateHumanResult,
|
|
8968
10574
|
});
|
|
@@ -9022,6 +10628,29 @@ function printGuideHumanResult(guide) {
|
|
|
9022
10628
|
console.log(lines.join("\n"));
|
|
9023
10629
|
}
|
|
9024
10630
|
|
|
10631
|
+
function printInvestigatorHumanResult(result) {
|
|
10632
|
+
const lines = [
|
|
10633
|
+
"Private-State Evidence Investigator",
|
|
10634
|
+
`HTML path: ${formatHumanValue(result.htmlPath)}`,
|
|
10635
|
+
`File URL: ${formatHumanValue(result.fileUrl)}`,
|
|
10636
|
+
`Browser opened: ${result.browserOpened ? "yes" : "no"}`,
|
|
10637
|
+
];
|
|
10638
|
+
if (!result.browserOpened) {
|
|
10639
|
+
lines.push(
|
|
10640
|
+
`Open command: ${formatHumanValue(result.browserOpenCommand)}`,
|
|
10641
|
+
`Open error: ${formatHumanValue(result.browserOpenError ?? "none")}`,
|
|
10642
|
+
);
|
|
10643
|
+
}
|
|
10644
|
+
if (Array.isArray(result.nextSteps) && result.nextSteps.length > 0) {
|
|
10645
|
+
lines.push(
|
|
10646
|
+
"",
|
|
10647
|
+
"Next Steps",
|
|
10648
|
+
...result.nextSteps.map((step) => `- ${step}`),
|
|
10649
|
+
);
|
|
10650
|
+
}
|
|
10651
|
+
console.log(lines.join("\n"));
|
|
10652
|
+
}
|
|
10653
|
+
|
|
9025
10654
|
function printTransactionFeesHumanResult(report) {
|
|
9026
10655
|
const lines = [
|
|
9027
10656
|
"Transaction Fees",
|
|
@@ -9356,17 +10985,6 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
9356
10985
|
hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
|
|
9357
10986
|
}
|
|
9358
10987
|
|
|
9359
|
-
if (error?.code === CLI_ERROR_CODES.MISSING_WALLET_SECRET) {
|
|
9360
|
-
hints.push("restore the wallet-local default secret file from backup before running wallet commands.");
|
|
9361
|
-
hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
|
|
9362
|
-
}
|
|
9363
|
-
|
|
9364
|
-
if (error?.code === CLI_ERROR_CODES.WALLET_DECRYPT_FAILED) {
|
|
9365
|
-
hints.push("verify that the wallet-local default secret file is the same secret used when the wallet was created.");
|
|
9366
|
-
hints.push("if the encrypted wallet file is corrupted but the wallet secret and L1 account secret still exist, rerun wallet recover-workspace.");
|
|
9367
|
-
hints.push("if the wallet secret was lost, the local L2 key cannot be recovered from the encrypted wallet file.");
|
|
9368
|
-
}
|
|
9369
|
-
|
|
9370
10988
|
if (
|
|
9371
10989
|
message.startsWith("Missing --account:")
|
|
9372
10990
|
|| message.includes("Missing --account.")
|
|
@@ -9384,7 +11002,7 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
9384
11002
|
}
|
|
9385
11003
|
|
|
9386
11004
|
if (error?.code === CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION) {
|
|
9387
|
-
hints.push(`private-state-cli channel join --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH
|
|
11005
|
+
hints.push(`private-state-cli channel join --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH> --acknowledge-action-impact`);
|
|
9388
11006
|
hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
|
|
9389
11007
|
}
|
|
9390
11008
|
|