@tokamak-private-dapps/private-state-cli 2.3.0 → 2.3.2
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 +17 -0
- package/README.md +33 -6
- package/lib/private-state-cli-command-registry.mjs +31 -19
- package/lib/runtime.mjs +252 -162
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 2.3.2 - 2026-05-21
|
|
6
|
+
|
|
7
|
+
- Clarified `wallet transfer-notes` JSON-array argument formats in CLI help and README guidance.
|
|
8
|
+
- Changed wallet note freshness to use the fresh channel workspace recovery frontier instead of the
|
|
9
|
+
provider's latest L1 block, so unrelated L1 blocks do not make wallet workspaces stale.
|
|
10
|
+
- Simplified wallet recovery and command-argument validation logic while preserving the channel-frontier
|
|
11
|
+
recovery model.
|
|
12
|
+
|
|
13
|
+
## 2.3.1 - 2026-05-20
|
|
14
|
+
|
|
15
|
+
- Added `wallet recover-workspace --wallet-secret-path` support for rederiving and storing an active
|
|
16
|
+
wallet spending key from the original L1 account and wallet secret source.
|
|
17
|
+
- Validated recovered spending keys against the current on-chain L2 address and channel token-vault
|
|
18
|
+
storage key before received-note recovery starts.
|
|
19
|
+
- Documented the active-wallet-only spending-key recovery policy in the CLI README and private-state
|
|
20
|
+
DApp README.
|
|
21
|
+
|
|
5
22
|
## 2.3.0 - 2026-05-20
|
|
6
23
|
|
|
7
24
|
- Added `help observer` to print the deployed public observer URL for the private-state monitoring
|
package/README.md
CHANGED
|
@@ -209,9 +209,11 @@ saved index instead of silently replaying from genesis.
|
|
|
209
209
|
Wallet commands that need channel state, including `wallet recover-workspace`, `wallet get-meta`,
|
|
210
210
|
`wallet get-channel-fund`, and `wallet get-notes`, refresh stale local channel workspaces through saved recovery
|
|
211
211
|
indexes before reading state. `wallet get-notes` and `wallet recover-workspace` also refresh received-note logs
|
|
212
|
-
through the saved wallet note recovery index.
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
through the saved wallet note recovery index. Wallet note freshness is measured against the fresh channel workspace
|
|
213
|
+
frontier, not the provider's latest L1 block, so unrelated new L1 blocks do not make a wallet stale by themselves.
|
|
214
|
+
Automatic refresh never replays from channel genesis and only runs when the recovery delta fits within the 7,200-block
|
|
215
|
+
pre-command budget. If a saved index is missing, unusable, or too far behind, the command stops and asks the user to
|
|
216
|
+
run the appropriate recovery command first.
|
|
215
217
|
|
|
216
218
|
Wallet note-delivery recovery checkpoints after each RPC log chunk by updating
|
|
217
219
|
`noteReceiveLastScannedBlock`. If an ordinary `wallet recover-workspace` run is interrupted during note recovery, the
|
|
@@ -323,6 +325,23 @@ note ownership and builds the ZK proof, but the selected local account submits `
|
|
|
323
325
|
Use this option when a separate imported local account should submit the L1 transaction and pay gas for a proof-backed
|
|
324
326
|
note command.
|
|
325
327
|
|
|
328
|
+
`wallet transfer-notes` takes JSON arrays for note selection and outputs. `--note-ids` is a JSON array of input note
|
|
329
|
+
commitment IDs from `wallet get-notes`; `--recipients` is a JSON array of recipient L2 addresses; `--amounts` is a JSON
|
|
330
|
+
array of token amounts. Quote decimal amounts to avoid shell or JSON ambiguity. The recipient count must match the
|
|
331
|
+
amount count, only `1->1`, `1->2`, and `2->1` transfer shapes are supported, and the output amount sum must equal the
|
|
332
|
+
selected input note value sum.
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
private-state-cli wallet transfer-notes \
|
|
336
|
+
--wallet <WALLET> \
|
|
337
|
+
--network mainnet \
|
|
338
|
+
--note-ids '["0xNOTE1","0xNOTE2"]' \
|
|
339
|
+
--recipients '["0xL2RECIPIENT1","0xL2RECIPIENT2"]' \
|
|
340
|
+
--amounts '["1.5","2"]' \
|
|
341
|
+
--acknowledge-action-impact \
|
|
342
|
+
--tx-submitter <ACCOUNT>
|
|
343
|
+
```
|
|
344
|
+
|
|
326
345
|
Channel policy warning:
|
|
327
346
|
|
|
328
347
|
- `channel create` commits to an immutable channel policy: verifier bindings, DApp execution metadata, function layout,
|
|
@@ -388,6 +407,13 @@ flow. The spending key needs the same L1 private key, the same channel context,
|
|
|
388
407
|
spending-key file is lost and the wallet secret source is also lost, the CLI cannot reconstruct the spending key and the
|
|
389
408
|
notes for that wallet cannot be spent, transferred, or redeemed through the normal note flow.
|
|
390
409
|
|
|
410
|
+
`wallet recover-workspace` restores the viewing key by default. Add `--wallet-secret-path <PATH>` only when the
|
|
411
|
+
account is currently active in the channel and you need to rederive the spending key. In that mode, the CLI checks the
|
|
412
|
+
derived L2 address and channel token-vault storage key against the current on-chain registration before received-note
|
|
413
|
+
recovery starts, then stores the protected spending-key file. Exited or non-active accounts must be recovered without
|
|
414
|
+
`--wallet-secret-path`; that restores viewing/evidence history but not spending authority. The wallet secret source is
|
|
415
|
+
read for derivation and is not stored.
|
|
416
|
+
|
|
391
417
|
### Wallet Backup, Viewing, And Spending Authority
|
|
392
418
|
|
|
393
419
|
The wallet workspace is split so that a backup is not a full-control wallet export. Backup metadata stores the
|
|
@@ -405,8 +431,9 @@ transactions and then proves authorized note use when inputs are consumed.
|
|
|
405
431
|
|
|
406
432
|
Key recovery is intentionally split. Recreating the viewing key requires the original L1 private key and the same channel
|
|
407
433
|
context. Recreating the spending key requires the original L1 private key, the same channel context, and the same wallet
|
|
408
|
-
secret source used at `channel join`.
|
|
409
|
-
|
|
434
|
+
secret source used at `channel join`. `wallet recover-workspace --wallet-secret-path <PATH>` performs this spending-key
|
|
435
|
+
rederivation only for active channel registrations. Importing `wallet-viewing.key` or `wallet-spending.key` restores the
|
|
436
|
+
corresponding capability without rerunning derivation, but a backup ZIP alone never restores either capability.
|
|
410
437
|
|
|
411
438
|
`wallet get-notes --export-evidence <PATH> --acknowledge-full-note-plaintext-export` writes a local raw evidence ZIP.
|
|
412
439
|
The bundle is not a key export. It includes plaintext note facts for locally known notes so that
|
|
@@ -572,7 +599,7 @@ Suggested interaction flow:
|
|
|
572
599
|
6. If needed, guide the user through `channel create`, `account deposit-bridge`, `channel join`, `wallet deposit-channel`, and
|
|
573
600
|
`wallet mint-notes`.
|
|
574
601
|
7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
|
|
575
|
-
`wallet get-meta`, then build `wallet transfer-notes`.
|
|
602
|
+
`wallet get-meta`, then build `wallet transfer-notes` with JSON arrays for `--note-ids`, `--recipients`, and `--amounts`.
|
|
576
603
|
8. After transfer, guide the recipient to run `wallet get-notes`; it refreshes received notes from the saved recovery index when the delta fits the 7,200-block pre-command budget. If the index is missing or too far behind, explain `wallet recover-workspace`.
|
|
577
604
|
|
|
578
605
|
Example onboarding explanation for `channel join`:
|
|
@@ -140,22 +140,25 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
|
|
|
140
140
|
amounts: {
|
|
141
141
|
label: "Amounts",
|
|
142
142
|
type: "textarea",
|
|
143
|
-
placeholder: "[1
|
|
144
|
-
valueLabel: "<
|
|
143
|
+
placeholder: "[\"1\",\"2\",\"3\"]",
|
|
144
|
+
valueLabel: "<JSON_ARRAY>",
|
|
145
|
+
hint: "JSON array of token amounts. Use quoted strings for decimal amounts, for example [\"1.5\",\"2\"].",
|
|
145
146
|
option: "--amounts",
|
|
146
147
|
},
|
|
147
148
|
noteIds: {
|
|
148
149
|
label: "Note IDs",
|
|
149
150
|
type: "textarea",
|
|
150
151
|
placeholder: "[\"0x...\"]",
|
|
151
|
-
valueLabel: "<
|
|
152
|
+
valueLabel: "<JSON_ARRAY>",
|
|
153
|
+
hint: "JSON array of note commitment IDs from wallet get-notes.",
|
|
152
154
|
option: "--note-ids",
|
|
153
155
|
},
|
|
154
156
|
recipients: {
|
|
155
157
|
label: "Recipients JSON",
|
|
156
158
|
type: "textarea",
|
|
157
159
|
placeholder: "[\"0xRecipientL2Address\"]",
|
|
158
|
-
valueLabel: "<
|
|
160
|
+
valueLabel: "<JSON_ARRAY>",
|
|
161
|
+
hint: "JSON array of recipient L2 addresses. Its length must match --amounts.",
|
|
159
162
|
option: "--recipients",
|
|
160
163
|
},
|
|
161
164
|
docker: {
|
|
@@ -484,11 +487,15 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
484
487
|
display: "wallet recover-workspace",
|
|
485
488
|
description: "Rebuild a recoverable local wallet from on-chain channel state.",
|
|
486
489
|
installMode: "read-only",
|
|
487
|
-
fields: ["channelName", "network", "account", "fromGenesis"],
|
|
488
|
-
|
|
490
|
+
fields: ["channelName", "network", "account", "walletSecretPath", "fromGenesis"],
|
|
491
|
+
optionalFields: ["walletSecretPath"],
|
|
492
|
+
usage: "--channel-name, --network, --account, optional --wallet-secret-path, optional --from-genesis",
|
|
489
493
|
help: [
|
|
490
|
-
"Rebuilds backup metadata from channel state without recreating the spending key",
|
|
494
|
+
"Rebuilds backup metadata from channel state without recreating the spending key by default",
|
|
491
495
|
"Derives and stores the viewing key when the local account signer can reproduce the registered viewing public key",
|
|
496
|
+
"Use --wallet-secret-path only for an active channel registration when you need to rederive and store the spending key",
|
|
497
|
+
"--wallet-secret-path requires the derived spending key to match the current on-chain L2 address and storage key before note recovery starts",
|
|
498
|
+
"Exited or non-active accounts can be recovered for viewing/evidence history only; omit --wallet-secret-path for those wallets",
|
|
492
499
|
"Before wallet recovery, refreshes stale channel workspace state only when the saved recovery index delta fits the pre-command budget",
|
|
493
500
|
"Fails and asks for channel recover-workspace first when the channel workspace is missing, unusable, or too stale for automatic recovery",
|
|
494
501
|
"Use --from-genesis to restart received-note scanning from channel genesis; it does not rebuild the channel workspace from genesis",
|
|
@@ -504,7 +511,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
504
511
|
fields: ["channelName", "network", "account", "walletSecretPath", "acknowledgeActionImpact"],
|
|
505
512
|
usage: "--channel-name, --network, --account, --wallet-secret-path, --acknowledge-action-impact",
|
|
506
513
|
help: [
|
|
507
|
-
"Refreshes the local channel workspace through the saved recovery index before joining when the scan fits the
|
|
514
|
+
"Refreshes the local channel workspace through the saved recovery index before joining when the scan fits the 7,200-block pre-command budget",
|
|
508
515
|
"Fails instead of replaying from genesis; run channel recover-workspace --source rpc --from-genesis when a genesis rebuild is required",
|
|
509
516
|
"--wallet-secret-path is read once for channel-bound L2 spending-key derivation and is not stored in the wallet workspace",
|
|
510
517
|
"Prints the immutable policy snapshot before first registration",
|
|
@@ -524,7 +531,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
524
531
|
fields: ["wallet", "network"],
|
|
525
532
|
usage: "--wallet and --network",
|
|
526
533
|
help: [
|
|
527
|
-
"Refreshes the local channel workspace through the saved recovery index before reading registration metadata when the scan fits the
|
|
534
|
+
"Refreshes the local channel workspace through the saved recovery index before reading registration metadata when the scan fits the 7,200-block pre-command budget",
|
|
528
535
|
"Reports the selected local wallet epoch and lifecycle status when the workspace uses the epoch-aware wallet format",
|
|
529
536
|
],
|
|
530
537
|
},
|
|
@@ -594,7 +601,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
594
601
|
fields: ["wallet", "network", "amount", "acknowledgeActionImpact"],
|
|
595
602
|
usage: "--wallet, --network, --amount, and --acknowledge-action-impact",
|
|
596
603
|
help: [
|
|
597
|
-
"Refreshes the local channel workspace through the saved recovery index before proving the deposit when the scan fits the
|
|
604
|
+
"Refreshes the local channel workspace through the saved recovery index before proving the deposit when the scan fits the 7,200-block pre-command budget",
|
|
598
605
|
"Action impact: emits public proof-backed bridge/channel accounting events exposing the L1 submitter, registered L2 address, amount, channel id, and transaction hash.",
|
|
599
606
|
"Private note state is not changed by this command.",
|
|
600
607
|
ACTION_IMPACT_HELP.policy,
|
|
@@ -611,7 +618,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
611
618
|
fields: ["wallet", "network", "amount", "acknowledgeActionImpact"],
|
|
612
619
|
usage: "--wallet, --network, --amount, and --acknowledge-action-impact",
|
|
613
620
|
help: [
|
|
614
|
-
"Refreshes the local channel workspace through the saved recovery index before proving the withdrawal when the scan fits the
|
|
621
|
+
"Refreshes the local channel workspace through the saved recovery index before proving the withdrawal when the scan fits the 7,200-block pre-command budget",
|
|
615
622
|
"Action impact: emits public proof-backed bridge/channel accounting events exposing the L1 submitter, registered L2 address, amount, channel id, and transaction hash.",
|
|
616
623
|
"Private note state is not changed by this command; prior note provenance is not public by default.",
|
|
617
624
|
ACTION_IMPACT_HELP.provenance,
|
|
@@ -628,7 +635,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
628
635
|
installMode: "read-only",
|
|
629
636
|
fields: ["wallet", "network"],
|
|
630
637
|
usage: "--wallet and --network",
|
|
631
|
-
help: ["Refreshes the local channel workspace through the saved recovery index before reading the L2 accounting balance when the scan fits the
|
|
638
|
+
help: ["Refreshes the local channel workspace through the saved recovery index before reading the L2 accounting balance when the scan fits the 7,200-block pre-command budget"],
|
|
632
639
|
},
|
|
633
640
|
{
|
|
634
641
|
id: "channel-exit",
|
|
@@ -638,7 +645,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
638
645
|
fields: ["wallet", "network"],
|
|
639
646
|
usage: "--wallet and --network",
|
|
640
647
|
help: [
|
|
641
|
-
"Refreshes the local channel workspace through the saved recovery index before checking the channel balance when the scan fits the
|
|
648
|
+
"Refreshes the local channel workspace through the saved recovery index before checking the channel balance when the scan fits the 7,200-block pre-command budget",
|
|
642
649
|
"Marks the current local wallet epoch as exited and keeps its note metadata available for historical evidence export",
|
|
643
650
|
],
|
|
644
651
|
},
|
|
@@ -650,7 +657,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
650
657
|
fields: ["wallet", "network", "amounts", "acknowledgeActionImpact", "txSubmitter"],
|
|
651
658
|
usage: "--wallet, --network, --amounts, --acknowledge-action-impact, and optional --tx-submitter",
|
|
652
659
|
help: [
|
|
653
|
-
"Refreshes the local channel workspace through the saved recovery index before proving the mint when the scan fits the
|
|
660
|
+
"Refreshes the local channel workspace through the saved recovery index before proving the mint when the scan fits the 7,200-block pre-command budget",
|
|
654
661
|
"Requires both viewing and spending key capability so the accepted mint can be recovered through the normal note event path",
|
|
655
662
|
"Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy",
|
|
656
663
|
"Action impact: emits public accepted-transition, commitment, encrypted note-delivery, root update, and transaction events.",
|
|
@@ -668,9 +675,14 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
668
675
|
description: "Spend input notes into the registered 1->1, 1->2, or 2->1 private transfer shapes.",
|
|
669
676
|
installMode: "full",
|
|
670
677
|
fields: ["wallet", "network", "noteIds", "recipients", "amounts", "acknowledgeActionImpact", "txSubmitter"],
|
|
671
|
-
usage: "--wallet, --network, --note-ids
|
|
678
|
+
usage: "--wallet, --network, --note-ids <JSON_ARRAY>, --recipients <JSON_ARRAY>, --amounts <JSON_ARRAY>, --acknowledge-action-impact, and optional --tx-submitter",
|
|
672
679
|
help: [
|
|
673
|
-
"
|
|
680
|
+
"--note-ids must be a JSON array of input note commitment IDs from wallet get-notes, for example '[\"0xNOTE1\",\"0xNOTE2\"]'",
|
|
681
|
+
"--recipients must be a JSON array of recipient L2 addresses, for example '[\"0xL2RECIPIENT1\",\"0xL2RECIPIENT2\"]'",
|
|
682
|
+
"--amounts must be a JSON array of token amounts, preferably quoted for decimals, for example '[\"1.5\",\"2\"]'",
|
|
683
|
+
"--recipients length must equal --amounts length; supported transfer shapes are 1->1, 1->2, and 2->1",
|
|
684
|
+
"The sum of output amounts must equal the sum of the selected input note values",
|
|
685
|
+
"Refreshes the local channel workspace and received-note logs through saved recovery indexes before proving the transfer when scans fit the 7,200-block pre-command budget",
|
|
674
686
|
"Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy",
|
|
675
687
|
"Action impact: emits public accepted-transition, input nullifier, output commitment, encrypted note-delivery, root update, and transaction events.",
|
|
676
688
|
"Private note state changes by consuming selected input notes and creating output notes; sender-recipient relationship, note plaintext, and note provenance are not public by default.",
|
|
@@ -689,7 +701,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
689
701
|
fields: ["wallet", "network", "noteIds", "acknowledgeActionImpact", "txSubmitter"],
|
|
690
702
|
usage: "--wallet, --network, --note-ids, --acknowledge-action-impact, and optional --tx-submitter",
|
|
691
703
|
help: [
|
|
692
|
-
"Refreshes the local channel workspace and received-note logs through saved recovery indexes before proving the redeem when scans fit the
|
|
704
|
+
"Refreshes the local channel workspace and received-note logs through saved recovery indexes before proving the redeem when scans fit the 7,200-block pre-command budget",
|
|
693
705
|
"Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy",
|
|
694
706
|
"Action impact: emits public accepted-transition, note nullifier, accounting update, root update, and transaction events.",
|
|
695
707
|
"Private note state changes by consuming selected notes; prior note provenance is not public by default.",
|
|
@@ -708,8 +720,8 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
708
720
|
fields: ["wallet", "network", "exportEvidence", "acknowledgeFullNotePlaintextExport"],
|
|
709
721
|
usage: "--wallet, --network, optional --export-evidence, and optional --acknowledge-full-note-plaintext-export",
|
|
710
722
|
help: [
|
|
711
|
-
"Refreshes the local channel workspace through the saved recovery index before reading notes when the scan fits the
|
|
712
|
-
"Refreshes received-note logs through the saved wallet note recovery index when the scan fits the
|
|
723
|
+
"Refreshes the local channel workspace through the saved recovery index before reading notes when the scan fits the 7,200-block pre-command budget",
|
|
724
|
+
"Refreshes received-note logs through the saved wallet note recovery index when the scan fits the 7,200-block pre-command budget",
|
|
713
725
|
"Fails instead of replaying from genesis; run wallet recover-workspace first when explicit wallet recovery is required",
|
|
714
726
|
"Use --export-evidence <PATH> with --acknowledge-full-note-plaintext-export to write a local full-note evidence ZIP for private-state-cli investigator",
|
|
715
727
|
"Evidence export includes all local epochs for the selected wallet, including exited epochs retained for dispute evidence",
|
package/lib/runtime.mjs
CHANGED
|
@@ -2331,10 +2331,19 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2331
2331
|
account: signer.address,
|
|
2332
2332
|
});
|
|
2333
2333
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
2334
|
+
const recoveredSpendingIdentity = await deriveRecoverWalletSpendingIdentity({
|
|
2335
|
+
args,
|
|
2336
|
+
signer,
|
|
2337
|
+
channelName,
|
|
2338
|
+
context,
|
|
2339
|
+
registration,
|
|
2340
|
+
});
|
|
2341
|
+
const walletRecoveryTargetBlock = walletNoteReceiveTargetBlock(context);
|
|
2334
2342
|
const recoveryEventScan = await scanWalletRecoveryEvents({
|
|
2335
2343
|
context,
|
|
2336
2344
|
provider,
|
|
2337
2345
|
l1Address: signer.address,
|
|
2346
|
+
toBlock: walletRecoveryTargetBlock,
|
|
2338
2347
|
progressAction: "wallet recover-workspace",
|
|
2339
2348
|
});
|
|
2340
2349
|
const lifecycleEpoch = selectWalletLifecycleEpoch({
|
|
@@ -2358,7 +2367,27 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2358
2367
|
Number(registeredNoteReceivePubKey.yParity) === Number(noteReceiveKeyMaterial.noteReceivePubKey.yParity),
|
|
2359
2368
|
"The existing note-receive public key parity does not match the derived note-receive public key.",
|
|
2360
2369
|
);
|
|
2361
|
-
|
|
2370
|
+
if (recoveredSpendingIdentity) {
|
|
2371
|
+
const expectedRecoveredStorageKey = deriveLiquidBalanceStorageKey(
|
|
2372
|
+
recoveredSpendingIdentity.l2Address,
|
|
2373
|
+
context.workspace.liquidBalancesSlot,
|
|
2374
|
+
);
|
|
2375
|
+
expect(
|
|
2376
|
+
lifecycleEpoch.lifecycleStatus === "active",
|
|
2377
|
+
"--wallet-secret-path can only recover the spending key for an active wallet epoch.",
|
|
2378
|
+
);
|
|
2379
|
+
expect(
|
|
2380
|
+
ethers.toBigInt(getAddress(lifecycleEpoch.l2Address))
|
|
2381
|
+
=== ethers.toBigInt(getAddress(recoveredSpendingIdentity.l2Address)),
|
|
2382
|
+
"The recovered spending key does not match the recovered wallet lifecycle L2 address.",
|
|
2383
|
+
);
|
|
2384
|
+
expect(
|
|
2385
|
+
ethers.toBigInt(normalizeBytes32Hex(lifecycleEpoch.channelTokenVaultKey))
|
|
2386
|
+
=== ethers.toBigInt(normalizeBytes32Hex(expectedRecoveredStorageKey)),
|
|
2387
|
+
"The recovered spending key does not match the recovered wallet lifecycle storage key.",
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
const l2Identity = recoveredSpendingIdentity ?? {
|
|
2362
2391
|
l2PrivateKey: null,
|
|
2363
2392
|
l2PublicKey: null,
|
|
2364
2393
|
l2Address: getAddress(lifecycleEpoch.l2Address),
|
|
@@ -2373,57 +2402,8 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2373
2402
|
walletDir,
|
|
2374
2403
|
})
|
|
2375
2404
|
: null;
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2379
|
-
applyWalletLifecycleEpoch(existingWallet.wallet, lifecycleEpoch);
|
|
2380
|
-
persistWalletKeys(existingWallet);
|
|
2381
|
-
persistWallet(existingWallet);
|
|
2382
|
-
persistWalletIndexForContext(existingWallet);
|
|
2383
|
-
const noteScanStartBlock = args.fromGenesis === true
|
|
2384
|
-
? Number(context.workspace.genesisBlockNumber)
|
|
2385
|
-
: requireUsableWalletNoteReceiveRecoveryIndex({
|
|
2386
|
-
walletContext: existingWallet,
|
|
2387
|
-
context,
|
|
2388
|
-
latestBlock: recoveryEventScan.scanRange.toBlock,
|
|
2389
|
-
});
|
|
2390
|
-
const recoveredDeliveryState = await recoverDeliveredNotesFromCollectedLogs({
|
|
2391
|
-
walletContext: existingWallet,
|
|
2392
|
-
context,
|
|
2393
|
-
noteReceivePrivateKey: noteReceiveKeyMaterial.privateKey,
|
|
2394
|
-
logs: recoveryEventScan.deliveryLogs,
|
|
2395
|
-
storageObservationLogs: recoveryEventScan.storageObservationLogs,
|
|
2396
|
-
scanStartBlock: noteScanStartBlock,
|
|
2397
|
-
latestBlock: recoveryEventScan.scanRange.toBlock,
|
|
2398
|
-
});
|
|
2399
|
-
printJson({
|
|
2400
|
-
action: "wallet recover-workspace",
|
|
2401
|
-
status: "already-recovered",
|
|
2402
|
-
wallet: walletName,
|
|
2403
|
-
walletDir: existingWallet.walletDir,
|
|
2404
|
-
recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
|
|
2405
|
-
channelAutoRecoveryBlockDelta: channelContextResult.autoRecoveryBlockDelta,
|
|
2406
|
-
workspace: context.workspaceName,
|
|
2407
|
-
channelName: context.workspace.channelName,
|
|
2408
|
-
channelId: context.workspace.channelId,
|
|
2409
|
-
l1Address: signer.address,
|
|
2410
|
-
l2Address: l2Identity.l2Address,
|
|
2411
|
-
l2StorageKey: storageKey,
|
|
2412
|
-
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2413
|
-
epochId: lifecycleEpoch.epochId,
|
|
2414
|
-
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
2415
|
-
exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
|
|
2416
|
-
noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
|
|
2417
|
-
l2Nonce: existingWallet.wallet.l2Nonce,
|
|
2418
|
-
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
2419
|
-
scannedDeliveryLogs: recoveredDeliveryState.scannedLogs,
|
|
2420
|
-
linkedEvidence: recoveredDeliveryState.linkedEvidence,
|
|
2421
|
-
noteReceiveScanRange: recoveredDeliveryState.scanRange,
|
|
2422
|
-
});
|
|
2423
|
-
return;
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
const walletContext = ensureWallet({
|
|
2405
|
+
const status = existingWallet ? "already-recovered" : "recovered";
|
|
2406
|
+
const walletContext = existingWallet ?? ensureWallet({
|
|
2427
2407
|
channelContext: context,
|
|
2428
2408
|
signerAddress: signer.address,
|
|
2429
2409
|
signerPrivateKey: signer.privateKey,
|
|
@@ -2435,16 +2415,29 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2435
2415
|
lifecycleEpoch,
|
|
2436
2416
|
rpcUrl,
|
|
2437
2417
|
});
|
|
2438
|
-
|
|
2439
|
-
|
|
2418
|
+
if (existingWallet) {
|
|
2419
|
+
walletContext.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
|
|
2420
|
+
applyWalletLifecycleEpoch(walletContext.wallet, lifecycleEpoch);
|
|
2421
|
+
if (recoveredSpendingIdentity) {
|
|
2422
|
+
walletContext.wallet.l2PrivateKey = ethers.hexlify(recoveredSpendingIdentity.l2PrivateKey);
|
|
2423
|
+
walletContext.wallet.l2PublicKey = ethers.hexlify(recoveredSpendingIdentity.l2PublicKey);
|
|
2424
|
+
walletContext.wallet.l2Address = recoveredSpendingIdentity.l2Address;
|
|
2425
|
+
walletContext.wallet.l2DerivationMode = CHANNEL_BOUND_L2_DERIVATION_MODE;
|
|
2426
|
+
walletContext.wallet.l2DerivationChannelName = channelName;
|
|
2427
|
+
walletContext.wallet.l2StorageKey = storageKey;
|
|
2428
|
+
}
|
|
2429
|
+
persistWalletKeys(walletContext);
|
|
2430
|
+
persistWallet(walletContext);
|
|
2431
|
+
persistWalletIndexForContext(walletContext);
|
|
2432
|
+
}
|
|
2440
2433
|
|
|
2441
2434
|
const noteScanStartBlock = args.fromGenesis === true
|
|
2442
2435
|
? Number(context.workspace.genesisBlockNumber)
|
|
2443
|
-
:
|
|
2436
|
+
: walletNoteReceiveCursorDelta({
|
|
2444
2437
|
walletContext,
|
|
2445
2438
|
context,
|
|
2446
|
-
|
|
2447
|
-
});
|
|
2439
|
+
targetNextBlock: recoveryEventScan.scanRange.toBlock + 1,
|
|
2440
|
+
}).localNextBlock;
|
|
2448
2441
|
const recoveredDeliveryState = await recoverDeliveredNotesFromCollectedLogs({
|
|
2449
2442
|
walletContext,
|
|
2450
2443
|
context,
|
|
@@ -2457,7 +2450,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2457
2450
|
|
|
2458
2451
|
printJson({
|
|
2459
2452
|
action: "wallet recover-workspace",
|
|
2460
|
-
status
|
|
2453
|
+
status,
|
|
2461
2454
|
wallet: walletName,
|
|
2462
2455
|
walletDir: walletContext.walletDir,
|
|
2463
2456
|
recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
|
|
@@ -2468,6 +2461,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2468
2461
|
l1Address: signer.address,
|
|
2469
2462
|
l2Address: l2Identity.l2Address,
|
|
2470
2463
|
l2StorageKey: storageKey,
|
|
2464
|
+
spendingKeyRecovered: Boolean(recoveredSpendingIdentity),
|
|
2471
2465
|
leafIndex: lifecycleEpoch.leafIndex.toString(),
|
|
2472
2466
|
epochId: lifecycleEpoch.epochId,
|
|
2473
2467
|
lifecycleStatus: lifecycleEpoch.lifecycleStatus,
|
|
@@ -2481,6 +2475,41 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2481
2475
|
});
|
|
2482
2476
|
}
|
|
2483
2477
|
|
|
2478
|
+
async function deriveRecoverWalletSpendingIdentity({
|
|
2479
|
+
args,
|
|
2480
|
+
signer,
|
|
2481
|
+
channelName,
|
|
2482
|
+
context,
|
|
2483
|
+
registration,
|
|
2484
|
+
}) {
|
|
2485
|
+
if (args.walletSecretPath === undefined) {
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
expect(
|
|
2489
|
+
registration.exists,
|
|
2490
|
+
[
|
|
2491
|
+
"--wallet-secret-path can only recover a spending key for an active channel registration.",
|
|
2492
|
+
"This account is not currently registered in the channel.",
|
|
2493
|
+
"Run wallet recover-workspace without --wallet-secret-path to recover viewing/evidence history for exited wallets.",
|
|
2494
|
+
].join(" "),
|
|
2495
|
+
);
|
|
2496
|
+
const walletSecret = readWalletSecretSourceFile(args);
|
|
2497
|
+
const l2Identity = await deriveParticipantIdentityFromSigner({
|
|
2498
|
+
channelName,
|
|
2499
|
+
walletSecret,
|
|
2500
|
+
signer,
|
|
2501
|
+
});
|
|
2502
|
+
const expectedStorageKey = deriveLiquidBalanceStorageKey(
|
|
2503
|
+
l2Identity.l2Address,
|
|
2504
|
+
context.workspace.liquidBalancesSlot,
|
|
2505
|
+
);
|
|
2506
|
+
expect(
|
|
2507
|
+
walletRegistrationMatchesIdentity({ registration, l2Identity, expectedStorageKey }),
|
|
2508
|
+
"The recovered spending key does not match the current registered L2 address or channel token vault key.",
|
|
2509
|
+
);
|
|
2510
|
+
return l2Identity;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2484
2513
|
async function handleInstallZkEvm({ args }) {
|
|
2485
2514
|
const installMode = args.readOnly === true
|
|
2486
2515
|
? PRIVATE_STATE_INSTALL_MODES.READ_ONLY
|
|
@@ -4130,18 +4159,18 @@ function applyGuideNextAction(guide) {
|
|
|
4130
4159
|
}
|
|
4131
4160
|
if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
|
|
4132
4161
|
setGuideNextAction(guide, {
|
|
4133
|
-
command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <
|
|
4162
|
+
command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <JSON_ARRAY> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
4134
4163
|
why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
4135
4164
|
});
|
|
4136
4165
|
return;
|
|
4137
4166
|
}
|
|
4138
4167
|
if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
|
|
4139
4168
|
setGuideNextAction(guide, {
|
|
4140
|
-
command: `wallet transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <
|
|
4169
|
+
command: `wallet transfer-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <JSON_ARRAY> --recipients <JSON_ARRAY> --amounts <JSON_ARRAY> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
4141
4170
|
why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
|
|
4142
4171
|
candidates: [
|
|
4143
4172
|
`wallet get-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
|
|
4144
|
-
`wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <
|
|
4173
|
+
`wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <JSON_ARRAY> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
|
|
4145
4174
|
],
|
|
4146
4175
|
});
|
|
4147
4176
|
return;
|
|
@@ -4308,10 +4337,7 @@ async function loadWalletChannelRegistrationState({
|
|
|
4308
4337
|
const l2Identity = restoreParticipantIdentityFromWallet(walletContext.wallet);
|
|
4309
4338
|
const registration = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
|
|
4310
4339
|
const expectedStorageKey = deriveLiquidBalanceStorageKey(l2Identity.l2Address, context.workspace.liquidBalancesSlot);
|
|
4311
|
-
const matchesWallet = registration
|
|
4312
|
-
&& ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
|
|
4313
|
-
&& ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
4314
|
-
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
|
|
4340
|
+
const matchesWallet = walletRegistrationMatchesIdentity({ registration, l2Identity, expectedStorageKey });
|
|
4315
4341
|
|
|
4316
4342
|
if (requireRegistration) {
|
|
4317
4343
|
expect(
|
|
@@ -4336,6 +4362,13 @@ async function loadWalletChannelRegistrationState({
|
|
|
4336
4362
|
};
|
|
4337
4363
|
}
|
|
4338
4364
|
|
|
4365
|
+
function walletRegistrationMatchesIdentity({ registration, l2Identity, expectedStorageKey }) {
|
|
4366
|
+
return registration.exists
|
|
4367
|
+
&& ethers.toBigInt(getAddress(registration.l2Address)) === ethers.toBigInt(getAddress(l2Identity.l2Address))
|
|
4368
|
+
&& ethers.toBigInt(normalizeBytes32Hex(registration.channelTokenVaultKey))
|
|
4369
|
+
=== ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4339
4372
|
function selectWalletLifecycleEpoch({ epochs, registration = null }) {
|
|
4340
4373
|
if (registration?.exists) {
|
|
4341
4374
|
const active = [...epochs].reverse().find((epoch) => (
|
|
@@ -4424,9 +4457,13 @@ async function walletEpochFromJoinReceipt({ receipt, context, provider, l1Addres
|
|
|
4424
4457
|
return epoch;
|
|
4425
4458
|
}
|
|
4426
4459
|
|
|
4427
|
-
async function scanWalletRecoveryEvents({ context, provider, l1Address, progressAction = null }) {
|
|
4460
|
+
async function scanWalletRecoveryEvents({ context, provider, l1Address, toBlock, progressAction = null }) {
|
|
4428
4461
|
const fromBlock = Number(context.workspace.genesisBlockNumber ?? 0);
|
|
4429
|
-
const
|
|
4462
|
+
const normalizedToBlock = Number(toBlock);
|
|
4463
|
+
expect(
|
|
4464
|
+
Number.isInteger(normalizedToBlock) && normalizedToBlock >= fromBlock - 1,
|
|
4465
|
+
"Wallet recovery event scan target block is invalid.",
|
|
4466
|
+
);
|
|
4430
4467
|
const registeredTopic = context.channelManager.interface.getEvent("ChannelTokenVaultIdentityRegistered").topicHash;
|
|
4431
4468
|
const exitedTopic = context.channelManager.interface.getEvent("ChannelTokenVaultIdentityExited").topicHash;
|
|
4432
4469
|
const normalizedRegisteredTopic = normalizeBytes32Hex(registeredTopic);
|
|
@@ -4443,7 +4480,7 @@ async function scanWalletRecoveryEvents({ context, provider, l1Address, progress
|
|
|
4443
4480
|
address: context.workspace.channelManager,
|
|
4444
4481
|
topics: [[registeredTopic, exitedTopic, NOTE_VALUE_ENCRYPTED_TOPIC, CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC]],
|
|
4445
4482
|
fromBlock,
|
|
4446
|
-
toBlock,
|
|
4483
|
+
toBlock: normalizedToBlock,
|
|
4447
4484
|
collectLogs: false,
|
|
4448
4485
|
onProgress: progressAction
|
|
4449
4486
|
? createRpcLogScanProgress({ action: progressAction, label: "wallet-recovery events" })
|
|
@@ -4488,7 +4525,7 @@ async function scanWalletRecoveryEvents({ context, provider, l1Address, progress
|
|
|
4488
4525
|
storageObservationLogs,
|
|
4489
4526
|
scanRange: {
|
|
4490
4527
|
fromBlock,
|
|
4491
|
-
toBlock,
|
|
4528
|
+
toBlock: normalizedToBlock,
|
|
4492
4529
|
},
|
|
4493
4530
|
};
|
|
4494
4531
|
}
|
|
@@ -5192,7 +5229,8 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
5192
5229
|
})
|
|
5193
5230
|
: {
|
|
5194
5231
|
nextBlock: wallet.wallet.noteReceiveLastScannedBlock,
|
|
5195
|
-
|
|
5232
|
+
targetBlock: walletNoteReceiveTargetBlock(context),
|
|
5233
|
+
targetNextBlock: channelWorkspaceRecoveryTargetNextBlock(context),
|
|
5196
5234
|
recoveredWalletWorkspace: false,
|
|
5197
5235
|
recoveredDeliveryState: null,
|
|
5198
5236
|
};
|
|
@@ -5244,7 +5282,8 @@ async function handleWalletGetNotes({ args, provider }) {
|
|
|
5244
5282
|
spentTotalTokens: spentTotal === null ? null : ethers.formatUnits(spentTotal, canonicalAssetDecimals),
|
|
5245
5283
|
bridgeStatusMismatches: [...unusedNotes, ...spentNotes].filter((note) => !note.walletStatusMatchesBridge).length,
|
|
5246
5284
|
noteReceiveLastScannedBlock: noteReceiveFreshness.nextBlock,
|
|
5247
|
-
|
|
5285
|
+
noteReceiveTargetBlock: noteReceiveFreshness.targetBlock,
|
|
5286
|
+
noteReceiveTargetNextBlock: noteReceiveFreshness.targetNextBlock,
|
|
5248
5287
|
viewingKeyAvailable: Boolean(wallet.wallet.noteReceivePrivateKey),
|
|
5249
5288
|
recoveredWalletWorkspace: noteReceiveFreshness.recoveredWalletWorkspace,
|
|
5250
5289
|
recoveredFromLogs: noteReceiveFreshness.recoveredDeliveryState?.importedNotes ?? 0,
|
|
@@ -6505,6 +6544,7 @@ async function recoverWalletReceivedNotes({
|
|
|
6505
6544
|
provider,
|
|
6506
6545
|
signer,
|
|
6507
6546
|
noteReceiveKeyMaterial = null,
|
|
6547
|
+
toBlock = null,
|
|
6508
6548
|
progressAction = null,
|
|
6509
6549
|
fromGenesis = false,
|
|
6510
6550
|
}) {
|
|
@@ -6518,6 +6558,7 @@ async function recoverWalletReceivedNotes({
|
|
|
6518
6558
|
context,
|
|
6519
6559
|
provider,
|
|
6520
6560
|
noteReceivePrivateKey: resolvedNoteReceiveKeyMaterial.privateKey,
|
|
6561
|
+
toBlock,
|
|
6521
6562
|
progressAction,
|
|
6522
6563
|
fromGenesis,
|
|
6523
6564
|
});
|
|
@@ -6532,18 +6573,23 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
6532
6573
|
context,
|
|
6533
6574
|
provider,
|
|
6534
6575
|
noteReceivePrivateKey,
|
|
6576
|
+
toBlock = null,
|
|
6535
6577
|
storageObservationLogs = null,
|
|
6536
6578
|
progressAction = null,
|
|
6537
6579
|
fromGenesis = false,
|
|
6538
6580
|
}) {
|
|
6539
|
-
const latestBlock = await fetchFreshBlockNumber(provider);
|
|
6581
|
+
const latestBlock = toBlock === null ? await fetchFreshBlockNumber(provider) : Number(toBlock);
|
|
6582
|
+
expect(
|
|
6583
|
+
Number.isInteger(latestBlock) && latestBlock >= Number(context.workspace.genesisBlockNumber) - 1,
|
|
6584
|
+
"Wallet note recovery target block is invalid.",
|
|
6585
|
+
);
|
|
6540
6586
|
const scanStartBlock = fromGenesis
|
|
6541
6587
|
? Number(context.workspace.genesisBlockNumber)
|
|
6542
|
-
:
|
|
6588
|
+
: walletNoteReceiveCursorDelta({
|
|
6543
6589
|
walletContext,
|
|
6544
6590
|
context,
|
|
6545
|
-
latestBlock,
|
|
6546
|
-
});
|
|
6591
|
+
targetNextBlock: latestBlock + 1,
|
|
6592
|
+
}).localNextBlock;
|
|
6547
6593
|
const scanRange = {
|
|
6548
6594
|
fromBlock: scanStartBlock,
|
|
6549
6595
|
toBlock: latestBlock,
|
|
@@ -6560,7 +6606,10 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
6560
6606
|
context,
|
|
6561
6607
|
storageObservationLogs: storageObservationLogs ?? [],
|
|
6562
6608
|
});
|
|
6563
|
-
walletContext.wallet.noteReceiveLastScannedBlock =
|
|
6609
|
+
walletContext.wallet.noteReceiveLastScannedBlock = Math.max(
|
|
6610
|
+
Number(walletContext.wallet.noteReceiveLastScannedBlock),
|
|
6611
|
+
latestBlock + 1,
|
|
6612
|
+
);
|
|
6564
6613
|
persistWallet(walletContext);
|
|
6565
6614
|
return {
|
|
6566
6615
|
importedNotes: [],
|
|
@@ -6670,7 +6719,10 @@ async function recoverDeliveredNotesFromCollectedLogs({
|
|
|
6670
6719
|
context,
|
|
6671
6720
|
storageObservationLogs,
|
|
6672
6721
|
});
|
|
6673
|
-
walletContext.wallet.noteReceiveLastScannedBlock =
|
|
6722
|
+
walletContext.wallet.noteReceiveLastScannedBlock = Math.max(
|
|
6723
|
+
Number(walletContext.wallet.noteReceiveLastScannedBlock),
|
|
6724
|
+
latestBlock + 1,
|
|
6725
|
+
);
|
|
6674
6726
|
persistWallet(walletContext);
|
|
6675
6727
|
return {
|
|
6676
6728
|
importedNotes: [],
|
|
@@ -6797,40 +6849,88 @@ async function recoverDeliveredNoteCandidatesFromLogs({
|
|
|
6797
6849
|
return importedCandidates;
|
|
6798
6850
|
}
|
|
6799
6851
|
|
|
6800
|
-
function
|
|
6801
|
-
const
|
|
6852
|
+
function channelWorkspaceRecoveryTargetNextBlock(context) {
|
|
6853
|
+
const targetNextBlock = Number(context.workspace.recoveryLastScannedBlock);
|
|
6802
6854
|
const genesisBlockNumber = Number(context.workspace.genesisBlockNumber);
|
|
6855
|
+
expect(
|
|
6856
|
+
Number.isInteger(targetNextBlock) && targetNextBlock >= genesisBlockNumber,
|
|
6857
|
+
"Channel workspace recovery frontier is missing or unusable.",
|
|
6858
|
+
);
|
|
6859
|
+
return targetNextBlock;
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
function walletNoteReceiveTargetBlock(context) {
|
|
6863
|
+
return channelWorkspaceRecoveryTargetNextBlock(context) - 1;
|
|
6864
|
+
}
|
|
6865
|
+
|
|
6866
|
+
function computeRecoveryCursorDelta({
|
|
6867
|
+
localNextBlock,
|
|
6868
|
+
targetNextBlock,
|
|
6869
|
+
genesisBlockNumber,
|
|
6870
|
+
label,
|
|
6871
|
+
}) {
|
|
6872
|
+
const normalizedLocalNextBlock = Number(localNextBlock);
|
|
6873
|
+
const normalizedTargetNextBlock = Number(targetNextBlock);
|
|
6874
|
+
const normalizedGenesisBlockNumber = Number(genesisBlockNumber);
|
|
6803
6875
|
if (
|
|
6804
|
-
!Number.isInteger(
|
|
6805
|
-
||
|
|
6806
|
-
||
|
|
6876
|
+
!Number.isInteger(normalizedLocalNextBlock)
|
|
6877
|
+
|| !Number.isInteger(normalizedTargetNextBlock)
|
|
6878
|
+
|| !Number.isInteger(normalizedGenesisBlockNumber)
|
|
6879
|
+
|| normalizedLocalNextBlock < normalizedGenesisBlockNumber
|
|
6880
|
+
|| normalizedTargetNextBlock < normalizedGenesisBlockNumber
|
|
6807
6881
|
) {
|
|
6882
|
+
throw new Error([
|
|
6883
|
+
`${label} recovery cursor is missing or unusable.`,
|
|
6884
|
+
`Expected localNextBlock and targetNextBlock to be integers greater than or equal to ${normalizedGenesisBlockNumber}.`,
|
|
6885
|
+
].join(" "));
|
|
6886
|
+
}
|
|
6887
|
+
const fromBlock = normalizedLocalNextBlock;
|
|
6888
|
+
const toBlock = normalizedTargetNextBlock - 1;
|
|
6889
|
+
const blockDelta = Math.max(0, normalizedTargetNextBlock - normalizedLocalNextBlock);
|
|
6890
|
+
return {
|
|
6891
|
+
fresh: normalizedLocalNextBlock >= normalizedTargetNextBlock,
|
|
6892
|
+
localNextBlock: normalizedLocalNextBlock,
|
|
6893
|
+
targetNextBlock: normalizedTargetNextBlock,
|
|
6894
|
+
fromBlock,
|
|
6895
|
+
toBlock,
|
|
6896
|
+
blockDelta,
|
|
6897
|
+
};
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6900
|
+
function walletNoteReceiveCursorDelta({ walletContext, context, targetNextBlock = channelWorkspaceRecoveryTargetNextBlock(context) }) {
|
|
6901
|
+
const nextBlock = Number(walletContext.wallet.noteReceiveLastScannedBlock);
|
|
6902
|
+
const genesisBlockNumber = Number(context.workspace.genesisBlockNumber);
|
|
6903
|
+
try {
|
|
6904
|
+
return computeRecoveryCursorDelta({
|
|
6905
|
+
localNextBlock: nextBlock,
|
|
6906
|
+
targetNextBlock,
|
|
6907
|
+
genesisBlockNumber,
|
|
6908
|
+
label: `Wallet note workspace ${walletContext.walletName}`,
|
|
6909
|
+
});
|
|
6910
|
+
} catch (error) {
|
|
6808
6911
|
throw new Error([
|
|
6809
6912
|
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
6810
|
-
`Expected noteReceiveLastScannedBlock to be an integer
|
|
6913
|
+
`Expected noteReceiveLastScannedBlock to be an integer greater than or equal to ${genesisBlockNumber}.`,
|
|
6811
6914
|
"Run wallet recover-workspace --from-genesis to restart received-note scanning from channel genesis.",
|
|
6915
|
+
`Details: ${error.message}`,
|
|
6812
6916
|
].join(" "));
|
|
6813
6917
|
}
|
|
6814
|
-
return nextBlock;
|
|
6815
6918
|
}
|
|
6816
6919
|
|
|
6817
|
-
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
walletContext,
|
|
6821
|
-
context,
|
|
6822
|
-
latestBlock,
|
|
6823
|
-
});
|
|
6824
|
-
if (nextBlock !== latestBlock + 1) {
|
|
6920
|
+
function assertWalletNoteReceiveStateFresh({ walletContext, context }) {
|
|
6921
|
+
const cursorDelta = walletNoteReceiveCursorDelta({ walletContext, context });
|
|
6922
|
+
if (!cursorDelta.fresh) {
|
|
6825
6923
|
throw new Error([
|
|
6826
6924
|
`Wallet note workspace is stale for wallet ${walletContext.walletName}.`,
|
|
6827
|
-
`noteReceiveLastScannedBlock is ${
|
|
6925
|
+
`noteReceiveLastScannedBlock is ${cursorDelta.localNextBlock}, but channel workspace recovery frontier requires ${cursorDelta.targetNextBlock}.`,
|
|
6828
6926
|
"Run wallet recover-workspace before using commands that read or spend wallet notes.",
|
|
6829
6927
|
].join(" "));
|
|
6830
6928
|
}
|
|
6831
6929
|
return {
|
|
6832
|
-
|
|
6833
|
-
|
|
6930
|
+
targetBlock: cursorDelta.toBlock,
|
|
6931
|
+
targetNextBlock: cursorDelta.targetNextBlock,
|
|
6932
|
+
nextBlock: cursorDelta.localNextBlock,
|
|
6933
|
+
blockDelta: cursorDelta.blockDelta,
|
|
6834
6934
|
};
|
|
6835
6935
|
}
|
|
6836
6936
|
|
|
@@ -6842,14 +6942,9 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6842
6942
|
progressAction = null,
|
|
6843
6943
|
preConsumedBlockDelta = 0,
|
|
6844
6944
|
}) {
|
|
6845
|
-
|
|
6846
|
-
let nextBlock;
|
|
6945
|
+
let cursorDelta;
|
|
6847
6946
|
try {
|
|
6848
|
-
|
|
6849
|
-
walletContext,
|
|
6850
|
-
context,
|
|
6851
|
-
latestBlock,
|
|
6852
|
-
});
|
|
6947
|
+
cursorDelta = walletNoteReceiveCursorDelta({ walletContext, context });
|
|
6853
6948
|
} catch (indexError) {
|
|
6854
6949
|
throw new Error([
|
|
6855
6950
|
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
@@ -6859,10 +6954,11 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6859
6954
|
].join(" "));
|
|
6860
6955
|
}
|
|
6861
6956
|
|
|
6862
|
-
if (
|
|
6957
|
+
if (cursorDelta.fresh) {
|
|
6863
6958
|
return {
|
|
6864
|
-
|
|
6865
|
-
|
|
6959
|
+
targetBlock: cursorDelta.toBlock,
|
|
6960
|
+
targetNextBlock: cursorDelta.targetNextBlock,
|
|
6961
|
+
nextBlock: cursorDelta.localNextBlock,
|
|
6866
6962
|
recoveredWalletWorkspace: false,
|
|
6867
6963
|
recoveredDeliveryState: null,
|
|
6868
6964
|
autoRecoveryBlockDelta: 0,
|
|
@@ -6871,8 +6967,8 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6871
6967
|
const remainingBlockBudget = AUTO_RECOVERY_BLOCK_BUDGET - Math.max(0, Number(preConsumedBlockDelta));
|
|
6872
6968
|
const autoRecoveryBlockDelta = assertAutoRecoveryBlockBudget({
|
|
6873
6969
|
label: `wallet note workspace ${walletContext.walletName}`,
|
|
6874
|
-
fromBlock:
|
|
6875
|
-
toBlock:
|
|
6970
|
+
fromBlock: cursorDelta.fromBlock,
|
|
6971
|
+
toBlock: cursorDelta.toBlock,
|
|
6876
6972
|
recoveryCommand: `wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT>`,
|
|
6877
6973
|
blockBudget: remainingBlockBudget,
|
|
6878
6974
|
});
|
|
@@ -6885,6 +6981,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6885
6981
|
context,
|
|
6886
6982
|
provider,
|
|
6887
6983
|
signer: resolvedSigner,
|
|
6984
|
+
toBlock: cursorDelta.toBlock,
|
|
6888
6985
|
progressAction,
|
|
6889
6986
|
fromGenesis: false,
|
|
6890
6987
|
}));
|
|
@@ -6896,22 +6993,12 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6896
6993
|
`Details: ${recoveryError.message}`,
|
|
6897
6994
|
].join(" "));
|
|
6898
6995
|
}
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
autoRecoveryBlockDelta,
|
|
6906
|
-
};
|
|
6907
|
-
} catch (postRecoveryError) {
|
|
6908
|
-
throw new Error([
|
|
6909
|
-
`Wallet workspace is still stale after recovery-index sync for wallet ${walletContext.walletName}.`,
|
|
6910
|
-
"Automatic wallet recovery will not replay from genesis.",
|
|
6911
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
6912
|
-
`Details: ${postRecoveryError.message}`,
|
|
6913
|
-
].join(" "));
|
|
6914
|
-
}
|
|
6996
|
+
return {
|
|
6997
|
+
...assertWalletNoteReceiveStateFresh({ walletContext, context }),
|
|
6998
|
+
recoveredWalletWorkspace: true,
|
|
6999
|
+
recoveredDeliveryState,
|
|
7000
|
+
autoRecoveryBlockDelta,
|
|
7001
|
+
};
|
|
6915
7002
|
}
|
|
6916
7003
|
|
|
6917
7004
|
function extractEncryptedNoteValueFromBridgeLog(log) {
|
|
@@ -7435,13 +7522,19 @@ async function requireChannelWorkspaceRecoveryIndexForAutoRefresh({
|
|
|
7435
7522
|
if (!recoveryIndex) {
|
|
7436
7523
|
fail(`Channel workspace recovery index is unusable for ${channelName} on ${networkName}.`);
|
|
7437
7524
|
}
|
|
7438
|
-
|
|
7525
|
+
const cursorDelta = computeRecoveryCursorDelta({
|
|
7526
|
+
localNextBlock: recoveryIndex.nextBlock,
|
|
7527
|
+
targetNextBlock: Number(latestBlock) + 1,
|
|
7528
|
+
genesisBlockNumber,
|
|
7529
|
+
label: `Channel workspace ${channelName} on ${networkName}`,
|
|
7530
|
+
});
|
|
7531
|
+
if (cursorDelta.fresh) {
|
|
7439
7532
|
fail(`Channel workspace recovery index has already scanned through block ${recoveryIndex.nextBlock - 1}, but the local snapshot is not current.`);
|
|
7440
7533
|
}
|
|
7441
7534
|
const autoRecoveryBlockDelta = assertAutoRecoveryBlockBudget({
|
|
7442
7535
|
label: `channel workspace ${channelName} on ${networkName}`,
|
|
7443
|
-
fromBlock:
|
|
7444
|
-
toBlock:
|
|
7536
|
+
fromBlock: cursorDelta.fromBlock,
|
|
7537
|
+
toBlock: cursorDelta.toBlock,
|
|
7445
7538
|
recoveryCommand: `channel recover-workspace --channel-name ${channelName} --network ${networkName}`,
|
|
7446
7539
|
});
|
|
7447
7540
|
return { alreadyCurrent: false, autoRecoveryBlockDelta };
|
|
@@ -7509,19 +7602,19 @@ async function recoverLocalWorkspacesAfterAcceptedNoteTransaction({
|
|
|
7509
7602
|
walletContext: wallet,
|
|
7510
7603
|
context,
|
|
7511
7604
|
provider,
|
|
7605
|
+
toBlock: walletNoteReceiveTargetBlock(context),
|
|
7512
7606
|
progressAction,
|
|
7513
7607
|
fromGenesis: false,
|
|
7514
7608
|
});
|
|
7515
7609
|
const freshness = await assertWalletNoteReceiveStateFresh({
|
|
7516
7610
|
walletContext: wallet,
|
|
7517
7611
|
context,
|
|
7518
|
-
provider,
|
|
7519
7612
|
});
|
|
7520
7613
|
return {
|
|
7521
7614
|
channelRecoveryLastScannedBlock: context.workspace.recoveryLastScannedBlock,
|
|
7522
7615
|
channelRecoveryScanRange: context.workspace.recoveryScanRange,
|
|
7523
7616
|
walletNoteReceiveNextBlock: freshness.nextBlock,
|
|
7524
|
-
|
|
7617
|
+
walletTargetBlock: freshness.targetBlock,
|
|
7525
7618
|
recoveredFromLogs: recoveredDeliveryState.importedNotes,
|
|
7526
7619
|
scannedDeliveryLogs: recoveredDeliveryState.scannedLogs,
|
|
7527
7620
|
linkedEvidence: recoveredDeliveryState.linkedEvidence,
|
|
@@ -9735,6 +9828,10 @@ function prepareJoinWalletSecretForName({
|
|
|
9735
9828
|
`For exited history, keep using wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${args.account ?? "<ACCOUNT>"}.`,
|
|
9736
9829
|
].join(" "),
|
|
9737
9830
|
);
|
|
9831
|
+
return readWalletSecretSourceFile(args);
|
|
9832
|
+
}
|
|
9833
|
+
|
|
9834
|
+
function readWalletSecretSourceFile(args) {
|
|
9738
9835
|
const sourcePath = path.resolve(String(requireArg(args.walletSecretPath, "--wallet-secret-path")));
|
|
9739
9836
|
return readImportSecretSourceFile(sourcePath, "--wallet-secret-path");
|
|
9740
9837
|
}
|
|
@@ -10188,15 +10285,19 @@ function assertAllowedCommandKeys(args, commandName, allowedKeys, acceptedUsage)
|
|
|
10188
10285
|
`${commandName} only accepts ${acceptedUsage}. Unsupported option(s): ${unsupported.join(", ")}.`,
|
|
10189
10286
|
);
|
|
10190
10287
|
}
|
|
10191
|
-
|
|
10192
|
-
throw new Error(`${commandName} option --json does not accept a value.`);
|
|
10193
|
-
}
|
|
10288
|
+
assertBooleanFlag(args, "json", `${commandName} option --json`);
|
|
10194
10289
|
expect(
|
|
10195
10290
|
(args.positional ?? []).length === 1,
|
|
10196
10291
|
`${commandName} does not accept positional arguments beyond the command name.`,
|
|
10197
10292
|
);
|
|
10198
10293
|
}
|
|
10199
10294
|
|
|
10295
|
+
function assertBooleanFlag(args, key, label) {
|
|
10296
|
+
if (args[key] !== undefined && args[key] !== true) {
|
|
10297
|
+
throw new Error(`${label} does not accept a value.`);
|
|
10298
|
+
}
|
|
10299
|
+
}
|
|
10300
|
+
|
|
10200
10301
|
function assertWalletChannelMoveArgs(args, commandName) {
|
|
10201
10302
|
assertAllowedCommandSchema(args, commandName);
|
|
10202
10303
|
assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
|
|
@@ -10204,9 +10305,7 @@ function assertWalletChannelMoveArgs(args, commandName) {
|
|
|
10204
10305
|
|
|
10205
10306
|
function assertInstallZkEvmArgs(args) {
|
|
10206
10307
|
assertAllowedCommandSchema(args, "install");
|
|
10207
|
-
|
|
10208
|
-
throw new Error("install option --read-only does not accept a value.");
|
|
10209
|
-
}
|
|
10308
|
+
assertBooleanFlag(args, "readOnly", "install option --read-only");
|
|
10210
10309
|
if (args.readOnly === true && args.docker !== undefined) {
|
|
10211
10310
|
throw new Error("install --read-only does not accept --docker because proof runtimes are not installed.");
|
|
10212
10311
|
}
|
|
@@ -10247,9 +10346,7 @@ function assertUpdateArgs(args) {
|
|
|
10247
10346
|
|
|
10248
10347
|
function assertDoctorArgs(args) {
|
|
10249
10348
|
assertAllowedCommandSchema(args, "help-doctor");
|
|
10250
|
-
|
|
10251
|
-
throw new Error("help doctor option --gpu does not accept a value.");
|
|
10252
|
-
}
|
|
10349
|
+
assertBooleanFlag(args, "gpu", "help doctor option --gpu");
|
|
10253
10350
|
}
|
|
10254
10351
|
|
|
10255
10352
|
function assertGuideArgs(args) {
|
|
@@ -10325,12 +10422,7 @@ function assertTxSubmitterArg(args) {
|
|
|
10325
10422
|
}
|
|
10326
10423
|
|
|
10327
10424
|
function assertActionImpactArg(args, commandName) {
|
|
10328
|
-
|
|
10329
|
-
args.acknowledgeActionImpact !== undefined
|
|
10330
|
-
&& args.acknowledgeActionImpact !== true
|
|
10331
|
-
) {
|
|
10332
|
-
throw new Error(`${commandName} option --acknowledge-action-impact does not accept a value.`);
|
|
10333
|
-
}
|
|
10425
|
+
assertBooleanFlag(args, "acknowledgeActionImpact", `${commandName} option --acknowledge-action-impact`);
|
|
10334
10426
|
if (args.acknowledgeActionImpact !== true && !process.stdin.isTTY) {
|
|
10335
10427
|
throw new Error(`${commandName} requires --acknowledge-action-impact after reviewing the action-impact warning.`);
|
|
10336
10428
|
}
|
|
@@ -10346,12 +10438,11 @@ function assertWalletGetNotesArgs(args) {
|
|
|
10346
10438
|
);
|
|
10347
10439
|
}
|
|
10348
10440
|
}
|
|
10349
|
-
|
|
10350
|
-
args
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
}
|
|
10441
|
+
assertBooleanFlag(
|
|
10442
|
+
args,
|
|
10443
|
+
"acknowledgeFullNotePlaintextExport",
|
|
10444
|
+
"wallet get-notes option --acknowledge-full-note-plaintext-export",
|
|
10445
|
+
);
|
|
10355
10446
|
}
|
|
10356
10447
|
|
|
10357
10448
|
function assertCreateChannelArgs(args) {
|
|
@@ -10361,12 +10452,8 @@ function assertCreateChannelArgs(args) {
|
|
|
10361
10452
|
function assertRecoverWorkspaceArgs(args) {
|
|
10362
10453
|
assertAllowedCommandSchema(args, "channel-recover-workspace");
|
|
10363
10454
|
const source = resolveWorkspaceRecoverySource(args);
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
}
|
|
10367
|
-
if (args.outputRaw !== undefined && args.outputRaw !== true) {
|
|
10368
|
-
throw new Error("channel recover-workspace option --output-raw does not accept a value.");
|
|
10369
|
-
}
|
|
10455
|
+
assertBooleanFlag(args, "fromGenesis", "channel recover-workspace option --from-genesis");
|
|
10456
|
+
assertBooleanFlag(args, "outputRaw", "channel recover-workspace option --output-raw");
|
|
10370
10457
|
if (args.outputRaw === true && source !== "rpc") {
|
|
10371
10458
|
throw new Error("channel recover-workspace option --output-raw requires --source rpc.");
|
|
10372
10459
|
}
|
|
@@ -10384,9 +10471,7 @@ function assertSetWorkspaceMirrorArgs(args) {
|
|
|
10384
10471
|
function assertPublishWorkspaceMirrorArgs(args) {
|
|
10385
10472
|
assertAllowedCommandSchema(args, "channel-publish-workspace-mirror");
|
|
10386
10473
|
requireArg(args.output, "--output");
|
|
10387
|
-
|
|
10388
|
-
throw new Error("channel publish-workspace-mirror option --force does not accept a value.");
|
|
10389
|
-
}
|
|
10474
|
+
assertBooleanFlag(args, "force", "channel publish-workspace-mirror option --force");
|
|
10390
10475
|
}
|
|
10391
10476
|
|
|
10392
10477
|
function assertDepositBridgeArgs(args) {
|
|
@@ -10400,9 +10485,7 @@ function assertAccountGetBridgeFundArgs(args) {
|
|
|
10400
10485
|
|
|
10401
10486
|
function assertRecoverWalletArgs(args) {
|
|
10402
10487
|
assertAllowedCommandSchema(args, "wallet-recover-workspace");
|
|
10403
|
-
|
|
10404
|
-
throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
|
|
10405
|
-
}
|
|
10488
|
+
assertBooleanFlag(args, "fromGenesis", "wallet recover-workspace option --from-genesis");
|
|
10406
10489
|
}
|
|
10407
10490
|
|
|
10408
10491
|
function assertJoinChannelArgs(args) {
|
|
@@ -11105,7 +11188,14 @@ function getUsableWorkspaceRecoveryIndex({
|
|
|
11105
11188
|
return null;
|
|
11106
11189
|
}
|
|
11107
11190
|
const recoveryRootVectorHash = normalizeBytes32Hex(workspace.recoveryRootVectorHash);
|
|
11108
|
-
|
|
11191
|
+
try {
|
|
11192
|
+
computeRecoveryCursorDelta({
|
|
11193
|
+
localNextBlock: nextBlock,
|
|
11194
|
+
targetNextBlock: Number(latestBlock) + 1,
|
|
11195
|
+
genesisBlockNumber,
|
|
11196
|
+
label: "Channel workspace",
|
|
11197
|
+
});
|
|
11198
|
+
} catch {
|
|
11109
11199
|
return null;
|
|
11110
11200
|
}
|
|
11111
11201
|
if (recoveryRootVectorHash === null) {
|
package/package.json
CHANGED