@tokamak-private-dapps/private-state-cli 1.2.0 → 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 +43 -2
- package/README.md +183 -54
- 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 +1629 -514
|
@@ -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",
|
|
@@ -207,8 +210,7 @@ const ZERO_TOPIC = normalizeBytes32Hex(ethers.ZeroHash);
|
|
|
207
210
|
const DEFAULT_LOG_CHUNK_SIZE = 2000;
|
|
208
211
|
const DEFAULT_LOG_REQUESTS_PER_SECOND = 5;
|
|
209
212
|
const LOG_REQUEST_INTERVAL_MS = Math.ceil(1000 / DEFAULT_LOG_REQUESTS_PER_SECOND);
|
|
210
|
-
const
|
|
211
|
-
const AUTO_RECOVERY_LOG_REQUEST_BUDGET = DEFAULT_LOG_REQUESTS_PER_SECOND * AUTO_RECOVERY_TIME_BUDGET_SECONDS;
|
|
213
|
+
const AUTO_RECOVERY_BLOCK_BUDGET = 7200;
|
|
212
214
|
let lastLogRequestStartedAtMs = 0;
|
|
213
215
|
|
|
214
216
|
function printImmutableChannelPolicyWarning({
|
|
@@ -248,6 +250,206 @@ function printImmutableChannelPolicyWarning({
|
|
|
248
250
|
console.error(details.join("\n"));
|
|
249
251
|
}
|
|
250
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
|
+
|
|
251
453
|
function normalizeDAppPolicySnapshot({
|
|
252
454
|
dappId,
|
|
253
455
|
metadataDigest,
|
|
@@ -387,6 +589,12 @@ async function main() {
|
|
|
387
589
|
return;
|
|
388
590
|
}
|
|
389
591
|
|
|
592
|
+
if (args.command === "investigator") {
|
|
593
|
+
assertInvestigatorArgs(args);
|
|
594
|
+
handleInvestigator();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
390
598
|
if (args.command === "account-get-l1-address") {
|
|
391
599
|
assertAccountGetL1AddressArgs(args);
|
|
392
600
|
handleAccountGetL1Address({ args });
|
|
@@ -405,15 +613,39 @@ async function main() {
|
|
|
405
613
|
return;
|
|
406
614
|
}
|
|
407
615
|
|
|
408
|
-
if (args.command === "wallet-export") {
|
|
409
|
-
|
|
410
|
-
|
|
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" });
|
|
411
631
|
return;
|
|
412
632
|
}
|
|
413
633
|
|
|
414
|
-
if (args.command === "wallet-import") {
|
|
415
|
-
|
|
416
|
-
|
|
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" });
|
|
417
649
|
return;
|
|
418
650
|
}
|
|
419
651
|
|
|
@@ -2162,6 +2394,11 @@ async function handleDepositBridge({ args, network, provider }) {
|
|
|
2162
2394
|
const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId: network.chainId });
|
|
2163
2395
|
const amountInput = requireArg(args.amount, "--amount");
|
|
2164
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
|
+
});
|
|
2165
2402
|
const bridgeTokenVault = new Contract(
|
|
2166
2403
|
bridgeVaultContext.bridgeTokenVaultAddress,
|
|
2167
2404
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
@@ -2224,10 +2461,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2224
2461
|
const channelName = requireArg(args.channelName, "--channel-name");
|
|
2225
2462
|
const signer = requireL1Signer(args, provider);
|
|
2226
2463
|
const walletName = walletNameForChannelAndAddress(channelName, signer.address);
|
|
2227
|
-
const walletSecret = resolveWalletSecretForName({
|
|
2228
|
-
networkName: network.name,
|
|
2229
|
-
walletName,
|
|
2230
|
-
});
|
|
2231
2464
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
2232
2465
|
const initialized = await syncChannelWorkspace({
|
|
2233
2466
|
workspaceName: channelName,
|
|
@@ -2261,11 +2494,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2261
2494
|
provider,
|
|
2262
2495
|
),
|
|
2263
2496
|
};
|
|
2264
|
-
const l2Identity = await deriveParticipantIdentityFromSigner({
|
|
2265
|
-
channelName,
|
|
2266
|
-
walletSecret,
|
|
2267
|
-
signer,
|
|
2268
|
-
});
|
|
2269
2497
|
const noteReceiveKeyMaterial = await deriveNoteReceiveKeyMaterial({
|
|
2270
2498
|
signer,
|
|
2271
2499
|
chainId: network.chainId,
|
|
@@ -2273,8 +2501,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2273
2501
|
channelName,
|
|
2274
2502
|
account: signer.address,
|
|
2275
2503
|
});
|
|
2276
|
-
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
2277
|
-
const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
|
|
2278
2504
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2279
2505
|
|
|
2280
2506
|
if (!registration.exists) {
|
|
@@ -2286,15 +2512,13 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2286
2512
|
wallet: walletName,
|
|
2287
2513
|
removedWalletDir: cleanup.removedWalletDir ? cleanup.walletDir : null,
|
|
2288
2514
|
removedWalletSecretFile: cleanup.removedWalletSecret ? cleanup.walletSecretFile : null,
|
|
2289
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2290
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2291
2515
|
workspace: context.workspaceName,
|
|
2292
2516
|
channelName: context.workspace.channelName,
|
|
2293
2517
|
channelId: context.workspace.channelId,
|
|
2294
2518
|
l1Address: signer.address,
|
|
2295
|
-
l2Address:
|
|
2296
|
-
l2StorageKey:
|
|
2297
|
-
leafIndex:
|
|
2519
|
+
l2Address: null,
|
|
2520
|
+
l2StorageKey: null,
|
|
2521
|
+
leafIndex: null,
|
|
2298
2522
|
reason: "The local wallet existed, but the L1 address is no longer registered in the channel.",
|
|
2299
2523
|
nextAction: buildRecoverWalletRemovedNextAction({
|
|
2300
2524
|
channelName,
|
|
@@ -2312,19 +2536,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2312
2536
|
),
|
|
2313
2537
|
);
|
|
2314
2538
|
}
|
|
2315
|
-
expect(
|
|
2316
|
-
ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
|
|
2317
|
-
"The existing channel registration L2 address does not match the derived L2 address.",
|
|
2318
|
-
);
|
|
2319
|
-
expect(
|
|
2320
|
-
ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
2321
|
-
=== ethers.toBigInt(normalizeBytes32Hex(storageKey)),
|
|
2322
|
-
"The existing channel registration key does not match the derived channelTokenVault key.",
|
|
2323
|
-
);
|
|
2324
|
-
expect(
|
|
2325
|
-
ethers.toBigInt(registration.leafIndex) === ethers.toBigInt(leafIndex),
|
|
2326
|
-
"The existing channel registration leaf index does not match the derived leaf index.",
|
|
2327
|
-
);
|
|
2328
2539
|
expect(
|
|
2329
2540
|
ethers.toBigInt(normalizeBytes32Hex(registration.noteReceivePubKey.x))
|
|
2330
2541
|
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
@@ -2334,21 +2545,21 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2334
2545
|
Number(registration.noteReceivePubKey.yParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2335
2546
|
"The existing note-receive public key parity does not match the derived note-receive public key.",
|
|
2336
2547
|
);
|
|
2548
|
+
const l2Identity = {
|
|
2549
|
+
l2PrivateKey: null,
|
|
2550
|
+
l2PublicKey: null,
|
|
2551
|
+
l2Address: getAddress(registration.l2Address),
|
|
2552
|
+
};
|
|
2553
|
+
const storageKey = normalizeBytes32Hex(registration.channelTokenVaultKey);
|
|
2337
2554
|
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
signerPrivateKey: signer.privateKey,
|
|
2343
|
-
l2Identity,
|
|
2344
|
-
storageKey,
|
|
2345
|
-
leafIndex,
|
|
2346
|
-
rpcUrl,
|
|
2347
|
-
channelContext: context,
|
|
2348
|
-
noteReceiveKeyMaterial,
|
|
2349
|
-
});
|
|
2555
|
+
const walletDir = walletPath(walletName, context.workspace.network);
|
|
2556
|
+
const existingWallet = walletConfigExists(walletDir)
|
|
2557
|
+
? loadWallet(walletName, context.workspace.network)
|
|
2558
|
+
: null;
|
|
2350
2559
|
|
|
2351
2560
|
if (existingWallet) {
|
|
2561
|
+
existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2562
|
+
persistWalletKeys(existingWallet);
|
|
2352
2563
|
const { recoveredDeliveryState } = await recoverWalletReceivedNotes({
|
|
2353
2564
|
walletContext: existingWallet,
|
|
2354
2565
|
context,
|
|
@@ -2363,8 +2574,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2363
2574
|
status: "already-recovered",
|
|
2364
2575
|
wallet: walletName,
|
|
2365
2576
|
walletDir: existingWallet.walletDir,
|
|
2366
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2367
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2368
2577
|
workspace: context.workspaceName,
|
|
2369
2578
|
channelName: context.workspace.channelName,
|
|
2370
2579
|
channelId: context.workspace.channelId,
|
|
@@ -2388,7 +2597,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2388
2597
|
signerAddress: signer.address,
|
|
2389
2598
|
signerPrivateKey: signer.privateKey,
|
|
2390
2599
|
l2Identity,
|
|
2391
|
-
walletSecret,
|
|
2600
|
+
walletSecret: noteReceiveKeyMaterial.privateKey,
|
|
2392
2601
|
storageKey,
|
|
2393
2602
|
leafIndex: registration.leafIndex,
|
|
2394
2603
|
noteReceiveKeyMaterial,
|
|
@@ -2412,8 +2621,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2412
2621
|
status: "recovered",
|
|
2413
2622
|
wallet: walletName,
|
|
2414
2623
|
walletDir: walletContext.walletDir,
|
|
2415
|
-
walletSecretSource: resolvedWalletSecretSource(args),
|
|
2416
|
-
walletSecretFile: resolvedWalletSecretFile(network.name, walletName),
|
|
2417
2624
|
workspace: context.workspaceName,
|
|
2418
2625
|
channelName: context.workspace.channelName,
|
|
2419
2626
|
channelId: context.workspace.channelId,
|
|
@@ -2429,135 +2636,6 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2429
2636
|
});
|
|
2430
2637
|
}
|
|
2431
2638
|
|
|
2432
|
-
function tryLoadRecoverableWallet({
|
|
2433
|
-
walletName,
|
|
2434
|
-
walletSecret,
|
|
2435
|
-
signerAddress,
|
|
2436
|
-
signerPrivateKey,
|
|
2437
|
-
l2Identity,
|
|
2438
|
-
storageKey,
|
|
2439
|
-
leafIndex,
|
|
2440
|
-
rpcUrl,
|
|
2441
|
-
channelContext,
|
|
2442
|
-
noteReceiveKeyMaterial,
|
|
2443
|
-
}) {
|
|
2444
|
-
const walletDir = walletPath(walletName, channelContext.workspace.network);
|
|
2445
|
-
if (!walletConfigExists(walletDir)) {
|
|
2446
|
-
return null;
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
try {
|
|
2450
|
-
const walletMetadata = loadWalletMetadata(walletName, channelContext.workspace.network);
|
|
2451
|
-
const walletContext = loadWallet(walletName, walletSecret, channelContext.workspace.network);
|
|
2452
|
-
assertWalletMatchesMetadata(walletContext, walletMetadata);
|
|
2453
|
-
assertExistingRecoverableWallet({
|
|
2454
|
-
walletContext,
|
|
2455
|
-
walletMetadata,
|
|
2456
|
-
signerAddress,
|
|
2457
|
-
signerPrivateKey,
|
|
2458
|
-
l2Identity,
|
|
2459
|
-
storageKey,
|
|
2460
|
-
leafIndex,
|
|
2461
|
-
rpcUrl,
|
|
2462
|
-
channelContext,
|
|
2463
|
-
noteReceiveKeyMaterial,
|
|
2464
|
-
});
|
|
2465
|
-
return walletContext;
|
|
2466
|
-
} catch {
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
function assertExistingRecoverableWallet({
|
|
2472
|
-
walletContext,
|
|
2473
|
-
walletMetadata,
|
|
2474
|
-
signerAddress,
|
|
2475
|
-
signerPrivateKey,
|
|
2476
|
-
l2Identity,
|
|
2477
|
-
storageKey,
|
|
2478
|
-
leafIndex,
|
|
2479
|
-
rpcUrl,
|
|
2480
|
-
channelContext,
|
|
2481
|
-
noteReceiveKeyMaterial,
|
|
2482
|
-
}) {
|
|
2483
|
-
const wallet = walletContext.wallet;
|
|
2484
|
-
expect(
|
|
2485
|
-
walletMetadata.network === channelContext.workspace.network,
|
|
2486
|
-
`Wallet ${walletContext.walletName} metadata network does not match the requested network.`,
|
|
2487
|
-
);
|
|
2488
|
-
expect(
|
|
2489
|
-
walletMetadata.channelName === channelContext.workspace.channelName,
|
|
2490
|
-
`Wallet ${walletContext.walletName} metadata channel does not match the requested channel.`,
|
|
2491
|
-
);
|
|
2492
|
-
expect(
|
|
2493
|
-
walletMetadata.rpcUrl === rpcUrl,
|
|
2494
|
-
`Wallet ${walletContext.walletName} metadata rpcUrl does not match the requested runtime RPC URL.`,
|
|
2495
|
-
);
|
|
2496
|
-
expect(
|
|
2497
|
-
normalizePrivateKey(wallet.l1PrivateKey) === normalizePrivateKey(signerPrivateKey),
|
|
2498
|
-
`Wallet ${walletContext.walletName} does not decrypt to the requested L1 private key.`,
|
|
2499
|
-
);
|
|
2500
|
-
expect(
|
|
2501
|
-
ethers.toBigInt(getAddress(wallet.l1Address)) === ethers.toBigInt(getAddress(signerAddress)),
|
|
2502
|
-
`Wallet ${walletContext.walletName} L1 address does not match the requested signer.`,
|
|
2503
|
-
);
|
|
2504
|
-
expect(
|
|
2505
|
-
ethers.toBigInt(getAddress(wallet.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address)),
|
|
2506
|
-
`Wallet ${walletContext.walletName} L2 address does not match the derived channel identity.`,
|
|
2507
|
-
);
|
|
2508
|
-
expect(
|
|
2509
|
-
ethers.toBigInt(normalizeBytes32Hex(wallet.l2StorageKey))
|
|
2510
|
-
=== ethers.toBigInt(normalizeBytes32Hex(storageKey)),
|
|
2511
|
-
`Wallet ${walletContext.walletName} storage key does not match the derived registration key.`,
|
|
2512
|
-
);
|
|
2513
|
-
expect(
|
|
2514
|
-
ethers.toBigInt(wallet.leafIndex) === ethers.toBigInt(leafIndex),
|
|
2515
|
-
`Wallet ${walletContext.walletName} leaf index does not match the derived registration leaf index.`,
|
|
2516
|
-
);
|
|
2517
|
-
expect(
|
|
2518
|
-
ethers.toBigInt(wallet.channelId) === ethers.toBigInt(channelContext.workspace.channelId),
|
|
2519
|
-
`Wallet ${walletContext.walletName} channel ID does not match the requested channel.`,
|
|
2520
|
-
);
|
|
2521
|
-
expect(
|
|
2522
|
-
wallet.channelName === channelContext.workspace.channelName,
|
|
2523
|
-
`Wallet ${walletContext.walletName} channel name does not match the requested channel.`,
|
|
2524
|
-
);
|
|
2525
|
-
expect(
|
|
2526
|
-
wallet.network === channelContext.workspace.network,
|
|
2527
|
-
`Wallet ${walletContext.walletName} network does not match the requested network.`,
|
|
2528
|
-
);
|
|
2529
|
-
expect(
|
|
2530
|
-
wallet.rpcUrl === rpcUrl,
|
|
2531
|
-
`Wallet ${walletContext.walletName} rpcUrl does not match the requested runtime RPC URL.`,
|
|
2532
|
-
);
|
|
2533
|
-
expect(
|
|
2534
|
-
ethers.toBigInt(getAddress(wallet.channelManager)) === ethers.toBigInt(getAddress(channelContext.workspace.channelManager)),
|
|
2535
|
-
`Wallet ${walletContext.walletName} channel manager does not match the recovered workspace.`,
|
|
2536
|
-
);
|
|
2537
|
-
expect(
|
|
2538
|
-
ethers.toBigInt(getAddress(wallet.bridgeTokenVault)) === ethers.toBigInt(getAddress(channelContext.workspace.bridgeTokenVault)),
|
|
2539
|
-
`Wallet ${walletContext.walletName} bridge token vault does not match the recovered workspace.`,
|
|
2540
|
-
);
|
|
2541
|
-
expect(
|
|
2542
|
-
ethers.toBigInt(getAddress(wallet.controller)) === ethers.toBigInt(getAddress(channelContext.workspace.controller)),
|
|
2543
|
-
`Wallet ${walletContext.walletName} controller does not match the recovered workspace.`,
|
|
2544
|
-
);
|
|
2545
|
-
expect(
|
|
2546
|
-
ethers.toBigInt(getAddress(wallet.l2AccountingVault))
|
|
2547
|
-
=== ethers.toBigInt(getAddress(channelContext.workspace.l2AccountingVault)),
|
|
2548
|
-
`Wallet ${walletContext.walletName} L2 accounting vault does not match the recovered workspace.`,
|
|
2549
|
-
);
|
|
2550
|
-
expect(
|
|
2551
|
-
ethers.toBigInt(normalizeBytes32Hex(wallet.noteReceivePubKeyX))
|
|
2552
|
-
=== ethers.toBigInt(normalizeBytes32Hex(noteReceiveKeyMaterial.noteReceivePubKey.x)),
|
|
2553
|
-
`Wallet ${walletContext.walletName} note-receive public key X does not match the derived key.`,
|
|
2554
|
-
);
|
|
2555
|
-
expect(
|
|
2556
|
-
Number(wallet.noteReceivePubKeyYParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2557
|
-
`Wallet ${walletContext.walletName} note-receive public key parity does not match the derived key.`,
|
|
2558
|
-
);
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
2639
|
function removeLocalWalletArtifacts(walletName, networkName) {
|
|
2562
2640
|
const walletDir = walletPath(walletName, networkName);
|
|
2563
2641
|
const walletSecretFile = walletSecretPath(networkName, walletName);
|
|
@@ -2581,7 +2659,7 @@ function removeLocalWalletArtifacts(walletName, networkName) {
|
|
|
2581
2659
|
|
|
2582
2660
|
function buildRecoverWalletRemovedNextAction({ channelName, networkName, accountName }) {
|
|
2583
2661
|
const account = accountName ? String(accountName) : "<ACCOUNT>";
|
|
2584
|
-
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`;
|
|
2585
2663
|
}
|
|
2586
2664
|
|
|
2587
2665
|
async function handleInstallZkEvm({ args }) {
|
|
@@ -2881,6 +2959,68 @@ async function handleDoctor({ args }) {
|
|
|
2881
2959
|
}
|
|
2882
2960
|
}
|
|
2883
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
|
+
|
|
2884
3024
|
async function handleTransactionFees({ network, provider, rpcUrl }) {
|
|
2885
3025
|
const feeAsset = loadTransactionFeeAsset();
|
|
2886
3026
|
const feeData = await provider.getFeeData();
|
|
@@ -3080,24 +3220,19 @@ function handleListLocalWallets({ args }) {
|
|
|
3080
3220
|
});
|
|
3081
3221
|
}
|
|
3082
3222
|
|
|
3083
|
-
function
|
|
3223
|
+
function handleWalletExportBackup({ args }) {
|
|
3084
3224
|
const outputPath = path.resolve(String(requireArg(args.output, "--output")));
|
|
3085
3225
|
expect(!fs.existsSync(outputPath), `Export output already exists: ${outputPath}.`);
|
|
3086
3226
|
ensureDir(path.dirname(outputPath));
|
|
3087
3227
|
|
|
3088
|
-
const
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
networkName: requireNetworkName(args),
|
|
3093
|
-
walletName: requireWalletName(args),
|
|
3094
|
-
})];
|
|
3228
|
+
const wallets = [resolveExportWalletInfo({
|
|
3229
|
+
networkName: requireNetworkName(args),
|
|
3230
|
+
walletName: requireWalletName(args),
|
|
3231
|
+
})];
|
|
3095
3232
|
|
|
3096
3233
|
expect(
|
|
3097
3234
|
wallets.length > 0,
|
|
3098
|
-
|
|
3099
|
-
? "No local mainnet wallets are available to export."
|
|
3100
|
-
: "No local wallet is available to export.",
|
|
3235
|
+
"No local wallet is available to export.",
|
|
3101
3236
|
);
|
|
3102
3237
|
|
|
3103
3238
|
const archive = new AdmZip();
|
|
@@ -3110,7 +3245,7 @@ function handleWalletExport({ args }) {
|
|
|
3110
3245
|
channelName: normalized.channelName,
|
|
3111
3246
|
wallet: normalized.wallet,
|
|
3112
3247
|
});
|
|
3113
|
-
for (const filePath of
|
|
3248
|
+
for (const filePath of walletBackupExportFilePaths(normalized)) {
|
|
3114
3249
|
const archivePath = archivePathForLocalCliFile(filePath);
|
|
3115
3250
|
if (!files.has(archivePath)) {
|
|
3116
3251
|
files.set(archivePath, filePath);
|
|
@@ -3119,44 +3254,65 @@ function handleWalletExport({ args }) {
|
|
|
3119
3254
|
}
|
|
3120
3255
|
|
|
3121
3256
|
const manifest = {
|
|
3122
|
-
format:
|
|
3257
|
+
format: WALLET_BACKUP_EXPORT_FORMAT,
|
|
3123
3258
|
formatVersion: WALLET_EXPORT_FORMAT_VERSION,
|
|
3124
3259
|
createdAt: new Date().toISOString(),
|
|
3125
3260
|
cliPackage: PRIVATE_STATE_CLI_PACKAGE_NAME,
|
|
3126
3261
|
cliVersion: privateStateCliPackageJson.version,
|
|
3127
|
-
exportMode:
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
]
|
|
3133
|
-
: [
|
|
3134
|
-
"Includes wallet identity, encrypted wallet state, metadata, and wallet-local secret only.",
|
|
3135
|
-
"Run channel recover-workspace after import before wallet commands need channel state.",
|
|
3136
|
-
],
|
|
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
|
+
],
|
|
3137
3267
|
wallets: exportedWallets,
|
|
3138
3268
|
files: [...files.keys()].sort(),
|
|
3139
3269
|
};
|
|
3140
3270
|
|
|
3141
3271
|
archive.addFile("manifest.json", Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"));
|
|
3142
3272
|
for (const archivePath of manifest.files) {
|
|
3143
|
-
|
|
3273
|
+
const filePath = files.get(archivePath);
|
|
3274
|
+
validateBackupExportFile(filePath);
|
|
3275
|
+
archive.addFile(archivePath, fs.readFileSync(filePath));
|
|
3144
3276
|
}
|
|
3145
3277
|
archive.writeZip(outputPath);
|
|
3146
3278
|
protectSecretFile(outputPath, "wallet export ZIP");
|
|
3147
3279
|
|
|
3148
3280
|
printJson({
|
|
3149
|
-
action: "wallet export",
|
|
3281
|
+
action: "wallet export backup",
|
|
3150
3282
|
output: outputPath,
|
|
3151
3283
|
exportMode: manifest.exportMode,
|
|
3152
|
-
includeNotes,
|
|
3153
3284
|
walletCount: exportedWallets.length,
|
|
3154
3285
|
fileCount: manifest.files.length,
|
|
3155
3286
|
wallets: exportedWallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
|
|
3156
3287
|
});
|
|
3157
3288
|
}
|
|
3158
3289
|
|
|
3159
|
-
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 }) {
|
|
3160
3316
|
const inputPath = path.resolve(String(requireArg(args.input, "--input")));
|
|
3161
3317
|
expect(fs.existsSync(inputPath), `Import ZIP does not exist: ${inputPath}.`);
|
|
3162
3318
|
|
|
@@ -3192,16 +3348,51 @@ function handleWalletImport({ args }) {
|
|
|
3192
3348
|
commitWalletImportFiles({ targetRoot, plannedWrites });
|
|
3193
3349
|
|
|
3194
3350
|
printJson({
|
|
3195
|
-
action: "wallet import",
|
|
3351
|
+
action: "wallet import backup",
|
|
3196
3352
|
input: inputPath,
|
|
3197
3353
|
exportMode: manifest.exportMode,
|
|
3198
|
-
includeNotes: Boolean(manifest.includeNotes),
|
|
3199
3354
|
walletCount: manifest.wallets.length,
|
|
3200
3355
|
fileCount: plannedWrites.length,
|
|
3201
3356
|
wallets: manifest.wallets.map(({ network, channelName, wallet }) => ({ network, channelName, wallet })),
|
|
3202
|
-
nextStep:
|
|
3203
|
-
|
|
3204
|
-
|
|
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,
|
|
3205
3396
|
});
|
|
3206
3397
|
}
|
|
3207
3398
|
|
|
@@ -3218,6 +3409,47 @@ function readWalletImportArchive(inputPath) {
|
|
|
3218
3409
|
}
|
|
3219
3410
|
}
|
|
3220
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
|
+
|
|
3221
3453
|
function commitWalletImportFiles({ targetRoot, plannedWrites }) {
|
|
3222
3454
|
const stagingRoot = fs.mkdtempSync(path.join(targetRoot, ".wallet-import-"));
|
|
3223
3455
|
const committedPaths = [];
|
|
@@ -3611,14 +3843,18 @@ async function inspectGuideAccount({ account, networkName, network, provider, ar
|
|
|
3611
3843
|
|
|
3612
3844
|
async function inspectGuideWallet({ walletName, networkName, provider, artifactsInstalled }) {
|
|
3613
3845
|
const walletDir = walletPath(walletName, networkName);
|
|
3846
|
+
const viewingKeyFile = walletViewingKeySecretPath(networkName, walletName);
|
|
3847
|
+
const spendingKeyFile = walletSpendingKeySecretPath(networkName, walletName);
|
|
3614
3848
|
const result = {
|
|
3615
3849
|
wallet: walletName,
|
|
3616
3850
|
network: networkName,
|
|
3617
3851
|
walletDir,
|
|
3618
3852
|
exists: walletConfigExists(walletDir),
|
|
3619
|
-
metadataExists: fs.existsSync(
|
|
3620
|
-
|
|
3621
|
-
|
|
3853
|
+
metadataExists: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
3854
|
+
viewingKeyFile,
|
|
3855
|
+
viewingKeyFileExists: fs.existsSync(viewingKeyFile),
|
|
3856
|
+
spendingKeyFile,
|
|
3857
|
+
spendingKeyFileExists: fs.existsSync(spendingKeyFile),
|
|
3622
3858
|
channelName: null,
|
|
3623
3859
|
l1Address: null,
|
|
3624
3860
|
l2Address: null,
|
|
@@ -3636,7 +3872,7 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3636
3872
|
}
|
|
3637
3873
|
|
|
3638
3874
|
try {
|
|
3639
|
-
const walletContext = loadWallet(walletName,
|
|
3875
|
+
const walletContext = loadWallet(walletName, networkName);
|
|
3640
3876
|
const walletMetadata = loadWalletMetadata(walletName, networkName);
|
|
3641
3877
|
assertWalletMatchesMetadata(walletContext, walletMetadata);
|
|
3642
3878
|
result.channelName = walletContext.wallet.channelName;
|
|
@@ -3644,10 +3880,13 @@ async function inspectGuideWallet({ walletName, networkName, provider, artifacts
|
|
|
3644
3880
|
result.l2Address = getAddress(walletContext.wallet.l2Address);
|
|
3645
3881
|
result.unusedNoteCount = Object.keys(walletContext.wallet.notes.unused).length;
|
|
3646
3882
|
result.spentNoteCount = Object.keys(walletContext.wallet.notes.spent).length;
|
|
3647
|
-
const
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
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
|
+
}
|
|
3651
3890
|
|
|
3652
3891
|
if (provider && artifactsInstalled && walletChannelWorkspaceIsReady(walletContext)) {
|
|
3653
3892
|
const context = await loadWorkspaceContext(walletContext.wallet.channelName, networkName, provider);
|
|
@@ -3718,7 +3957,7 @@ function applyGuideNextAction(guide) {
|
|
|
3718
3957
|
const channelName = guide.selectors.channelName ?? guide.state.channel?.channelName ?? "<CHANNEL>";
|
|
3719
3958
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3720
3959
|
setGuideNextAction(guide, {
|
|
3721
|
-
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`,
|
|
3722
3961
|
why: "The selected local wallet does not exist. Join the channel to create the wallet and register the channel L2 identity.",
|
|
3723
3962
|
});
|
|
3724
3963
|
return;
|
|
@@ -3727,7 +3966,7 @@ function applyGuideNextAction(guide) {
|
|
|
3727
3966
|
const channelName = guide.state.wallet.channelName ?? guide.selectors.channelName ?? "<CHANNEL>";
|
|
3728
3967
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3729
3968
|
setGuideNextAction(guide, {
|
|
3730
|
-
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`,
|
|
3731
3970
|
why: "The local wallet exists, but the corresponding L1 address is not registered in the channel.",
|
|
3732
3971
|
});
|
|
3733
3972
|
return;
|
|
@@ -3744,32 +3983,32 @@ function applyGuideNextAction(guide) {
|
|
|
3744
3983
|
if (guide.state.wallet?.exists && bridgeBalance === 0n && (channelBalance === null || channelBalance === 0n) && unusedNotes === 0) {
|
|
3745
3984
|
const account = guide.selectors.account ?? "<ACCOUNT>";
|
|
3746
3985
|
setGuideNextAction(guide, {
|
|
3747
|
-
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`,
|
|
3748
3987
|
why: "The wallet is joined, but there is no bridge balance, channel balance, or local unused note to spend.",
|
|
3749
3988
|
});
|
|
3750
3989
|
return;
|
|
3751
3990
|
}
|
|
3752
3991
|
if (guide.state.wallet?.exists && bridgeBalance !== null && bridgeBalance > 0n && channelBalance === 0n) {
|
|
3753
3992
|
setGuideNextAction(guide, {
|
|
3754
|
-
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`,
|
|
3755
3994
|
why: "The account has funds in the shared bridge vault, but the wallet has no channel L2 accounting balance.",
|
|
3756
3995
|
});
|
|
3757
3996
|
return;
|
|
3758
3997
|
}
|
|
3759
3998
|
if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
|
|
3760
3999
|
setGuideNextAction(guide, {
|
|
3761
|
-
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>]`,
|
|
3762
4001
|
why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
3763
4002
|
});
|
|
3764
4003
|
return;
|
|
3765
4004
|
}
|
|
3766
4005
|
if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
|
|
3767
4006
|
setGuideNextAction(guide, {
|
|
3768
|
-
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>]`,
|
|
3769
4008
|
why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
3770
4009
|
candidates: [
|
|
3771
4010
|
`wallet get-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
|
|
3772
|
-
`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>]`,
|
|
3773
4012
|
],
|
|
3774
4013
|
});
|
|
3775
4014
|
return;
|
|
@@ -3880,6 +4119,12 @@ async function handleWalletGetMeta({ args, provider }) {
|
|
|
3880
4119
|
registeredL2Address: registration.exists ? getAddress(registration.l2Address) : null,
|
|
3881
4120
|
registeredL2StorageKey: registration.exists ? normalizeBytes32Hex(registration.channelTokenVaultKey) : null,
|
|
3882
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,
|
|
3883
4128
|
});
|
|
3884
4129
|
}
|
|
3885
4130
|
|
|
@@ -3925,7 +4170,8 @@ async function loadWalletChannelRegistrationState({
|
|
|
3925
4170
|
provider,
|
|
3926
4171
|
requireRegistration = false,
|
|
3927
4172
|
}) {
|
|
3928
|
-
const
|
|
4173
|
+
const signer = requireWalletOwnerSigner(walletContext, provider);
|
|
4174
|
+
const l2Identity = restoreParticipantIdentityFromWallet(walletContext.wallet);
|
|
3929
4175
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
3930
4176
|
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
3931
4177
|
const matchesWallet = registration.exists
|
|
@@ -4026,13 +4272,9 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4026
4272
|
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4027
4273
|
const leafIndex = deriveChannelTokenVaultLeafIndex(storageKey);
|
|
4028
4274
|
|
|
4029
|
-
const resolvedLeafIndex = leafIndex;
|
|
4030
4275
|
let approveReceipt = null;
|
|
4031
4276
|
let receipt = null;
|
|
4032
|
-
|
|
4033
|
-
let status = null;
|
|
4034
|
-
|
|
4035
|
-
joinToll = ethers.toBigInt(await context.channelManager.joinToll());
|
|
4277
|
+
const joinToll = ethers.toBigInt(await context.channelManager.joinToll());
|
|
4036
4278
|
const asset = new Contract(
|
|
4037
4279
|
context.workspace.canonicalAsset,
|
|
4038
4280
|
context.bridgeAbiManifest.contracts.erc20.abi,
|
|
@@ -4046,6 +4288,14 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4046
4288
|
channelManager: context.workspace.channelManager,
|
|
4047
4289
|
policySnapshot: context.workspace.policySnapshot,
|
|
4048
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
|
+
});
|
|
4049
4299
|
if (joinToll !== 0n) {
|
|
4050
4300
|
approveReceipt = await waitForReceipt(
|
|
4051
4301
|
await asset.approve(context.workspace.bridgeTokenVault, joinToll, { nonce: nextNonce++ }),
|
|
@@ -4061,8 +4311,6 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4061
4311
|
{ nonce: nextNonce++ },
|
|
4062
4312
|
),
|
|
4063
4313
|
);
|
|
4064
|
-
status = "joined";
|
|
4065
|
-
|
|
4066
4314
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
4067
4315
|
context,
|
|
4068
4316
|
provider,
|
|
@@ -4077,7 +4325,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4077
4325
|
l2Identity,
|
|
4078
4326
|
walletSecret,
|
|
4079
4327
|
storageKey,
|
|
4080
|
-
leafIndex
|
|
4328
|
+
leafIndex,
|
|
4081
4329
|
noteReceiveKeyMaterial,
|
|
4082
4330
|
rpcUrl,
|
|
4083
4331
|
});
|
|
@@ -4086,14 +4334,15 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4086
4334
|
action: "channel join",
|
|
4087
4335
|
workspace: context.workspaceName,
|
|
4088
4336
|
wallet: walletContext.walletName,
|
|
4089
|
-
walletSecretSource:
|
|
4090
|
-
|
|
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.",
|
|
4091
4340
|
channelName: context.workspace.channelName,
|
|
4092
4341
|
channelId: context.workspace.channelId,
|
|
4093
4342
|
l1Address: signer.address,
|
|
4094
4343
|
l2Address: l2Identity.l2Address,
|
|
4095
4344
|
l2StorageKey: storageKey,
|
|
4096
|
-
leafIndex:
|
|
4345
|
+
leafIndex: leafIndex.toString(),
|
|
4097
4346
|
joinTollBaseUnits: joinToll.toString(),
|
|
4098
4347
|
joinTollTokens: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
|
|
4099
4348
|
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
@@ -4104,7 +4353,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
|
|
|
4104
4353
|
txUrl: receipt ? explorerTxUrl(network, receipt.hash) : null,
|
|
4105
4354
|
approveReceipt: approveReceipt ? sanitizeReceipt(approveReceipt) : null,
|
|
4106
4355
|
receipt: receipt ? sanitizeReceipt(receipt) : null,
|
|
4107
|
-
status,
|
|
4356
|
+
status: "joined",
|
|
4108
4357
|
});
|
|
4109
4358
|
}
|
|
4110
4359
|
|
|
@@ -4115,18 +4364,19 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4115
4364
|
provider,
|
|
4116
4365
|
progressAction: "channel exit",
|
|
4117
4366
|
});
|
|
4367
|
+
const ownerSigner = requireWalletOwnerSigner(walletContext, provider);
|
|
4118
4368
|
const network = contextResult.network;
|
|
4119
4369
|
expect(
|
|
4120
4370
|
channelFund === 0n,
|
|
4121
4371
|
[
|
|
4122
|
-
`The current channel fund for ${
|
|
4372
|
+
`The current channel fund for ${ownerSigner.address} is ${channelFund.toString()}.`,
|
|
4123
4373
|
"channel exit requires a zero channel balance.",
|
|
4124
4374
|
"Run wallet withdraw-channel first, then retry channel exit.",
|
|
4125
4375
|
].join(" "),
|
|
4126
4376
|
);
|
|
4127
|
-
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(
|
|
4377
|
+
const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(ownerSigner.address);
|
|
4128
4378
|
const receipt = await waitForReceipt(
|
|
4129
|
-
await context.bridgeTokenVault.connect(
|
|
4379
|
+
await context.bridgeTokenVault.connect(ownerSigner).exitChannel(ethers.toBigInt(context.workspace.channelId)),
|
|
4130
4380
|
);
|
|
4131
4381
|
const cleanup = removeLocalWalletArtifacts(walletContext.walletName, walletMetadata.network);
|
|
4132
4382
|
|
|
@@ -4136,7 +4386,7 @@ async function handleExitChannel({ args, provider }) {
|
|
|
4136
4386
|
network: walletMetadata.network,
|
|
4137
4387
|
channelName: walletMetadata.channelName,
|
|
4138
4388
|
channelId: context.workspace.channelId,
|
|
4139
|
-
l1Address:
|
|
4389
|
+
l1Address: ownerSigner.address,
|
|
4140
4390
|
currentUserValue: channelFund.toString(),
|
|
4141
4391
|
refundAmountBaseUnits: refundAmount.toString(),
|
|
4142
4392
|
refundAmountTokens: ethers.formatUnits(refundAmount, Number(context.workspace.canonicalAssetDecimals)),
|
|
@@ -4175,6 +4425,13 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4175
4425
|
const { signer, l2Identity } = restoreWalletParticipant(walletContext, provider);
|
|
4176
4426
|
const amountInput = requireArg(args.amount, "--amount");
|
|
4177
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
|
+
});
|
|
4178
4435
|
const storageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4179
4436
|
const bridgeTokenVault = new Contract(
|
|
4180
4437
|
context.workspace.bridgeTokenVault,
|
|
@@ -4257,7 +4514,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
|
|
|
4257
4514
|
writeJson(path.join(operationDir, `${operationName}-receipt.json`), sanitizeReceipt(receipt));
|
|
4258
4515
|
writeJson(path.join(operationDir, "state_snapshot.json"), transition.nextSnapshot);
|
|
4259
4516
|
writeJson(path.join(operationDir, "state_snapshot.normalized.json"), transition.nextSnapshot);
|
|
4260
|
-
sealWalletOperationDir(operationDir, walletContext
|
|
4517
|
+
sealWalletOperationDir(operationDir, walletOperationSealSecret(walletContext));
|
|
4261
4518
|
|
|
4262
4519
|
context.currentSnapshot = transition.nextSnapshot;
|
|
4263
4520
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -4292,6 +4549,11 @@ async function handleWithdrawBridge({ args, network, provider }) {
|
|
|
4292
4549
|
const bridgeVaultContext = await loadBridgeVaultContext({ provider, chainId });
|
|
4293
4550
|
const amountInput = requireArg(args.amount, "--amount");
|
|
4294
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
|
+
});
|
|
4295
4557
|
const bridgeTokenVault = new Contract(
|
|
4296
4558
|
bridgeVaultContext.bridgeTokenVaultAddress,
|
|
4297
4559
|
bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
@@ -4372,6 +4634,7 @@ function resolveFunctionMetadataProofForExecution({
|
|
|
4372
4634
|
|
|
4373
4635
|
async function handleMintNotes({ args, provider }) {
|
|
4374
4636
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4637
|
+
requireWalletSpendingCapability(wallet);
|
|
4375
4638
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
4376
4639
|
const amountInputs = parseAmountVector(requireArg(args.amounts, "--amounts"), {
|
|
4377
4640
|
allowZeroEntries: true,
|
|
@@ -4401,6 +4664,19 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4401
4664
|
`${channelFund.toString()}. Run wallet get-channel-fund to inspect the available balance.`,
|
|
4402
4665
|
].join(" "),
|
|
4403
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
|
+
});
|
|
4404
4680
|
const templatePayload = buildMintNotesTemplatePayload({
|
|
4405
4681
|
wallet,
|
|
4406
4682
|
baseUnitAmounts: baseUnitAmounts.map(({ amountBaseUnits }) => amountBaseUnits),
|
|
@@ -4433,6 +4709,7 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4433
4709
|
sourceFunction: templatePayload.method,
|
|
4434
4710
|
sourceTxHash: execution.receipt.hash,
|
|
4435
4711
|
bridgeCommitmentKeys: execution.noteLifecycle.outputCommitmentKeys,
|
|
4712
|
+
sourceBlockNumber: execution.receipt.blockNumber,
|
|
4436
4713
|
}),
|
|
4437
4714
|
gasUsed: receiptGasUsed(execution.receipt),
|
|
4438
4715
|
txUrl: explorerTxUrl(contextResult.network, execution.receipt.hash),
|
|
@@ -4445,6 +4722,8 @@ async function handleMintNotes({ args, provider }) {
|
|
|
4445
4722
|
|
|
4446
4723
|
async function handleRedeemNotes({ args, provider }) {
|
|
4447
4724
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
4725
|
+
requireWalletViewingCapability(wallet);
|
|
4726
|
+
requireWalletSpendingCapability(wallet);
|
|
4448
4727
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
4449
4728
|
const preparedContextResult = await loadFreshWalletChannelContext({
|
|
4450
4729
|
walletContext: wallet,
|
|
@@ -4456,9 +4735,22 @@ async function handleRedeemNotes({ args, provider }) {
|
|
|
4456
4735
|
context: preparedContextResult.context,
|
|
4457
4736
|
provider,
|
|
4458
4737
|
progressAction: "wallet redeem-notes",
|
|
4459
|
-
|
|
4738
|
+
preConsumedBlockDelta: preparedContextResult.autoRecoveryBlockDelta,
|
|
4460
4739
|
});
|
|
4461
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
|
+
});
|
|
4462
4754
|
const templatePayload = buildRedeemNotesTemplatePayload({
|
|
4463
4755
|
wallet,
|
|
4464
4756
|
inputNotes,
|
|
@@ -4514,13 +4806,20 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4514
4806
|
progressAction: "wallet get-notes",
|
|
4515
4807
|
});
|
|
4516
4808
|
const context = contextResult.context;
|
|
4517
|
-
const noteReceiveFreshness =
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
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
|
+
};
|
|
4524
4823
|
|
|
4525
4824
|
const unusedTrackedNotes = wallet.wallet.notes.unusedOrder
|
|
4526
4825
|
.map((commitment) => wallet.wallet.notes.unused[commitment])
|
|
@@ -4540,8 +4839,20 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4540
4839
|
canonicalAssetDecimals,
|
|
4541
4840
|
})));
|
|
4542
4841
|
|
|
4543
|
-
const
|
|
4544
|
-
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;
|
|
4545
4856
|
|
|
4546
4857
|
printJson({
|
|
4547
4858
|
action: "wallet get-notes",
|
|
@@ -4551,22 +4862,447 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
4551
4862
|
controller: wallet.wallet.controller,
|
|
4552
4863
|
unusedNotes,
|
|
4553
4864
|
spentNotes,
|
|
4554
|
-
unusedTotalBaseUnits: unusedTotal
|
|
4555
|
-
unusedTotalTokens: ethers.formatUnits(unusedTotal, canonicalAssetDecimals),
|
|
4556
|
-
spentTotalBaseUnits: spentTotal
|
|
4557
|
-
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),
|
|
4558
4869
|
bridgeStatusMismatches: [...unusedNotes, ...spentNotes].filter((note) => !note.walletStatusMatchesBridge).length,
|
|
4559
4870
|
noteReceiveLastScannedBlock: noteReceiveFreshness.nextBlock,
|
|
4560
4871
|
latestBlock: noteReceiveFreshness.latestBlock,
|
|
4872
|
+
viewingKeyAvailable: Boolean(wallet.wallet.noteReceivePrivateKey),
|
|
4561
4873
|
recoveredWalletWorkspace: noteReceiveFreshness.recoveredWalletWorkspace,
|
|
4562
4874
|
recoveredFromLogs: noteReceiveFreshness.recoveredDeliveryState?.importedNotes ?? 0,
|
|
4563
4875
|
scannedDeliveryLogs: noteReceiveFreshness.recoveredDeliveryState?.scannedLogs ?? 0,
|
|
4564
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
|
+
},
|
|
4565
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
|
+
},
|
|
5277
|
+
});
|
|
5278
|
+
}
|
|
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))];
|
|
4566
5300
|
}
|
|
4567
5301
|
|
|
4568
5302
|
async function handleTransferNotes({ args, provider }) {
|
|
4569
5303
|
const { wallet } = loadUnlockedWalletWithMetadata(args);
|
|
5304
|
+
requireWalletViewingCapability(wallet);
|
|
5305
|
+
requireWalletSpendingCapability(wallet);
|
|
4570
5306
|
const { signer } = restoreWalletParticipant(wallet, provider);
|
|
4571
5307
|
const preparedContextResult = await loadFreshWalletChannelContext({
|
|
4572
5308
|
walletContext: wallet,
|
|
@@ -4580,7 +5316,7 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4580
5316
|
provider,
|
|
4581
5317
|
signer,
|
|
4582
5318
|
progressAction: "wallet transfer-notes",
|
|
4583
|
-
|
|
5319
|
+
preConsumedBlockDelta: preparedContextResult.autoRecoveryBlockDelta,
|
|
4584
5320
|
});
|
|
4585
5321
|
const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
|
|
4586
5322
|
const noteIds = parseNoteIdVector(requireArg(args.noteIds, "--note-ids"));
|
|
@@ -4604,6 +5340,19 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4604
5340
|
"The sum of --amounts must equal the sum of the selected input note values.",
|
|
4605
5341
|
);
|
|
4606
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
|
+
});
|
|
4607
5356
|
const templatePayload = await buildTransferNotesTemplatePayload({
|
|
4608
5357
|
context,
|
|
4609
5358
|
signer,
|
|
@@ -4624,6 +5373,9 @@ async function handleTransferNotes({ args, provider }) {
|
|
|
4624
5373
|
sourceFunction: templatePayload.method,
|
|
4625
5374
|
sourceTxHash: execution.receipt.hash,
|
|
4626
5375
|
bridgeCommitmentKeys: execution.noteLifecycle.outputCommitmentKeys,
|
|
5376
|
+
sourceBlockNumber: execution.receipt.blockNumber,
|
|
5377
|
+
counterpartyL2Addresses: templatePayload.recipientAddresses,
|
|
5378
|
+
counterpartyDirection: "sent",
|
|
4627
5379
|
});
|
|
4628
5380
|
|
|
4629
5381
|
printJson({
|
|
@@ -4754,6 +5506,7 @@ function ensureWallet({
|
|
|
4754
5506
|
ensureDir(path.join(walletDir, "operations"));
|
|
4755
5507
|
|
|
4756
5508
|
const wallet = normalizeWallet({
|
|
5509
|
+
walletFormatVersion: WALLET_WORKSPACE_FORMAT_VERSION,
|
|
4757
5510
|
name: walletName,
|
|
4758
5511
|
network: channelContext.workspace.network,
|
|
4759
5512
|
rpcUrl,
|
|
@@ -4770,10 +5523,8 @@ function ensureWallet({
|
|
|
4770
5523
|
l2AccountingVault: channelContext.workspace.l2AccountingVault,
|
|
4771
5524
|
liquidBalancesSlot: channelContext.workspace.liquidBalancesSlot,
|
|
4772
5525
|
l1Address: signerAddress,
|
|
4773
|
-
l1PrivateKey: normalizePrivateKey(signerPrivateKey),
|
|
4774
5526
|
l2Address: l2Identity.l2Address,
|
|
4775
|
-
|
|
4776
|
-
l2PublicKey: ethers.hexlify(l2Identity.l2PublicKey),
|
|
5527
|
+
l2PublicKey: l2Identity.l2PublicKey ? ethers.hexlify(l2Identity.l2PublicKey) : null,
|
|
4777
5528
|
l2DerivationMode: CHANNEL_BOUND_L2_DERIVATION_MODE,
|
|
4778
5529
|
l2DerivationChannelName: channelContext.workspace.channelName,
|
|
4779
5530
|
l2StorageKey: storageKey,
|
|
@@ -4789,13 +5540,18 @@ function ensureWallet({
|
|
|
4789
5540
|
spent: {},
|
|
4790
5541
|
},
|
|
4791
5542
|
});
|
|
5543
|
+
if (l2Identity.l2PrivateKey) {
|
|
5544
|
+
wallet.l2PrivateKey = ethers.hexlify(l2Identity.l2PrivateKey);
|
|
5545
|
+
}
|
|
5546
|
+
wallet.noteReceivePrivateKey = normalizePrivateKey(noteReceiveKeyMaterial.privateKey);
|
|
4792
5547
|
|
|
4793
5548
|
const context = {
|
|
4794
5549
|
walletName,
|
|
4795
5550
|
walletDir,
|
|
4796
5551
|
wallet,
|
|
4797
|
-
walletSecret,
|
|
5552
|
+
walletSecret: wallet.l2PrivateKey,
|
|
4798
5553
|
};
|
|
5554
|
+
persistWalletKeys(context);
|
|
4799
5555
|
persistWallet(context);
|
|
4800
5556
|
persistWalletMetadata(context);
|
|
4801
5557
|
return context;
|
|
@@ -4811,9 +5567,9 @@ function normalizeWallet(wallet) {
|
|
|
4811
5567
|
...wallet,
|
|
4812
5568
|
canonicalAssetDecimals: Number(wallet.canonicalAssetDecimals),
|
|
4813
5569
|
l2Nonce: Number(wallet.l2Nonce),
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
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,
|
|
4817
5573
|
noteReceiveDerivationVersion: Number(wallet.noteReceiveDerivationVersion),
|
|
4818
5574
|
noteReceiveTypedDataMethod: wallet.noteReceiveTypedDataMethod,
|
|
4819
5575
|
noteReceivePubKeyX: normalizeBytes32Hex(wallet.noteReceivePubKeyX),
|
|
@@ -4823,18 +5579,18 @@ function normalizeWallet(wallet) {
|
|
|
4823
5579
|
unused: Object.fromEntries(unusedNotes.map((note) => [note.commitment, note])),
|
|
4824
5580
|
spent: Object.fromEntries(spentNotes.map((note) => [note.nullifier, note])),
|
|
4825
5581
|
unusedOrder: unusedNotes.map((note) => note.commitment),
|
|
4826
|
-
unusedBalance: unusedNotes.
|
|
5582
|
+
unusedBalance: unusedNotes.every((note) => note.value !== null)
|
|
5583
|
+
? unusedNotes.reduce((sum, note) => sum + ethers.toBigInt(note.value), 0n).toString()
|
|
5584
|
+
: null,
|
|
4827
5585
|
},
|
|
4828
5586
|
};
|
|
4829
5587
|
}
|
|
4830
5588
|
|
|
4831
5589
|
function assertWalletHasCurrentFormat(wallet, walletName) {
|
|
4832
5590
|
const requiredKeys = [
|
|
5591
|
+
"walletFormatVersion",
|
|
4833
5592
|
"canonicalAssetDecimals",
|
|
4834
5593
|
"l2Nonce",
|
|
4835
|
-
"l1PrivateKey",
|
|
4836
|
-
"l2PrivateKey",
|
|
4837
|
-
"l2PublicKey",
|
|
4838
5594
|
"noteReceiveDerivationVersion",
|
|
4839
5595
|
"noteReceiveTypedDataMethod",
|
|
4840
5596
|
"noteReceivePubKeyX",
|
|
@@ -4850,18 +5606,48 @@ function assertWalletHasCurrentFormat(wallet, walletName) {
|
|
|
4850
5606
|
wallet.notes && typeof wallet.notes.unused === "object" && typeof wallet.notes.spent === "object",
|
|
4851
5607
|
`Wallet ${walletName} was not created with the current CLI notes format.`,
|
|
4852
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
|
+
);
|
|
4853
5616
|
}
|
|
4854
5617
|
|
|
4855
5618
|
function normalizeTrackedNote(note) {
|
|
4856
5619
|
return {
|
|
4857
|
-
owner: getAddress(note.owner),
|
|
4858
|
-
value: ethers.toBigInt(note.value).toString(),
|
|
4859
|
-
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,
|
|
4860
5623
|
commitment: normalizeBytes32Hex(note.commitment),
|
|
4861
5624
|
nullifier: normalizeBytes32Hex(note.nullifier),
|
|
5625
|
+
encryptedNoteValue: note.encryptedNoteValue ? normalizeEncryptedNoteValueWords(note.encryptedNoteValue) : null,
|
|
4862
5626
|
status: note.status,
|
|
4863
5627
|
sourceFunction: note.sourceFunction ?? null,
|
|
4864
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,
|
|
4865
5651
|
bridgeCommitmentKey: note.bridgeCommitmentKey ? normalizeBytes32Hex(note.bridgeCommitmentKey) : null,
|
|
4866
5652
|
bridgeNullifierKey: note.bridgeNullifierKey ? normalizeBytes32Hex(note.bridgeNullifierKey) : null,
|
|
4867
5653
|
};
|
|
@@ -4887,15 +5673,28 @@ async function buildWalletNoteBridgeStatus({
|
|
|
4887
5673
|
return {
|
|
4888
5674
|
owner: note.owner,
|
|
4889
5675
|
valueBaseUnits: note.value,
|
|
4890
|
-
valueTokens: ethers.formatUnits(ethers.toBigInt(note.value), canonicalAssetDecimals),
|
|
5676
|
+
valueTokens: note.value === null ? null : ethers.formatUnits(ethers.toBigInt(note.value), canonicalAssetDecimals),
|
|
4891
5677
|
commitment: note.commitment,
|
|
4892
5678
|
nullifier: note.nullifier,
|
|
5679
|
+
encryptedNoteValue: note.encryptedNoteValue,
|
|
4893
5680
|
walletStatus: note.status,
|
|
4894
5681
|
bridgeCommitmentExists: commitmentExists,
|
|
4895
5682
|
bridgeNullifierUsed: nullifierUsed,
|
|
4896
5683
|
walletStatusMatchesBridge: commitmentExists && nullifierUsed === expectedNullifierUsed,
|
|
4897
5684
|
sourceFunction: note.sourceFunction ?? null,
|
|
4898
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,
|
|
4899
5698
|
};
|
|
4900
5699
|
}
|
|
4901
5700
|
|
|
@@ -4913,8 +5712,8 @@ async function readBooleanStorageValueFromSnapshot({ snapshot, storageAddress, s
|
|
|
4913
5712
|
}
|
|
4914
5713
|
|
|
4915
5714
|
function compareNotesByValueDesc(left, right) {
|
|
4916
|
-
const leftValue = ethers.toBigInt(left.value);
|
|
4917
|
-
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);
|
|
4918
5717
|
if (leftValue === rightValue) {
|
|
4919
5718
|
return left.commitment.localeCompare(right.commitment);
|
|
4920
5719
|
}
|
|
@@ -4923,13 +5722,28 @@ function compareNotesByValueDesc(left, right) {
|
|
|
4923
5722
|
|
|
4924
5723
|
function buildTrackedNote(note, sourceFunction, sourceTxHash, bridgeKeys = {}) {
|
|
4925
5724
|
const normalizedNote = normalizePlaintextNote(note);
|
|
5725
|
+
const createdTxHash = bridgeKeys.createdAtTxHash ?? sourceTxHash ?? null;
|
|
5726
|
+
const createdFunction = bridgeKeys.createdByFunction ?? sourceFunction ?? null;
|
|
4926
5727
|
return {
|
|
4927
5728
|
...normalizedNote,
|
|
4928
5729
|
commitment: normalizeBytes32Hex(computeNoteCommitment(normalizedNote)),
|
|
4929
5730
|
nullifier: normalizeBytes32Hex(computeNullifier(normalizedNote)),
|
|
5731
|
+
encryptedNoteValue: note.encryptedNoteValue ? normalizeEncryptedNoteValueWords(note.encryptedNoteValue) : null,
|
|
4930
5732
|
status: "unused",
|
|
4931
5733
|
sourceFunction,
|
|
4932
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,
|
|
4933
5747
|
bridgeCommitmentKey: bridgeKeys.bridgeCommitmentKey
|
|
4934
5748
|
? normalizeBytes32Hex(bridgeKeys.bridgeCommitmentKey)
|
|
4935
5749
|
: null,
|
|
@@ -4944,9 +5758,19 @@ function buildLifecycleTrackedOutputs({
|
|
|
4944
5758
|
sourceFunction,
|
|
4945
5759
|
sourceTxHash,
|
|
4946
5760
|
bridgeCommitmentKeys,
|
|
5761
|
+
sourceBlockNumber = null,
|
|
5762
|
+
counterpartyL2Addresses = [],
|
|
5763
|
+
counterpartyDirection = null,
|
|
4947
5764
|
}) {
|
|
4948
5765
|
return (outputNotes ?? []).map((note, index) => buildTrackedNote(note, sourceFunction, sourceTxHash, {
|
|
4949
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,
|
|
4950
5774
|
}));
|
|
4951
5775
|
}
|
|
4952
5776
|
|
|
@@ -4959,13 +5783,11 @@ async function recoverWalletReceivedNotes({
|
|
|
4959
5783
|
progressAction = null,
|
|
4960
5784
|
fromGenesis = false,
|
|
4961
5785
|
}) {
|
|
4962
|
-
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ??
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
account: signer.address,
|
|
4968
|
-
});
|
|
5786
|
+
const resolvedNoteReceiveKeyMaterial = noteReceiveKeyMaterial ?? {
|
|
5787
|
+
privateKey: walletContext.wallet.noteReceivePrivateKey,
|
|
5788
|
+
noteReceivePubKey: walletNoteReceivePubKey(walletContext),
|
|
5789
|
+
};
|
|
5790
|
+
requireWalletViewingCapability(walletContext);
|
|
4969
5791
|
const recoveredDeliveryState = await recoverDeliveredNotesFromEventLogs({
|
|
4970
5792
|
walletContext,
|
|
4971
5793
|
context,
|
|
@@ -5065,12 +5887,22 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
5065
5887
|
owner: walletContext.wallet.l2Address,
|
|
5066
5888
|
value: recoveredValue,
|
|
5067
5889
|
salt: computeEncryptedNoteSalt(encryptedNoteValue),
|
|
5890
|
+
encryptedNoteValue,
|
|
5068
5891
|
});
|
|
5069
5892
|
const commitment = normalizeBytes32Hex(computeNoteCommitment(plaintextNote));
|
|
5070
5893
|
const nullifier = normalizeBytes32Hex(computeNullifier(plaintextNote));
|
|
5071
|
-
const trackedNote = buildTrackedNote(
|
|
5894
|
+
const trackedNote = buildTrackedNote({
|
|
5895
|
+
...plaintextNote,
|
|
5896
|
+
encryptedNoteValue,
|
|
5897
|
+
}, sourceFunction, log.transactionHash, {
|
|
5072
5898
|
bridgeCommitmentKey: derivePrivateStateControllerMappingStorageKey(commitment, commitmentExistsSlot),
|
|
5073
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",
|
|
5074
5906
|
});
|
|
5075
5907
|
const commitmentExists = await readBooleanStorageValueFromSnapshot({
|
|
5076
5908
|
snapshot: context.currentSnapshot,
|
|
@@ -5142,7 +5974,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
5142
5974
|
provider,
|
|
5143
5975
|
signer = null,
|
|
5144
5976
|
progressAction = null,
|
|
5145
|
-
|
|
5977
|
+
preConsumedBlockDelta = 0,
|
|
5146
5978
|
}) {
|
|
5147
5979
|
const latestBlock = await provider.getBlockNumber();
|
|
5148
5980
|
let nextBlock;
|
|
@@ -5167,17 +5999,16 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
5167
5999
|
nextBlock,
|
|
5168
6000
|
recoveredWalletWorkspace: false,
|
|
5169
6001
|
recoveredDeliveryState: null,
|
|
5170
|
-
|
|
6002
|
+
autoRecoveryBlockDelta: 0,
|
|
5171
6003
|
};
|
|
5172
6004
|
}
|
|
5173
|
-
const
|
|
5174
|
-
const
|
|
6005
|
+
const remainingBlockBudget = AUTO_RECOVERY_BLOCK_BUDGET - Math.max(0, Number(preConsumedBlockDelta));
|
|
6006
|
+
const autoRecoveryBlockDelta = assertAutoRecoveryBlockBudget({
|
|
5175
6007
|
label: `wallet note workspace ${walletContext.walletName}`,
|
|
5176
6008
|
fromBlock: nextBlock,
|
|
5177
6009
|
toBlock: latestBlock,
|
|
5178
|
-
logScanCount: 1,
|
|
5179
6010
|
recoveryCommand: `wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT>`,
|
|
5180
|
-
|
|
6011
|
+
blockBudget: remainingBlockBudget,
|
|
5181
6012
|
});
|
|
5182
6013
|
|
|
5183
6014
|
const resolvedSigner = signer ?? restoreWalletParticipant(walletContext, provider).signer;
|
|
@@ -5205,7 +6036,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
5205
6036
|
...freshness,
|
|
5206
6037
|
recoveredWalletWorkspace: true,
|
|
5207
6038
|
recoveredDeliveryState,
|
|
5208
|
-
|
|
6039
|
+
autoRecoveryBlockDelta,
|
|
5209
6040
|
};
|
|
5210
6041
|
} catch (postRecoveryError) {
|
|
5211
6042
|
throw new Error([
|
|
@@ -5422,7 +6253,7 @@ function snapshotRootForAddress(snapshot, storageAddress) {
|
|
|
5422
6253
|
return snapshot.stateRoots[addressIndex];
|
|
5423
6254
|
}
|
|
5424
6255
|
|
|
5425
|
-
function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, sourceTxHash) {
|
|
6256
|
+
function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, sourceTxHash, sourceBlockNumber = null) {
|
|
5426
6257
|
for (const [index, inputNote] of lifecycle.inputs.entries()) {
|
|
5427
6258
|
const trackedInput = buildTrackedNote(inputNote, sourceFunction, sourceTxHash);
|
|
5428
6259
|
const existingUnusedNote = walletContext.wallet.notes.unused[trackedInput.commitment];
|
|
@@ -5436,12 +6267,26 @@ function applyNoteLifecycleToWallet(walletContext, lifecycle, sourceFunction, so
|
|
|
5436
6267
|
sourceFunction,
|
|
5437
6268
|
sourceTxHash,
|
|
5438
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,
|
|
5439
6277
|
};
|
|
5440
6278
|
}
|
|
5441
6279
|
|
|
5442
6280
|
for (const [index, outputNote] of lifecycle.outputs.entries()) {
|
|
5443
6281
|
const trackedOutput = buildTrackedNote(outputNote, sourceFunction, sourceTxHash, {
|
|
5444
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,
|
|
5445
6290
|
});
|
|
5446
6291
|
if (trackedOutput.owner !== walletContext.wallet.l2Address) {
|
|
5447
6292
|
continue;
|
|
@@ -5526,6 +6371,7 @@ function buildMintEncryptedOutputs({ wallet, values }) {
|
|
|
5526
6371
|
owner: wallet.wallet.l2Address,
|
|
5527
6372
|
value: ethers.toBigInt(value).toString(),
|
|
5528
6373
|
salt: computeEncryptedNoteSalt(encryptedNoteValue),
|
|
6374
|
+
encryptedNoteValue,
|
|
5529
6375
|
});
|
|
5530
6376
|
}
|
|
5531
6377
|
return {
|
|
@@ -5583,6 +6429,7 @@ async function buildTransferNotesTemplatePayload({
|
|
|
5583
6429
|
owner: recipient,
|
|
5584
6430
|
value: ethers.toBigInt(outputAmounts[index]).toString(),
|
|
5585
6431
|
salt,
|
|
6432
|
+
encryptedNoteValue,
|
|
5586
6433
|
});
|
|
5587
6434
|
}
|
|
5588
6435
|
return {
|
|
@@ -5591,6 +6438,7 @@ async function buildTransferNotesTemplatePayload({
|
|
|
5591
6438
|
args: [transferOutputs, inputNotes],
|
|
5592
6439
|
lifecycleInputs: inputNotes,
|
|
5593
6440
|
lifecycleOutputs,
|
|
6441
|
+
recipientAddresses,
|
|
5594
6442
|
};
|
|
5595
6443
|
}
|
|
5596
6444
|
|
|
@@ -5611,6 +6459,10 @@ function loadWalletUnusedInputNotes(walletContext, noteIds) {
|
|
|
5611
6459
|
return noteIds.map((noteId) => {
|
|
5612
6460
|
const trackedNote = walletContext.wallet.notes.unused[noteId];
|
|
5613
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
|
+
);
|
|
5614
6466
|
return normalizePlaintextNote(trackedNote);
|
|
5615
6467
|
});
|
|
5616
6468
|
}
|
|
@@ -5721,7 +6573,7 @@ async function loadFreshWalletChannelContext({
|
|
|
5721
6573
|
network: resolveCliNetwork(contextResult.context.workspace.network),
|
|
5722
6574
|
usingWorkspaceCache: !contextResult.recoveredWorkspace,
|
|
5723
6575
|
recoveredWorkspace: contextResult.recoveredWorkspace,
|
|
5724
|
-
|
|
6576
|
+
autoRecoveryBlockDelta: contextResult.autoRecoveryBlockDelta,
|
|
5725
6577
|
};
|
|
5726
6578
|
}
|
|
5727
6579
|
|
|
@@ -5753,7 +6605,7 @@ async function loadFreshChannelWorkspaceContextResult({
|
|
|
5753
6605
|
return {
|
|
5754
6606
|
context,
|
|
5755
6607
|
recoveredWorkspace: false,
|
|
5756
|
-
|
|
6608
|
+
autoRecoveryBlockDelta: 0,
|
|
5757
6609
|
};
|
|
5758
6610
|
} catch (error) {
|
|
5759
6611
|
const recovery = await recoverChannelWorkspaceFromIndexOnly({
|
|
@@ -5766,7 +6618,7 @@ async function loadFreshChannelWorkspaceContextResult({
|
|
|
5766
6618
|
return {
|
|
5767
6619
|
context: recovery.context,
|
|
5768
6620
|
recoveredWorkspace: true,
|
|
5769
|
-
|
|
6621
|
+
autoRecoveryBlockDelta: recovery.autoRecoveryBlockDelta,
|
|
5770
6622
|
};
|
|
5771
6623
|
}
|
|
5772
6624
|
}
|
|
@@ -5790,7 +6642,7 @@ async function recoverChannelWorkspaceFromIndexOnly({
|
|
|
5790
6642
|
if (readiness.alreadyCurrent) {
|
|
5791
6643
|
return {
|
|
5792
6644
|
context: await loadWorkspaceContext(channelName, networkName, provider),
|
|
5793
|
-
|
|
6645
|
+
autoRecoveryBlockDelta: 0,
|
|
5794
6646
|
};
|
|
5795
6647
|
}
|
|
5796
6648
|
try {
|
|
@@ -5830,7 +6682,7 @@ async function recoverChannelWorkspaceFromIndexOnly({
|
|
|
5830
6682
|
}
|
|
5831
6683
|
return {
|
|
5832
6684
|
context,
|
|
5833
|
-
|
|
6685
|
+
autoRecoveryBlockDelta: readiness.autoRecoveryBlockDelta,
|
|
5834
6686
|
};
|
|
5835
6687
|
}
|
|
5836
6688
|
|
|
@@ -5886,14 +6738,13 @@ async function requireChannelWorkspaceRecoveryIndexForAutoRefresh({
|
|
|
5886
6738
|
if (Number(recoveryIndex.nextBlock) > Number(latestBlock)) {
|
|
5887
6739
|
fail(`Channel workspace recovery index has already scanned through block ${recoveryIndex.nextBlock - 1}, but the local snapshot is not current.`);
|
|
5888
6740
|
}
|
|
5889
|
-
const
|
|
6741
|
+
const autoRecoveryBlockDelta = assertAutoRecoveryBlockBudget({
|
|
5890
6742
|
label: `channel workspace ${channelName} on ${networkName}`,
|
|
5891
6743
|
fromBlock: recoveryIndex.nextBlock,
|
|
5892
6744
|
toBlock: latestBlock,
|
|
5893
|
-
logScanCount: 2,
|
|
5894
6745
|
recoveryCommand: `channel recover-workspace --channel-name ${channelName} --network ${networkName}`,
|
|
5895
6746
|
});
|
|
5896
|
-
return { alreadyCurrent: false,
|
|
6747
|
+
return { alreadyCurrent: false, autoRecoveryBlockDelta };
|
|
5897
6748
|
}
|
|
5898
6749
|
|
|
5899
6750
|
async function refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -5960,6 +6811,7 @@ async function executeWalletDirectTemplateCommand({
|
|
|
5960
6811
|
}) {
|
|
5961
6812
|
emitProgress(operationName, "loading");
|
|
5962
6813
|
const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
|
|
6814
|
+
requireWalletSpendingCapability(wallet);
|
|
5963
6815
|
const {
|
|
5964
6816
|
txSubmitter,
|
|
5965
6817
|
source: txSubmitterSource,
|
|
@@ -6101,7 +6953,16 @@ async function executeWalletTemplateSend({
|
|
|
6101
6953
|
|
|
6102
6954
|
emitProgress(operationName, "persisting");
|
|
6103
6955
|
wallet.wallet.l2Nonce = nonce + 1;
|
|
6104
|
-
|
|
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);
|
|
6105
6966
|
context.currentSnapshot = nextSnapshot;
|
|
6106
6967
|
persistWallet(wallet);
|
|
6107
6968
|
await refreshPersistedWorkspaceAfterLocalTransaction({
|
|
@@ -6110,7 +6971,7 @@ async function executeWalletTemplateSend({
|
|
|
6110
6971
|
receipt,
|
|
6111
6972
|
progressAction: operationName,
|
|
6112
6973
|
});
|
|
6113
|
-
sealWalletOperationDir(operationDir, wallet
|
|
6974
|
+
sealWalletOperationDir(operationDir, walletOperationSealSecret(wallet));
|
|
6114
6975
|
|
|
6115
6976
|
return {
|
|
6116
6977
|
wallet,
|
|
@@ -6198,34 +7059,54 @@ async function loadJoinChannelContext({ args, network, provider }) {
|
|
|
6198
7059
|
};
|
|
6199
7060
|
}
|
|
6200
7061
|
|
|
6201
|
-
function loadWallet(walletName,
|
|
7062
|
+
function loadWallet(walletName, networkName) {
|
|
6202
7063
|
const normalizedWalletName = requireWalletName({ wallet: walletName });
|
|
6203
7064
|
const normalizedNetworkName = requireNetworkName({ network: networkName });
|
|
6204
7065
|
const walletDir = walletPath(normalizedWalletName, normalizedNetworkName);
|
|
6205
7066
|
if (!walletConfigExists(walletDir)) {
|
|
6206
7067
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
6207
7068
|
}
|
|
6208
|
-
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
|
+
}
|
|
6209
7087
|
assertWalletHasRequiredKeys(rawWallet, normalizedWalletName);
|
|
6210
7088
|
const wallet = normalizeWallet(rawWallet);
|
|
6211
7089
|
assertWalletUsesChannelBoundDerivation(wallet, normalizedWalletName);
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
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);
|
|
6217
7098
|
const context = {
|
|
6218
7099
|
walletName: normalizedWalletName,
|
|
6219
7100
|
walletDir,
|
|
6220
7101
|
wallet,
|
|
6221
|
-
walletSecret,
|
|
7102
|
+
walletSecret: wallet.l2PrivateKey ?? wallet.noteReceivePrivateKey ?? null,
|
|
6222
7103
|
};
|
|
6223
7104
|
return context;
|
|
6224
7105
|
}
|
|
6225
7106
|
|
|
6226
7107
|
function loadUnlockedWalletWithMetadata(args) {
|
|
6227
7108
|
const networkName = requireNetworkName(args);
|
|
6228
|
-
const wallet = loadWallet(requireWalletName(args),
|
|
7109
|
+
const wallet = loadWallet(requireWalletName(args), networkName);
|
|
6229
7110
|
const walletMetadata = loadWalletMetadata(wallet.walletName, networkName);
|
|
6230
7111
|
assertWalletMatchesMetadata(wallet, walletMetadata);
|
|
6231
7112
|
expect(
|
|
@@ -6241,19 +7122,71 @@ function loadUnlockedWalletWithMetadata(args) {
|
|
|
6241
7122
|
};
|
|
6242
7123
|
}
|
|
6243
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
|
+
|
|
6244
7188
|
function assertWalletHasRequiredKeys(wallet, walletName) {
|
|
6245
|
-
expect(
|
|
6246
|
-
typeof wallet.l1PrivateKey === "string" && wallet.l1PrivateKey.length > 0,
|
|
6247
|
-
`Wallet ${walletName} is missing the stored L1 private key.`,
|
|
6248
|
-
);
|
|
6249
|
-
expect(
|
|
6250
|
-
typeof wallet.l2PrivateKey === "string" && wallet.l2PrivateKey.length > 0,
|
|
6251
|
-
`Wallet ${walletName} is missing the stored L2 private key.`,
|
|
6252
|
-
);
|
|
6253
|
-
expect(
|
|
6254
|
-
typeof wallet.l2PublicKey === "string" && wallet.l2PublicKey.length > 0,
|
|
6255
|
-
`Wallet ${walletName} is missing the stored L2 public key.`,
|
|
6256
|
-
);
|
|
7189
|
+
expect(wallet.walletFormatVersion !== undefined, `Wallet ${walletName} is missing walletFormatVersion.`);
|
|
6257
7190
|
}
|
|
6258
7191
|
|
|
6259
7192
|
function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
|
|
@@ -6274,6 +7207,13 @@ function assertWalletUsesChannelBoundDerivation(wallet, walletName) {
|
|
|
6274
7207
|
}
|
|
6275
7208
|
|
|
6276
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
|
+
}
|
|
6277
7217
|
const l2PrivateKey = Uint8Array.from(ethers.getBytes(wallet.l2PrivateKey));
|
|
6278
7218
|
const l2PublicKey = Uint8Array.from(ethers.getBytes(wallet.l2PublicKey));
|
|
6279
7219
|
const l2Address = getAddress(fromEdwardsToAddress(l2PublicKey).toString());
|
|
@@ -6285,7 +7225,14 @@ function restoreParticipantIdentityFromWallet(wallet) {
|
|
|
6285
7225
|
}
|
|
6286
7226
|
|
|
6287
7227
|
function restoreWalletSigner(walletContext, provider) {
|
|
6288
|
-
|
|
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
|
+
};
|
|
6289
7236
|
}
|
|
6290
7237
|
|
|
6291
7238
|
function restoreWalletParticipant(walletContext, provider) {
|
|
@@ -6295,6 +7242,75 @@ function restoreWalletParticipant(walletContext, provider) {
|
|
|
6295
7242
|
};
|
|
6296
7243
|
}
|
|
6297
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
|
+
|
|
6298
7314
|
function loadBridgeResources({ chainId }) {
|
|
6299
7315
|
const bridgeDeploymentPath = defaultBridgeDeploymentPath(chainId);
|
|
6300
7316
|
const bridgeDeployment = readJson(bridgeDeploymentPath);
|
|
@@ -6316,7 +7332,7 @@ function loadWalletMetadata(walletName, networkName) {
|
|
|
6316
7332
|
if (!walletConfigExists(walletDir)) {
|
|
6317
7333
|
throw cliError(CLI_ERROR_CODES.UNKNOWN_WALLET, `Unknown wallet: ${normalizedWalletName} on ${normalizedNetworkName}.`);
|
|
6318
7334
|
}
|
|
6319
|
-
const metadataPath =
|
|
7335
|
+
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
6320
7336
|
if (!fs.existsSync(metadataPath)) {
|
|
6321
7337
|
throw new Error(`Wallet ${normalizedWalletName} is missing unencrypted metadata at ${metadataPath}.`);
|
|
6322
7338
|
}
|
|
@@ -6345,14 +7361,14 @@ function assertWalletMatchesMetadata(walletContext, walletMetadata) {
|
|
|
6345
7361
|
walletContext.wallet.network === walletMetadata.network,
|
|
6346
7362
|
[
|
|
6347
7363
|
`Wallet ${walletContext.walletName} metadata network (${walletMetadata.network}) does not match`,
|
|
6348
|
-
`the
|
|
7364
|
+
`the wallet note metadata network (${walletContext.wallet.network}).`,
|
|
6349
7365
|
].join(" "),
|
|
6350
7366
|
);
|
|
6351
7367
|
expect(
|
|
6352
7368
|
walletContext.wallet.channelName === walletMetadata.channelName,
|
|
6353
7369
|
[
|
|
6354
7370
|
`Wallet ${walletContext.walletName} metadata channelName (${walletMetadata.channelName}) does not match`,
|
|
6355
|
-
`the
|
|
7371
|
+
`the wallet note metadata channel (${walletContext.wallet.channelName}).`,
|
|
6356
7372
|
].join(" "),
|
|
6357
7373
|
);
|
|
6358
7374
|
}
|
|
@@ -7416,12 +8432,7 @@ async function fetchLogsChunked(provider, {
|
|
|
7416
8432
|
return aggregatedLogs;
|
|
7417
8433
|
}
|
|
7418
8434
|
|
|
7419
|
-
function
|
|
7420
|
-
fromBlock,
|
|
7421
|
-
toBlock,
|
|
7422
|
-
logScanCount = 1,
|
|
7423
|
-
chunkSize = DEFAULT_LOG_CHUNK_SIZE,
|
|
7424
|
-
}) {
|
|
8435
|
+
function recoveryBlockDelta({ fromBlock, toBlock }) {
|
|
7425
8436
|
const normalizedFromBlock = Number(fromBlock);
|
|
7426
8437
|
const normalizedToBlock = Number(toBlock);
|
|
7427
8438
|
if (!Number.isInteger(normalizedFromBlock) || !Number.isInteger(normalizedToBlock)) {
|
|
@@ -7430,38 +8441,27 @@ function estimateLogScanRequestCount({
|
|
|
7430
8441
|
if (normalizedFromBlock > normalizedToBlock) {
|
|
7431
8442
|
return 0;
|
|
7432
8443
|
}
|
|
7433
|
-
|
|
7434
|
-
return Math.ceil(totalBlocks / Math.max(1, Number(chunkSize))) * Math.max(1, Number(logScanCount));
|
|
8444
|
+
return normalizedToBlock - normalizedFromBlock + 1;
|
|
7435
8445
|
}
|
|
7436
8446
|
|
|
7437
|
-
function
|
|
8447
|
+
function assertAutoRecoveryBlockBudget({
|
|
7438
8448
|
label,
|
|
7439
8449
|
fromBlock,
|
|
7440
8450
|
toBlock,
|
|
7441
|
-
logScanCount,
|
|
7442
8451
|
recoveryCommand,
|
|
7443
|
-
|
|
8452
|
+
blockBudget = AUTO_RECOVERY_BLOCK_BUDGET,
|
|
7444
8453
|
}) {
|
|
7445
|
-
const
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
});
|
|
7450
|
-
const normalizedBudget = Math.max(0, Number(logRequestBudget));
|
|
7451
|
-
if (estimatedRequests <= normalizedBudget) {
|
|
7452
|
-
return estimatedRequests;
|
|
8454
|
+
const blockDelta = recoveryBlockDelta({ fromBlock, toBlock });
|
|
8455
|
+
const normalizedBudget = Math.max(0, Number(blockBudget));
|
|
8456
|
+
if (blockDelta <= normalizedBudget) {
|
|
8457
|
+
return blockDelta;
|
|
7453
8458
|
}
|
|
7454
8459
|
const normalizedFromBlock = Number(fromBlock);
|
|
7455
8460
|
const normalizedToBlock = Number(toBlock);
|
|
7456
|
-
const totalBlocks = normalizedFromBlock <= normalizedToBlock
|
|
7457
|
-
? normalizedToBlock - normalizedFromBlock + 1
|
|
7458
|
-
: 0;
|
|
7459
|
-
const estimatedSeconds = estimatedRequests / DEFAULT_LOG_REQUESTS_PER_SECOND;
|
|
7460
8461
|
throw new Error([
|
|
7461
|
-
`Automatic recovery for ${label} would exceed the ${
|
|
7462
|
-
`Recovery delta is ${
|
|
7463
|
-
`
|
|
7464
|
-
`Estimated minimum scan time: ${estimatedSeconds.toFixed(1)}s.`,
|
|
8462
|
+
`Automatic recovery for ${label} would exceed the ${AUTO_RECOVERY_BLOCK_BUDGET}-block pre-command budget.`,
|
|
8463
|
+
`Recovery delta is ${blockDelta} blocks from ${normalizedFromBlock} to ${normalizedToBlock}.`,
|
|
8464
|
+
`Remaining automatic recovery budget is ${normalizedBudget} blocks.`,
|
|
7465
8465
|
`Run ${recoveryCommand} first; add --from-genesis only if the saved recovery index is unusable.`,
|
|
7466
8466
|
].join(" "));
|
|
7467
8467
|
}
|
|
@@ -7554,6 +8554,7 @@ const OUTPUT_BYTES32_SCALAR_KEYS = new Set([
|
|
|
7554
8554
|
"commitment",
|
|
7555
8555
|
"currentRootVectorHash",
|
|
7556
8556
|
"currentUserKey",
|
|
8557
|
+
"createdAtTxHash",
|
|
7557
8558
|
"emittedRootVectorHash",
|
|
7558
8559
|
"ephemeralPubKeyX",
|
|
7559
8560
|
"hash",
|
|
@@ -7567,6 +8568,7 @@ const OUTPUT_BYTES32_SCALAR_KEYS = new Set([
|
|
|
7567
8568
|
"rootVectorHash",
|
|
7568
8569
|
"salt",
|
|
7569
8570
|
"sourceTxHash",
|
|
8571
|
+
"spentAtTxHash",
|
|
7570
8572
|
"topic0",
|
|
7571
8573
|
"transactionHash",
|
|
7572
8574
|
"txHash",
|
|
@@ -7763,6 +8765,13 @@ function parseArgs(argv) {
|
|
|
7763
8765
|
&& parsed.positional[1]
|
|
7764
8766
|
) {
|
|
7765
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
|
+
}
|
|
7766
8775
|
parsed.positional = [parsed.command];
|
|
7767
8776
|
}
|
|
7768
8777
|
return parsed;
|
|
@@ -7792,18 +8801,6 @@ function parseTokenAmount(value, decimals) {
|
|
|
7792
8801
|
}
|
|
7793
8802
|
}
|
|
7794
8803
|
|
|
7795
|
-
function requireWalletSecret(args) {
|
|
7796
|
-
if (args.wallet !== undefined && args.network !== undefined) {
|
|
7797
|
-
return resolveWalletSecretForName({
|
|
7798
|
-
networkName: requireNetworkName(args),
|
|
7799
|
-
walletName: requireWalletName(args),
|
|
7800
|
-
});
|
|
7801
|
-
}
|
|
7802
|
-
throw new Error(
|
|
7803
|
-
"Missing --wallet and --network. Wallet commands use the wallet-local default secret file.",
|
|
7804
|
-
);
|
|
7805
|
-
}
|
|
7806
|
-
|
|
7807
8804
|
function requireArg(value, label) {
|
|
7808
8805
|
if (value === undefined || value === null || value === "") {
|
|
7809
8806
|
throw new Error(`Missing ${label}.`);
|
|
@@ -7868,6 +8865,13 @@ function requireL1Signer(args, provider) {
|
|
|
7868
8865
|
|
|
7869
8866
|
function resolveTxSubmitterSigner({ args, ownerSigner, provider }) {
|
|
7870
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
|
+
);
|
|
7871
8875
|
return {
|
|
7872
8876
|
txSubmitter: ownerSigner,
|
|
7873
8877
|
source: "wallet-owner",
|
|
@@ -7902,39 +8906,11 @@ function resolveStandalonePrivateKeySource(args) {
|
|
|
7902
8906
|
));
|
|
7903
8907
|
}
|
|
7904
8908
|
|
|
7905
|
-
function resolveWalletSecretForName({ networkName, walletName }) {
|
|
7906
|
-
return resolveWalletDefaultSecret(networkName, walletName);
|
|
7907
|
-
}
|
|
7908
|
-
|
|
7909
|
-
function resolvedWalletSecretSource(args) {
|
|
7910
|
-
if (args.walletSecretPath !== undefined) return "wallet-secret-path";
|
|
7911
|
-
return "wallet-default";
|
|
7912
|
-
}
|
|
7913
|
-
|
|
7914
|
-
function resolvedWalletSecretFile(networkName, walletName) {
|
|
7915
|
-
return walletSecretPath(networkName, walletName);
|
|
7916
|
-
}
|
|
7917
|
-
|
|
7918
|
-
function resolveWalletDefaultSecret(networkName, walletName) {
|
|
7919
|
-
const secretPath = walletSecretPath(networkName, walletName);
|
|
7920
|
-
if (!fs.existsSync(secretPath)) {
|
|
7921
|
-
throw cliError(
|
|
7922
|
-
CLI_ERROR_CODES.MISSING_WALLET_SECRET,
|
|
7923
|
-
[
|
|
7924
|
-
`Missing wallet default secret file: ${secretPath}.`,
|
|
7925
|
-
"Run channel join with --wallet-secret-path before wallet commands.",
|
|
7926
|
-
].join(" "),
|
|
7927
|
-
);
|
|
7928
|
-
}
|
|
7929
|
-
return readSecretFile(secretPath, "wallet default secret file");
|
|
7930
|
-
}
|
|
7931
|
-
|
|
7932
8909
|
function prepareJoinWalletSecretForName({
|
|
7933
8910
|
args,
|
|
7934
8911
|
networkName,
|
|
7935
8912
|
walletName,
|
|
7936
8913
|
}) {
|
|
7937
|
-
const secretPath = walletSecretPath(networkName, walletName);
|
|
7938
8914
|
const { channelName } = parseWalletName(walletName);
|
|
7939
8915
|
const walletDir = walletPath(walletName, networkName);
|
|
7940
8916
|
expect(
|
|
@@ -7949,14 +8925,7 @@ function prepareJoinWalletSecretForName({
|
|
|
7949
8925
|
].join(" "),
|
|
7950
8926
|
);
|
|
7951
8927
|
const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
|
|
7952
|
-
|
|
7953
|
-
const walletSecret = sourcePath === canonicalPath
|
|
7954
|
-
? readSecretFile(sourcePath, "--wallet-secret-path")
|
|
7955
|
-
: readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
|
|
7956
|
-
if (sourcePath !== canonicalPath) {
|
|
7957
|
-
writeSecretFile(canonicalPath, walletSecret);
|
|
7958
|
-
}
|
|
7959
|
-
return walletSecret;
|
|
8928
|
+
return readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
|
|
7960
8929
|
}
|
|
7961
8930
|
|
|
7962
8931
|
function channelWorkspacePath(networkName, name) {
|
|
@@ -8005,6 +8974,24 @@ function walletSecretPath(networkName, walletName) {
|
|
|
8005
8974
|
);
|
|
8006
8975
|
}
|
|
8007
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
|
+
|
|
8008
8995
|
function resolveWalletPathCandidates(walletName) {
|
|
8009
8996
|
if (!fs.existsSync(workspaceRoot)) {
|
|
8010
8997
|
return [];
|
|
@@ -8063,9 +9050,12 @@ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
|
|
|
8063
9050
|
network: networkEntry.name,
|
|
8064
9051
|
channelName: channelEntry.name,
|
|
8065
9052
|
walletDir,
|
|
8066
|
-
metadataPath:
|
|
8067
|
-
hasMetadata: fs.existsSync(
|
|
8068
|
-
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)),
|
|
8069
9059
|
});
|
|
8070
9060
|
}
|
|
8071
9061
|
}
|
|
@@ -8093,8 +9083,8 @@ function resolveExportWalletInfo({ networkName, walletName }) {
|
|
|
8093
9083
|
network: networkName,
|
|
8094
9084
|
channelName: parseWalletName(walletName).channelName,
|
|
8095
9085
|
walletDir,
|
|
8096
|
-
metadataPath:
|
|
8097
|
-
hasMetadata: fs.existsSync(
|
|
9086
|
+
metadataPath: walletNotesMetadataPath(walletDir),
|
|
9087
|
+
hasMetadata: fs.existsSync(walletNotesMetadataPath(walletDir)),
|
|
8098
9088
|
hasEncryptedWallet: walletConfigExists(walletDir),
|
|
8099
9089
|
};
|
|
8100
9090
|
}
|
|
@@ -8103,15 +9093,11 @@ function normalizeExportWalletInfo(walletInfo) {
|
|
|
8103
9093
|
const wallet = requireWalletName({ wallet: walletInfo.wallet });
|
|
8104
9094
|
const network = requireNetworkName({ network: walletInfo.network });
|
|
8105
9095
|
const walletDir = walletInfo.walletDir ?? walletPath(wallet, network);
|
|
8106
|
-
const metadataPath =
|
|
8107
|
-
const encryptedWalletPath = walletConfigPath(walletDir);
|
|
9096
|
+
const metadataPath = walletNotesMetadataPath(walletDir);
|
|
8108
9097
|
const metadata = readJsonIfExists(metadataPath);
|
|
8109
9098
|
const channelName = metadata?.channelName ?? walletInfo.channelName ?? parseWalletName(wallet).channelName;
|
|
8110
|
-
const walletSecret = walletSecretPath(network, wallet);
|
|
8111
9099
|
|
|
8112
|
-
expect(fs.existsSync(encryptedWalletPath), `Wallet export cannot find encrypted wallet file: ${encryptedWalletPath}.`);
|
|
8113
9100
|
expect(fs.existsSync(metadataPath), `Wallet export cannot find wallet metadata file: ${metadataPath}.`);
|
|
8114
|
-
expect(fs.existsSync(walletSecret), `Wallet export cannot find wallet-local secret file: ${walletSecret}.`);
|
|
8115
9101
|
expect(
|
|
8116
9102
|
metadata.network === network,
|
|
8117
9103
|
`Wallet export metadata network ${metadata.network} does not match ${network}.`,
|
|
@@ -8126,18 +9112,20 @@ function normalizeExportWalletInfo(walletInfo) {
|
|
|
8126
9112
|
channelName,
|
|
8127
9113
|
wallet,
|
|
8128
9114
|
walletDir,
|
|
8129
|
-
walletSecretPath: walletSecret,
|
|
8130
9115
|
};
|
|
8131
9116
|
}
|
|
8132
9117
|
|
|
8133
|
-
function
|
|
9118
|
+
function walletBackupExportFilePaths(walletInfo) {
|
|
8134
9119
|
const walletFiles = [
|
|
8135
|
-
walletInfo.
|
|
8136
|
-
walletConfigPath(walletInfo.walletDir),
|
|
8137
|
-
walletMetadataPath(walletInfo.walletDir),
|
|
9120
|
+
walletNotesMetadataPath(walletInfo.walletDir),
|
|
8138
9121
|
];
|
|
8139
|
-
|
|
8140
|
-
|
|
9122
|
+
for (const metadataPath of [
|
|
9123
|
+
walletViewingKeyMetadataPath(walletInfo.walletDir),
|
|
9124
|
+
walletSpendingKeyMetadataPath(walletInfo.walletDir),
|
|
9125
|
+
]) {
|
|
9126
|
+
if (fs.existsSync(metadataPath)) {
|
|
9127
|
+
walletFiles.push(metadataPath);
|
|
9128
|
+
}
|
|
8141
9129
|
}
|
|
8142
9130
|
|
|
8143
9131
|
const workspaceDir = channelWorkspacePath(walletInfo.network, walletInfo.channelName);
|
|
@@ -8153,8 +9141,8 @@ function walletExportFilePaths(walletInfo, { includeNotes }) {
|
|
|
8153
9141
|
expect(
|
|
8154
9142
|
fs.existsSync(filePath),
|
|
8155
9143
|
[
|
|
8156
|
-
`wallet export
|
|
8157
|
-
"Run channel recover-workspace first
|
|
9144
|
+
`wallet export backup requires channel workspace cache file: ${filePath}.`,
|
|
9145
|
+
"Run channel recover-workspace first.",
|
|
8158
9146
|
].join(" "),
|
|
8159
9147
|
);
|
|
8160
9148
|
}
|
|
@@ -8169,14 +9157,13 @@ function archivePathForLocalCliFile(filePath) {
|
|
|
8169
9157
|
}
|
|
8170
9158
|
|
|
8171
9159
|
function validateWalletExportManifest(manifest) {
|
|
8172
|
-
expect(manifest?.format ===
|
|
9160
|
+
expect(manifest?.format === WALLET_BACKUP_EXPORT_FORMAT, "Wallet import ZIP has an unsupported format.");
|
|
8173
9161
|
expect(
|
|
8174
9162
|
Number(manifest.formatVersion) === WALLET_EXPORT_FORMAT_VERSION,
|
|
8175
9163
|
`Wallet import ZIP format version ${manifest?.formatVersion} is not supported.`,
|
|
8176
9164
|
);
|
|
8177
9165
|
expect(Array.isArray(manifest.files), "Wallet import ZIP manifest is missing files[].");
|
|
8178
9166
|
expect(Array.isArray(manifest.wallets), "Wallet import ZIP manifest is missing wallets[].");
|
|
8179
|
-
expect(typeof manifest.includeNotes === "boolean", "Wallet import ZIP manifest is missing includeNotes.");
|
|
8180
9167
|
expect(manifest.wallets.length > 0, "Wallet import ZIP manifest does not list any wallets.");
|
|
8181
9168
|
const uniqueFiles = new Set(manifest.files);
|
|
8182
9169
|
expect(uniqueFiles.size === manifest.files.length, "Wallet import ZIP manifest contains duplicate file paths.");
|
|
@@ -8198,8 +9185,8 @@ function validateWalletArchivePath(archivePath) {
|
|
|
8198
9185
|
expect(!path.posix.isAbsolute(archivePath), `Wallet import ZIP path must be relative: ${archivePath}.`);
|
|
8199
9186
|
expect(path.posix.normalize(archivePath) === archivePath, `Wallet import ZIP path is not normalized: ${archivePath}.`);
|
|
8200
9187
|
expect(
|
|
8201
|
-
archivePath.startsWith("
|
|
8202
|
-
`Wallet import ZIP path must start with
|
|
9188
|
+
archivePath.startsWith("workspace/"),
|
|
9189
|
+
`Wallet backup import ZIP path must start with workspace/: ${archivePath}.`,
|
|
8203
9190
|
);
|
|
8204
9191
|
}
|
|
8205
9192
|
|
|
@@ -8210,9 +9197,9 @@ function expectPathWithinRoot(targetPath, rootPath, message) {
|
|
|
8210
9197
|
|
|
8211
9198
|
function applyImportedWalletFileMode(archivePath, targetPath) {
|
|
8212
9199
|
if (
|
|
8213
|
-
archivePath.
|
|
8214
|
-
|| archivePath.endsWith("/wallet.json")
|
|
8215
|
-
|| 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")
|
|
8216
9203
|
) {
|
|
8217
9204
|
protectSecretFile(targetPath, `imported wallet file ${archivePath}`);
|
|
8218
9205
|
}
|
|
@@ -8234,16 +9221,20 @@ function channelWorkspaceOperationsPath(workspaceDir) {
|
|
|
8234
9221
|
return path.join(channelDataPath(workspaceDir), "operations");
|
|
8235
9222
|
}
|
|
8236
9223
|
|
|
8237
|
-
function
|
|
8238
|
-
return path.join(walletDir, "wallet.json");
|
|
9224
|
+
function walletNotesMetadataPath(walletDir) {
|
|
9225
|
+
return path.join(walletDir, "wallet-notes.metadata.json");
|
|
8239
9226
|
}
|
|
8240
9227
|
|
|
8241
|
-
function
|
|
8242
|
-
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");
|
|
8243
9234
|
}
|
|
8244
9235
|
|
|
8245
9236
|
function walletConfigExists(walletDir) {
|
|
8246
|
-
return fs.existsSync(
|
|
9237
|
+
return fs.existsSync(walletNotesMetadataPath(walletDir));
|
|
8247
9238
|
}
|
|
8248
9239
|
|
|
8249
9240
|
const COMMAND_ARG_SCHEMAS = Object.freeze(
|
|
@@ -8300,6 +9291,7 @@ function assertWalletSecretArgs(args, commandName, extraOptionKeys = [], accepte
|
|
|
8300
9291
|
|
|
8301
9292
|
function assertWalletChannelMoveArgs(args, commandName) {
|
|
8302
9293
|
assertWalletSecretArgs(args, commandName, ["amount"], "--wallet, --network, and --amount");
|
|
9294
|
+
assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
|
|
8303
9295
|
}
|
|
8304
9296
|
|
|
8305
9297
|
function assertInstallZkEvmArgs(args) {
|
|
@@ -8351,12 +9343,17 @@ function assertTransactionFeesArgs(args) {
|
|
|
8351
9343
|
assertAllowedCommandSchema(args, "help-transaction-fees");
|
|
8352
9344
|
}
|
|
8353
9345
|
|
|
9346
|
+
function assertInvestigatorArgs(args) {
|
|
9347
|
+
assertAllowedCommandSchema(args, "investigator");
|
|
9348
|
+
}
|
|
9349
|
+
|
|
8354
9350
|
function assertAccountImportArgs(args) {
|
|
8355
9351
|
assertAllowedCommandSchema(args, "account-import");
|
|
8356
9352
|
}
|
|
8357
9353
|
|
|
8358
9354
|
function assertMintNotesArgs(args) {
|
|
8359
9355
|
assertAllowedCommandSchema(args, "wallet-mint-notes");
|
|
9356
|
+
assertActionImpactArg(args, "wallet mint-notes");
|
|
8360
9357
|
assertTxSubmitterArg(args);
|
|
8361
9358
|
parseAmountVector(args.amounts, {
|
|
8362
9359
|
allowZeroEntries: true,
|
|
@@ -8366,12 +9363,14 @@ function assertMintNotesArgs(args) {
|
|
|
8366
9363
|
|
|
8367
9364
|
function assertRedeemNotesArgs(args) {
|
|
8368
9365
|
assertAllowedCommandSchema(args, "wallet-redeem-notes");
|
|
9366
|
+
assertActionImpactArg(args, "wallet redeem-notes");
|
|
8369
9367
|
assertTxSubmitterArg(args);
|
|
8370
9368
|
selectRedeemNotesMethod(parseNoteIdVector(args.noteIds).length);
|
|
8371
9369
|
}
|
|
8372
9370
|
|
|
8373
9371
|
function assertTransferNotesArgs(args) {
|
|
8374
9372
|
assertAllowedCommandSchema(args, "wallet-transfer-notes");
|
|
9373
|
+
assertActionImpactArg(args, "wallet transfer-notes");
|
|
8375
9374
|
assertTxSubmitterArg(args);
|
|
8376
9375
|
const noteIds = parseNoteIdVector(args.noteIds);
|
|
8377
9376
|
const recipients = parseRecipientVector(args.recipients);
|
|
@@ -8392,8 +9391,34 @@ function assertTxSubmitterArg(args) {
|
|
|
8392
9391
|
}
|
|
8393
9392
|
}
|
|
8394
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
|
+
|
|
8395
9406
|
function assertWalletGetNotesArgs(args) {
|
|
8396
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
|
+
}
|
|
8397
9422
|
}
|
|
8398
9423
|
|
|
8399
9424
|
function assertCreateChannelArgs(args) {
|
|
@@ -8427,6 +9452,7 @@ function assertPublishWorkspaceMirrorArgs(args) {
|
|
|
8427
9452
|
|
|
8428
9453
|
function assertDepositBridgeArgs(args) {
|
|
8429
9454
|
assertAllowedCommandSchema(args, "account-deposit-bridge");
|
|
9455
|
+
assertActionImpactArg(args, "account deposit-bridge");
|
|
8430
9456
|
}
|
|
8431
9457
|
|
|
8432
9458
|
function assertAccountGetBridgeFundArgs(args) {
|
|
@@ -8446,6 +9472,7 @@ function assertRecoverWalletArgs(args) {
|
|
|
8446
9472
|
|
|
8447
9473
|
function assertJoinChannelArgs(args) {
|
|
8448
9474
|
assertAllowedCommandSchema(args, "channel-join");
|
|
9475
|
+
assertActionImpactArg(args, "channel join");
|
|
8449
9476
|
}
|
|
8450
9477
|
|
|
8451
9478
|
function assertWalletGetMetaArgs(args) {
|
|
@@ -8466,34 +9493,31 @@ function assertListLocalWalletsArgs(args) {
|
|
|
8466
9493
|
assertAllowedCommandSchema(args, "wallet-list");
|
|
8467
9494
|
}
|
|
8468
9495
|
|
|
8469
|
-
function
|
|
8470
|
-
assertAllowedCommandSchema(args, "wallet-export");
|
|
8471
|
-
|
|
8472
|
-
|
|
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);
|
|
8473
9505
|
requireArg(args.output, "--output");
|
|
8474
|
-
if (args.all === true) {
|
|
8475
|
-
expect(
|
|
8476
|
-
args.network === undefined && args.wallet === undefined,
|
|
8477
|
-
"wallet export --all exports every local mainnet wallet and does not accept --network or --wallet.",
|
|
8478
|
-
);
|
|
8479
|
-
return;
|
|
8480
|
-
}
|
|
8481
9506
|
requireNetworkName(args);
|
|
8482
9507
|
requireWalletName(args);
|
|
8483
9508
|
}
|
|
8484
9509
|
|
|
8485
|
-
function
|
|
8486
|
-
assertAllowedCommandSchema(args, "wallet-import");
|
|
9510
|
+
function assertWalletImportBackupArgs(args) {
|
|
9511
|
+
assertAllowedCommandSchema(args, "wallet-import-backup");
|
|
8487
9512
|
}
|
|
8488
9513
|
|
|
8489
|
-
function
|
|
8490
|
-
|
|
8491
|
-
throw new Error(`${commandName} option --${toKebabCase(key)} does not accept a value.`);
|
|
8492
|
-
}
|
|
9514
|
+
function assertWalletImportKeyArgs(args, commandName) {
|
|
9515
|
+
assertAllowedCommandSchema(args, commandName);
|
|
8493
9516
|
}
|
|
8494
9517
|
|
|
8495
9518
|
function assertWithdrawBridgeArgs(args) {
|
|
8496
9519
|
assertAllowedCommandSchema(args, "account-withdraw-bridge");
|
|
9520
|
+
assertActionImpactArg(args, "account withdraw-bridge");
|
|
8497
9521
|
}
|
|
8498
9522
|
|
|
8499
9523
|
function assertWalletGetChannelFundArgs(args) {
|
|
@@ -8516,14 +9540,126 @@ function createWalletOperationDir(walletName, networkName, suffix) {
|
|
|
8516
9540
|
}
|
|
8517
9541
|
|
|
8518
9542
|
function persistWallet(context) {
|
|
8519
|
-
|
|
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
|
+
}
|
|
8520
9577
|
}
|
|
8521
9578
|
|
|
8522
9579
|
function persistWalletMetadata(context) {
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
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
|
+
},
|
|
8527
9663
|
});
|
|
8528
9664
|
}
|
|
8529
9665
|
|
|
@@ -8543,10 +9679,10 @@ Secret source options:
|
|
|
8543
9679
|
A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
|
|
8544
9680
|
Create one before joining a channel, for example:
|
|
8545
9681
|
openssl rand -hex 32 > ./wallet-secret.txt
|
|
8546
|
-
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
|
|
8547
9683
|
Bridge-facing commands accept optional --rpc-url. When provided, it is saved to
|
|
8548
9684
|
~/tokamak-private-channels/secrets/<network>/.env as RPC_URL. When omitted, the CLI reads RPC_URL from that file.
|
|
8549
|
-
Wallet commands use
|
|
9685
|
+
Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
|
|
8550
9686
|
Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
|
|
8551
9687
|
canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
|
|
8552
9688
|
|
|
@@ -8880,23 +10016,6 @@ function getUsableWorkspaceRecoveryIndex({
|
|
|
8880
10016
|
};
|
|
8881
10017
|
}
|
|
8882
10018
|
|
|
8883
|
-
function writeEncryptedWalletJson(filePath, value, walletSecret) {
|
|
8884
|
-
const normalizedValue = normalizeCliOutput(value);
|
|
8885
|
-
writeEncryptedWalletFile(filePath, Buffer.from(`${JSON.stringify(normalizedValue, null, 2)}\n`, "utf8"), walletSecret);
|
|
8886
|
-
}
|
|
8887
|
-
|
|
8888
|
-
function readEncryptedWalletJson(filePath, walletSecret) {
|
|
8889
|
-
try {
|
|
8890
|
-
return JSON.parse(readEncryptedWalletFile(filePath, walletSecret).toString("utf8"));
|
|
8891
|
-
} catch (error) {
|
|
8892
|
-
throw cliError(
|
|
8893
|
-
CLI_ERROR_CODES.WALLET_DECRYPT_FAILED,
|
|
8894
|
-
`Unable to decrypt wallet data at ${filePath}. Check the wallet-local default secret file.`,
|
|
8895
|
-
{ cause: error },
|
|
8896
|
-
);
|
|
8897
|
-
}
|
|
8898
|
-
}
|
|
8899
|
-
|
|
8900
10019
|
function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
8901
10020
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8902
10021
|
const salt = randomBytes(16);
|
|
@@ -8917,23 +10036,6 @@ function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
|
8917
10036
|
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
8918
10037
|
}
|
|
8919
10038
|
|
|
8920
|
-
function readEncryptedWalletFile(filePath, walletSecret) {
|
|
8921
|
-
const envelope = readJson(filePath);
|
|
8922
|
-
expect(
|
|
8923
|
-
envelope.version === WALLET_ENCRYPTION_VERSION
|
|
8924
|
-
&& envelope.algorithm === WALLET_ENCRYPTION_ALGORITHM
|
|
8925
|
-
&& envelope.kdf === "scrypt",
|
|
8926
|
-
`Unsupported wallet encryption envelope at ${filePath}.`,
|
|
8927
|
-
);
|
|
8928
|
-
const encryptionKey = deriveWalletEncryptionKey(walletSecret, Buffer.from(ethers.getBytes(envelope.salt)));
|
|
8929
|
-
const decipher = createDecipheriv("aes-256-gcm", encryptionKey, Buffer.from(ethers.getBytes(envelope.iv)));
|
|
8930
|
-
decipher.setAuthTag(Buffer.from(ethers.getBytes(envelope.tag)));
|
|
8931
|
-
return Buffer.concat([
|
|
8932
|
-
decipher.update(Buffer.from(ethers.getBytes(envelope.ciphertext))),
|
|
8933
|
-
decipher.final(),
|
|
8934
|
-
]);
|
|
8935
|
-
}
|
|
8936
|
-
|
|
8937
10039
|
function deriveWalletEncryptionKey(walletSecret, salt) {
|
|
8938
10040
|
return scryptSync(String(walletSecret), salt, 32);
|
|
8939
10041
|
}
|
|
@@ -8982,6 +10084,7 @@ function loadWalletCommandRuntime(args) {
|
|
|
8982
10084
|
|
|
8983
10085
|
const HUMAN_RESULT_RENDERERS = Object.freeze({
|
|
8984
10086
|
guide: printGuideHumanResult,
|
|
10087
|
+
investigator: printInvestigatorHumanResult,
|
|
8985
10088
|
"transaction-fees": printTransactionFeesHumanResult,
|
|
8986
10089
|
update: printUpdateHumanResult,
|
|
8987
10090
|
});
|
|
@@ -9041,6 +10144,29 @@ function printGuideHumanResult(guide) {
|
|
|
9041
10144
|
console.log(lines.join("\n"));
|
|
9042
10145
|
}
|
|
9043
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
|
+
|
|
9044
10170
|
function printTransactionFeesHumanResult(report) {
|
|
9045
10171
|
const lines = [
|
|
9046
10172
|
"Transaction Fees",
|
|
@@ -9375,17 +10501,6 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
9375
10501
|
hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
|
|
9376
10502
|
}
|
|
9377
10503
|
|
|
9378
|
-
if (error?.code === CLI_ERROR_CODES.MISSING_WALLET_SECRET) {
|
|
9379
|
-
hints.push("restore the wallet-local default secret file from backup before running wallet commands.");
|
|
9380
|
-
hints.push(`private-state-cli help guide --network ${networkName} --wallet ${walletName}`);
|
|
9381
|
-
}
|
|
9382
|
-
|
|
9383
|
-
if (error?.code === CLI_ERROR_CODES.WALLET_DECRYPT_FAILED) {
|
|
9384
|
-
hints.push("verify that the wallet-local default secret file is the same secret used when the wallet was created.");
|
|
9385
|
-
hints.push("if the encrypted wallet file is corrupted but the wallet secret and L1 account secret still exist, rerun wallet recover-workspace.");
|
|
9386
|
-
hints.push("if the wallet secret was lost, the local L2 key cannot be recovered from the encrypted wallet file.");
|
|
9387
|
-
}
|
|
9388
|
-
|
|
9389
10504
|
if (
|
|
9390
10505
|
message.startsWith("Missing --account:")
|
|
9391
10506
|
|| message.includes("Missing --account.")
|
|
@@ -9403,7 +10518,7 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
9403
10518
|
}
|
|
9404
10519
|
|
|
9405
10520
|
if (error?.code === CLI_ERROR_CODES.MISSING_CHANNEL_REGISTRATION) {
|
|
9406
|
-
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`);
|
|
9407
10522
|
hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName} --account ${accountName}`);
|
|
9408
10523
|
}
|
|
9409
10524
|
|