@tokamak-private-dapps/private-state-cli 1.2.1 → 2.0.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 +38 -0
- package/README.md +179 -50
- package/cli-assistant.html +14 -9
- package/investigator/README.md +37 -0
- package/investigator/app.js +657 -0
- package/investigator/index.html +161 -0
- package/investigator/styles.css +249 -0
- package/lib/private-state-cli-command-registry.mjs +165 -52
- package/lib/private-state-cli-shared.mjs +0 -4
- package/package.json +2 -1
- package/private-state-bridge-cli.mjs +1602 -468
|
@@ -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,17 @@ 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_EVIDENCE_BUNDLE_FORMAT = "tokamak-private-state-raw-evidence-bundle";
|
|
141
|
+
const WALLET_EXPORT_FORMAT_VERSION = 2;
|
|
142
|
+
const WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION = 1;
|
|
143
|
+
const WALLET_WORKSPACE_FORMAT_VERSION = 2;
|
|
139
144
|
const CHANNEL_WORKSPACE_MIRROR_PROTOCOL_VERSION = 2;
|
|
140
145
|
const CHANNEL_WORKSPACE_MIRROR_MANIFEST_PATH_PREFIX =
|
|
141
146
|
".well-known/tokamak-private-state/channel-workspace";
|
|
@@ -151,8 +156,6 @@ let activeCliArgs = {};
|
|
|
151
156
|
const CLI_ERROR_CODES = Object.freeze({
|
|
152
157
|
MISSING_RPC_URL: "MISSING_RPC_URL",
|
|
153
158
|
UNKNOWN_WALLET: "UNKNOWN_WALLET",
|
|
154
|
-
MISSING_WALLET_SECRET: "MISSING_WALLET_SECRET",
|
|
155
|
-
WALLET_DECRYPT_FAILED: "WALLET_DECRYPT_FAILED",
|
|
156
159
|
MISSING_DEPLOYMENT_ARTIFACTS: "MISSING_DEPLOYMENT_ARTIFACTS",
|
|
157
160
|
MISSING_CHANNEL_REGISTRATION: "MISSING_CHANNEL_REGISTRATION",
|
|
158
161
|
STALE_WORKSPACE: "STALE_WORKSPACE",
|
|
@@ -247,6 +250,206 @@ function printImmutableChannelPolicyWarning({
|
|
|
247
250
|
console.error(details.join("\n"));
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
const ACTION_IMPACT_SUMMARIES = Object.freeze({
|
|
254
|
+
"account-deposit-bridge": {
|
|
255
|
+
display: "account deposit-bridge",
|
|
256
|
+
l1PublicEvent: "Yes. ERC-20 approval and bridge vault funding transactions are public L1 events.",
|
|
257
|
+
privateNoteState: "No. This action only moves canonical tokens into the shared bridge vault.",
|
|
258
|
+
publicFields: ({ l1Address, amountInput, bridgeTokenVault }) => [
|
|
259
|
+
`L1 account: ${l1Address}`,
|
|
260
|
+
`Bridge token vault: ${bridgeTokenVault}`,
|
|
261
|
+
`Amount: ${amountInput}`,
|
|
262
|
+
"Approval and funding transaction hashes, block numbers, and event logs.",
|
|
263
|
+
],
|
|
264
|
+
notPublic: [
|
|
265
|
+
"No private note owner, value, salt, counterparty, or note provenance is created by this action.",
|
|
266
|
+
],
|
|
267
|
+
noteProvenance: "Not applicable for this bridge-edge action.",
|
|
268
|
+
cexWarning: "Do not use a centralized-exchange controlled address as a self-custody bridge source.",
|
|
269
|
+
policy: "No channel policy is accepted by this action.",
|
|
270
|
+
},
|
|
271
|
+
"account-withdraw-bridge": {
|
|
272
|
+
display: "account withdraw-bridge",
|
|
273
|
+
l1PublicEvent: "Yes. The bridge withdrawal transaction and claim event are public L1 data.",
|
|
274
|
+
privateNoteState: "No. This action claims shared bridge-vault balance to the local L1 account.",
|
|
275
|
+
publicFields: ({ l1Address, amountInput, bridgeTokenVault }) => [
|
|
276
|
+
`L1 recipient/account: ${l1Address}`,
|
|
277
|
+
`Bridge token vault: ${bridgeTokenVault}`,
|
|
278
|
+
`Amount: ${amountInput}`,
|
|
279
|
+
"Withdrawal transaction hash, block number, and event log.",
|
|
280
|
+
],
|
|
281
|
+
notPublic: [
|
|
282
|
+
"The private note path that produced any prior channel balance is not reconstructed from this event alone.",
|
|
283
|
+
],
|
|
284
|
+
noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this withdrawal alone.",
|
|
285
|
+
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.",
|
|
286
|
+
policy: "No channel policy is accepted by this action.",
|
|
287
|
+
},
|
|
288
|
+
"channel-join": {
|
|
289
|
+
display: "channel join",
|
|
290
|
+
l1PublicEvent: "Yes. Channel join and token-vault registration transactions are public L1 data.",
|
|
291
|
+
privateNoteState: "No. This action registers identity and note-receive metadata; it does not create or spend notes.",
|
|
292
|
+
publicFields: ({ l1Address, l2Address, noteReceivePubKey, joinToll, channelName, channelId }) => [
|
|
293
|
+
`Channel: ${channelName} (${channelId})`,
|
|
294
|
+
`L1 account: ${l1Address}`,
|
|
295
|
+
`L2 address: ${l2Address}`,
|
|
296
|
+
`Note-receive public key: ${noteReceivePubKey}`,
|
|
297
|
+
`Join toll: ${joinToll}`,
|
|
298
|
+
],
|
|
299
|
+
notPublic: [
|
|
300
|
+
"Wallet secret, L2 spending private key, note-receive private key, and future note plaintext.",
|
|
301
|
+
],
|
|
302
|
+
noteProvenance: "Future note provenance is not made public by joining.",
|
|
303
|
+
policy: "Joining accepts the displayed immutable channel policy snapshot.",
|
|
304
|
+
},
|
|
305
|
+
"wallet-deposit-channel": {
|
|
306
|
+
display: "wallet deposit-channel",
|
|
307
|
+
l1PublicEvent: "Yes. The proof-backed channel accounting transaction is public L1 data.",
|
|
308
|
+
privateNoteState: "No. This action increases liquid channel accounting balance; it does not create notes.",
|
|
309
|
+
publicFields: ({ l1Address, l2Address, amountInput, channelName, channelId }) => [
|
|
310
|
+
`Channel: ${channelName} (${channelId})`,
|
|
311
|
+
`L1 submitter/account: ${l1Address}`,
|
|
312
|
+
`Registered L2 address: ${l2Address}`,
|
|
313
|
+
`Amount: ${amountInput}`,
|
|
314
|
+
"Transaction hash, accepted proof surface, and accounting root update.",
|
|
315
|
+
],
|
|
316
|
+
notPublic: [
|
|
317
|
+
"No note owner, value, salt, counterparty, or note provenance is created by this action.",
|
|
318
|
+
],
|
|
319
|
+
noteProvenance: "Not applicable; this action does not transfer note ownership.",
|
|
320
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
321
|
+
},
|
|
322
|
+
"wallet-withdraw-channel": {
|
|
323
|
+
display: "wallet withdraw-channel",
|
|
324
|
+
l1PublicEvent: "Yes. The proof-backed channel accounting transaction is public L1 data.",
|
|
325
|
+
privateNoteState: "No. This action decreases liquid channel accounting balance; it does not spend notes directly.",
|
|
326
|
+
publicFields: ({ l1Address, l2Address, amountInput, channelName, channelId }) => [
|
|
327
|
+
`Channel: ${channelName} (${channelId})`,
|
|
328
|
+
`L1 submitter/account: ${l1Address}`,
|
|
329
|
+
`Registered L2 address: ${l2Address}`,
|
|
330
|
+
`Amount: ${amountInput}`,
|
|
331
|
+
"Transaction hash, accepted proof surface, and accounting root update.",
|
|
332
|
+
],
|
|
333
|
+
notPublic: [
|
|
334
|
+
"Any prior private note path that produced the liquid balance is not reconstructed from this action alone.",
|
|
335
|
+
],
|
|
336
|
+
noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this withdrawal-channel action alone.",
|
|
337
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
338
|
+
},
|
|
339
|
+
"wallet-mint-notes": {
|
|
340
|
+
display: "wallet mint-notes",
|
|
341
|
+
l1PublicEvent: "Yes. executeChannelTransaction, accepted transition, commitments, encrypted note events, and root updates are public L1 data.",
|
|
342
|
+
privateNoteState: "Yes. This action creates private-state notes tracked by the local wallet.",
|
|
343
|
+
publicFields: ({ l1Address, l2Address, amounts, channelName, channelId }) => [
|
|
344
|
+
`Channel: ${channelName} (${channelId})`,
|
|
345
|
+
`L1 submitter/account: ${l1Address}`,
|
|
346
|
+
`Registered L2 address: ${l2Address}`,
|
|
347
|
+
`Requested note amounts: ${amounts}`,
|
|
348
|
+
"New commitments, encrypted note-delivery events, transaction hash, and root updates.",
|
|
349
|
+
],
|
|
350
|
+
notPublic: [
|
|
351
|
+
"Note owner, value, salt, plaintext note contents, and later note provenance are not public by default.",
|
|
352
|
+
],
|
|
353
|
+
noteProvenance: "Public observers cannot reconstruct later note provenance without user-controlled disclosure.",
|
|
354
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
355
|
+
},
|
|
356
|
+
"wallet-transfer-notes": {
|
|
357
|
+
display: "wallet transfer-notes",
|
|
358
|
+
l1PublicEvent: "Yes. executeChannelTransaction, nullifiers, output commitments, encrypted note events, and root updates are public L1 data.",
|
|
359
|
+
privateNoteState: "Yes. This action spends selected input notes and creates output notes.",
|
|
360
|
+
publicFields: ({ l1Address, l2Address, noteIds, amounts, channelName, channelId }) => [
|
|
361
|
+
`Channel: ${channelName} (${channelId})`,
|
|
362
|
+
`L1 submitter/account: ${l1Address}`,
|
|
363
|
+
`Registered L2 address: ${l2Address}`,
|
|
364
|
+
`Input note commitments: ${noteIds}`,
|
|
365
|
+
`Output amounts supplied to the CLI: ${amounts}`,
|
|
366
|
+
"Input nullifiers, output commitments, encrypted note-delivery events, transaction hash, and root updates.",
|
|
367
|
+
],
|
|
368
|
+
notPublic: [
|
|
369
|
+
"Sender-recipient relationship, recipient note plaintext, and note provenance are not public by default.",
|
|
370
|
+
],
|
|
371
|
+
noteProvenance: "Public observers cannot reconstruct private note counterparty relationships or provenance from public contract state alone.",
|
|
372
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
373
|
+
},
|
|
374
|
+
"wallet-redeem-notes": {
|
|
375
|
+
display: "wallet redeem-notes",
|
|
376
|
+
l1PublicEvent: "Yes. executeChannelTransaction, nullifier usage, accounting update, and root updates are public L1 data.",
|
|
377
|
+
privateNoteState: "Yes. This action consumes selected notes and credits liquid channel accounting balance.",
|
|
378
|
+
publicFields: ({ l1Address, l2Address, noteIds, channelName, channelId }) => [
|
|
379
|
+
`Channel: ${channelName} (${channelId})`,
|
|
380
|
+
`L1 submitter/account: ${l1Address}`,
|
|
381
|
+
`Registered L2 address: ${l2Address}`,
|
|
382
|
+
`Input note commitments: ${noteIds}`,
|
|
383
|
+
"Input nullifiers, accounting update, transaction hash, and root updates.",
|
|
384
|
+
],
|
|
385
|
+
notPublic: [
|
|
386
|
+
"The prior path by which the redeemed note was received is not public by default.",
|
|
387
|
+
],
|
|
388
|
+
noteProvenance: "Public observers cannot reconstruct prior internal note provenance from this redeem action alone.",
|
|
389
|
+
policy: "This action uses the channel policy snapshot accepted by the registered wallet.",
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
async function requireActionImpactAcknowledgement(commandId, args, details = {}) {
|
|
394
|
+
const summary = ACTION_IMPACT_SUMMARIES[commandId];
|
|
395
|
+
if (!summary) {
|
|
396
|
+
throw new Error(`Missing action-impact summary for ${commandId}.`);
|
|
397
|
+
}
|
|
398
|
+
printActionImpactSummary(summary, details);
|
|
399
|
+
if (args.acknowledgeActionImpact === true) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (args.acknowledgeActionImpact !== undefined) {
|
|
403
|
+
throw new Error(`${summary.display} option --acknowledge-action-impact does not accept a value.`);
|
|
404
|
+
}
|
|
405
|
+
if (!process.stdin.isTTY) {
|
|
406
|
+
throw new Error(`${summary.display} requires --acknowledge-action-impact after reviewing the action-impact warning.`);
|
|
407
|
+
}
|
|
408
|
+
const prompt = [
|
|
409
|
+
`Type exactly: ${ACTION_IMPACT_CONFIRMATION}`,
|
|
410
|
+
"> ",
|
|
411
|
+
].join("\n");
|
|
412
|
+
const rl = readline.createInterface({
|
|
413
|
+
input: process.stdin,
|
|
414
|
+
output: process.stderr,
|
|
415
|
+
terminal: process.stdin.isTTY && process.stderr.isTTY,
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
const answer = await rl.question(prompt);
|
|
419
|
+
if (answer !== ACTION_IMPACT_CONFIRMATION) {
|
|
420
|
+
throw new Error(`${summary.display} action-impact confirmation did not match. No transaction was submitted.`);
|
|
421
|
+
}
|
|
422
|
+
} finally {
|
|
423
|
+
rl.close();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function printActionImpactSummary(summary, details) {
|
|
428
|
+
const lines = [
|
|
429
|
+
`ACTION IMPACT SUMMARY: ${summary.display}`,
|
|
430
|
+
`- L1 public event: ${summary.l1PublicEvent}`,
|
|
431
|
+
`- Private note state change: ${summary.privateNoteState}`,
|
|
432
|
+
"- Public addresses and amounts:",
|
|
433
|
+
...normalizeImpactLines(summary.publicFields, details).map((line) => ` - ${line}`),
|
|
434
|
+
"- Not public by default:",
|
|
435
|
+
...normalizeImpactLines(summary.notPublic, details).map((line) => ` - ${line}`),
|
|
436
|
+
`- Note provenance: ${summary.noteProvenance}`,
|
|
437
|
+
`- Illegal-use prohibition: Do not use this command for money laundering, sanctions evasion, terrorist financing, illegal gambling, criminal-proceeds concealment, or regulatory evasion.`,
|
|
438
|
+
`- Secret recovery: Losing wallet secrets, viewing keys, or spending keys can prevent note discovery or note use. The CLI cannot recover lost secrets.`,
|
|
439
|
+
`- Channel policy: ${summary.policy}`,
|
|
440
|
+
];
|
|
441
|
+
if (summary.cexWarning) {
|
|
442
|
+
lines.push(`- CEX address warning: ${summary.cexWarning}`);
|
|
443
|
+
}
|
|
444
|
+
lines.push(`- Confirmation: pass --acknowledge-action-impact or type the exact confirmation phrase when prompted.`);
|
|
445
|
+
console.error(lines.join("\n"));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function normalizeImpactLines(value, details) {
|
|
449
|
+
const resolved = typeof value === "function" ? value(details) : value;
|
|
450
|
+
return Array.isArray(resolved) ? resolved : [resolved];
|
|
451
|
+
}
|
|
452
|
+
|
|
250
453
|
function normalizeDAppPolicySnapshot({
|
|
251
454
|
dappId,
|
|
252
455
|
metadataDigest,
|
|
@@ -386,6 +589,12 @@ async function main() {
|
|
|
386
589
|
return;
|
|
387
590
|
}
|
|
388
591
|
|
|
592
|
+
if (args.command === "investigator") {
|
|
593
|
+
assertInvestigatorArgs(args);
|
|
594
|
+
handleInvestigator();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
389
598
|
if (args.command === "account-get-l1-address") {
|
|
390
599
|
assertAccountGetL1AddressArgs(args);
|
|
391
600
|
handleAccountGetL1Address({ args });
|
|
@@ -404,15 +613,39 @@ async function main() {
|
|
|
404
613
|
return;
|
|
405
614
|
}
|
|
406
615
|
|
|
407
|
-
if (args.command === "wallet-export") {
|
|
408
|
-
|
|
409
|
-
|
|
616
|
+
if (args.command === "wallet-export-backup") {
|
|
617
|
+
assertWalletExportBackupArgs(args);
|
|
618
|
+
handleWalletExportBackup({ args });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (args.command === "wallet-export-viewing-key") {
|
|
623
|
+
assertWalletExportKeyArgs(args, "wallet-export-viewing-key");
|
|
624
|
+
handleWalletExportKey({ args, keyKind: "viewing" });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (args.command === "wallet-export-spending-key") {
|
|
629
|
+
assertWalletExportKeyArgs(args, "wallet-export-spending-key");
|
|
630
|
+
handleWalletExportKey({ args, keyKind: "spending" });
|
|
410
631
|
return;
|
|
411
632
|
}
|
|
412
633
|
|
|
413
|
-
if (args.command === "wallet-import") {
|
|
414
|
-
|
|
415
|
-
|
|
634
|
+
if (args.command === "wallet-import-backup") {
|
|
635
|
+
assertWalletImportBackupArgs(args);
|
|
636
|
+
handleWalletImportBackup({ args });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (args.command === "wallet-import-viewing-key") {
|
|
641
|
+
assertWalletImportKeyArgs(args, "wallet-import-viewing-key");
|
|
642
|
+
handleWalletImportKey({ args, keyKind: "viewing" });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (args.command === "wallet-import-spending-key") {
|
|
647
|
+
assertWalletImportKeyArgs(args, "wallet-import-spending-key");
|
|
648
|
+
handleWalletImportKey({ args, keyKind: "spending" });
|
|
416
649
|
return;
|
|
417
650
|
}
|
|
418
651
|
|
|
@@ -2161,6 +2394,11 @@ async function handleDepositBridge({ args, network, provider }) {
|
|
|
2161
2394
|
const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId: network.chainId });
|
|
2162
2395
|
const amountInput = requireArg(args.amount, "--amount");
|
|
2163
2396
|
const amount = parseTokenAmount(amountInput, Number(bridgeVaultContext.canonicalAssetDecimals));
|
|
2397
|
+
await requireActionImpactAcknowledgement("account-deposit-bridge", args, {
|
|
2398
|
+
l1Address: signer.address,
|
|
2399
|
+
amountInput,
|
|
2400
|
+
bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
|
|
2401
|
+
});
|
|
2164
2402
|
const bridgeTokenVault = new Contract(
|
|
2165
2403
|
bridgeVaultContext.bridgeTokenVaultAddress,
|
|
2166
2404
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
@@ -2223,10 +2461,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2223
2461
|
const channelName = requireArg(args.channelName, "--channel-name");
|
|
2224
2462
|
const signer = requireL1Signer(args, provider);
|
|
2225
2463
|
const walletName = walletNameForChannelAndAddress(channelName, signer.address);
|
|
2226
|
-
const walletSecret = resolveWalletSecretForName({
|
|
2227
|
-
networkName: network.name,
|
|
2228
|
-
walletName,
|
|
2229
|
-
});
|
|
2230
2464
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
2231
2465
|
const initialized = await syncChannelWorkspace({
|
|
2232
2466
|
workspaceName: channelName,
|
|
@@ -2260,11 +2494,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2260
2494
|
provider,
|
|
2261
2495
|
),
|
|
2262
2496
|
};
|
|
2263
|
-
const l2Identity = await deriveParticipantIdentityFromSigner({
|
|
2264
|
-
channelName,
|
|
2265
|
-
walletSecret,
|
|
2266
|
-
signer,
|
|
2267
|
-
});
|
|
2268
2497
|
const noteReceiveKeyMaterial = await deriveNoteReceiveKeyMaterial({
|
|
2269
2498
|
signer,
|
|
2270
2499
|
chainId: network.chainId,
|
|
@@ -2272,8 +2501,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2272
2501
|
channelName,
|
|
2273
2502
|
account: signer.address,
|
|
2274
2503
|
});
|
|
2275
|
-
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
2276
|
-
const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
|
|
2277
2504
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2278
2505
|
|
|
2279
2506
|
if (!registration.exists) {
|
|
@@ -2285,15 +2512,13 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2285
2512
|
wallet: walletName,
|
|
2286
2513
|
removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
|
|
2287
2514
|
removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
|
|
2288
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2289
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2290
2515
|
workspace: context.workspaceName,
|
|
2291
2516
|
channelName: context.workspace.channelName,
|
|
2292
2517
|
channelId: context.workspace.channelId,
|
|
2293
2518
|
l1Address: signer.address,
|
|
2294
|
-
l2Address:
|
|
2295
|
-
l2StorageKey:
|
|
2296
|
-
leafIndex:
|
|
2519
|
+
l2Address: null,
|
|
2520
|
+
l2StorageKey: null,
|
|
2521
|
+
leafIndex: null,
|
|
2297
2522
|
reason: "The local wallet existed, but the L1 address is no longer registered in the channel.",
|
|
2298
2523
|
nextAction: buildRecoverWalletRemovedNextAction({
|
|
2299
2524
|
channelName,
|
|
@@ -2311,19 +2536,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2311
2536
|
),
|
|
2312
2537
|
);
|
|
2313
2538
|
}
|
|
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
|
-
);
|
|
2323
|
-
expect(
|
|
2324
|
-
ethers.toBigInt(registration.leafIndex) === ethers.toBigInt(leafIndex),
|
|
2325
|
-
"The existing channel registration leaf index does not match the derived leaf index.",
|
|
2326
|
-
);
|
|
2327
2539
|
expect(
|
|
2328
2540
|
ethers.toBigInt(normalizeBytes32Hex(registration.noteReceivePubKey.x))
|
|
2329
2541
|
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
@@ -2333,21 +2545,21 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2333
2545
|
Number(registration.noteReceivePubKey.yParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2334
2546
|
"The existing note-receive public key parity does not match the derived note-receive public key.",
|
|
2335
2547
|
);
|
|
2548
|
+
const l2Identity = {
|
|
2549
|
+
l2PrivateKey: null,
|
|
2550
|
+
l2PublicKey: null,
|
|
2551
|
+
l2Address: getAddress(registration.l2Address),
|
|
2552
|
+
};
|
|
2553
|
+
const storageKey = normalizeBytes32Hex(registration.channelTokenVaultKey);
|
|
2336
2554
|
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
signerPrivateKey: signer.privateKey,
|
|
2342
|
-
l2Identity,
|
|
2343
|
-
storageKey,
|
|
2344
|
-
leafIndex,
|
|
2345
|
-
rpcUrl,
|
|
2346
|
-
channelContext: context,
|
|
2347
|
-
noteReceiveKeyMaterial,
|
|
2348
|
-
});
|
|
2555
|
+
const walletDir = walletPath(walletName, context.workspace.network);
|
|
2556
|
+
const existingWallet = walletConfigExists(walletDir)
|
|
2557
|
+
? loadWallet(walletName, context.workspace.network)
|
|
2558
|
+
: null;
|
|
2349
2559
|
|
|
2350
2560
|
if (existingWallet) {
|
|
2561
|
+
existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2562
|
+
persistWalletKeys(existingWallet);
|
|
2351
2563
|
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2352
2564
|
walletContext: existingWallet,
|
|
2353
2565
|
context,
|
|
@@ -2362,8 +2574,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2362
2574
|
status: "already-recovered",
|
|
2363
2575
|
wallet: walletName,
|
|
2364
2576
|
walletDir: existingWallet.walletDir,
|
|
2365
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2366
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2367
2577
|
workspace: context.workspaceName,
|
|
2368
2578
|
channelName: context.workspace.channelName,
|
|
2369
2579
|
channelId: context.workspace.channelId,
|
|
@@ -2387,7 +2597,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2387
2597
|
signerAddress: signer.address,
|
|
2388
2598
|
signerPrivateKey: signer.privateKey,
|
|
2389
2599
|
l2Identity,
|
|
2390
|
-
walletSecret,
|
|
2600
|
+
walletSecret: noteReceiveKeyMaterial.privateKey,
|
|
2391
2601
|
storageKey,
|
|
2392
2602
|
leafIndex: registration.leafIndex,
|
|
2393
2603
|
noteReceiveKeyMaterial,
|
|
@@ -2411,8 +2621,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2411
2621
|
status: "recovered",
|
|
2412
2622
|
wallet: walletName,
|
|
2413
2623
|
walletDir: walletContext.walletDir,
|
|
2414
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2415
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2416
2624
|
workspace: context.workspaceName,
|
|
2417
2625
|
channelName: context.workspace.channelName,
|
|
2418
2626
|
channelId: context.workspace.channelId,
|
|
@@ -2428,135 +2636,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2428
2636
|
});
|
|
2429
2637
|
}
|
|
2430
2638
|
|
|
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
2639
|
function removeLocalWalletArtifacts(walletName, networkName) {
|
|
2561
2640
|
const walletDir = walletPath(walletName, networkName);
|
|
2562
2641
|
const walletSecretFile = walletSecretPath(networkName, walletName);
|
|
@@ -2580,7 +2659,7 @@ function removeLocalWalletArtifacts(walletName, networkName) {
|
|
|
2580
2659
|
|
|
2581
2660
|
function buildRecoverWalletRemovedNextAction({ channelName, networkName, accountName }) {
|
|
2582
2661
|
const account = accountName ? String(accountName) : "<ACCOUNT>";
|
|
2583
|
-
return `channel join --channel-name ${channelName} --network ${networkName} --account ${account} --wallet-secret-path <PATH
|
|
2662
|
+
return `channel join --channel-name ${channelName} --network ${networkName} --account ${account} --wallet-secret-path <PATH> --acknowledge-action-impact`;
|
|
2584
2663
|
}
|
|
2585
2664
|
|
|
2586
2665
|
async function handleInstallZkEvm({ args }) {
|
|
@@ -2880,6 +2959,68 @@ async function handleDoctor({ args }) {
|
|
|
2880
2959
|
}
|
|
2881
2960
|
}
|
|
2882
2961
|
|
|
2962
|
+
function handleInvestigator() {
|
|
2963
|
+
const htmlPath = resolveInvestigatorIndexPath();
|
|
2964
|
+
const fileUrl = pathToFileURL(htmlPath).href;
|
|
2965
|
+
const browser = openFileInDefaultBrowser(fileUrl);
|
|
2966
|
+
printJson({
|
|
2967
|
+
action: "investigator",
|
|
2968
|
+
htmlPath,
|
|
2969
|
+
fileUrl,
|
|
2970
|
+
browserOpened: browser.opened,
|
|
2971
|
+
browserOpenCommand: browser.command,
|
|
2972
|
+
browserOpenError: browser.error,
|
|
2973
|
+
nextSteps: [
|
|
2974
|
+
"Create a raw evidence ZIP with wallet get-notes --export-evidence and --acknowledge-full-note-plaintext-export.",
|
|
2975
|
+
"Load the raw evidence ZIP in the browser investigator.",
|
|
2976
|
+
"Filter the raw bundle and export a user-consent disclosure ZIP.",
|
|
2977
|
+
"Do not submit the raw evidence ZIP unless full wallet-history disclosure is intended.",
|
|
2978
|
+
],
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
function resolveInvestigatorIndexPath() {
|
|
2983
|
+
const candidates = [
|
|
2984
|
+
path.join(privateStateCliPackageRoot, "investigator", "index.html"),
|
|
2985
|
+
path.resolve(privateStateCliPackageRoot, "..", "investigator", "index.html"),
|
|
2986
|
+
];
|
|
2987
|
+
const htmlPath = candidates.find((candidate) => fs.existsSync(candidate));
|
|
2988
|
+
if (!htmlPath) {
|
|
2989
|
+
throw new Error(
|
|
2990
|
+
[
|
|
2991
|
+
"Missing investigator HTML asset.",
|
|
2992
|
+
`Checked: ${candidates.join(", ")}`,
|
|
2993
|
+
"Reinstall the private-state CLI package or run from a complete repository checkout.",
|
|
2994
|
+
].join(" "),
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
return htmlPath;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function openFileInDefaultBrowser(fileUrl) {
|
|
3001
|
+
const opener = defaultBrowserOpenCommand(fileUrl);
|
|
3002
|
+
const result = spawnSync(opener.command, opener.args, {
|
|
3003
|
+
stdio: "ignore",
|
|
3004
|
+
windowsHide: true,
|
|
3005
|
+
});
|
|
3006
|
+
return {
|
|
3007
|
+
command: [opener.command, ...opener.args].join(" "),
|
|
3008
|
+
opened: result.status === 0,
|
|
3009
|
+
status: result.status,
|
|
3010
|
+
error: result.error?.message ?? null,
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function defaultBrowserOpenCommand(fileUrl) {
|
|
3015
|
+
if (process.platform === "darwin") {
|
|
3016
|
+
return { command: "open", args: [fileUrl] };
|
|
3017
|
+
}
|
|
3018
|
+
if (process.platform === "win32") {
|
|
3019
|
+
return { command: "cmd", args: ["/c", "start", "", fileUrl] };
|
|
3020
|
+
}
|
|
3021
|
+
return { command: "xdg-open", args: [fileUrl] };
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2883
3024
|
async function handleTransactionFees({ network, provider, rpcUrl }) {
|
|
2884
3025
|
const feeAsset = loadTransactionFeeAsset();
|
|
2885
3026
|
const feeData = await provider.getFeeData();
|
|
@@ -3079,24 +3220,19 @@ function handleListLocalWallets({ args }) {
|
|
|
3079
3220
|
});
|
|
3080
3221
|
}
|
|
3081
3222
|
|
|
3082
|
-
function
|
|
3223
|
+
function handleWalletExportBackup({ args }) {
|
|
3083
3224
|
const outputPath = path.resolve(String(requireArg(args.output, "--output")));
|
|
3084
3225
|
expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
|
|
3085
3226
|
ensureDir(path.dirname(outputPath));
|
|
3086
3227
|
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
networkName: requireNetworkName(args),
|
|
3092
|
-
walletName: requireWalletName(args),
|
|
3093
|
-
})];
|
|
3228
|
+
const wallets = [resolveExportWalletInfo({
|
|
3229
|
+
networkName: requireNetworkName(args),
|
|
3230
|
+
walletName: requireWalletName(args),
|
|
3231
|
+
})];
|
|
3094
3232
|
|
|
3095
3233
|
expect(
|
|
3096
3234
|
wallets.length > 0,
|
|
3097
|
-
|
|
3098
|
-
? "No local mainnet wallets are available to export."
|
|
3099
|
-
: "No local wallet is available to export.",
|
|
3235
|
+
"No local wallet is available to export.",
|
|
3100
3236
|
);
|
|
3101
3237
|
|
|
3102
3238
|
const archive = new AdmZip();
|
|
@@ -3109,7 +3245,7 @@ function handleWalletExport({ args }) {
|
|
|
3109
3245
|
channelName: normalized.channelName,
|
|
3110
3246
|
wallet: normalized.wallet,
|
|
3111
3247
|
});
|
|
3112
|
-
for (const filePath of
|
|
3248
|
+
for (const filePath of walletBackupExportFilePaths(normalized)) {
|
|
3113
3249
|
const archivePath = archivePathForLocalCliFile(filePath);
|
|
3114
3250
|
if (!files.has(archivePath)) {
|
|
3115
3251
|
files.set(archivePath, filePath);
|
|
@@ -3118,44 +3254,65 @@ function handleWalletExport({ args }) {
|
|
|
3118
3254
|
}
|
|
3119
3255
|
|
|
3120
3256
|
const manifest = {
|
|
3121
|
-
format:
|
|
3257
|
+
format: WALLET_BACKUP_EXPORT_FORMAT,
|
|
3122
3258
|
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
3123
3259
|
createdAt: new Date().toISOString(),
|
|
3124
3260
|
cliPackage: PRIVATE_STATE_CLI_PACKAGE_NAME,
|
|
3125
3261
|
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
|
-
],
|
|
3262
|
+
exportMode: "backup",
|
|
3263
|
+
notes: [
|
|
3264
|
+
"Includes wallet note-tracking metadata, public key metadata, and channel workspace cache.",
|
|
3265
|
+
"Excludes spending keys, viewing keys, key derivation material, owner, value, and salt.",
|
|
3266
|
+
],
|
|
3136
3267
|
wallets: exportedWallets,
|
|
3137
3268
|
files: [...files.keys()].sort(),
|
|
3138
3269
|
};
|
|
3139
3270
|
|
|
3140
3271
|
archive.addFile("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
|
|
3141
3272
|
for (const archivePath of manifest.files) {
|
|
3142
|
-
|
|
3273
|
+
const filePath = files.get(archivePath);
|
|
3274
|
+
validateBackupExportFile(filePath);
|
|
3275
|
+
archive.addFile(archivePath, fs.readFileSync(filePath));
|
|
3143
3276
|
}
|
|
3144
3277
|
archive.writeZip(outputPath);
|
|
3145
3278
|
protectSecretFile(outputPath, "wallet export ZIP");
|
|
3146
3279
|
|
|
3147
3280
|
printJson({
|
|
3148
|
-
action: "wallet export",
|
|
3281
|
+
action: "wallet export backup",
|
|
3149
3282
|
output: outputPath,
|
|
3150
3283
|
exportMode: manifest.exportMode,
|
|
3151
|
-
includeNotes,
|
|
3152
3284
|
walletCount: exportedWallets.length,
|
|
3153
3285
|
fileCount: manifest.files.length,
|
|
3154
3286
|
wallets: exportedWallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
|
|
3155
3287
|
});
|
|
3156
3288
|
}
|
|
3157
3289
|
|
|
3158
|
-
function
|
|
3290
|
+
function handleWalletExportKey({ args, keyKind }) {
|
|
3291
|
+
const outputPath = path.resolve(String(requireArg(args.output, "--output")));
|
|
3292
|
+
expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
|
|
3293
|
+
ensureDir(path.dirname(outputPath));
|
|
3294
|
+
const networkName = requireNetworkName(args);
|
|
3295
|
+
const walletName = requireWalletName(args);
|
|
3296
|
+
const wallet = loadWallet(walletName, networkName);
|
|
3297
|
+
const secretPath = keyKind === "spending"
|
|
3298
|
+
? walletSpendingKeySecretPath(networkName, walletName)
|
|
3299
|
+
: walletViewingKeySecretPath(networkName, walletName);
|
|
3300
|
+
expect(fs.existsSync(secretPath), `Wallet ${walletName} is missing its ${keyKind} key.`);
|
|
3301
|
+
const payload = JSON.parse(readSecretFile(secretPath, `${keyKind} key`));
|
|
3302
|
+
validateWalletKeyPayload(payload, keyKind);
|
|
3303
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
3304
|
+
protectSecretFile(outputPath, `${keyKind} key export`);
|
|
3305
|
+
printJson({
|
|
3306
|
+
action: `wallet export ${keyKind}-key`,
|
|
3307
|
+
wallet: wallet.walletName,
|
|
3308
|
+
network: networkName,
|
|
3309
|
+
output: outputPath,
|
|
3310
|
+
keyKind,
|
|
3311
|
+
metadata: payload.metadata,
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
function handleWalletImportBackup({ args }) {
|
|
3159
3316
|
const inputPath = path.resolve(String(requireArg(args.input, "--input")));
|
|
3160
3317
|
expect(fs.existsSync(inputPath), `Import ZIP does not exist: ${inputPath}.`);
|
|
3161
3318
|
|
|
@@ -3191,16 +3348,51 @@ function handleWalletImport({ args }) {
|
|
|
3191
3348
|
commitWalletImportFiles({ targetRoot, plannedWrites });
|
|
3192
3349
|
|
|
3193
3350
|
printJson({
|
|
3194
|
-
action: "wallet import",
|
|
3351
|
+
action: "wallet import backup",
|
|
3195
3352
|
input: inputPath,
|
|
3196
3353
|
exportMode: manifest.exportMode,
|
|
3197
|
-
includeNotes: Boolean(manifest.includeNotes),
|
|
3198
3354
|
walletCount: manifest.wallets.length,
|
|
3199
3355
|
fileCount: plannedWrites.length,
|
|
3200
3356
|
wallets: manifest.wallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
|
|
3201
|
-
nextStep:
|
|
3202
|
-
|
|
3203
|
-
|
|
3357
|
+
nextStep: "Import viewing-key and spending-key files separately when the wallet needs those capabilities.",
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
function handleWalletImportKey({ args, keyKind }) {
|
|
3362
|
+
const inputPath = path.resolve(String(requireArg(args.input, "--input")));
|
|
3363
|
+
expect(fs.existsSync(inputPath), `Key import file does not exist: ${inputPath}.`);
|
|
3364
|
+
const payload = JSON.parse(readImportSecretSourceFile(inputPath, "--input"));
|
|
3365
|
+
validateWalletKeyPayload(payload, keyKind);
|
|
3366
|
+
const metadata = payload.metadata;
|
|
3367
|
+
const networkName = requireNetworkName({ network: metadata.network });
|
|
3368
|
+
const walletName = requireWalletName({ wallet: metadata.wallet });
|
|
3369
|
+
const targetPath = keyKind === "spending"
|
|
3370
|
+
? walletSpendingKeySecretPath(networkName, walletName)
|
|
3371
|
+
: walletViewingKeySecretPath(networkName, walletName);
|
|
3372
|
+
expect(!fs.existsSync(targetPath), `Refusing to overwrite existing ${keyKind} key: ${targetPath}.`);
|
|
3373
|
+
writeSecretFile(targetPath, JSON.stringify(payload, null, 2));
|
|
3374
|
+
const walletDir = walletPath(walletName, networkName);
|
|
3375
|
+
ensureDir(walletDir);
|
|
3376
|
+
if (walletConfigExists(walletDir)) {
|
|
3377
|
+
const metadataPath = keyKind === "spending"
|
|
3378
|
+
? walletSpendingKeyMetadataPath(walletDir)
|
|
3379
|
+
: walletViewingKeyMetadataPath(walletDir);
|
|
3380
|
+
if (fs.existsSync(metadataPath)) {
|
|
3381
|
+
expect(
|
|
3382
|
+
JSON.stringify(readJson(metadataPath)) === JSON.stringify(normalizeCliOutput(metadata)),
|
|
3383
|
+
`Refusing to overwrite mismatched ${keyKind} key metadata: ${metadataPath}.`,
|
|
3384
|
+
);
|
|
3385
|
+
} else {
|
|
3386
|
+
writeJson(metadataPath, metadata);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
printJson({
|
|
3390
|
+
action: `wallet import ${keyKind}-key`,
|
|
3391
|
+
input: inputPath,
|
|
3392
|
+
network: networkName,
|
|
3393
|
+
wallet: walletName,
|
|
3394
|
+
keyKind,
|
|
3395
|
+
metadata,
|
|
3204
3396
|
});
|
|
3205
3397
|
}
|
|
3206
3398
|
|
|
@@ -3217,6 +3409,47 @@ function readWalletImportArchive(inputPath) {
|
|
|
3217
3409
|
}
|
|
3218
3410
|
}
|
|
3219
3411
|
|
|
3412
|
+
function validateBackupExportFile(filePath) {
|
|
3413
|
+
if (path.basename(filePath) !== "wallet-notes.metadata.json") {
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
const metadata = readJson(filePath);
|
|
3417
|
+
const forbidden = findForbiddenBackupMetadataPaths(metadata);
|
|
3418
|
+
expect(
|
|
3419
|
+
forbidden.length === 0,
|
|
3420
|
+
`wallet export backup refuses to export plaintext note secrets or key material: ${forbidden.join(", ")}.`,
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function findForbiddenBackupMetadataPaths(value, pathParts = []) {
|
|
3425
|
+
const forbiddenNames = new Set([
|
|
3426
|
+
"owner",
|
|
3427
|
+
"value",
|
|
3428
|
+
"salt",
|
|
3429
|
+
"l1PrivateKey",
|
|
3430
|
+
"l2PrivateKey",
|
|
3431
|
+
"noteReceivePrivateKey",
|
|
3432
|
+
"walletSecret",
|
|
3433
|
+
"seedSignature",
|
|
3434
|
+
]);
|
|
3435
|
+
if (Array.isArray(value)) {
|
|
3436
|
+
return value.flatMap((entry, index) => findForbiddenBackupMetadataPaths(entry, [...pathParts, String(index)]));
|
|
3437
|
+
}
|
|
3438
|
+
if (!value || typeof value !== "object") {
|
|
3439
|
+
return [];
|
|
3440
|
+
}
|
|
3441
|
+
const found = [];
|
|
3442
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3443
|
+
const nextPath = [...pathParts, key];
|
|
3444
|
+
if (forbiddenNames.has(key) && entry !== undefined && entry !== null) {
|
|
3445
|
+
found.push(nextPath.join("."));
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
found.push(...findForbiddenBackupMetadataPaths(entry, nextPath));
|
|
3449
|
+
}
|
|
3450
|
+
return found;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3220
3453
|
function commitWalletImportFiles({ targetRoot, plannedWrites }) {
|
|
3221
3454
|
const stagingRoot = fs.mkdtempSync(path.join(targetRoot, ".wallet-import-"));
|
|
3222
3455
|
const committedPaths = [];
|
|
@@ -3610,14 +3843,18 @@ async function inspectGuideAccount({ account, networkName, network, provider, ar
|
|
|
3610
3843
|
|
|
3611
3844
|
async function inspectGuideWallet({ walletName, networkName, provider, artifactsInstalled }) {
|
|
3612
3845
|
const walletDir = walletPath(walletName, networkName);
|
|
3846
|
+
const viewingKeyFile = walletViewingKeySecretPath(networkName, walletName);
|
|
3847
|
+
const spendingKeyFile = walletSpendingKeySecretPath(networkName, walletName);
|
|
3613
3848
|
const result = {
|
|
3614
3849
|
wallet: walletName,
|
|
3615
3850
|
network: networkName,
|
|
3616
3851
|
walletDir,
|
|
3617
3852
|
exists: walletConfigExists(walletDir),
|
|
3618
|
-
metadataExists: fs.existsSync(
|
|
3619
|
-
|
|
3620
|
-
|
|
3853
|
+
metadataExists: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
3854
|
+
viewingKeyFile,
|
|
3855
|
+
viewingKeyFileExists: fs.existsSync(viewingKeyFile),
|
|
3856
|
+
spendingKeyFile,
|
|
3857
|
+
spendingKeyFileExists: fs.existsSync(spendingKeyFile),
|
|
3621
3858
|
channelName: null,
|
|
3622
3859
|
l1Address: null,
|
|
3623
3860
|
l2Address: null,
|
|
@@ -3635,7 +3872,7 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3635
3872
|
}
|
|
3636
3873
|
|
|
3637
3874
|
try {
|
|
3638
|
-
const walletContext = loadWallet(walletName,
|
|
3875
|
+
const walletContext = loadWallet(walletName, networkName);
|
|
3639
3876
|
const walletMetadata = loadWalletMetadata(walletName, networkName);
|
|
3640
3877
|
assertWalletMatchesMetadata(walletContext, walletMetadata);
|
|
3641
3878
|
result.channelName = walletContext.wallet.channelName;
|
|
@@ -3643,10 +3880,13 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3643
3880
|
result.l2Address = getAddress(walletContext.wallet.l2Address);
|
|
3644
3881
|
result.unusedNoteCount = Object.keys(walletContext.wallet.notes.unused).length;
|
|
3645
3882
|
result.spentNoteCount = Object.keys(walletContext.wallet.notes.spent).length;
|
|
3646
|
-
const
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3883
|
+
const unusedValues = Object.values(walletContext.wallet.notes.unused).map((note) => note.value);
|
|
3884
|
+
if (unusedValues.every((value) => value !== null)) {
|
|
3885
|
+
const unusedNoteBalance = Object.values(walletContext.wallet.notes.unused)
|
|
3886
|
+
.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n);
|
|
3887
|
+
result.unusedNoteBalanceBaseUnits = unusedNoteBalance.toString();
|
|
3888
|
+
result.unusedNoteBalanceTokens = ethers.formatUnits(unusedNoteBalance, Number(walletContext.wallet.canonicalAssetDecimals));
|
|
3889
|
+
}
|
|
3650
3890
|
|
|
3651
3891
|
if (provider && artifactsInstalled && walletChannelWorkspaceIsReady(walletContext)) {
|
|
3652
3892
|
const context = await loadWorkspaceContext(walletContext.wallet.channelName, networkName, provider);
|
|
@@ -3717,7 +3957,7 @@ function applyGuideNextAction(guide) {
|
|
|
3717
3957
|
const channelName = guide.selectors.channelName ?? guide.state.channel?.channelName ?? "<CHANNEL>";
|
|
3718
3958
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3719
3959
|
setGuideNextAction(guide, {
|
|
3720
|
-
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH
|
|
3960
|
+
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH> --acknowledge-action-impact`,
|
|
3721
3961
|
why: "The selected local wallet does not exist. Join the channel to create the wallet and register the channel L2 identity.",
|
|
3722
3962
|
});
|
|
3723
3963
|
return;
|
|
@@ -3726,7 +3966,7 @@ function applyGuideNextAction(guide) {
|
|
|
3726
3966
|
const channelName = guide.state.wallet.channelName ?? guide.selectors.channelName ?? "<CHANNEL>";
|
|
3727
3967
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3728
3968
|
setGuideNextAction(guide, {
|
|
3729
|
-
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH
|
|
3969
|
+
command: `channel join --channel-name ${channelName} --network ${guide.selectors.network} --account ${account} --wallet-secret-path <PATH> --acknowledge-action-impact`,
|
|
3730
3970
|
why: "The local wallet exists, but the corresponding L1 address is not registered in the channel.",
|
|
3731
3971
|
});
|
|
3732
3972
|
return;
|
|
@@ -3743,32 +3983,32 @@ function applyGuideNextAction(guide) {
|
|
|
3743
3983
|
if (guide.state.wallet?.exists && bridgeBalance === 0n && (channelBalance === null || channelBalance === 0n) && unusedNotes === 0) {
|
|
3744
3984
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3745
3985
|
setGuideNextAction(guide, {
|
|
3746
|
-
command: `account deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account}`,
|
|
3986
|
+
command: `account deposit-bridge --amount <TOKENS> --network ${guide.selectors.network} --account ${account} --acknowledge-action-impact`,
|
|
3747
3987
|
why: "The wallet is joined, but there is no bridge balance, channel balance, or local unused note to spend.",
|
|
3748
3988
|
});
|
|
3749
3989
|
return;
|
|
3750
3990
|
}
|
|
3751
3991
|
if (guide.state.wallet?.exists && bridgeBalance !== null && bridgeBalance > 0n && channelBalance === 0n) {
|
|
3752
3992
|
setGuideNextAction(guide, {
|
|
3753
|
-
command: `wallet deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS
|
|
3993
|
+
command: `wallet deposit-channel --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amount <TOKENS> --acknowledge-action-impact`,
|
|
3754
3994
|
why: "The account has funds in the shared bridge vault, but the wallet has no channel L2 accounting balance.",
|
|
3755
3995
|
});
|
|
3756
3996
|
return;
|
|
3757
3997
|
}
|
|
3758
3998
|
if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
|
|
3759
3999
|
setGuideNextAction(guide, {
|
|
3760
|
-
command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> [--tx-submitter <ACCOUNT>]`,
|
|
4000
|
+
command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
3761
4001
|
why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
3762
4002
|
});
|
|
3763
4003
|
return;
|
|
3764
4004
|
}
|
|
3765
4005
|
if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
|
|
3766
4006
|
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>]`,
|
|
4007
|
+
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
4008
|
why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
3769
4009
|
candidates: [
|
|
3770
4010
|
`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>]`,
|
|
4011
|
+
`wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
3772
4012
|
],
|
|
3773
4013
|
});
|
|
3774
4014
|
return;
|
|
@@ -3879,6 +4119,12 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
3879
4119
|
registeredL2Address: registration.exists ? getAddress(registration.l2Address) : null,
|
|
3880
4120
|
registeredL2StorageKey: registration.exists ? normalizeBytes32Hex(registration.channelTokenVaultKey) : null,
|
|
3881
4121
|
registeredLeafIndex: registration.exists ? registration.leafIndex.toString() : null,
|
|
4122
|
+
registeredNoteReceivePubKey: registration.exists
|
|
4123
|
+
? {
|
|
4124
|
+
x: normalizeBytes32Hex(registration.noteReceivePubKey.x),
|
|
4125
|
+
yParity: Number(registration.noteReceivePubKey.yParity),
|
|
4126
|
+
}
|
|
4127
|
+
: null,
|
|
3882
4128
|
});
|
|
3883
4129
|
}
|
|
3884
4130
|
|
|
@@ -3924,7 +4170,8 @@ async function loadWalletChannelRegistrationState({
|
|
|
3924
4170
|
provider,
|
|
3925
4171
|
requireRegistration = false,
|
|
3926
4172
|
}) {
|
|
3927
|
-
const
|
|
4173
|
+
const signer = requireWalletOwnerSigner(walletContext, provider);
|
|
4174
|
+
const l2Identity = restoreParticipantIdentityFromWallet(walletContext.wallet);
|
|
3928
4175
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
3929
4176
|
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
3930
4177
|
const matchesWallet = registration.exists
|
|
@@ -4025,13 +4272,9 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4025
4272
|
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4026
4273
|
const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
|
|
4027
4274
|
|
|
4028
|
-
const resolvedLeafIndex = leafIndex;
|
|
4029
4275
|
let approveReceipt = null;
|
|
4030
4276
|
let receipt = null;
|
|
4031
|
-
|
|
4032
|
-
let status = null;
|
|
4033
|
-
|
|
4034
|
-
joinToll = ethers.toBigInt(await context.channelManager.joinToll());
|
|
4277
|
+
const joinToll = ethers.toBigInt(await context.channelManager.joinToll());
|
|
4035
4278
|
const asset = new Contract(
|
|
4036
4279
|
context.workspace.canonicalAsset,
|
|
4037
4280
|
context.bridgeAbiManifest.contracts.erc20.abi,
|
|
@@ -4045,6 +4288,14 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4045
4288
|
channelManager: context.workspace.channelManager,
|
|
4046
4289
|
policySnapshot: context.workspace.policySnapshot,
|
|
4047
4290
|
});
|
|
4291
|
+
await requireActionImpactAcknowledgement("channel-join", args, {
|
|
4292
|
+
l1Address: signer.address,
|
|
4293
|
+
l2Address: l2Identity.l2Address,
|
|
4294
|
+
noteReceivePubKey: JSON.stringify(noteReceiveKeyMaterial.noteReceivePubKey),
|
|
4295
|
+
joinToll: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4296
|
+
channelName: context.workspace.channelName,
|
|
4297
|
+
channelId: context.workspace.channelId,
|
|
4298
|
+
});
|
|
4048
4299
|
if (joinToll !== 0n) {
|
|
4049
4300
|
approveReceipt = await waitForReceipt(
|
|
4050
4301
|
await asset.approve(context.workspace.bridgeTokenVault, joinToll, { nonce: nextNonce++ }),
|
|
@@ -4060,8 +4311,6 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4060
4311
|
{ nonce: nextNonce++ },
|
|
4061
4312
|
),
|
|
4062
4313
|
);
|
|
4063
|
-
status = "joined";
|
|
4064
|
-
|
|
4065
4314
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
4066
4315
|
context,
|
|
4067
4316
|
provider,
|
|
@@ -4076,7 +4325,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4076
4325
|
l2Identity,
|
|
4077
4326
|
walletSecret,
|
|
4078
4327
|
storageKey,
|
|
4079
|
-
leafIndex
|
|
4328
|
+
leafIndex,
|
|
4080
4329
|
noteReceiveKeyMaterial,
|
|
4081
4330
|
rpcUrl,
|
|
4082
4331
|
});
|
|
@@ -4085,14 +4334,15 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4085
4334
|
action: "channel join",
|
|
4086
4335
|
workspace: context.workspaceName,
|
|
4087
4336
|
wallet: walletContext.walletName,
|
|
4088
|
-
walletSecretSource:
|
|
4089
|
-
|
|
4337
|
+
walletSecretSource: "wallet-secret-path-one-time-derivation",
|
|
4338
|
+
walletSecretStored: false,
|
|
4339
|
+
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
4340
|
channelName: context.workspace.channelName,
|
|
4091
4341
|
channelId: context.workspace.channelId,
|
|
4092
4342
|
l1Address: signer.address,
|
|
4093
4343
|
l2Address: l2Identity.l2Address,
|
|
4094
4344
|
l2StorageKey: storageKey,
|
|
4095
|
-
leafIndex:
|
|
4345
|
+
leafIndex: leafIndex.toString(),
|
|
4096
4346
|
joinTollBaseUnits: joinToll.toString(),
|
|
4097
4347
|
joinTollTokens: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4098
4348
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
@@ -4103,7 +4353,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4103
4353
|
txUrl: receipt ? explorerTxUrl(network, receipt.hash) : null,
|
|
4104
4354
|
approveReceipt: approveReceipt ? sanitizeReceipt(approveReceipt) : null,
|
|
4105
4355
|
receipt: receipt ? sanitizeReceipt(receipt) : null,
|
|
4106
|
-
status,
|
|
4356
|
+
status: "joined",
|
|
4107
4357
|
});
|
|
4108
4358
|
}
|
|
4109
4359
|
|
|
@@ -4114,18 +4364,19 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4114
4364
|
provider,
|
|
4115
4365
|
progressAction: "channel exit",
|
|
4116
4366
|
});
|
|
4367
|
+
const ownerSigner = requireWalletOwnerSigner(walletContext, provider);
|
|
4117
4368
|
const network = contextResult.network;
|
|
4118
4369
|
expect(
|
|
4119
4370
|
channelFund === 0n,
|
|
4120
4371
|
[
|
|
4121
|
-
`The current channel fund for ${
|
|
4372
|
+
`The current channel fund for ${ownerSigner.address} is ${channelFund.toString()}.`,
|
|
4122
4373
|
"channel exit requires a zero channel balance.",
|
|
4123
4374
|
"Run wallet withdraw-channel first, then retry channel exit.",
|
|
4124
4375
|
].join(" "),
|
|
4125
4376
|
);
|
|
4126
|
-
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(
|
|
4377
|
+
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(ownerSigner.address);
|
|
4127
4378
|
const receipt = await waitForReceipt(
|
|
4128
|
-
await context.bridgeTokenVault.connect(
|
|
4379
|
+
await context.bridgeTokenVault.connect(ownerSigner).exitChannel(ethers.toBigInt(context.workspace.channelId)),
|
|
4129
4380
|
);
|
|
4130
4381
|
const cleanup = removeLocalWalletArtifacts(walletContext.walletName, walletMetadata.network);
|
|
4131
4382
|
|
|
@@ -4135,7 +4386,7 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4135
4386
|
network: walletMetadata.network,
|
|
4136
4387
|
channelName: walletMetadata.channelName,
|
|
4137
4388
|
channelId: context.workspace.channelId,
|
|
4138
|
-
l1Address:
|
|
4389
|
+
l1Address: ownerSigner.address,
|
|
4139
4390
|
currentUserValue: channelFund.toString(),
|
|
4140
4391
|
refundAmountBaseUnits: refundAmount.toString(),
|
|
4141
4392
|
refundAmountTokens: ethers.formatUnits(refundAmount, Number(context.workspace.canonicalAssetDecimals)),
|
|
@@ -4174,6 +4425,13 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4174
4425
|
const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
|
|
4175
4426
|
const amountInput = requireArg(args.amount, "--amount");
|
|
4176
4427
|
const amount = parseTokenAmount(amountInput, Number(context.workspace.canonicalAssetDecimals));
|
|
4428
|
+
await requireActionImpactAcknowledgement(args.command, args, {
|
|
4429
|
+
l1Address: signer.address,
|
|
4430
|
+
l2Address: l2Identity.l2Address,
|
|
4431
|
+
amountInput,
|
|
4432
|
+
channelName: context.workspace.channelName,
|
|
4433
|
+
channelId: context.workspace.channelId,
|
|
4434
|
+
});
|
|
4177
4435
|
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4178
4436
|
const bridgeTokenVault = new Contract(
|
|
4179
4437
|
context.workspace.bridgeTokenVault,
|
|
@@ -4256,7 +4514,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4256
4514
|
writeJson(path.join(operationDir, `${operationName}-receipt.json`), sanitizeReceipt(receipt));
|
|
4257
4515
|
writeJson(path.join(operationDir, "state_snapshot.json"), transition.nextSnapshot);
|
|
4258
4516
|
writeJson(path.join(operationDir, "state_snapshot.normalized.json"), transition.nextSnapshot);
|
|
4259
|
-
sealWalletOperationDir(operationDir, walletContext
|
|
4517
|
+
sealWalletOperationDir(operationDir, walletOperationSealSecret(walletContext));
|
|
4260
4518
|
|
|
4261
4519
|
context.currentSnapshot = transition.nextSnapshot;
|
|
4262
4520
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -4291,6 +4549,11 @@ async function handleWithdrawBridge({ args, network, provider }) {
|
|
|
4291
4549
|
const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId });
|
|
4292
4550
|
const amountInput = requireArg(args.amount, "--amount");
|
|
4293
4551
|
const amount = parseTokenAmount(amountInput, Number(bridgeVaultContext.canonicalAssetDecimals));
|
|
4552
|
+
await requireActionImpactAcknowledgement("account-withdraw-bridge", args, {
|
|
4553
|
+
l1Address: signer.address,
|
|
4554
|
+
amountInput,
|
|
4555
|
+
bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
|
|
4556
|
+
});
|
|
4294
4557
|
const bridgeTokenVault = new Contract(
|
|
4295
4558
|
bridgeVaultContext.bridgeTokenVaultAddress,
|
|
4296
4559
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
@@ -4371,6 +4634,7 @@ function resolveFunctionMetadataProofForExecution({
|
|
|
4371
4634
|
|
|
4372
4635
|
async function handleMintNotes({ args, provider }) {
|
|
4373
4636
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4637
|
+
requireWalletSpendingCapability(wallet);
|
|
4374
4638
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
4375
4639
|
const amountInputs = parseAmountVector(requireArg(args.amounts, "--amounts"), {
|
|
4376
4640
|
allowZeroEntries: true,
|
|
@@ -4400,6 +4664,19 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4400
4664
|
`${channelFund.toString()}. Run wallet get-channel-fund to inspect the available balance.`,
|
|
4401
4665
|
].join(" "),
|
|
4402
4666
|
);
|
|
4667
|
+
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
4668
|
+
const { txSubmitter } = resolveTxSubmitterSigner({
|
|
4669
|
+
args,
|
|
4670
|
+
ownerSigner: signer,
|
|
4671
|
+
provider,
|
|
4672
|
+
});
|
|
4673
|
+
await requireActionImpactAcknowledgement("wallet-mint-notes", args, {
|
|
4674
|
+
l1Address: txSubmitter.address,
|
|
4675
|
+
l2Address: l2Identity.l2Address,
|
|
4676
|
+
amounts: baseUnitAmounts.map(({ amountInput }) => amountInput).join(", "),
|
|
4677
|
+
channelName: wallet.wallet.channelName,
|
|
4678
|
+
channelId: wallet.wallet.channelId,
|
|
4679
|
+
});
|
|
4403
4680
|
const templatePayload = buildMintNotesTemplatePayload({
|
|
4404
4681
|
wallet,
|
|
4405
4682
|
baseUnitAmounts: baseUnitAmounts.map(({ amountBaseUnits }) => amountBaseUnits),
|
|
@@ -4432,6 +4709,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4432
4709
|
sourceFunction: templatePayload.method,
|
|
4433
4710
|
sourceTxHash: execution.receipt.hash,
|
|
4434
4711
|
bridgeCommitmentKeys: execution.noteLifecycle.outputCommitmentKeys,
|
|
4712
|
+
sourceBlockNumber: execution.receipt.blockNumber,
|
|
4435
4713
|
}),
|
|
4436
4714
|
gasUsed: receiptGasUsed(execution.receipt),
|
|
4437
4715
|
txUrl: explorerTxUrl(contextResult.network, execution.receipt.hash),
|
|
@@ -4444,6 +4722,8 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4444
4722
|
|
|
4445
4723
|
async function handleRedeemNotes({ args, provider }) {
|
|
4446
4724
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4725
|
+
requireWalletViewingCapability(wallet);
|
|
4726
|
+
requireWalletSpendingCapability(wallet);
|
|
4447
4727
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
4448
4728
|
const preparedContextResult = await loadFreshWalletChannelContext({
|
|
4449
4729
|
walletContext: wallet,
|
|
@@ -4458,6 +4738,19 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
4458
4738
|
preConsumedBlockDelta: preparedContextResult.autoRecoveryBlockDelta,
|
|
4459
4739
|
});
|
|
4460
4740
|
const inputNotes = loadWalletUnusedInputNotes(wallet, noteIds);
|
|
4741
|
+
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
4742
|
+
const { txSubmitter } = resolveTxSubmitterSigner({
|
|
4743
|
+
args,
|
|
4744
|
+
ownerSigner: signer,
|
|
4745
|
+
provider,
|
|
4746
|
+
});
|
|
4747
|
+
await requireActionImpactAcknowledgement("wallet-redeem-notes", args, {
|
|
4748
|
+
l1Address: txSubmitter.address,
|
|
4749
|
+
l2Address: l2Identity.l2Address,
|
|
4750
|
+
noteIds: noteIds.join(", "),
|
|
4751
|
+
channelName: wallet.wallet.channelName,
|
|
4752
|
+
channelId: wallet.wallet.channelId,
|
|
4753
|
+
});
|
|
4461
4754
|
const templatePayload = buildRedeemNotesTemplatePayload({
|
|
4462
4755
|
wallet,
|
|
4463
4756
|
inputNotes,
|
|
@@ -4513,13 +4806,20 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4513
4806
|
progressAction: "wallet get-notes",
|
|
4514
4807
|
});
|
|
4515
4808
|
const context = contextResult.context;
|
|
4516
|
-
const noteReceiveFreshness =
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4809
|
+
const noteReceiveFreshness = wallet.wallet.noteReceivePrivateKey
|
|
4810
|
+
? await ensureWalletNoteReceiveStateCurrent({
|
|
4811
|
+
walletContext: wallet,
|
|
4812
|
+
context,
|
|
4813
|
+
provider,
|
|
4814
|
+
progressAction: "wallet get-notes",
|
|
4815
|
+
preConsumedBlockDelta: contextResult.autoRecoveryBlockDelta,
|
|
4816
|
+
})
|
|
4817
|
+
: {
|
|
4818
|
+
nextBlock: wallet.wallet.noteReceiveLastScannedBlock,
|
|
4819
|
+
latestBlock: await provider.getBlockNumber(),
|
|
4820
|
+
recoveredWalletWorkspace: false,
|
|
4821
|
+
recoveredDeliveryState: null,
|
|
4822
|
+
};
|
|
4523
4823
|
|
|
4524
4824
|
const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
|
|
4525
4825
|
.map((commitment) => wallet.wallet.notes.unused[commitment])
|
|
@@ -4539,8 +4839,20 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4539
4839
|
canonicalAssetDecimals,
|
|
4540
4840
|
})));
|
|
4541
4841
|
|
|
4542
|
-
const
|
|
4543
|
-
const
|
|
4842
|
+
const canComputeTotals = [...unusedTrackedNotes, ...spentTrackedNotes].every((note) => note.value !== null);
|
|
4843
|
+
const unusedTotal = canComputeTotals ? unusedTrackedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n) : null;
|
|
4844
|
+
const spentTotal = canComputeTotals ? spentTrackedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n) : null;
|
|
4845
|
+
const evidenceExport = args.exportEvidence
|
|
4846
|
+
? await exportWalletGetNotesEvidenceBundle({
|
|
4847
|
+
args,
|
|
4848
|
+
provider,
|
|
4849
|
+
walletContext: wallet,
|
|
4850
|
+
walletMetadata,
|
|
4851
|
+
context,
|
|
4852
|
+
unusedTrackedNotes,
|
|
4853
|
+
spentTrackedNotes,
|
|
4854
|
+
})
|
|
4855
|
+
: null;
|
|
4544
4856
|
|
|
4545
4857
|
printJson({
|
|
4546
4858
|
action: "wallet get-notes",
|
|
@@ -4550,22 +4862,447 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4550
4862
|
controller: wallet.wallet.controller,
|
|
4551
4863
|
unusedNotes,
|
|
4552
4864
|
spentNotes,
|
|
4553
|
-
unusedTotalBaseUnits: unusedTotal
|
|
4554
|
-
unusedTotalTokens: ethers.formatUnits(unusedTotal, canonicalAssetDecimals),
|
|
4555
|
-
spentTotalBaseUnits: spentTotal
|
|
4556
|
-
spentTotalTokens: ethers.formatUnits(spentTotal, canonicalAssetDecimals),
|
|
4865
|
+
unusedTotalBaseUnits: unusedTotal?.toString() ?? null,
|
|
4866
|
+
unusedTotalTokens: unusedTotal === null ? null : ethers.formatUnits(unusedTotal, canonicalAssetDecimals),
|
|
4867
|
+
spentTotalBaseUnits: spentTotal?.toString() ?? null,
|
|
4868
|
+
spentTotalTokens: spentTotal === null ? null : ethers.formatUnits(spentTotal, canonicalAssetDecimals),
|
|
4557
4869
|
bridgeStatusMismatches: [...unusedNotes, ...spentNotes].filter((note) => !note.walletStatusMatchesBridge).length,
|
|
4558
4870
|
noteReceiveLastScannedBlock: noteReceiveFreshness.nextBlock,
|
|
4559
4871
|
latestBlock: noteReceiveFreshness.latestBlock,
|
|
4872
|
+
viewingKeyAvailable: Boolean(wallet.wallet.noteReceivePrivateKey),
|
|
4560
4873
|
recoveredWalletWorkspace: noteReceiveFreshness.recoveredWalletWorkspace,
|
|
4561
4874
|
recoveredFromLogs: noteReceiveFreshness.recoveredDeliveryState?.importedNotes ?? 0,
|
|
4562
4875
|
scannedDeliveryLogs: noteReceiveFreshness.recoveredDeliveryState?.scannedLogs ?? 0,
|
|
4563
4876
|
noteReceiveScanRange: noteReceiveFreshness.recoveredDeliveryState?.scanRange ?? null,
|
|
4877
|
+
evidenceExport,
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
async function exportWalletGetNotesEvidenceBundle({
|
|
4882
|
+
args,
|
|
4883
|
+
provider,
|
|
4884
|
+
walletContext,
|
|
4885
|
+
walletMetadata,
|
|
4886
|
+
context,
|
|
4887
|
+
unusedTrackedNotes,
|
|
4888
|
+
spentTrackedNotes,
|
|
4889
|
+
}) {
|
|
4890
|
+
const outputPath = path.resolve(String(requireArg(args.exportEvidence, "--export-evidence")));
|
|
4891
|
+
ensureDir(path.dirname(outputPath));
|
|
4892
|
+
|
|
4893
|
+
const notes = [
|
|
4894
|
+
...unusedTrackedNotes.map(normalizeTrackedNote),
|
|
4895
|
+
...spentTrackedNotes.map(normalizeTrackedNote),
|
|
4896
|
+
].sort((left, right) => left.commitment.localeCompare(right.commitment));
|
|
4897
|
+
|
|
4898
|
+
for (const note of notes) {
|
|
4899
|
+
validateEvidenceNotePlaintext(note, walletContext.wallet);
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
const txHashes = uniqueNonNull([
|
|
4903
|
+
...notes.map((note) => note.createdAtTxHash),
|
|
4904
|
+
...notes.map((note) => note.spentAtTxHash),
|
|
4905
|
+
]);
|
|
4906
|
+
const transactionEvidence = await buildTransactionEvidenceMap({ provider, txHashes });
|
|
4907
|
+
const blockTimestampCache = buildBlockTimestampCache(transactionEvidence);
|
|
4908
|
+
const noteRecords = notes.map((note) => buildEvidenceNoteRecord({
|
|
4909
|
+
note,
|
|
4910
|
+
walletContext,
|
|
4911
|
+
walletMetadata,
|
|
4912
|
+
context,
|
|
4913
|
+
transactionEvidence,
|
|
4914
|
+
blockTimestampCache,
|
|
4915
|
+
}));
|
|
4916
|
+
const indexes = buildEvidenceIndexes(noteRecords);
|
|
4917
|
+
const manifest = buildEvidenceManifest({
|
|
4918
|
+
outputPath,
|
|
4919
|
+
walletContext,
|
|
4920
|
+
walletMetadata,
|
|
4921
|
+
context,
|
|
4922
|
+
noteRecords,
|
|
4923
|
+
txHashes,
|
|
4924
|
+
});
|
|
4925
|
+
|
|
4926
|
+
const archive = new AdmZip();
|
|
4927
|
+
addEvidenceJson(archive, "manifest.json", manifest);
|
|
4928
|
+
addEvidenceJson(archive, "indexes/by-commitment.json", indexes.byCommitment);
|
|
4929
|
+
addEvidenceJson(archive, "indexes/by-nullifier.json", indexes.byNullifier);
|
|
4930
|
+
addEvidenceJson(archive, "indexes/by-creation-tx.json", indexes.byCreationTx);
|
|
4931
|
+
addEvidenceJson(archive, "indexes/by-spend-tx.json", indexes.bySpendTx);
|
|
4932
|
+
addEvidenceJson(archive, "indexes/by-block-range.json", indexes.byBlockRange);
|
|
4933
|
+
addEvidenceJson(archive, "indexes/by-counterparty.json", indexes.byCounterparty);
|
|
4934
|
+
for (const record of noteRecords) {
|
|
4935
|
+
addEvidenceJson(archive, `notes/${record.derived.commitment}.json`, record);
|
|
4936
|
+
}
|
|
4937
|
+
for (const [txHash, txRecord] of Object.entries(transactionEvidence)) {
|
|
4938
|
+
addEvidenceJson(archive, `transactions/${txHash}.json`, txRecord.transaction);
|
|
4939
|
+
addEvidenceJson(archive, `receipts/${txHash}.json`, txRecord.receipt);
|
|
4940
|
+
addEvidenceJson(archive, `events/${txHash}.json`, txRecord.events);
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4943
|
+
assertEvidenceBundleDoesNotContainSecrets({
|
|
4944
|
+
wallet: walletContext.wallet,
|
|
4945
|
+
payload: {
|
|
4946
|
+
manifest,
|
|
4947
|
+
indexes,
|
|
4948
|
+
noteRecords,
|
|
4949
|
+
transactionEvidence,
|
|
4950
|
+
},
|
|
4951
|
+
});
|
|
4952
|
+
fs.rmSync(outputPath, { force: true });
|
|
4953
|
+
archive.writeZip(outputPath);
|
|
4954
|
+
protectSecretFile(outputPath, "wallet evidence export ZIP");
|
|
4955
|
+
|
|
4956
|
+
return {
|
|
4957
|
+
output: outputPath,
|
|
4958
|
+
format: WALLET_EVIDENCE_BUNDLE_FORMAT,
|
|
4959
|
+
formatVersion: WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION,
|
|
4960
|
+
noteCount: noteRecords.length,
|
|
4961
|
+
transactionCount: txHashes.length,
|
|
4962
|
+
containsNotePlaintext: true,
|
|
4963
|
+
containsSpendingKey: false,
|
|
4964
|
+
containsViewingKey: false,
|
|
4965
|
+
containsWalletSecret: false,
|
|
4966
|
+
warning: "Local full-note evidence bundle. Do not submit as-is unless full wallet-history disclosure is intended.",
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function validateEvidenceNotePlaintext(note, wallet) {
|
|
4971
|
+
expect(
|
|
4972
|
+
note.owner && note.value !== null && note.salt && note.encryptedNoteValue,
|
|
4973
|
+
[
|
|
4974
|
+
`Cannot export evidence for note ${note.commitment} because plaintext note data is incomplete.`,
|
|
4975
|
+
"Import the wallet viewing key and run wallet recover-workspace before exporting evidence.",
|
|
4976
|
+
].join(" "),
|
|
4977
|
+
);
|
|
4978
|
+
expect(
|
|
4979
|
+
getAddress(note.owner) === getAddress(wallet.l2Address),
|
|
4980
|
+
`Cannot export evidence for note ${note.commitment}: owner does not match wallet L2 address.`,
|
|
4981
|
+
);
|
|
4982
|
+
const recomputedSalt = computeEncryptedNoteSalt(note.encryptedNoteValue);
|
|
4983
|
+
expect(
|
|
4984
|
+
ethers.toBigInt(recomputedSalt) === ethers.toBigInt(note.salt),
|
|
4985
|
+
`Cannot export evidence for note ${note.commitment}: encrypted note salt mismatch.`,
|
|
4986
|
+
);
|
|
4987
|
+
const plaintext = normalizePlaintextNote(note);
|
|
4988
|
+
expect(
|
|
4989
|
+
ethers.toBigInt(computeNoteCommitment(plaintext)) === ethers.toBigInt(note.commitment),
|
|
4990
|
+
`Cannot export evidence for note ${note.commitment}: commitment mismatch.`,
|
|
4991
|
+
);
|
|
4992
|
+
expect(
|
|
4993
|
+
ethers.toBigInt(computeNullifier(plaintext)) === ethers.toBigInt(note.nullifier),
|
|
4994
|
+
`Cannot export evidence for note ${note.commitment}: nullifier mismatch.`,
|
|
4995
|
+
);
|
|
4996
|
+
}
|
|
4997
|
+
|
|
4998
|
+
async function buildTransactionEvidenceMap({ provider, txHashes }) {
|
|
4999
|
+
const entries = {};
|
|
5000
|
+
for (const txHash of txHashes) {
|
|
5001
|
+
const [transaction, receipt] = await Promise.all([
|
|
5002
|
+
provider.getTransaction(txHash).catch(() => null),
|
|
5003
|
+
provider.getTransactionReceipt(txHash).catch(() => null),
|
|
5004
|
+
]);
|
|
5005
|
+
const blockNumber = receipt?.blockNumber ?? transaction?.blockNumber ?? null;
|
|
5006
|
+
const block = blockNumber === null ? null : await provider.getBlock(blockNumber).catch(() => null);
|
|
5007
|
+
entries[txHash] = {
|
|
5008
|
+
transaction: sanitizeTransactionEvidence(transaction, block),
|
|
5009
|
+
receipt: receipt ? sanitizeReceipt(receipt) : null,
|
|
5010
|
+
events: receipt ? sanitizeReceiptEvents(receipt) : [],
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
return entries;
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
function sanitizeTransactionEvidence(transaction, block) {
|
|
5017
|
+
if (!transaction) {
|
|
5018
|
+
return null;
|
|
5019
|
+
}
|
|
5020
|
+
return normalizeCliOutput(serializeBigInts({
|
|
5021
|
+
hash: transaction.hash,
|
|
5022
|
+
from: transaction.from,
|
|
5023
|
+
to: transaction.to,
|
|
5024
|
+
nonce: transaction.nonce,
|
|
5025
|
+
data: transaction.data,
|
|
5026
|
+
value: transaction.value,
|
|
5027
|
+
chainId: transaction.chainId,
|
|
5028
|
+
blockHash: transaction.blockHash,
|
|
5029
|
+
blockNumber: transaction.blockNumber,
|
|
5030
|
+
blockTimestamp: block?.timestamp ?? null,
|
|
5031
|
+
blockTimestampIso: block?.timestamp ? new Date(Number(block.timestamp) * 1000).toISOString() : null,
|
|
5032
|
+
}));
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
function sanitizeReceiptEvents(receipt) {
|
|
5036
|
+
return (receipt.logs ?? []).map((log) => normalizeCliOutput(serializeBigInts({
|
|
5037
|
+
address: log.address,
|
|
5038
|
+
blockHash: log.blockHash,
|
|
5039
|
+
blockNumber: log.blockNumber,
|
|
5040
|
+
transactionHash: log.transactionHash,
|
|
5041
|
+
transactionIndex: log.transactionIndex,
|
|
5042
|
+
logIndex: log.index ?? log.logIndex ?? null,
|
|
5043
|
+
topics: log.topics,
|
|
5044
|
+
data: log.data,
|
|
5045
|
+
})));
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
function buildBlockTimestampCache(transactionEvidence) {
|
|
5049
|
+
const cache = {};
|
|
5050
|
+
for (const txRecord of Object.values(transactionEvidence)) {
|
|
5051
|
+
const tx = txRecord.transaction;
|
|
5052
|
+
if (tx?.blockNumber !== null && tx?.blockNumber !== undefined) {
|
|
5053
|
+
cache[Number(tx.blockNumber)] = {
|
|
5054
|
+
timestamp: tx.blockTimestamp ?? null,
|
|
5055
|
+
iso: tx.blockTimestampIso ?? null,
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
return cache;
|
|
5060
|
+
}
|
|
5061
|
+
|
|
5062
|
+
function buildEvidenceNoteRecord({
|
|
5063
|
+
note,
|
|
5064
|
+
walletContext,
|
|
5065
|
+
walletMetadata,
|
|
5066
|
+
context,
|
|
5067
|
+
transactionEvidence,
|
|
5068
|
+
blockTimestampCache,
|
|
5069
|
+
}) {
|
|
5070
|
+
const creationBlockNumber = note.createdAtBlockNumber
|
|
5071
|
+
?? transactionEvidence[note.createdAtTxHash]?.transaction?.blockNumber
|
|
5072
|
+
?? null;
|
|
5073
|
+
const spentBlockNumber = note.spentAtBlockNumber
|
|
5074
|
+
?? transactionEvidence[note.spentAtTxHash]?.transaction?.blockNumber
|
|
5075
|
+
?? null;
|
|
5076
|
+
const scheme = note.encryptedNoteValue ? unpackEncryptedNoteValue(note.encryptedNoteValue).scheme : null;
|
|
5077
|
+
return normalizeCliOutput({
|
|
5078
|
+
recordType: "note-evidence",
|
|
5079
|
+
recordVersion: 1,
|
|
5080
|
+
noteId: note.commitment,
|
|
5081
|
+
walletScope: {
|
|
5082
|
+
network: walletMetadata.network,
|
|
5083
|
+
chainId: walletContext.wallet.chainId,
|
|
5084
|
+
channelName: walletMetadata.channelName,
|
|
5085
|
+
channelId: walletContext.wallet.channelId,
|
|
5086
|
+
wallet: walletContext.walletName,
|
|
5087
|
+
walletL1Address: walletContext.wallet.l1Address,
|
|
5088
|
+
walletL2Address: walletContext.wallet.l2Address,
|
|
5089
|
+
controller: context.workspace.controller,
|
|
5090
|
+
},
|
|
5091
|
+
plaintext: {
|
|
5092
|
+
owner: note.owner,
|
|
5093
|
+
value: note.value,
|
|
5094
|
+
salt: note.salt,
|
|
5095
|
+
},
|
|
5096
|
+
derived: {
|
|
5097
|
+
commitment: note.commitment,
|
|
5098
|
+
nullifier: note.nullifier,
|
|
5099
|
+
commitmentStorageKey: note.bridgeCommitmentKey,
|
|
5100
|
+
nullifierStorageKey: note.bridgeNullifierKey,
|
|
5101
|
+
},
|
|
5102
|
+
encryptedDelivery: {
|
|
5103
|
+
encryptedNoteValue: note.encryptedNoteValue,
|
|
5104
|
+
saltDerivation: "poseidon(encryptedNoteValue)",
|
|
5105
|
+
scheme: encryptedNoteSchemeLabel(scheme),
|
|
5106
|
+
event: {
|
|
5107
|
+
txHash: note.createdAtTxHash,
|
|
5108
|
+
blockNumber: creationBlockNumber,
|
|
5109
|
+
blockTimestamp: blockTimestampCache[Number(creationBlockNumber)]?.timestamp ?? null,
|
|
5110
|
+
blockTimestampIso: blockTimestampCache[Number(creationBlockNumber)]?.iso ?? null,
|
|
5111
|
+
logIndex: note.createdAtLogIndex,
|
|
5112
|
+
contract: context.workspace.channelManager,
|
|
5113
|
+
},
|
|
5114
|
+
},
|
|
5115
|
+
creation: {
|
|
5116
|
+
txHash: note.createdAtTxHash,
|
|
5117
|
+
blockNumber: creationBlockNumber,
|
|
5118
|
+
blockTimestamp: blockTimestampCache[Number(creationBlockNumber)]?.timestamp ?? null,
|
|
5119
|
+
blockTimestampIso: blockTimestampCache[Number(creationBlockNumber)]?.iso ?? null,
|
|
5120
|
+
function: note.createdByFunction,
|
|
5121
|
+
outputIndex: note.createdOutputIndex,
|
|
5122
|
+
acceptedTransition: acceptedTransitionReference(note.createdAtTxHash),
|
|
5123
|
+
},
|
|
5124
|
+
spend: {
|
|
5125
|
+
status: note.status,
|
|
5126
|
+
txHash: note.spentAtTxHash,
|
|
5127
|
+
blockNumber: spentBlockNumber,
|
|
5128
|
+
blockTimestamp: blockTimestampCache[Number(spentBlockNumber)]?.timestamp ?? null,
|
|
5129
|
+
blockTimestampIso: blockTimestampCache[Number(spentBlockNumber)]?.iso ?? null,
|
|
5130
|
+
function: note.spentByFunction,
|
|
5131
|
+
inputIndex: note.spentInputIndex,
|
|
5132
|
+
acceptedTransition: note.spentAtTxHash ? acceptedTransitionReference(note.spentAtTxHash) : null,
|
|
5133
|
+
},
|
|
5134
|
+
relationshipHints: {
|
|
5135
|
+
direction: note.counterpartyDirection ?? inferEvidenceDirection(note, scheme),
|
|
5136
|
+
counterpartyL2Address: note.counterpartyL2Address,
|
|
5137
|
+
counterpartyL1Address: null,
|
|
5138
|
+
confidence: note.counterpartyConfidence ?? (note.counterpartyL2Address ? "direct-local-metadata" : "unavailable"),
|
|
5139
|
+
},
|
|
5140
|
+
verificationClaims: {
|
|
5141
|
+
commitmentRecomputesFromPlaintext: true,
|
|
5142
|
+
nullifierRecomputesFromPlaintext: true,
|
|
5143
|
+
ownerMatchesWalletL2Address: true,
|
|
5144
|
+
spendingKeyIncluded: false,
|
|
5145
|
+
viewingKeyIncluded: false,
|
|
5146
|
+
walletSecretIncluded: false,
|
|
5147
|
+
fullWalletHistoryRequiredForFinalDisclosure: false,
|
|
5148
|
+
},
|
|
5149
|
+
});
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
function acceptedTransitionReference(txHash) {
|
|
5153
|
+
if (!txHash) {
|
|
5154
|
+
return null;
|
|
5155
|
+
}
|
|
5156
|
+
return {
|
|
5157
|
+
txHash,
|
|
5158
|
+
transactionPath: `transactions/${txHash}.json`,
|
|
5159
|
+
receiptPath: `receipts/${txHash}.json`,
|
|
5160
|
+
eventsPath: `events/${txHash}.json`,
|
|
5161
|
+
proofCalldataLocation: "transactions[].data",
|
|
5162
|
+
localProofArtifactIncluded: false,
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
function encryptedNoteSchemeLabel(scheme) {
|
|
5167
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT) {
|
|
5168
|
+
return "self-mint";
|
|
5169
|
+
}
|
|
5170
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_TRANSFER) {
|
|
5171
|
+
return "transfer";
|
|
5172
|
+
}
|
|
5173
|
+
return "unknown";
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
function inferEvidenceDirection(note, scheme) {
|
|
5177
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT) {
|
|
5178
|
+
return "self-mint";
|
|
5179
|
+
}
|
|
5180
|
+
if (note.spentByFunction?.startsWith("transferNotes")) {
|
|
5181
|
+
return "sent";
|
|
5182
|
+
}
|
|
5183
|
+
if (note.createdByFunction?.startsWith("transferNotes")) {
|
|
5184
|
+
return "received";
|
|
5185
|
+
}
|
|
5186
|
+
return "unknown";
|
|
5187
|
+
}
|
|
5188
|
+
|
|
5189
|
+
function buildEvidenceIndexes(noteRecords) {
|
|
5190
|
+
const indexes = {
|
|
5191
|
+
byCommitment: {},
|
|
5192
|
+
byNullifier: {},
|
|
5193
|
+
byCreationTx: {},
|
|
5194
|
+
bySpendTx: {},
|
|
5195
|
+
byBlockRange: [],
|
|
5196
|
+
byCounterparty: {
|
|
5197
|
+
unavailable: [],
|
|
5198
|
+
},
|
|
5199
|
+
};
|
|
5200
|
+
for (const record of noteRecords) {
|
|
5201
|
+
const pathName = `notes/${record.derived.commitment}.json`;
|
|
5202
|
+
indexes.byCommitment[record.derived.commitment] = pathName;
|
|
5203
|
+
indexes.byNullifier[record.derived.nullifier] = pathName;
|
|
5204
|
+
pushIndexEntry(indexes.byCreationTx, record.creation.txHash, pathName);
|
|
5205
|
+
pushIndexEntry(indexes.bySpendTx, record.spend.txHash, pathName);
|
|
5206
|
+
indexes.byBlockRange.push({
|
|
5207
|
+
commitment: record.derived.commitment,
|
|
5208
|
+
createdAtBlockNumber: record.creation.blockNumber,
|
|
5209
|
+
spentAtBlockNumber: record.spend.blockNumber,
|
|
5210
|
+
path: pathName,
|
|
5211
|
+
});
|
|
5212
|
+
const counterparty = record.relationshipHints.counterpartyL2Address;
|
|
5213
|
+
if (counterparty) {
|
|
5214
|
+
if (!indexes.byCounterparty[counterparty]) {
|
|
5215
|
+
indexes.byCounterparty[counterparty] = { sent: [], received: [], both: [] };
|
|
5216
|
+
}
|
|
5217
|
+
const direction = record.relationshipHints.direction === "received" ? "received" : "sent";
|
|
5218
|
+
indexes.byCounterparty[counterparty][direction].push(pathName);
|
|
5219
|
+
indexes.byCounterparty[counterparty].both.push(pathName);
|
|
5220
|
+
} else {
|
|
5221
|
+
indexes.byCounterparty.unavailable.push(pathName);
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
indexes.byBlockRange.sort((left, right) =>
|
|
5225
|
+
Number(left.createdAtBlockNumber ?? Number.MAX_SAFE_INTEGER)
|
|
5226
|
+
- Number(right.createdAtBlockNumber ?? Number.MAX_SAFE_INTEGER));
|
|
5227
|
+
return indexes;
|
|
5228
|
+
}
|
|
5229
|
+
|
|
5230
|
+
function pushIndexEntry(index, key, value) {
|
|
5231
|
+
if (!key) {
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
5234
|
+
if (!index[key]) {
|
|
5235
|
+
index[key] = [];
|
|
5236
|
+
}
|
|
5237
|
+
index[key].push(value);
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
function buildEvidenceManifest({
|
|
5241
|
+
outputPath,
|
|
5242
|
+
walletContext,
|
|
5243
|
+
walletMetadata,
|
|
5244
|
+
context,
|
|
5245
|
+
noteRecords,
|
|
5246
|
+
txHashes,
|
|
5247
|
+
}) {
|
|
5248
|
+
return normalizeCliOutput({
|
|
5249
|
+
format: WALLET_EVIDENCE_BUNDLE_FORMAT,
|
|
5250
|
+
formatVersion: WALLET_EVIDENCE_BUNDLE_FORMAT_VERSION,
|
|
5251
|
+
bundleType: "local-full-note-evidence",
|
|
5252
|
+
generatedAt: new Date().toISOString(),
|
|
5253
|
+
outputFileName: path.basename(outputPath),
|
|
5254
|
+
network: walletMetadata.network,
|
|
5255
|
+
chainId: walletContext.wallet.chainId,
|
|
5256
|
+
channelName: walletMetadata.channelName,
|
|
5257
|
+
channelId: walletContext.wallet.channelId,
|
|
5258
|
+
wallet: walletContext.walletName,
|
|
5259
|
+
walletL1Address: walletContext.wallet.l1Address,
|
|
5260
|
+
walletL2Address: walletContext.wallet.l2Address,
|
|
5261
|
+
controller: context.workspace.controller,
|
|
5262
|
+
channelManager: context.workspace.channelManager,
|
|
5263
|
+
bridgeTokenVault: context.workspace.bridgeTokenVault,
|
|
5264
|
+
containsAllLocallyKnownNotes: true,
|
|
5265
|
+
containsNotePlaintext: true,
|
|
5266
|
+
noteCount: noteRecords.length,
|
|
5267
|
+
transactionCount: txHashes.length,
|
|
5268
|
+
intendedUse: "Input for private-state-cli investigator; not a default exchange submission package.",
|
|
5269
|
+
warning: "DO_NOT_SUBMIT_AS_IS unless full wallet-history disclosure is intended.",
|
|
5270
|
+
excludedSecrets: {
|
|
5271
|
+
spendingKey: true,
|
|
5272
|
+
viewingKey: true,
|
|
5273
|
+
walletSecret: true,
|
|
5274
|
+
accountPrivateKey: true,
|
|
5275
|
+
keyFiles: true,
|
|
5276
|
+
},
|
|
4564
5277
|
});
|
|
4565
5278
|
}
|
|
4566
5279
|
|
|
5280
|
+
function addEvidenceJson(archive, archivePath, value) {
|
|
5281
|
+
archive.addFile(archivePath, Buffer.from(`${JSON.stringify(normalizeCliOutput(value), null, 2)}\n`, "utf8"));
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
function assertEvidenceBundleDoesNotContainSecrets({ wallet, payload }) {
|
|
5285
|
+
const serialized = JSON.stringify(payload);
|
|
5286
|
+
const forbiddenValues = [
|
|
5287
|
+
wallet.l2PrivateKey,
|
|
5288
|
+
wallet.noteReceivePrivateKey,
|
|
5289
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
5290
|
+
for (const value of forbiddenValues) {
|
|
5291
|
+
expect(
|
|
5292
|
+
!serialized.includes(value),
|
|
5293
|
+
"Evidence export refused to write authority-bearing wallet secret material.",
|
|
5294
|
+
);
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
function uniqueNonNull(values) {
|
|
5299
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
5300
|
+
}
|
|
5301
|
+
|
|
4567
5302
|
async function handleTransferNotes({ args, provider }) {
|
|
4568
5303
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
5304
|
+
requireWalletViewingCapability(wallet);
|
|
5305
|
+
requireWalletSpendingCapability(wallet);
|
|
4569
5306
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
4570
5307
|
const preparedContextResult = await loadFreshWalletChannelContext({
|
|
4571
5308
|
walletContext: wallet,
|
|
@@ -4603,6 +5340,19 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4603
5340
|
"The sum of --amounts must equal the sum of the selected input note values.",
|
|
4604
5341
|
);
|
|
4605
5342
|
|
|
5343
|
+
const { txSubmitter } = resolveTxSubmitterSigner({
|
|
5344
|
+
args,
|
|
5345
|
+
ownerSigner: signer,
|
|
5346
|
+
provider,
|
|
5347
|
+
});
|
|
5348
|
+
await requireActionImpactAcknowledgement("wallet-transfer-notes", args, {
|
|
5349
|
+
l1Address: txSubmitter.address,
|
|
5350
|
+
l2Address: wallet.wallet.l2Address,
|
|
5351
|
+
noteIds: noteIds.join(", "),
|
|
5352
|
+
amounts: amountInputs.join(", "),
|
|
5353
|
+
channelName: context.workspace.channelName,
|
|
5354
|
+
channelId: context.workspace.channelId,
|
|
5355
|
+
});
|
|
4606
5356
|
const templatePayload = await buildTransferNotesTemplatePayload({
|
|
4607
5357
|
context,
|
|
4608
5358
|
signer,
|
|
@@ -4623,6 +5373,9 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4623
5373
|
sourceFunction: templatePayload.method,
|
|
4624
5374
|
sourceTxHash: execution.receipt.hash,
|
|
4625
5375
|
bridgeCommitmentKeys: execution.noteLifecycle.outputCommitmentKeys,
|
|
5376
|
+
sourceBlockNumber: execution.receipt.blockNumber,
|
|
5377
|
+
counterpartyL2Addresses: templatePayload.recipientAddresses,
|
|
5378
|
+
counterpartyDirection: "sent",
|
|
4626
5379
|
});
|
|
4627
5380
|
|
|
4628
5381
|
printJson({
|
|
@@ -4753,6 +5506,7 @@ function ensureWallet({
|
|
|
4753
5506
|
ensureDir(path.join(walletDir, "operations"));
|
|
4754
5507
|
|
|
4755
5508
|
const wallet = normalizeWallet({
|
|
5509
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
4756
5510
|
name: walletName,
|
|
4757
5511
|
network: channelContext.workspace.network,
|
|
4758
5512
|
rpcUrl,
|
|
@@ -4769,10 +5523,8 @@ function ensureWallet({
|
|
|
4769
5523
|
l2AccountingVault: channelContext.workspace.l2AccountingVault,
|
|
4770
5524
|
liquidBalancesSlot: channelContext.workspace.liquidBalancesSlot,
|
|
4771
5525
|
l1Address: signerAddress,
|
|
4772
|
-
l1PrivateKey: normalizePrivateKey(signerPrivateKey),
|
|
4773
5526
|
l2Address: l2Identity.l2Address,
|
|
4774
|
-
|
|
4775
|
-
l2PublicKey: ethers.hexlify(l2Identity.l2PublicKey),
|
|
5527
|
+
l2PublicKey: l2Identity.l2PublicKey ? ethers.hexlify(l2Identity.l2PublicKey) : null,
|
|
4776
5528
|
l2DerivationMode: CHANNEL_BOUND_L2_DERIVATION_MODE,
|
|
4777
5529
|
l2DerivationChannelName: channelContext.workspace.channelName,
|
|
4778
5530
|
l2StorageKey: storageKey,
|
|
@@ -4788,13 +5540,18 @@ function ensureWallet({
|
|
|
4788
5540
|
spent: {},
|
|
4789
5541
|
},
|
|
4790
5542
|
});
|
|
5543
|
+
if (l2Identity.l2PrivateKey) {
|
|
5544
|
+
wallet.l2PrivateKey = ethers.hexlify(l2Identity.l2PrivateKey);
|
|
5545
|
+
}
|
|
5546
|
+
wallet.noteReceivePrivateKey = normalizePrivateKey(noteReceiveKeyMaterial.privateKey);
|
|
4791
5547
|
|
|
4792
5548
|
const context = {
|
|
4793
5549
|
walletName,
|
|
4794
5550
|
walletDir,
|
|
4795
5551
|
wallet,
|
|
4796
|
-
walletSecret,
|
|
5552
|
+
walletSecret: wallet.l2PrivateKey,
|
|
4797
5553
|
};
|
|
5554
|
+
persistWalletKeys(context);
|
|
4798
5555
|
persistWallet(context);
|
|
4799
5556
|
persistWalletMetadata(context);
|
|
4800
5557
|
return context;
|
|
@@ -4810,9 +5567,9 @@ function normalizeWallet(wallet) {
|
|
|
4810
5567
|
...wallet,
|
|
4811
5568
|
canonicalAssetDecimals: Number(wallet.canonicalAssetDecimals),
|
|
4812
5569
|
l2Nonce: Number(wallet.l2Nonce),
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5570
|
+
l2PrivateKey: wallet.l2PrivateKey ? ethers.hexlify(wallet.l2PrivateKey) : null,
|
|
5571
|
+
l2PublicKey: wallet.l2PublicKey ? ethers.hexlify(wallet.l2PublicKey) : null,
|
|
5572
|
+
noteReceivePrivateKey: wallet.noteReceivePrivateKey ? normalizePrivateKey(wallet.noteReceivePrivateKey) : null,
|
|
4816
5573
|
noteReceiveDerivationVersion: Number(wallet.noteReceiveDerivationVersion),
|
|
4817
5574
|
noteReceiveTypedDataMethod: wallet.noteReceiveTypedDataMethod,
|
|
4818
5575
|
noteReceivePubKeyX: normalizeBytes32Hex(wallet.noteReceivePubKeyX),
|
|
@@ -4822,18 +5579,18 @@ function normalizeWallet(wallet) {
|
|
|
4822
5579
|
unused: Object.fromEntries(unusedNotes.map((note) => [note.commitment, note])),
|
|
4823
5580
|
spent: Object.fromEntries(spentNotes.map((note) => [note.nullifier, note])),
|
|
4824
5581
|
unusedOrder: unusedNotes.map((note) => note.commitment),
|
|
4825
|
-
unusedBalance: unusedNotes.
|
|
5582
|
+
unusedBalance: unusedNotes.every((note) => note.value !== null)
|
|
5583
|
+
? unusedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n).toString()
|
|
5584
|
+
: null,
|
|
4826
5585
|
},
|
|
4827
5586
|
};
|
|
4828
5587
|
}
|
|
4829
5588
|
|
|
4830
5589
|
function assertWalletHasCurrentFormat(wallet, walletName) {
|
|
4831
5590
|
const requiredKeys = [
|
|
5591
|
+
"walletFormatVersion",
|
|
4832
5592
|
"canonicalAssetDecimals",
|
|
4833
5593
|
"l2Nonce",
|
|
4834
|
-
"l1PrivateKey",
|
|
4835
|
-
"l2PrivateKey",
|
|
4836
|
-
"l2PublicKey",
|
|
4837
5594
|
"noteReceiveDerivationVersion",
|
|
4838
5595
|
"noteReceiveTypedDataMethod",
|
|
4839
5596
|
"noteReceivePubKeyX",
|
|
@@ -4849,18 +5606,48 @@ function assertWalletHasCurrentFormat(wallet, walletName) {
|
|
|
4849
5606
|
wallet.notes && typeof wallet.notes.unused === "object" && typeof wallet.notes.spent === "object",
|
|
4850
5607
|
`Wallet ${walletName} was not created with the current CLI notes format.`,
|
|
4851
5608
|
);
|
|
5609
|
+
expect(
|
|
5610
|
+
Number(wallet.walletFormatVersion) === WALLET_WORKSPACE_FORMAT_VERSION,
|
|
5611
|
+
[
|
|
5612
|
+
`Wallet ${walletName} uses unsupported wallet workspace format ${wallet.walletFormatVersion ?? "missing"}.`,
|
|
5613
|
+
"Rebuild the wallet metadata with wallet recover-workspace.",
|
|
5614
|
+
].join(" "),
|
|
5615
|
+
);
|
|
4852
5616
|
}
|
|
4853
5617
|
|
|
4854
5618
|
function normalizeTrackedNote(note) {
|
|
4855
5619
|
return {
|
|
4856
|
-
owner: getAddress(note.owner),
|
|
4857
|
-
value: ethers.toBigInt(note.value).toString(),
|
|
4858
|
-
salt: normalizeBytes32Hex(note.salt),
|
|
5620
|
+
owner: note.owner ? getAddress(note.owner) : null,
|
|
5621
|
+
value: note.value !== undefined && note.value !== null ? ethers.toBigInt(note.value).toString() : null,
|
|
5622
|
+
salt: note.salt ? normalizeBytes32Hex(note.salt) : null,
|
|
4859
5623
|
commitment: normalizeBytes32Hex(note.commitment),
|
|
4860
5624
|
nullifier: normalizeBytes32Hex(note.nullifier),
|
|
5625
|
+
encryptedNoteValue: note.encryptedNoteValue ? normalizeEncryptedNoteValueWords(note.encryptedNoteValue) : null,
|
|
4861
5626
|
status: note.status,
|
|
4862
5627
|
sourceFunction: note.sourceFunction ?? null,
|
|
4863
5628
|
sourceTxHash: note.sourceTxHash ?? null,
|
|
5629
|
+
createdAtTxHash: note.createdAtTxHash ?? (note.status === "unused" ? note.sourceTxHash ?? null : null),
|
|
5630
|
+
createdAtBlockNumber: note.createdAtBlockNumber !== undefined && note.createdAtBlockNumber !== null
|
|
5631
|
+
? Number(note.createdAtBlockNumber)
|
|
5632
|
+
: null,
|
|
5633
|
+
createdAtLogIndex: note.createdAtLogIndex !== undefined && note.createdAtLogIndex !== null
|
|
5634
|
+
? Number(note.createdAtLogIndex)
|
|
5635
|
+
: null,
|
|
5636
|
+
createdByFunction: note.createdByFunction ?? (note.status === "unused" ? note.sourceFunction ?? null : null),
|
|
5637
|
+
createdOutputIndex: note.createdOutputIndex !== undefined && note.createdOutputIndex !== null
|
|
5638
|
+
? Number(note.createdOutputIndex)
|
|
5639
|
+
: null,
|
|
5640
|
+
spentAtTxHash: note.spentAtTxHash ?? (note.status === "spent" ? note.sourceTxHash ?? null : null),
|
|
5641
|
+
spentAtBlockNumber: note.spentAtBlockNumber !== undefined && note.spentAtBlockNumber !== null
|
|
5642
|
+
? Number(note.spentAtBlockNumber)
|
|
5643
|
+
: null,
|
|
5644
|
+
spentByFunction: note.spentByFunction ?? (note.status === "spent" ? note.sourceFunction ?? null : null),
|
|
5645
|
+
spentInputIndex: note.spentInputIndex !== undefined && note.spentInputIndex !== null
|
|
5646
|
+
? Number(note.spentInputIndex)
|
|
5647
|
+
: null,
|
|
5648
|
+
counterpartyL2Address: note.counterpartyL2Address ? getAddress(note.counterpartyL2Address) : null,
|
|
5649
|
+
counterpartyDirection: note.counterpartyDirection ?? null,
|
|
5650
|
+
counterpartyConfidence: note.counterpartyConfidence ?? null,
|
|
4864
5651
|
bridgeCommitmentKey: note.bridgeCommitmentKey ? normalizeBytes32Hex(note.bridgeCommitmentKey) : null,
|
|
4865
5652
|
bridgeNullifierKey: note.bridgeNullifierKey ? normalizeBytes32Hex(note.bridgeNullifierKey) : null,
|
|
4866
5653
|
};
|
|
@@ -4886,15 +5673,28 @@ async function buildWalletNoteBridgeStatus({
|
|
|
4886
5673
|
return {
|
|
4887
5674
|
owner: note.owner,
|
|
4888
5675
|
valueBaseUnits: note.value,
|
|
4889
|
-
valueTokens: ethers.formatUnits(ethers.toBigInt(note.value), canonicalAssetDecimals),
|
|
5676
|
+
valueTokens: note.value === null ? null : ethers.formatUnits(ethers.toBigInt(note.value), canonicalAssetDecimals),
|
|
4890
5677
|
commitment: note.commitment,
|
|
4891
5678
|
nullifier: note.nullifier,
|
|
5679
|
+
encryptedNoteValue: note.encryptedNoteValue,
|
|
4892
5680
|
walletStatus: note.status,
|
|
4893
5681
|
bridgeCommitmentExists: commitmentExists,
|
|
4894
5682
|
bridgeNullifierUsed: nullifierUsed,
|
|
4895
5683
|
walletStatusMatchesBridge: commitmentExists && nullifierUsed === expectedNullifierUsed,
|
|
4896
5684
|
sourceFunction: note.sourceFunction ?? null,
|
|
4897
5685
|
sourceTxHash: note.sourceTxHash ?? null,
|
|
5686
|
+
createdAtTxHash: note.createdAtTxHash ?? null,
|
|
5687
|
+
createdAtBlockNumber: note.createdAtBlockNumber ?? null,
|
|
5688
|
+
createdAtLogIndex: note.createdAtLogIndex ?? null,
|
|
5689
|
+
createdByFunction: note.createdByFunction ?? null,
|
|
5690
|
+
createdOutputIndex: note.createdOutputIndex ?? null,
|
|
5691
|
+
spentAtTxHash: note.spentAtTxHash ?? null,
|
|
5692
|
+
spentAtBlockNumber: note.spentAtBlockNumber ?? null,
|
|
5693
|
+
spentByFunction: note.spentByFunction ?? null,
|
|
5694
|
+
spentInputIndex: note.spentInputIndex ?? null,
|
|
5695
|
+
counterpartyL2Address: note.counterpartyL2Address ?? null,
|
|
5696
|
+
counterpartyDirection: note.counterpartyDirection ?? null,
|
|
5697
|
+
counterpartyConfidence: note.counterpartyConfidence ?? null,
|
|
4898
5698
|
};
|
|
4899
5699
|
}
|
|
4900
5700
|
|
|
@@ -4912,8 +5712,8 @@ async function readBooleanStorageValueFromSnapshot({ snapshot, storageAddress, s
|
|
|
4912
5712
|
}
|
|
4913
5713
|
|
|
4914
5714
|
function compareNotesByValueDesc(left, right) {
|
|
4915
|
-
const leftValue = ethers.toBigInt(left.value);
|
|
4916
|
-
const rightValue = ethers.toBigInt(right.value);
|
|
5715
|
+
const leftValue = left.value === null || left.value === undefined ? 0n : ethers.toBigInt(left.value);
|
|
5716
|
+
const rightValue = right.value === null || right.value === undefined ? 0n : ethers.toBigInt(right.value);
|
|
4917
5717
|
if (leftValue === rightValue) {
|
|
4918
5718
|
return left.commitment.localeCompare(right.commitment);
|
|
4919
5719
|
}
|
|
@@ -4922,13 +5722,28 @@ function compareNotesByValueDesc(left, right) {
|
|
|
4922
5722
|
|
|
4923
5723
|
function buildTrackedNote(note, sourceFunction, sourceTxHash, bridgeKeys = {}) {
|
|
4924
5724
|
const normalizedNote = normalizePlaintextNote(note);
|
|
5725
|
+
const createdTxHash = bridgeKeys.createdAtTxHash ?? sourceTxHash ?? null;
|
|
5726
|
+
const createdFunction = bridgeKeys.createdByFunction ?? sourceFunction ?? null;
|
|
4925
5727
|
return {
|
|
4926
5728
|
...normalizedNote,
|
|
4927
5729
|
commitment: normalizeBytes32Hex(computeNoteCommitment(normalizedNote)),
|
|
4928
5730
|
nullifier: normalizeBytes32Hex(computeNullifier(normalizedNote)),
|
|
5731
|
+
encryptedNoteValue: note.encryptedNoteValue ? normalizeEncryptedNoteValueWords(note.encryptedNoteValue) : null,
|
|
4929
5732
|
status: "unused",
|
|
4930
5733
|
sourceFunction,
|
|
4931
5734
|
sourceTxHash,
|
|
5735
|
+
createdAtTxHash: createdTxHash,
|
|
5736
|
+
createdAtBlockNumber: bridgeKeys.createdAtBlockNumber ?? null,
|
|
5737
|
+
createdAtLogIndex: bridgeKeys.createdAtLogIndex ?? null,
|
|
5738
|
+
createdByFunction: createdFunction,
|
|
5739
|
+
createdOutputIndex: bridgeKeys.createdOutputIndex ?? null,
|
|
5740
|
+
spentAtTxHash: bridgeKeys.spentAtTxHash ?? null,
|
|
5741
|
+
spentAtBlockNumber: bridgeKeys.spentAtBlockNumber ?? null,
|
|
5742
|
+
spentByFunction: bridgeKeys.spentByFunction ?? null,
|
|
5743
|
+
spentInputIndex: bridgeKeys.spentInputIndex ?? null,
|
|
5744
|
+
counterpartyL2Address: bridgeKeys.counterpartyL2Address ? getAddress(bridgeKeys.counterpartyL2Address) : null,
|
|
5745
|
+
counterpartyDirection: bridgeKeys.counterpartyDirection ?? null,
|
|
5746
|
+
counterpartyConfidence: bridgeKeys.counterpartyConfidence ?? null,
|
|
4932
5747
|
bridgeCommitmentKey: bridgeKeys.bridgeCommitmentKey
|
|
4933
5748
|
? normalizeBytes32Hex(bridgeKeys.bridgeCommitmentKey)
|
|
4934
5749
|
: null,
|
|
@@ -4943,9 +5758,19 @@ function buildLifecycleTrackedOutputs({
|
|
|
4943
5758
|
sourceFunction,
|
|
4944
5759
|
sourceTxHash,
|
|
4945
5760
|
bridgeCommitmentKeys,
|
|
5761
|
+
sourceBlockNumber = null,
|
|
5762
|
+
counterpartyL2Addresses = [],
|
|
5763
|
+
counterpartyDirection = null,
|
|
4946
5764
|
}) {
|
|
4947
5765
|
return (outputNotes ?? []).map((note, index) => buildTrackedNote(note, sourceFunction, sourceTxHash, {
|
|
4948
5766
|
bridgeCommitmentKey: bridgeCommitmentKeys?.[index] ?? null,
|
|
5767
|
+
createdAtTxHash: sourceTxHash,
|
|
5768
|
+
createdAtBlockNumber: sourceBlockNumber,
|
|
5769
|
+
createdByFunction: sourceFunction,
|
|
5770
|
+
createdOutputIndex: index,
|
|
5771
|
+
counterpartyL2Address: counterpartyL2Addresses?.[index] ?? null,
|
|
5772
|
+
counterpartyDirection,
|
|
5773
|
+
counterpartyConfidence: counterpartyL2Addresses?.[index] ? "direct-local-metadata" : null,
|
|
4949
5774
|
}));
|
|
4950
5775
|
}
|
|
4951
5776
|
|
|
@@ -4958,13 +5783,11 @@ async function recoverWalletReceivedNotes({
|
|
|
4958
5783
|
progressAction = null,
|
|
4959
5784
|
fromGenesis = false,
|
|
4960
5785
|
}) {
|
|
4961
|
-
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ??
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
account: signer.address,
|
|
4967
|
-
});
|
|
5786
|
+
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ?? {
|
|
5787
|
+
privateKey: walletContext.wallet.noteReceivePrivateKey,
|
|
5788
|
+
noteReceivePubKey: walletNoteReceivePubKey(walletContext),
|
|
5789
|
+
};
|
|
5790
|
+
requireWalletViewingCapability(walletContext);
|
|
4968
5791
|
const recoveredDeliveryState = await recoverDeliveredNotesFromEventLogs({
|
|
4969
5792
|
walletContext,
|
|
4970
5793
|
context,
|
|
@@ -5064,12 +5887,22 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
5064
5887
|
owner: walletContext.wallet.l2Address,
|
|
5065
5888
|
value: recoveredValue,
|
|
5066
5889
|
salt: computeEncryptedNoteSalt(encryptedNoteValue),
|
|
5890
|
+
encryptedNoteValue,
|
|
5067
5891
|
});
|
|
5068
5892
|
const commitment = normalizeBytes32Hex(computeNoteCommitment(plaintextNote));
|
|
5069
5893
|
const nullifier = normalizeBytes32Hex(computeNullifier(plaintextNote));
|
|
5070
|
-
const trackedNote = buildTrackedNote(
|
|
5894
|
+
const trackedNote = buildTrackedNote({
|
|
5895
|
+
...plaintextNote,
|
|
5896
|
+
encryptedNoteValue,
|
|
5897
|
+
}, sourceFunction, log.transactionHash, {
|
|
5071
5898
|
bridgeCommitmentKey: derivePrivateStateControllerMappingStorageKey(commitment, commitmentExistsSlot),
|
|
5072
5899
|
bridgeNullifierKey: derivePrivateStateControllerMappingStorageKey(nullifier, nullifierUsedSlot),
|
|
5900
|
+
createdAtTxHash: log.transactionHash,
|
|
5901
|
+
createdAtBlockNumber: log.blockNumber !== undefined ? Number(log.blockNumber) : null,
|
|
5902
|
+
createdAtLogIndex: log.index ?? log.logIndex ?? null,
|
|
5903
|
+
createdByFunction: sourceFunction,
|
|
5904
|
+
counterpartyDirection: scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT ? "self-mint" : "received",
|
|
5905
|
+
counterpartyConfidence: scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT ? "direct-local-metadata" : "unavailable",
|
|
5073
5906
|
});
|
|
5074
5907
|
const commitmentExists = await readBooleanStorageValueFromSnapshot({
|
|
5075
5908
|
snapshot: context.currentSnapshot,
|
|
@@ -5420,7 +6253,7 @@ function snapshotRootForAddress(snapshot, storageAddress) {
|
|
|
5420
6253
|
return snapshot.stateRoots[addressIndex];
|
|
5421
6254
|
}
|
|
5422
6255
|
|
|
5423
|
-
function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, sourceTxHash) {
|
|
6256
|
+
function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, sourceTxHash, sourceBlockNumber = null) {
|
|
5424
6257
|
for (const [index, inputNote] of lifecycle.inputs.entries()) {
|
|
5425
6258
|
const trackedInput = buildTrackedNote(inputNote, sourceFunction, sourceTxHash);
|
|
5426
6259
|
const existingUnusedNote = walletContext.wallet.notes.unused[trackedInput.commitment];
|
|
@@ -5434,12 +6267,26 @@ function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, so
|
|
|
5434
6267
|
sourceFunction,
|
|
5435
6268
|
sourceTxHash,
|
|
5436
6269
|
bridgeNullifierKey: lifecycle.inputNullifierKeys?.[index] ?? existingUnusedNote.bridgeNullifierKey ?? null,
|
|
6270
|
+
spentAtTxHash: sourceTxHash,
|
|
6271
|
+
spentAtBlockNumber: sourceBlockNumber,
|
|
6272
|
+
spentByFunction: sourceFunction,
|
|
6273
|
+
spentInputIndex: index,
|
|
6274
|
+
counterpartyL2Address: lifecycle.spendCounterpartyL2Address ?? existingUnusedNote.counterpartyL2Address ?? null,
|
|
6275
|
+
counterpartyDirection: lifecycle.spendCounterpartyDirection ?? existingUnusedNote.counterpartyDirection ?? null,
|
|
6276
|
+
counterpartyConfidence: lifecycle.spendCounterpartyConfidence ?? existingUnusedNote.counterpartyConfidence ?? null,
|
|
5437
6277
|
};
|
|
5438
6278
|
}
|
|
5439
6279
|
|
|
5440
6280
|
for (const [index, outputNote] of lifecycle.outputs.entries()) {
|
|
5441
6281
|
const trackedOutput = buildTrackedNote(outputNote, sourceFunction, sourceTxHash, {
|
|
5442
6282
|
bridgeCommitmentKey: lifecycle.outputCommitmentKeys?.[index] ?? null,
|
|
6283
|
+
createdAtTxHash: sourceTxHash,
|
|
6284
|
+
createdAtBlockNumber: sourceBlockNumber,
|
|
6285
|
+
createdByFunction: sourceFunction,
|
|
6286
|
+
createdOutputIndex: index,
|
|
6287
|
+
counterpartyL2Address: lifecycle.outputCounterpartyL2Addresses?.[index] ?? null,
|
|
6288
|
+
counterpartyDirection: lifecycle.outputCounterpartyDirection ?? null,
|
|
6289
|
+
counterpartyConfidence: lifecycle.outputCounterpartyL2Addresses?.[index] ? "direct-local-metadata" : null,
|
|
5443
6290
|
});
|
|
5444
6291
|
if (trackedOutput.owner !== walletContext.wallet.l2Address) {
|
|
5445
6292
|
continue;
|
|
@@ -5524,6 +6371,7 @@ function buildMintEncryptedOutputs({ wallet, values }) {
|
|
|
5524
6371
|
owner: wallet.wallet.l2Address,
|
|
5525
6372
|
value: ethers.toBigInt(value).toString(),
|
|
5526
6373
|
salt: computeEncryptedNoteSalt(encryptedNoteValue),
|
|
6374
|
+
encryptedNoteValue,
|
|
5527
6375
|
});
|
|
5528
6376
|
}
|
|
5529
6377
|
return {
|
|
@@ -5581,6 +6429,7 @@ async function buildTransferNotesTemplatePayload({
|
|
|
5581
6429
|
owner: recipient,
|
|
5582
6430
|
value: ethers.toBigInt(outputAmounts[index]).toString(),
|
|
5583
6431
|
salt,
|
|
6432
|
+
encryptedNoteValue,
|
|
5584
6433
|
});
|
|
5585
6434
|
}
|
|
5586
6435
|
return {
|
|
@@ -5589,6 +6438,7 @@ async function buildTransferNotesTemplatePayload({
|
|
|
5589
6438
|
args: [transferOutputs, inputNotes],
|
|
5590
6439
|
lifecycleInputs: inputNotes,
|
|
5591
6440
|
lifecycleOutputs,
|
|
6441
|
+
recipientAddresses,
|
|
5592
6442
|
};
|
|
5593
6443
|
}
|
|
5594
6444
|
|
|
@@ -5609,6 +6459,10 @@ function loadWalletUnusedInputNotes(walletContext, noteIds) {
|
|
|
5609
6459
|
return noteIds.map((noteId) => {
|
|
5610
6460
|
const trackedNote = walletContext.wallet.notes.unused[noteId];
|
|
5611
6461
|
expect(trackedNote, `Unknown unused note commitment: ${noteId}.`);
|
|
6462
|
+
expect(
|
|
6463
|
+
trackedNote.owner && trackedNote.value !== null && trackedNote.salt,
|
|
6464
|
+
`Note ${noteId} is encrypted-only. Import the wallet viewing key before spending it.`,
|
|
6465
|
+
);
|
|
5612
6466
|
return normalizePlaintextNote(trackedNote);
|
|
5613
6467
|
});
|
|
5614
6468
|
}
|
|
@@ -5957,6 +6811,7 @@ async function executeWalletDirectTemplateCommand({
|
|
|
5957
6811
|
}) {
|
|
5958
6812
|
emitProgress(operationName, "loading");
|
|
5959
6813
|
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
6814
|
+
requireWalletSpendingCapability(wallet);
|
|
5960
6815
|
const {
|
|
5961
6816
|
txSubmitter,
|
|
5962
6817
|
source: txSubmitterSource,
|
|
@@ -6098,7 +6953,16 @@ async function executeWalletTemplateSend({
|
|
|
6098
6953
|
|
|
6099
6954
|
emitProgress(operationName, "persisting");
|
|
6100
6955
|
wallet.wallet.l2Nonce = nonce + 1;
|
|
6101
|
-
|
|
6956
|
+
if (functionName.startsWith("transferNotes")) {
|
|
6957
|
+
noteLifecycle.spendCounterpartyL2Address = templatePayload.recipientAddresses?.[0] ?? null;
|
|
6958
|
+
noteLifecycle.spendCounterpartyDirection = "sent";
|
|
6959
|
+
noteLifecycle.spendCounterpartyConfidence = noteLifecycle.spendCounterpartyL2Address
|
|
6960
|
+
? "direct-local-metadata"
|
|
6961
|
+
: null;
|
|
6962
|
+
noteLifecycle.outputCounterpartyL2Addresses = templatePayload.recipientAddresses ?? [];
|
|
6963
|
+
noteLifecycle.outputCounterpartyDirection = "sent";
|
|
6964
|
+
}
|
|
6965
|
+
applyNoteLifecycleToWallet(wallet, noteLifecycle, functionName, receipt.hash, receipt.blockNumber);
|
|
6102
6966
|
context.currentSnapshot = nextSnapshot;
|
|
6103
6967
|
persistWallet(wallet);
|
|
6104
6968
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -6107,7 +6971,7 @@ async function executeWalletTemplateSend({
|
|
|
6107
6971
|
receipt,
|
|
6108
6972
|
progressAction: operationName,
|
|
6109
6973
|
});
|
|
6110
|
-
sealWalletOperationDir(operationDir, wallet
|
|
6974
|
+
sealWalletOperationDir(operationDir, walletOperationSealSecret(wallet));
|
|
6111
6975
|
|
|
6112
6976
|
return {
|
|
6113
6977
|
wallet,
|
|
@@ -6195,34 +7059,54 @@ async function loadJoinChannelContext({ args, network, provider }) {
|
|
|
6195
7059
|
};
|
|
6196
7060
|
}
|
|
6197
7061
|
|
|
6198
|
-
function loadWallet(walletName,
|
|
7062
|
+
function loadWallet(walletName, networkName) {
|
|
6199
7063
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
6200
7064
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
6201
7065
|
const walletDir = walletPath(normalizedWalletName, normalizedNetworkName);
|
|
6202
7066
|
if (!walletConfigExists(walletDir)) {
|
|
6203
7067
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
6204
7068
|
}
|
|
6205
|
-
const rawWallet =
|
|
7069
|
+
const rawWallet = readJson(walletNotesMetadataPath(walletDir));
|
|
7070
|
+
const spendingKey = readWalletKeySecretIfExists({
|
|
7071
|
+
networkName: normalizedNetworkName,
|
|
7072
|
+
walletName: normalizedWalletName,
|
|
7073
|
+
keyKind: "spending",
|
|
7074
|
+
});
|
|
7075
|
+
const viewingKey = readWalletKeySecretIfExists({
|
|
7076
|
+
networkName: normalizedNetworkName,
|
|
7077
|
+
walletName: normalizedWalletName,
|
|
7078
|
+
keyKind: "viewing",
|
|
7079
|
+
});
|
|
7080
|
+
if (spendingKey) {
|
|
7081
|
+
rawWallet.l2PrivateKey = spendingKey.privateKey;
|
|
7082
|
+
rawWallet.l2PublicKey = spendingKey.metadata?.l2PublicKey ?? rawWallet.l2PublicKey;
|
|
7083
|
+
}
|
|
7084
|
+
if (viewingKey) {
|
|
7085
|
+
rawWallet.noteReceivePrivateKey = viewingKey.privateKey;
|
|
7086
|
+
}
|
|
6206
7087
|
assertWalletHasRequiredKeys(rawWallet, normalizedWalletName);
|
|
6207
7088
|
const wallet = normalizeWallet(rawWallet);
|
|
6208
7089
|
assertWalletUsesChannelBoundDerivation(wallet, normalizedWalletName);
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
7090
|
+
if (wallet.l2PrivateKey) {
|
|
7091
|
+
const restoredIdentity = restoreParticipantIdentityFromWallet(wallet);
|
|
7092
|
+
expect(
|
|
7093
|
+
wallet.l2Address === restoredIdentity.l2Address,
|
|
7094
|
+
`Wallet ${normalizedWalletName} is internally inconsistent: stored keys do not match the stored L2 address.`,
|
|
7095
|
+
);
|
|
7096
|
+
}
|
|
7097
|
+
hydrateWalletNotesWithViewingKey(wallet);
|
|
6214
7098
|
const context = {
|
|
6215
7099
|
walletName: normalizedWalletName,
|
|
6216
7100
|
walletDir,
|
|
6217
7101
|
wallet,
|
|
6218
|
-
walletSecret,
|
|
7102
|
+
walletSecret: wallet.l2PrivateKey ?? wallet.noteReceivePrivateKey ?? null,
|
|
6219
7103
|
};
|
|
6220
7104
|
return context;
|
|
6221
7105
|
}
|
|
6222
7106
|
|
|
6223
7107
|
function loadUnlockedWalletWithMetadata(args) {
|
|
6224
7108
|
const networkName = requireNetworkName(args);
|
|
6225
|
-
const wallet = loadWallet(requireWalletName(args),
|
|
7109
|
+
const wallet = loadWallet(requireWalletName(args), networkName);
|
|
6226
7110
|
const walletMetadata = loadWalletMetadata(wallet.walletName, networkName);
|
|
6227
7111
|
assertWalletMatchesMetadata(wallet, walletMetadata);
|
|
6228
7112
|
expect(
|
|
@@ -6238,19 +7122,71 @@ function loadUnlockedWalletWithMetadata(args) {
|
|
|
6238
7122
|
};
|
|
6239
7123
|
}
|
|
6240
7124
|
|
|
7125
|
+
function readWalletKeySecretIfExists({ networkName, walletName, keyKind }) {
|
|
7126
|
+
const secretPath = keyKind === "spending"
|
|
7127
|
+
? walletSpendingKeySecretPath(networkName, walletName)
|
|
7128
|
+
: walletViewingKeySecretPath(networkName, walletName);
|
|
7129
|
+
if (!fs.existsSync(secretPath)) {
|
|
7130
|
+
return null;
|
|
7131
|
+
}
|
|
7132
|
+
const payload = JSON.parse(readSecretFile(secretPath, `${keyKind} key`));
|
|
7133
|
+
validateWalletKeyPayload(payload, keyKind);
|
|
7134
|
+
return payload;
|
|
7135
|
+
}
|
|
7136
|
+
|
|
7137
|
+
function validateWalletKeyPayload(payload, keyKind) {
|
|
7138
|
+
expect(payload?.format === WALLET_KEY_EXPORT_FORMAT, `Invalid ${keyKind} key file format.`);
|
|
7139
|
+
expect(Number(payload.formatVersion) === WALLET_EXPORT_FORMAT_VERSION, `Unsupported ${keyKind} key file version.`);
|
|
7140
|
+
expect(payload.keyKind === keyKind, `Expected ${keyKind} key file, received ${payload.keyKind}.`);
|
|
7141
|
+
expect(typeof payload.privateKey === "string" && payload.privateKey.length > 0, `Missing ${keyKind} private key.`);
|
|
7142
|
+
expect(payload.metadata && typeof payload.metadata === "object", `Missing ${keyKind} key metadata.`);
|
|
7143
|
+
}
|
|
7144
|
+
|
|
7145
|
+
function hydrateWalletNotesWithViewingKey(wallet) {
|
|
7146
|
+
if (!wallet.noteReceivePrivateKey) {
|
|
7147
|
+
return;
|
|
7148
|
+
}
|
|
7149
|
+
const noteGroups = [wallet.notes?.unused ?? {}, wallet.notes?.spent ?? {}];
|
|
7150
|
+
for (const notes of noteGroups) {
|
|
7151
|
+
for (const note of Object.values(notes)) {
|
|
7152
|
+
if (!note.encryptedNoteValue || note.value !== null) {
|
|
7153
|
+
continue;
|
|
7154
|
+
}
|
|
7155
|
+
try {
|
|
7156
|
+
const { scheme } = unpackEncryptedNoteValue(note.encryptedNoteValue);
|
|
7157
|
+
let value;
|
|
7158
|
+
if (scheme === ENCRYPTED_NOTE_SCHEME_TRANSFER) {
|
|
7159
|
+
value = decryptEncryptedNoteValue({
|
|
7160
|
+
encryptedValue: note.encryptedNoteValue,
|
|
7161
|
+
noteReceivePrivateKey: wallet.noteReceivePrivateKey,
|
|
7162
|
+
chainId: wallet.chainId,
|
|
7163
|
+
channelId: wallet.channelId,
|
|
7164
|
+
owner: wallet.l2Address,
|
|
7165
|
+
});
|
|
7166
|
+
} else if (scheme === ENCRYPTED_NOTE_SCHEME_SELF_MINT) {
|
|
7167
|
+
value = decryptMintEncryptedNoteValue({
|
|
7168
|
+
encryptedValue: note.encryptedNoteValue,
|
|
7169
|
+
noteReceivePrivateKey: wallet.noteReceivePrivateKey,
|
|
7170
|
+
chainId: wallet.chainId,
|
|
7171
|
+
channelId: wallet.channelId,
|
|
7172
|
+
owner: wallet.l2Address,
|
|
7173
|
+
});
|
|
7174
|
+
} else {
|
|
7175
|
+
continue;
|
|
7176
|
+
}
|
|
7177
|
+
note.owner = wallet.l2Address;
|
|
7178
|
+
note.value = ethers.toBigInt(value).toString();
|
|
7179
|
+
note.salt = computeEncryptedNoteSalt(note.encryptedNoteValue);
|
|
7180
|
+
} catch {
|
|
7181
|
+
// Keep encrypted-only note records readable even when the local viewing key cannot decrypt them.
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
}
|
|
7185
|
+
wallet.notes = normalizeWallet(wallet).notes;
|
|
7186
|
+
}
|
|
7187
|
+
|
|
6241
7188
|
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
|
-
);
|
|
7189
|
+
expect(wallet.walletFormatVersion !== undefined, `Wallet ${walletName} is missing walletFormatVersion.`);
|
|
6254
7190
|
}
|
|
6255
7191
|
|
|
6256
7192
|
function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
|
|
@@ -6271,6 +7207,13 @@ function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
|
|
|
6271
7207
|
}
|
|
6272
7208
|
|
|
6273
7209
|
function restoreParticipantIdentityFromWallet(wallet) {
|
|
7210
|
+
if (!wallet.l2PrivateKey) {
|
|
7211
|
+
return {
|
|
7212
|
+
l2PrivateKey: null,
|
|
7213
|
+
l2PublicKey: wallet.l2PublicKey ? Uint8Array.from(ethers.getBytes(wallet.l2PublicKey)) : null,
|
|
7214
|
+
l2Address: getAddress(wallet.l2Address),
|
|
7215
|
+
};
|
|
7216
|
+
}
|
|
6274
7217
|
const l2PrivateKey = Uint8Array.from(ethers.getBytes(wallet.l2PrivateKey));
|
|
6275
7218
|
const l2PublicKey = Uint8Array.from(ethers.getBytes(wallet.l2PublicKey));
|
|
6276
7219
|
const l2Address = getAddress(fromEdwardsToAddress(l2PublicKey).toString());
|
|
@@ -6282,7 +7225,14 @@ function restoreParticipantIdentityFromWallet(wallet) {
|
|
|
6282
7225
|
}
|
|
6283
7226
|
|
|
6284
7227
|
function restoreWalletSigner(walletContext, provider) {
|
|
6285
|
-
|
|
7228
|
+
const privateKey = findAccountPrivateKeyForAddress(walletContext.wallet.network, walletContext.wallet.l1Address);
|
|
7229
|
+
if (privateKey) {
|
|
7230
|
+
return new Wallet(privateKey, provider);
|
|
7231
|
+
}
|
|
7232
|
+
return {
|
|
7233
|
+
address: getAddress(walletContext.wallet.l1Address),
|
|
7234
|
+
provider,
|
|
7235
|
+
};
|
|
6286
7236
|
}
|
|
6287
7237
|
|
|
6288
7238
|
function restoreWalletParticipant(walletContext, provider) {
|
|
@@ -6292,6 +7242,75 @@ function restoreWalletParticipant(walletContext, provider) {
|
|
|
6292
7242
|
};
|
|
6293
7243
|
}
|
|
6294
7244
|
|
|
7245
|
+
function requireWalletOwnerSigner(walletContext, provider) {
|
|
7246
|
+
const signer = restoreWalletSigner(walletContext, provider);
|
|
7247
|
+
expect(
|
|
7248
|
+
typeof signer.privateKey === "string",
|
|
7249
|
+
[
|
|
7250
|
+
`Missing local account secret for wallet owner ${walletContext.wallet.l1Address}.`,
|
|
7251
|
+
"Import the matching account secret or use a command-specific transaction submitter where supported.",
|
|
7252
|
+
].join(" "),
|
|
7253
|
+
);
|
|
7254
|
+
return signer;
|
|
7255
|
+
}
|
|
7256
|
+
|
|
7257
|
+
function requireWalletSpendingCapability(walletContext) {
|
|
7258
|
+
expect(
|
|
7259
|
+
walletContext.wallet.l2PrivateKey,
|
|
7260
|
+
[
|
|
7261
|
+
`Wallet ${walletContext.walletName} is missing its spending key.`,
|
|
7262
|
+
"Import it with wallet import spending-key before commands that spend notes or change L2 channel accounting state.",
|
|
7263
|
+
].join(" "),
|
|
7264
|
+
);
|
|
7265
|
+
}
|
|
7266
|
+
|
|
7267
|
+
function requireWalletViewingCapability(walletContext) {
|
|
7268
|
+
expect(
|
|
7269
|
+
walletContext.wallet.noteReceivePrivateKey,
|
|
7270
|
+
[
|
|
7271
|
+
`Wallet ${walletContext.walletName} is missing its viewing key.`,
|
|
7272
|
+
"Import it with wallet import viewing-key before commands that decrypt or refresh received notes.",
|
|
7273
|
+
].join(" "),
|
|
7274
|
+
);
|
|
7275
|
+
}
|
|
7276
|
+
|
|
7277
|
+
function walletOperationSealSecret(walletContext) {
|
|
7278
|
+
const secret = walletContext.wallet.l2PrivateKey
|
|
7279
|
+
?? walletContext.wallet.noteReceivePrivateKey
|
|
7280
|
+
?? findAccountPrivateKeyForAddress(walletContext.wallet.network, walletContext.wallet.l1Address);
|
|
7281
|
+
expect(
|
|
7282
|
+
secret,
|
|
7283
|
+
`Wallet ${walletContext.walletName} needs a local key to seal operation artifacts.`,
|
|
7284
|
+
);
|
|
7285
|
+
return secret;
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
function findAccountPrivateKeyForAddress(networkName, l1Address) {
|
|
7289
|
+
const accountsRoot = path.join(secretRoot, requireNetworkName({ network: networkName }), "accounts");
|
|
7290
|
+
if (!fs.existsSync(accountsRoot)) {
|
|
7291
|
+
return null;
|
|
7292
|
+
}
|
|
7293
|
+
for (const entry of fs.readdirSync(accountsRoot, { withFileTypes: true })) {
|
|
7294
|
+
if (!entry.isDirectory()) {
|
|
7295
|
+
continue;
|
|
7296
|
+
}
|
|
7297
|
+
const privateKeyPath = path.join(accountsRoot, entry.name, "private-key");
|
|
7298
|
+
if (!fs.existsSync(privateKeyPath)) {
|
|
7299
|
+
continue;
|
|
7300
|
+
}
|
|
7301
|
+
try {
|
|
7302
|
+
const privateKey = normalizePrivateKey(readSecretFile(privateKeyPath, "--account"));
|
|
7303
|
+
const signer = new Wallet(privateKey);
|
|
7304
|
+
if (ethers.toBigInt(getAddress(signer.address)) === ethers.toBigInt(getAddress(l1Address))) {
|
|
7305
|
+
return privateKey;
|
|
7306
|
+
}
|
|
7307
|
+
} catch {
|
|
7308
|
+
continue;
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
return null;
|
|
7312
|
+
}
|
|
7313
|
+
|
|
6295
7314
|
function loadBridgeResources({ chainId }) {
|
|
6296
7315
|
const bridgeDeploymentPath = defaultBridgeDeploymentPath(chainId);
|
|
6297
7316
|
const bridgeDeployment = readJson(bridgeDeploymentPath);
|
|
@@ -6313,7 +7332,7 @@ function loadWalletMetadata(walletName, networkName) {
|
|
|
6313
7332
|
if (!walletConfigExists(walletDir)) {
|
|
6314
7333
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
6315
7334
|
}
|
|
6316
|
-
const metadataPath =
|
|
7335
|
+
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
6317
7336
|
if (!fs.existsSync(metadataPath)) {
|
|
6318
7337
|
throw new Error(`Wallet ${normalizedWalletName} is missing unencrypted metadata at ${metadataPath}.`);
|
|
6319
7338
|
}
|
|
@@ -6342,14 +7361,14 @@ function assertWalletMatchesMetadata(walletContext, walletMetadata) {
|
|
|
6342
7361
|
walletContext.wallet.network === walletMetadata.network,
|
|
6343
7362
|
[
|
|
6344
7363
|
`Wallet ${walletContext.walletName} metadata network (${walletMetadata.network}) does not match`,
|
|
6345
|
-
`the
|
|
7364
|
+
`the wallet note metadata network (${walletContext.wallet.network}).`,
|
|
6346
7365
|
].join(" "),
|
|
6347
7366
|
);
|
|
6348
7367
|
expect(
|
|
6349
7368
|
walletContext.wallet.channelName === walletMetadata.channelName,
|
|
6350
7369
|
[
|
|
6351
7370
|
`Wallet ${walletContext.walletName} metadata channelName (${walletMetadata.channelName}) does not match`,
|
|
6352
|
-
`the
|
|
7371
|
+
`the wallet note metadata channel (${walletContext.wallet.channelName}).`,
|
|
6353
7372
|
].join(" "),
|
|
6354
7373
|
);
|
|
6355
7374
|
}
|
|
@@ -7535,6 +8554,7 @@ const OUTPUT_BYTES32_SCALAR_KEYS = new Set([
|
|
|
7535
8554
|
"commitment",
|
|
7536
8555
|
"currentRootVectorHash",
|
|
7537
8556
|
"currentUserKey",
|
|
8557
|
+
"createdAtTxHash",
|
|
7538
8558
|
"emittedRootVectorHash",
|
|
7539
8559
|
"ephemeralPubKeyX",
|
|
7540
8560
|
"hash",
|
|
@@ -7548,6 +8568,7 @@ const OUTPUT_BYTES32_SCALAR_KEYS = new Set([
|
|
|
7548
8568
|
"rootVectorHash",
|
|
7549
8569
|
"salt",
|
|
7550
8570
|
"sourceTxHash",
|
|
8571
|
+
"spentAtTxHash",
|
|
7551
8572
|
"topic0",
|
|
7552
8573
|
"transactionHash",
|
|
7553
8574
|
"txHash",
|
|
@@ -7744,6 +8765,13 @@ function parseArgs(argv) {
|
|
|
7744
8765
|
&& parsed.positional[1]
|
|
7745
8766
|
) {
|
|
7746
8767
|
parsed.command = `${parsed.command}-${parsed.positional[1]}`;
|
|
8768
|
+
if (
|
|
8769
|
+
parsed.positional[0] === "wallet"
|
|
8770
|
+
&& (parsed.positional[1] === "export" || parsed.positional[1] === "import")
|
|
8771
|
+
&& parsed.positional[2]
|
|
8772
|
+
) {
|
|
8773
|
+
parsed.command = `${parsed.command}-${parsed.positional[2]}`;
|
|
8774
|
+
}
|
|
7747
8775
|
parsed.positional = [parsed.command];
|
|
7748
8776
|
}
|
|
7749
8777
|
return parsed;
|
|
@@ -7773,18 +8801,6 @@ function parseTokenAmount(value, decimals) {
|
|
|
7773
8801
|
}
|
|
7774
8802
|
}
|
|
7775
8803
|
|
|
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
8804
|
function requireArg(value, label) {
|
|
7789
8805
|
if (value === undefined || value === null || value === "") {
|
|
7790
8806
|
throw new Error(`Missing ${label}.`);
|
|
@@ -7849,6 +8865,13 @@ function requireL1Signer(args, provider) {
|
|
|
7849
8865
|
|
|
7850
8866
|
function resolveTxSubmitterSigner({ args, ownerSigner, provider }) {
|
|
7851
8867
|
if (args.txSubmitter === undefined) {
|
|
8868
|
+
expect(
|
|
8869
|
+
typeof ownerSigner.privateKey === "string",
|
|
8870
|
+
[
|
|
8871
|
+
`Missing local account secret for wallet owner ${ownerSigner.address}.`,
|
|
8872
|
+
"Pass --tx-submitter <ACCOUNT> or import the matching local account secret.",
|
|
8873
|
+
].join(" "),
|
|
8874
|
+
);
|
|
7852
8875
|
return {
|
|
7853
8876
|
txSubmitter: ownerSigner,
|
|
7854
8877
|
source: "wallet-owner",
|
|
@@ -7883,39 +8906,11 @@ function resolveStandalonePrivateKeySource(args) {
|
|
|
7883
8906
|
));
|
|
7884
8907
|
}
|
|
7885
8908
|
|
|
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
8909
|
function prepareJoinWalletSecretForName({
|
|
7914
8910
|
args,
|
|
7915
8911
|
networkName,
|
|
7916
8912
|
walletName,
|
|
7917
8913
|
}) {
|
|
7918
|
-
const secretPath = walletSecretPath(networkName, walletName);
|
|
7919
8914
|
const { channelName } = parseWalletName(walletName);
|
|
7920
8915
|
const walletDir = walletPath(walletName, networkName);
|
|
7921
8916
|
expect(
|
|
@@ -7930,14 +8925,7 @@ function prepareJoinWalletSecretForName({
|
|
|
7930
8925
|
].join(" "),
|
|
7931
8926
|
);
|
|
7932
8927
|
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;
|
|
8928
|
+
return readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
|
|
7941
8929
|
}
|
|
7942
8930
|
|
|
7943
8931
|
function channelWorkspacePath(networkName, name) {
|
|
@@ -7986,6 +8974,24 @@ function walletSecretPath(networkName, walletName) {
|
|
|
7986
8974
|
);
|
|
7987
8975
|
}
|
|
7988
8976
|
|
|
8977
|
+
function walletViewingKeySecretPath(networkName, walletName) {
|
|
8978
|
+
return walletKeySecretPath(networkName, walletName, "viewing");
|
|
8979
|
+
}
|
|
8980
|
+
|
|
8981
|
+
function walletSpendingKeySecretPath(networkName, walletName) {
|
|
8982
|
+
return walletKeySecretPath(networkName, walletName, "spending");
|
|
8983
|
+
}
|
|
8984
|
+
|
|
8985
|
+
function walletKeySecretPath(networkName, walletName, keyKind) {
|
|
8986
|
+
return path.join(
|
|
8987
|
+
secretRoot,
|
|
8988
|
+
requireNetworkName({ network: networkName }),
|
|
8989
|
+
"wallets",
|
|
8990
|
+
slugifyPathComponent(walletName),
|
|
8991
|
+
`${keyKind}.key`,
|
|
8992
|
+
);
|
|
8993
|
+
}
|
|
8994
|
+
|
|
7989
8995
|
function resolveWalletPathCandidates(walletName) {
|
|
7990
8996
|
if (!fs.existsSync(workspaceRoot)) {
|
|
7991
8997
|
return [];
|
|
@@ -8044,9 +9050,12 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
|
|
|
8044
9050
|
network: networkEntry.name,
|
|
8045
9051
|
channelName: channelEntry.name,
|
|
8046
9052
|
walletDir,
|
|
8047
|
-
metadataPath:
|
|
8048
|
-
hasMetadata: fs.existsSync(
|
|
8049
|
-
hasEncryptedWallet:
|
|
9053
|
+
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9054
|
+
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
9055
|
+
hasEncryptedWallet: false,
|
|
9056
|
+
hasBackupMetadata: walletConfigExists(walletDir),
|
|
9057
|
+
hasViewingKey: fs.existsSync(walletViewingKeySecretPath(networkEntry.name, walletEntry.name)),
|
|
9058
|
+
hasSpendingKey: fs.existsSync(walletSpendingKeySecretPath(networkEntry.name, walletEntry.name)),
|
|
8050
9059
|
});
|
|
8051
9060
|
}
|
|
8052
9061
|
}
|
|
@@ -8074,8 +9083,8 @@ function resolveExportWalletInfo({ networkName, walletName }) {
|
|
|
8074
9083
|
network: networkName,
|
|
8075
9084
|
channelName: parseWalletName(walletName).channelName,
|
|
8076
9085
|
walletDir,
|
|
8077
|
-
metadataPath:
|
|
8078
|
-
hasMetadata: fs.existsSync(
|
|
9086
|
+
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9087
|
+
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
8079
9088
|
hasEncryptedWallet: walletConfigExists(walletDir),
|
|
8080
9089
|
};
|
|
8081
9090
|
}
|
|
@@ -8084,15 +9093,11 @@ function normalizeExportWalletInfo(walletInfo) {
|
|
|
8084
9093
|
const wallet = requireWalletName({ wallet: walletInfo.wallet });
|
|
8085
9094
|
const network = requireNetworkName({ network: walletInfo.network });
|
|
8086
9095
|
const walletDir = walletInfo.walletDir ?? walletPath(wallet, network);
|
|
8087
|
-
const metadataPath =
|
|
8088
|
-
const encryptedWalletPath = walletConfigPath(walletDir);
|
|
9096
|
+
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
8089
9097
|
const metadata = readJsonIfExists(metadataPath);
|
|
8090
9098
|
const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
|
|
8091
|
-
const walletSecret = walletSecretPath(network, wallet);
|
|
8092
9099
|
|
|
8093
|
-
expect(fs.existsSync(encryptedWalletPath), `Wallet export cannot find encrypted wallet file: ${encryptedWalletPath}.`);
|
|
8094
9100
|
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
9101
|
expect(
|
|
8097
9102
|
metadata.network === network,
|
|
8098
9103
|
`Wallet export metadata network ${metadata.network} does not match ${network}.`,
|
|
@@ -8107,18 +9112,20 @@ function normalizeExportWalletInfo(walletInfo) {
|
|
|
8107
9112
|
channelName,
|
|
8108
9113
|
wallet,
|
|
8109
9114
|
walletDir,
|
|
8110
|
-
walletSecretPath: walletSecret,
|
|
8111
9115
|
};
|
|
8112
9116
|
}
|
|
8113
9117
|
|
|
8114
|
-
function
|
|
9118
|
+
function walletBackupExportFilePaths(walletInfo) {
|
|
8115
9119
|
const walletFiles = [
|
|
8116
|
-
walletInfo.
|
|
8117
|
-
walletConfigPath(walletInfo.walletDir),
|
|
8118
|
-
walletMetadataPath(walletInfo.walletDir),
|
|
9120
|
+
walletNotesMetadataPath(walletInfo.walletDir),
|
|
8119
9121
|
];
|
|
8120
|
-
|
|
8121
|
-
|
|
9122
|
+
for (const metadataPath of [
|
|
9123
|
+
walletViewingKeyMetadataPath(walletInfo.walletDir),
|
|
9124
|
+
walletSpendingKeyMetadataPath(walletInfo.walletDir),
|
|
9125
|
+
]) {
|
|
9126
|
+
if (fs.existsSync(metadataPath)) {
|
|
9127
|
+
walletFiles.push(metadataPath);
|
|
9128
|
+
}
|
|
8122
9129
|
}
|
|
8123
9130
|
|
|
8124
9131
|
const workspaceDir = channelWorkspacePath(walletInfo.network, walletInfo.channelName);
|
|
@@ -8134,8 +9141,8 @@ function walletExportFilePaths(walletInfo, { includeNotes }) {
|
|
|
8134
9141
|
expect(
|
|
8135
9142
|
fs.existsSync(filePath),
|
|
8136
9143
|
[
|
|
8137
|
-
`wallet export
|
|
8138
|
-
"Run channel recover-workspace first
|
|
9144
|
+
`wallet export backup requires channel workspace cache file: ${filePath}.`,
|
|
9145
|
+
"Run channel recover-workspace first.",
|
|
8139
9146
|
].join(" "),
|
|
8140
9147
|
);
|
|
8141
9148
|
}
|
|
@@ -8150,14 +9157,13 @@ function archivePathForLocalCliFile(filePath) {
|
|
|
8150
9157
|
}
|
|
8151
9158
|
|
|
8152
9159
|
function validateWalletExportManifest(manifest) {
|
|
8153
|
-
expect(manifest?.format ===
|
|
9160
|
+
expect(manifest?.format === WALLET_BACKUP_EXPORT_FORMAT, "Wallet import ZIP has an unsupported format.");
|
|
8154
9161
|
expect(
|
|
8155
9162
|
Number(manifest.formatVersion) === WALLET_EXPORT_FORMAT_VERSION,
|
|
8156
9163
|
`Wallet import ZIP format version ${manifest?.formatVersion} is not supported.`,
|
|
8157
9164
|
);
|
|
8158
9165
|
expect(Array.isArray(manifest.files), "Wallet import ZIP manifest is missing files[].");
|
|
8159
9166
|
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
9167
|
expect(manifest.wallets.length > 0, "Wallet import ZIP manifest does not list any wallets.");
|
|
8162
9168
|
const uniqueFiles = new Set(manifest.files);
|
|
8163
9169
|
expect(uniqueFiles.size === manifest.files.length, "Wallet import ZIP manifest contains duplicate file paths.");
|
|
@@ -8179,8 +9185,8 @@ function validateWalletArchivePath(archivePath) {
|
|
|
8179
9185
|
expect(!path.posix.isAbsolute(archivePath), `Wallet import ZIP path must be relative: ${archivePath}.`);
|
|
8180
9186
|
expect(path.posix.normalize(archivePath) === archivePath, `Wallet import ZIP path is not normalized: ${archivePath}.`);
|
|
8181
9187
|
expect(
|
|
8182
|
-
archivePath.startsWith("
|
|
8183
|
-
`Wallet import ZIP path must start with
|
|
9188
|
+
archivePath.startsWith("workspace/"),
|
|
9189
|
+
`Wallet backup import ZIP path must start with workspace/: ${archivePath}.`,
|
|
8184
9190
|
);
|
|
8185
9191
|
}
|
|
8186
9192
|
|
|
@@ -8191,9 +9197,9 @@ function expectPathWithinRoot(targetPath, rootPath, message) {
|
|
|
8191
9197
|
|
|
8192
9198
|
function applyImportedWalletFileMode(archivePath, targetPath) {
|
|
8193
9199
|
if (
|
|
8194
|
-
archivePath.
|
|
8195
|
-
|| archivePath.endsWith("/wallet.json")
|
|
8196
|
-
|| archivePath.endsWith("/wallet.metadata.json")
|
|
9200
|
+
archivePath.endsWith("/wallet-notes.metadata.json")
|
|
9201
|
+
|| archivePath.endsWith("/wallet-viewing-key.metadata.json")
|
|
9202
|
+
|| archivePath.endsWith("/wallet-spending-key.metadata.json")
|
|
8197
9203
|
) {
|
|
8198
9204
|
protectSecretFile(targetPath, `imported wallet file ${archivePath}`);
|
|
8199
9205
|
}
|
|
@@ -8215,16 +9221,20 @@ function channelWorkspaceOperationsPath(workspaceDir) {
|
|
|
8215
9221
|
return path.join(channelDataPath(workspaceDir), "operations");
|
|
8216
9222
|
}
|
|
8217
9223
|
|
|
8218
|
-
function
|
|
8219
|
-
return path.join(walletDir, "wallet.json");
|
|
9224
|
+
function walletNotesMetadataPath(walletDir) {
|
|
9225
|
+
return path.join(walletDir, "wallet-notes.metadata.json");
|
|
8220
9226
|
}
|
|
8221
9227
|
|
|
8222
|
-
function
|
|
8223
|
-
return
|
|
9228
|
+
function walletViewingKeyMetadataPath(walletDir) {
|
|
9229
|
+
return path.join(walletDir, "wallet-viewing-key.metadata.json");
|
|
9230
|
+
}
|
|
9231
|
+
|
|
9232
|
+
function walletSpendingKeyMetadataPath(walletDir) {
|
|
9233
|
+
return path.join(walletDir, "wallet-spending-key.metadata.json");
|
|
8224
9234
|
}
|
|
8225
9235
|
|
|
8226
9236
|
function walletConfigExists(walletDir) {
|
|
8227
|
-
return fs.existsSync(
|
|
9237
|
+
return fs.existsSync(walletNotesMetadataPath(walletDir));
|
|
8228
9238
|
}
|
|
8229
9239
|
|
|
8230
9240
|
const COMMAND_ARG_SCHEMAS = Object.freeze(
|
|
@@ -8281,6 +9291,7 @@ function assertWalletSecretArgs(args, commandName, extraOptionKeys = [], accepte
|
|
|
8281
9291
|
|
|
8282
9292
|
function assertWalletChannelMoveArgs(args, commandName) {
|
|
8283
9293
|
assertWalletSecretArgs(args, commandName, ["amount"], "--wallet, --network, and --amount");
|
|
9294
|
+
assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
|
|
8284
9295
|
}
|
|
8285
9296
|
|
|
8286
9297
|
function assertInstallZkEvmArgs(args) {
|
|
@@ -8332,12 +9343,17 @@ function assertTransactionFeesArgs(args) {
|
|
|
8332
9343
|
assertAllowedCommandSchema(args, "help-transaction-fees");
|
|
8333
9344
|
}
|
|
8334
9345
|
|
|
9346
|
+
function assertInvestigatorArgs(args) {
|
|
9347
|
+
assertAllowedCommandSchema(args, "investigator");
|
|
9348
|
+
}
|
|
9349
|
+
|
|
8335
9350
|
function assertAccountImportArgs(args) {
|
|
8336
9351
|
assertAllowedCommandSchema(args, "account-import");
|
|
8337
9352
|
}
|
|
8338
9353
|
|
|
8339
9354
|
function assertMintNotesArgs(args) {
|
|
8340
9355
|
assertAllowedCommandSchema(args, "wallet-mint-notes");
|
|
9356
|
+
assertActionImpactArg(args, "wallet mint-notes");
|
|
8341
9357
|
assertTxSubmitterArg(args);
|
|
8342
9358
|
parseAmountVector(args.amounts, {
|
|
8343
9359
|
allowZeroEntries: true,
|
|
@@ -8347,12 +9363,14 @@ function assertMintNotesArgs(args) {
|
|
|
8347
9363
|
|
|
8348
9364
|
function assertRedeemNotesArgs(args) {
|
|
8349
9365
|
assertAllowedCommandSchema(args, "wallet-redeem-notes");
|
|
9366
|
+
assertActionImpactArg(args, "wallet redeem-notes");
|
|
8350
9367
|
assertTxSubmitterArg(args);
|
|
8351
9368
|
selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
|
|
8352
9369
|
}
|
|
8353
9370
|
|
|
8354
9371
|
function assertTransferNotesArgs(args) {
|
|
8355
9372
|
assertAllowedCommandSchema(args, "wallet-transfer-notes");
|
|
9373
|
+
assertActionImpactArg(args, "wallet transfer-notes");
|
|
8356
9374
|
assertTxSubmitterArg(args);
|
|
8357
9375
|
const noteIds = parseNoteIdVector(args.noteIds);
|
|
8358
9376
|
const recipients = parseRecipientVector(args.recipients);
|
|
@@ -8373,8 +9391,34 @@ function assertTxSubmitterArg(args) {
|
|
|
8373
9391
|
}
|
|
8374
9392
|
}
|
|
8375
9393
|
|
|
9394
|
+
function assertActionImpactArg(args, commandName) {
|
|
9395
|
+
if (
|
|
9396
|
+
args.acknowledgeActionImpact !== undefined
|
|
9397
|
+
&& args.acknowledgeActionImpact !== true
|
|
9398
|
+
) {
|
|
9399
|
+
throw new Error(`${commandName} option --acknowledge-action-impact does not accept a value.`);
|
|
9400
|
+
}
|
|
9401
|
+
if (args.acknowledgeActionImpact !== true && !process.stdin.isTTY) {
|
|
9402
|
+
throw new Error(`${commandName} requires --acknowledge-action-impact after reviewing the action-impact warning.`);
|
|
9403
|
+
}
|
|
9404
|
+
}
|
|
9405
|
+
|
|
8376
9406
|
function assertWalletGetNotesArgs(args) {
|
|
8377
9407
|
assertWalletSecretArgs(args, "wallet-get-notes");
|
|
9408
|
+
if (args.exportEvidence !== undefined) {
|
|
9409
|
+
requireArg(args.exportEvidence, "--export-evidence");
|
|
9410
|
+
if (args.acknowledgeFullNotePlaintextExport !== true) {
|
|
9411
|
+
throw new Error(
|
|
9412
|
+
"wallet get-notes --export-evidence requires --acknowledge-full-note-plaintext-export.",
|
|
9413
|
+
);
|
|
9414
|
+
}
|
|
9415
|
+
}
|
|
9416
|
+
if (
|
|
9417
|
+
args.acknowledgeFullNotePlaintextExport !== undefined
|
|
9418
|
+
&& args.acknowledgeFullNotePlaintextExport !== true
|
|
9419
|
+
) {
|
|
9420
|
+
throw new Error("wallet get-notes option --acknowledge-full-note-plaintext-export does not accept a value.");
|
|
9421
|
+
}
|
|
8378
9422
|
}
|
|
8379
9423
|
|
|
8380
9424
|
function assertCreateChannelArgs(args) {
|
|
@@ -8408,6 +9452,7 @@ function assertPublishWorkspaceMirrorArgs(args) {
|
|
|
8408
9452
|
|
|
8409
9453
|
function assertDepositBridgeArgs(args) {
|
|
8410
9454
|
assertAllowedCommandSchema(args, "account-deposit-bridge");
|
|
9455
|
+
assertActionImpactArg(args, "account deposit-bridge");
|
|
8411
9456
|
}
|
|
8412
9457
|
|
|
8413
9458
|
function assertAccountGetBridgeFundArgs(args) {
|
|
@@ -8427,6 +9472,7 @@ function assertRecoverWalletArgs(args) {
|
|
|
8427
9472
|
|
|
8428
9473
|
function assertJoinChannelArgs(args) {
|
|
8429
9474
|
assertAllowedCommandSchema(args, "channel-join");
|
|
9475
|
+
assertActionImpactArg(args, "channel join");
|
|
8430
9476
|
}
|
|
8431
9477
|
|
|
8432
9478
|
function assertWalletGetMetaArgs(args) {
|
|
@@ -8447,34 +9493,31 @@ function assertListLocalWalletsArgs(args) {
|
|
|
8447
9493
|
assertAllowedCommandSchema(args, "wallet-list");
|
|
8448
9494
|
}
|
|
8449
9495
|
|
|
8450
|
-
function
|
|
8451
|
-
assertAllowedCommandSchema(args, "wallet-export");
|
|
8452
|
-
|
|
8453
|
-
|
|
9496
|
+
function assertWalletExportBackupArgs(args) {
|
|
9497
|
+
assertAllowedCommandSchema(args, "wallet-export-backup");
|
|
9498
|
+
requireArg(args.output, "--output");
|
|
9499
|
+
requireNetworkName(args);
|
|
9500
|
+
requireWalletName(args);
|
|
9501
|
+
}
|
|
9502
|
+
|
|
9503
|
+
function assertWalletExportKeyArgs(args, commandName) {
|
|
9504
|
+
assertAllowedCommandSchema(args, commandName);
|
|
8454
9505
|
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
9506
|
requireNetworkName(args);
|
|
8463
9507
|
requireWalletName(args);
|
|
8464
9508
|
}
|
|
8465
9509
|
|
|
8466
|
-
function
|
|
8467
|
-
assertAllowedCommandSchema(args, "wallet-import");
|
|
9510
|
+
function assertWalletImportBackupArgs(args) {
|
|
9511
|
+
assertAllowedCommandSchema(args, "wallet-import-backup");
|
|
8468
9512
|
}
|
|
8469
9513
|
|
|
8470
|
-
function
|
|
8471
|
-
|
|
8472
|
-
throw new Error(`${commandName} option --${toKebabCase(key)} does not accept a value.`);
|
|
8473
|
-
}
|
|
9514
|
+
function assertWalletImportKeyArgs(args, commandName) {
|
|
9515
|
+
assertAllowedCommandSchema(args, commandName);
|
|
8474
9516
|
}
|
|
8475
9517
|
|
|
8476
9518
|
function assertWithdrawBridgeArgs(args) {
|
|
8477
9519
|
assertAllowedCommandSchema(args, "account-withdraw-bridge");
|
|
9520
|
+
assertActionImpactArg(args, "account withdraw-bridge");
|
|
8478
9521
|
}
|
|
8479
9522
|
|
|
8480
9523
|
function assertWalletGetChannelFundArgs(args) {
|
|
@@ -8497,14 +9540,126 @@ function createWalletOperationDir(walletName, networkName, suffix) {
|
|
|
8497
9540
|
}
|
|
8498
9541
|
|
|
8499
9542
|
function persistWallet(context) {
|
|
8500
|
-
|
|
9543
|
+
writeJson(walletNotesMetadataPath(context.walletDir), sanitizeWalletForNotesMetadata(context.wallet));
|
|
9544
|
+
if (context.wallet?.l2PrivateKey || context.wallet?.l2PublicKey || context.wallet?.l2Address) {
|
|
9545
|
+
writeJson(walletSpendingKeyMetadataPath(context.walletDir), buildWalletSpendingKeyMetadata(context.wallet));
|
|
9546
|
+
}
|
|
9547
|
+
if (context.wallet?.noteReceivePubKeyX || context.wallet?.noteReceivePubKeyYParity !== undefined) {
|
|
9548
|
+
writeJson(walletViewingKeyMetadataPath(context.walletDir), buildWalletViewingKeyMetadata(context.wallet));
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
|
|
9552
|
+
function persistWalletKeys(context) {
|
|
9553
|
+
if (context.wallet?.l2PrivateKey) {
|
|
9554
|
+
writeSecretFile(
|
|
9555
|
+
walletSpendingKeySecretPath(context.wallet.network, context.walletName),
|
|
9556
|
+
JSON.stringify({
|
|
9557
|
+
format: WALLET_KEY_EXPORT_FORMAT,
|
|
9558
|
+
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
9559
|
+
keyKind: "spending",
|
|
9560
|
+
metadata: buildWalletSpendingKeyMetadata(context.wallet),
|
|
9561
|
+
privateKey: normalizePrivateKey(context.wallet.l2PrivateKey),
|
|
9562
|
+
}, null, 2),
|
|
9563
|
+
);
|
|
9564
|
+
}
|
|
9565
|
+
if (context.wallet?.noteReceivePrivateKey) {
|
|
9566
|
+
writeSecretFile(
|
|
9567
|
+
walletViewingKeySecretPath(context.wallet.network, context.walletName),
|
|
9568
|
+
JSON.stringify({
|
|
9569
|
+
format: WALLET_KEY_EXPORT_FORMAT,
|
|
9570
|
+
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
9571
|
+
keyKind: "viewing",
|
|
9572
|
+
metadata: buildWalletViewingKeyMetadata(context.wallet),
|
|
9573
|
+
privateKey: normalizePrivateKey(context.wallet.noteReceivePrivateKey),
|
|
9574
|
+
}, null, 2),
|
|
9575
|
+
);
|
|
9576
|
+
}
|
|
8501
9577
|
}
|
|
8502
9578
|
|
|
8503
9579
|
function persistWalletMetadata(context) {
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
9580
|
+
persistWallet(context);
|
|
9581
|
+
}
|
|
9582
|
+
|
|
9583
|
+
function sanitizeWalletForNotesMetadata(wallet) {
|
|
9584
|
+
const normalized = normalizeWallet({
|
|
9585
|
+
...wallet,
|
|
9586
|
+
l2PrivateKey: null,
|
|
9587
|
+
noteReceivePrivateKey: null,
|
|
9588
|
+
});
|
|
9589
|
+
const { l2PrivateKey: _l2PrivateKey, noteReceivePrivateKey: _noteReceivePrivateKey, ...publicWallet } = normalized;
|
|
9590
|
+
return {
|
|
9591
|
+
...publicWallet,
|
|
9592
|
+
notes: {
|
|
9593
|
+
unused: sanitizeTrackedNoteMap(normalized.notes.unused),
|
|
9594
|
+
spent: sanitizeTrackedNoteMap(normalized.notes.spent),
|
|
9595
|
+
unusedOrder: normalized.notes.unusedOrder,
|
|
9596
|
+
unusedBalance: null,
|
|
9597
|
+
},
|
|
9598
|
+
};
|
|
9599
|
+
}
|
|
9600
|
+
|
|
9601
|
+
function sanitizeTrackedNoteMap(notes) {
|
|
9602
|
+
return Object.fromEntries(Object.entries(notes ?? {}).map(([key, note]) => [key, sanitizeTrackedNoteForPersistence(note)]));
|
|
9603
|
+
}
|
|
9604
|
+
|
|
9605
|
+
function sanitizeTrackedNoteForPersistence(note) {
|
|
9606
|
+
const normalized = normalizeTrackedNote(note);
|
|
9607
|
+
return {
|
|
9608
|
+
commitment: normalized.commitment,
|
|
9609
|
+
nullifier: normalized.nullifier,
|
|
9610
|
+
encryptedNoteValue: normalized.encryptedNoteValue,
|
|
9611
|
+
status: normalized.status,
|
|
9612
|
+
sourceFunction: normalized.sourceFunction,
|
|
9613
|
+
sourceTxHash: normalized.sourceTxHash,
|
|
9614
|
+
createdAtTxHash: normalized.createdAtTxHash,
|
|
9615
|
+
createdAtBlockNumber: normalized.createdAtBlockNumber,
|
|
9616
|
+
createdAtLogIndex: normalized.createdAtLogIndex,
|
|
9617
|
+
createdByFunction: normalized.createdByFunction,
|
|
9618
|
+
createdOutputIndex: normalized.createdOutputIndex,
|
|
9619
|
+
spentAtTxHash: normalized.spentAtTxHash,
|
|
9620
|
+
spentAtBlockNumber: normalized.spentAtBlockNumber,
|
|
9621
|
+
spentByFunction: normalized.spentByFunction,
|
|
9622
|
+
spentInputIndex: normalized.spentInputIndex,
|
|
9623
|
+
counterpartyL2Address: normalized.counterpartyL2Address,
|
|
9624
|
+
counterpartyDirection: normalized.counterpartyDirection,
|
|
9625
|
+
counterpartyConfidence: normalized.counterpartyConfidence,
|
|
9626
|
+
bridgeCommitmentKey: normalized.bridgeCommitmentKey,
|
|
9627
|
+
bridgeNullifierKey: normalized.bridgeNullifierKey,
|
|
9628
|
+
};
|
|
9629
|
+
}
|
|
9630
|
+
|
|
9631
|
+
function buildWalletSpendingKeyMetadata(wallet) {
|
|
9632
|
+
return normalizeCliOutput({
|
|
9633
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
9634
|
+
network: wallet.network,
|
|
9635
|
+
wallet: wallet.name,
|
|
9636
|
+
channelName: wallet.channelName,
|
|
9637
|
+
channelId: wallet.channelId,
|
|
9638
|
+
l1Address: wallet.l1Address,
|
|
9639
|
+
l2Address: wallet.l2Address,
|
|
9640
|
+
l2PublicKey: wallet.l2PublicKey,
|
|
9641
|
+
l2DerivationMode: wallet.l2DerivationMode,
|
|
9642
|
+
l2DerivationChannelName: wallet.l2DerivationChannelName,
|
|
9643
|
+
l2StorageKey: wallet.l2StorageKey,
|
|
9644
|
+
leafIndex: wallet.leafIndex,
|
|
9645
|
+
});
|
|
9646
|
+
}
|
|
9647
|
+
|
|
9648
|
+
function buildWalletViewingKeyMetadata(wallet) {
|
|
9649
|
+
return normalizeCliOutput({
|
|
9650
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
9651
|
+
network: wallet.network,
|
|
9652
|
+
wallet: wallet.name,
|
|
9653
|
+
channelName: wallet.channelName,
|
|
9654
|
+
channelId: wallet.channelId,
|
|
9655
|
+
l1Address: wallet.l1Address,
|
|
9656
|
+
l2Address: wallet.l2Address,
|
|
9657
|
+
noteReceiveDerivationVersion: wallet.noteReceiveDerivationVersion,
|
|
9658
|
+
noteReceiveTypedDataMethod: wallet.noteReceiveTypedDataMethod,
|
|
9659
|
+
noteReceivePubKey: {
|
|
9660
|
+
x: wallet.noteReceivePubKeyX,
|
|
9661
|
+
yParity: wallet.noteReceivePubKeyYParity,
|
|
9662
|
+
},
|
|
8508
9663
|
});
|
|
8509
9664
|
}
|
|
8510
9665
|
|
|
@@ -8524,10 +9679,10 @@ Secret source options:
|
|
|
8524
9679
|
A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
|
|
8525
9680
|
Create one before joining a channel, for example:
|
|
8526
9681
|
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
|
|
9682
|
+
private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt --acknowledge-action-impact
|
|
8528
9683
|
Bridge-facing commands accept optional --rpc-url. When provided, it is saved to
|
|
8529
9684
|
~/tokamak-private-channels/secrets/<network>/.env as RPC_URL. When omitted, the CLI reads RPC_URL from that file.
|
|
8530
|
-
Wallet commands use
|
|
9685
|
+
Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
|
|
8531
9686
|
Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
|
|
8532
9687
|
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
8533
9688
|
|
|
@@ -8861,23 +10016,6 @@ function getUsableWorkspaceRecoveryIndex({
|
|
|
8861
10016
|
};
|
|
8862
10017
|
}
|
|
8863
10018
|
|
|
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
10019
|
function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
8882
10020
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8883
10021
|
const salt = randomBytes(16);
|
|
@@ -8898,23 +10036,6 @@ function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
|
8898
10036
|
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
8899
10037
|
}
|
|
8900
10038
|
|
|
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
10039
|
function deriveWalletEncryptionKey(walletSecret, salt) {
|
|
8919
10040
|
return scryptSync(String(walletSecret), salt, 32);
|
|
8920
10041
|
}
|
|
@@ -8963,6 +10084,7 @@ function loadWalletCommandRuntime(args) {
|
|
|
8963
10084
|
|
|
8964
10085
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
8965
10086
|
guide: printGuideHumanResult,
|
|
10087
|
+
investigator: printInvestigatorHumanResult,
|
|
8966
10088
|
"transaction-fees": printTransactionFeesHumanResult,
|
|
8967
10089
|
update: printUpdateHumanResult,
|
|
8968
10090
|
});
|
|
@@ -9022,6 +10144,29 @@ function printGuideHumanResult(guide) {
|
|
|
9022
10144
|
console.log(lines.join("\n"));
|
|
9023
10145
|
}
|
|
9024
10146
|
|
|
10147
|
+
function printInvestigatorHumanResult(result) {
|
|
10148
|
+
const lines = [
|
|
10149
|
+
"Private-State Evidence Investigator",
|
|
10150
|
+
`HTML path: ${formatHumanValue(result.htmlPath)}`,
|
|
10151
|
+
`File URL: ${formatHumanValue(result.fileUrl)}`,
|
|
10152
|
+
`Browser opened: ${result.browserOpened ? "yes" : "no"}`,
|
|
10153
|
+
];
|
|
10154
|
+
if (!result.browserOpened) {
|
|
10155
|
+
lines.push(
|
|
10156
|
+
`Open command: ${formatHumanValue(result.browserOpenCommand)}`,
|
|
10157
|
+
`Open error: ${formatHumanValue(result.browserOpenError ?? "none")}`,
|
|
10158
|
+
);
|
|
10159
|
+
}
|
|
10160
|
+
if (Array.isArray(result.nextSteps) && result.nextSteps.length > 0) {
|
|
10161
|
+
lines.push(
|
|
10162
|
+
"",
|
|
10163
|
+
"Next Steps",
|
|
10164
|
+
...result.nextSteps.map((step) => `- ${step}`),
|
|
10165
|
+
);
|
|
10166
|
+
}
|
|
10167
|
+
console.log(lines.join("\n"));
|
|
10168
|
+
}
|
|
10169
|
+
|
|
9025
10170
|
function printTransactionFeesHumanResult(report) {
|
|
9026
10171
|
const lines = [
|
|
9027
10172
|
"Transaction Fees",
|
|
@@ -9356,17 +10501,6 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
9356
10501
|
hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
|
|
9357
10502
|
}
|
|
9358
10503
|
|
|
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
10504
|
if (
|
|
9371
10505
|
message.startsWith("Missing --account:")
|
|
9372
10506
|
|| message.includes("Missing --account.")
|
|
@@ -9384,7 +10518,7 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
9384
10518
|
}
|
|
9385
10519
|
|
|
9386
10520
|
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
|
|
10521
|
+
hints.push(`private-state-cli channel join --channel-name ${channelName} --network ${networkName} --account ${accountName} --wallet-secret-path <PATH> --acknowledge-action-impact`);
|
|
9388
10522
|
hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
|
|
9389
10523
|
}
|
|
9390
10524
|
|