@tokamak-private-dapps/private-state-cli 2.3.1 → 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 CHANGED
@@ -2,6 +2,14 @@
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
+
5
13
  ## 2.3.1 - 2026-05-20
6
14
 
7
15
  - Added `wallet recover-workspace --wallet-secret-path` support for rederiving and storing an active
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. Automatic refresh never replays from channel genesis and only runs when
213
- the recovery delta fits within the 7,200-block pre-command budget. If a saved index is missing, unusable, or too far
214
- behind, the command stops and asks the user to run the appropriate recovery command first.
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,
@@ -580,7 +599,7 @@ Suggested interaction flow:
580
599
  6. If needed, guide the user through `channel create`, `account deposit-bridge`, `channel join`, `wallet deposit-channel`, and
581
600
  `wallet mint-notes`.
582
601
  7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
583
- `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`.
584
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`.
585
604
 
586
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,2,3]",
144
- valueLabel: "<A,B,...>",
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: "<ID,ID,...>",
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: "<ADDR,ADDR,...>",
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: {
@@ -508,7 +511,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
508
511
  fields: ["channelName", "network", "account", "walletSecretPath", "acknowledgeActionImpact"],
509
512
  usage: "--channel-name, --network, --account, --wallet-secret-path, --acknowledge-action-impact",
510
513
  help: [
511
- "Refreshes the local channel workspace through the saved recovery index before joining when the scan fits the 10 second pre-command budget",
514
+ "Refreshes the local channel workspace through the saved recovery index before joining when the scan fits the 7,200-block pre-command budget",
512
515
  "Fails instead of replaying from genesis; run channel recover-workspace --source rpc --from-genesis when a genesis rebuild is required",
513
516
  "--wallet-secret-path is read once for channel-bound L2 spending-key derivation and is not stored in the wallet workspace",
514
517
  "Prints the immutable policy snapshot before first registration",
@@ -528,7 +531,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
528
531
  fields: ["wallet", "network"],
529
532
  usage: "--wallet and --network",
530
533
  help: [
531
- "Refreshes the local channel workspace through the saved recovery index before reading registration metadata when the scan fits the 10 second pre-command budget",
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",
532
535
  "Reports the selected local wallet epoch and lifecycle status when the workspace uses the epoch-aware wallet format",
533
536
  ],
534
537
  },
@@ -598,7 +601,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
598
601
  fields: ["wallet", "network", "amount", "acknowledgeActionImpact"],
599
602
  usage: "--wallet, --network, --amount, and --acknowledge-action-impact",
600
603
  help: [
601
- "Refreshes the local channel workspace through the saved recovery index before proving the deposit when the scan fits the 10 second pre-command budget",
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",
602
605
  "Action impact: emits public proof-backed bridge/channel accounting events exposing the L1 submitter, registered L2 address, amount, channel id, and transaction hash.",
603
606
  "Private note state is not changed by this command.",
604
607
  ACTION_IMPACT_HELP.policy,
@@ -615,7 +618,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
615
618
  fields: ["wallet", "network", "amount", "acknowledgeActionImpact"],
616
619
  usage: "--wallet, --network, --amount, and --acknowledge-action-impact",
617
620
  help: [
618
- "Refreshes the local channel workspace through the saved recovery index before proving the withdrawal when the scan fits the 10 second pre-command budget",
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",
619
622
  "Action impact: emits public proof-backed bridge/channel accounting events exposing the L1 submitter, registered L2 address, amount, channel id, and transaction hash.",
620
623
  "Private note state is not changed by this command; prior note provenance is not public by default.",
621
624
  ACTION_IMPACT_HELP.provenance,
@@ -632,7 +635,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
632
635
  installMode: "read-only",
633
636
  fields: ["wallet", "network"],
634
637
  usage: "--wallet and --network",
635
- help: ["Refreshes the local channel workspace through the saved recovery index before reading the L2 accounting balance when the scan fits the 10 second pre-command budget"],
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"],
636
639
  },
637
640
  {
638
641
  id: "channel-exit",
@@ -642,7 +645,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
642
645
  fields: ["wallet", "network"],
643
646
  usage: "--wallet and --network",
644
647
  help: [
645
- "Refreshes the local channel workspace through the saved recovery index before checking the channel balance when the scan fits the 10 second pre-command budget",
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",
646
649
  "Marks the current local wallet epoch as exited and keeps its note metadata available for historical evidence export",
647
650
  ],
648
651
  },
@@ -654,7 +657,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
654
657
  fields: ["wallet", "network", "amounts", "acknowledgeActionImpact", "txSubmitter"],
655
658
  usage: "--wallet, --network, --amounts, --acknowledge-action-impact, and optional --tx-submitter",
656
659
  help: [
657
- "Refreshes the local channel workspace through the saved recovery index before proving the mint when the scan fits the 10 second pre-command budget",
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",
658
661
  "Requires both viewing and spending key capability so the accepted mint can be recovered through the normal note event path",
659
662
  "Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy",
660
663
  "Action impact: emits public accepted-transition, commitment, encrypted note-delivery, root update, and transaction events.",
@@ -672,9 +675,14 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
672
675
  description: "Spend input notes into the registered 1->1, 1->2, or 2->1 private transfer shapes.",
673
676
  installMode: "full",
674
677
  fields: ["wallet", "network", "noteIds", "recipients", "amounts", "acknowledgeActionImpact", "txSubmitter"],
675
- usage: "--wallet, --network, --note-ids, --recipients, --amounts, --acknowledge-action-impact, and optional --tx-submitter",
678
+ usage: "--wallet, --network, --note-ids <JSON_ARRAY>, --recipients <JSON_ARRAY>, --amounts <JSON_ARRAY>, --acknowledge-action-impact, and optional --tx-submitter",
676
679
  help: [
677
- "Refreshes the local channel workspace and received-note logs through saved recovery indexes before proving the transfer when scans fit the 10 second pre-command budget",
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",
678
686
  "Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy",
679
687
  "Action impact: emits public accepted-transition, input nullifier, output commitment, encrypted note-delivery, root update, and transaction events.",
680
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.",
@@ -693,7 +701,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
693
701
  fields: ["wallet", "network", "noteIds", "acknowledgeActionImpact", "txSubmitter"],
694
702
  usage: "--wallet, --network, --note-ids, --acknowledge-action-impact, and optional --tx-submitter",
695
703
  help: [
696
- "Refreshes the local channel workspace and received-note logs through saved recovery indexes before proving the redeem when scans fit the 10 second pre-command budget",
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",
697
705
  "Use --tx-submitter <ACCOUNT> to let a separate local L1 account pay gas for stronger transaction privacy",
698
706
  "Action impact: emits public accepted-transition, note nullifier, accounting update, root update, and transaction events.",
699
707
  "Private note state changes by consuming selected notes; prior note provenance is not public by default.",
@@ -712,8 +720,8 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
712
720
  fields: ["wallet", "network", "exportEvidence", "acknowledgeFullNotePlaintextExport"],
713
721
  usage: "--wallet, --network, optional --export-evidence, and optional --acknowledge-full-note-plaintext-export",
714
722
  help: [
715
- "Refreshes the local channel workspace through the saved recovery index before reading notes when the scan fits the 10 second pre-command budget",
716
- "Refreshes received-note logs through the saved wallet note recovery index when the scan fits the 10 second pre-command budget",
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",
717
725
  "Fails instead of replaying from genesis; run wallet recover-workspace first when explicit wallet recovery is required",
718
726
  "Use --export-evidence <PATH> with --acknowledge-full-note-plaintext-export to write a local full-note evidence ZIP for private-state-cli investigator",
719
727
  "Evidence export includes all local epochs for the selected wallet, including exited epochs retained for dispute evidence",
package/lib/runtime.mjs CHANGED
@@ -2338,10 +2338,12 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
2338
2338
  context,
2339
2339
  registration,
2340
2340
  });
2341
+ const walletRecoveryTargetBlock = walletNoteReceiveTargetBlock(context);
2341
2342
  const recoveryEventScan = await scanWalletRecoveryEvents({
2342
2343
  context,
2343
2344
  provider,
2344
2345
  l1Address: signer.address,
2346
+ toBlock: walletRecoveryTargetBlock,
2345
2347
  progressAction: "wallet recover-workspace",
2346
2348
  });
2347
2349
  const lifecycleEpoch = selectWalletLifecycleEpoch({
@@ -2400,66 +2402,8 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
2400
2402
  walletDir,
2401
2403
  })
2402
2404
  : null;
2403
-
2404
- if (existingWallet) {
2405
- existingWallet.wallet.noteReceivePrivateKey = noteReceiveKeyMaterial.privateKey;
2406
- applyWalletLifecycleEpoch(existingWallet.wallet, lifecycleEpoch);
2407
- if (recoveredSpendingIdentity) {
2408
- existingWallet.wallet.l2PrivateKey = ethers.hexlify(recoveredSpendingIdentity.l2PrivateKey);
2409
- existingWallet.wallet.l2PublicKey = ethers.hexlify(recoveredSpendingIdentity.l2PublicKey);
2410
- existingWallet.wallet.l2Address = recoveredSpendingIdentity.l2Address;
2411
- existingWallet.wallet.l2DerivationMode = CHANNEL_BOUND_L2_DERIVATION_MODE;
2412
- existingWallet.wallet.l2DerivationChannelName = channelName;
2413
- existingWallet.wallet.l2StorageKey = storageKey;
2414
- }
2415
- persistWalletKeys(existingWallet);
2416
- persistWallet(existingWallet);
2417
- persistWalletIndexForContext(existingWallet);
2418
- const noteScanStartBlock = args.fromGenesis === true
2419
- ? Number(context.workspace.genesisBlockNumber)
2420
- : requireUsableWalletNoteReceiveRecoveryIndex({
2421
- walletContext: existingWallet,
2422
- context,
2423
- latestBlock: recoveryEventScan.scanRange.toBlock,
2424
- });
2425
- const recoveredDeliveryState = await recoverDeliveredNotesFromCollectedLogs({
2426
- walletContext: existingWallet,
2427
- context,
2428
- noteReceivePrivateKey: noteReceiveKeyMaterial.privateKey,
2429
- logs: recoveryEventScan.deliveryLogs,
2430
- storageObservationLogs: recoveryEventScan.storageObservationLogs,
2431
- scanStartBlock: noteScanStartBlock,
2432
- latestBlock: recoveryEventScan.scanRange.toBlock,
2433
- });
2434
- printJson({
2435
- action: "wallet recover-workspace",
2436
- status: "already-recovered",
2437
- wallet: walletName,
2438
- walletDir: existingWallet.walletDir,
2439
- recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
2440
- channelAutoRecoveryBlockDelta: channelContextResult.autoRecoveryBlockDelta,
2441
- workspace: context.workspaceName,
2442
- channelName: context.workspace.channelName,
2443
- channelId: context.workspace.channelId,
2444
- l1Address: signer.address,
2445
- l2Address: l2Identity.l2Address,
2446
- l2StorageKey: storageKey,
2447
- spendingKeyRecovered: Boolean(recoveredSpendingIdentity),
2448
- leafIndex: lifecycleEpoch.leafIndex.toString(),
2449
- epochId: lifecycleEpoch.epochId,
2450
- lifecycleStatus: lifecycleEpoch.lifecycleStatus,
2451
- exitedAtTxHash: lifecycleEpoch.exitedAtTxHash,
2452
- noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
2453
- l2Nonce: existingWallet.wallet.l2Nonce,
2454
- recoveredFromLogs: recoveredDeliveryState.importedNotes,
2455
- scannedDeliveryLogs: recoveredDeliveryState.scannedLogs,
2456
- linkedEvidence: recoveredDeliveryState.linkedEvidence,
2457
- noteReceiveScanRange: recoveredDeliveryState.scanRange,
2458
- });
2459
- return;
2460
- }
2461
-
2462
- const walletContext = ensureWallet({
2405
+ const status = existingWallet ? "already-recovered" : "recovered";
2406
+ const walletContext = existingWallet ?? ensureWallet({
2463
2407
  channelContext: context,
2464
2408
  signerAddress: signer.address,
2465
2409
  signerPrivateKey: signer.privateKey,
@@ -2471,16 +2415,29 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
2471
2415
  lifecycleEpoch,
2472
2416
  rpcUrl,
2473
2417
  });
2474
- walletContext.wallet.l2Nonce = 0;
2475
- persistWallet(walletContext);
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
+ }
2476
2433
 
2477
2434
  const noteScanStartBlock = args.fromGenesis === true
2478
2435
  ? Number(context.workspace.genesisBlockNumber)
2479
- : requireUsableWalletNoteReceiveRecoveryIndex({
2436
+ : walletNoteReceiveCursorDelta({
2480
2437
  walletContext,
2481
2438
  context,
2482
- latestBlock: recoveryEventScan.scanRange.toBlock,
2483
- });
2439
+ targetNextBlock: recoveryEventScan.scanRange.toBlock + 1,
2440
+ }).localNextBlock;
2484
2441
  const recoveredDeliveryState = await recoverDeliveredNotesFromCollectedLogs({
2485
2442
  walletContext,
2486
2443
  context,
@@ -2493,7 +2450,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
2493
2450
 
2494
2451
  printJson({
2495
2452
  action: "wallet recover-workspace",
2496
- status: "recovered",
2453
+ status,
2497
2454
  wallet: walletName,
2498
2455
  walletDir: walletContext.walletDir,
2499
2456
  recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
@@ -4202,18 +4159,18 @@ function applyGuideNextAction(guide) {
4202
4159
  }
4203
4160
  if (guide.state.wallet?.exists && channelBalance !== null && channelBalance > 0n && unusedNotes === 0) {
4204
4161
  setGuideNextAction(guide, {
4205
- command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <A,B> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
4162
+ command: `wallet mint-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --amounts <JSON_ARRAY> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
4206
4163
  why: "The wallet has channel L2 balance and no unused private notes yet. Use --tx-submitter for stronger transaction-submission privacy.",
4207
4164
  });
4208
4165
  return;
4209
4166
  }
4210
4167
  if (guide.state.wallet?.exists && unusedNotes !== null && unusedNotes > 0) {
4211
4168
  setGuideNextAction(guide, {
4212
- 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>]`,
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>]`,
4213
4170
  why: "The wallet has unused private notes. It can transfer or redeem those notes. Use --tx-submitter for stronger transaction-submission privacy.",
4214
4171
  candidates: [
4215
4172
  `wallet get-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network}`,
4216
- `wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <ID> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
4173
+ `wallet redeem-notes --wallet ${guide.selectors.wallet} --network ${guide.selectors.network} --note-ids <JSON_ARRAY> --acknowledge-action-impact [--tx-submitter <ACCOUNT>]`,
4217
4174
  ],
4218
4175
  });
4219
4176
  return;
@@ -4500,9 +4457,13 @@ async function walletEpochFromJoinReceipt({ receipt, context, provider, l1Addres
4500
4457
  return epoch;
4501
4458
  }
4502
4459
 
4503
- async function scanWalletRecoveryEvents({ context, provider, l1Address, progressAction = null }) {
4460
+ async function scanWalletRecoveryEvents({ context, provider, l1Address, toBlock, progressAction = null }) {
4504
4461
  const fromBlock = Number(context.workspace.genesisBlockNumber ?? 0);
4505
- const toBlock = await fetchFreshBlockNumber(provider);
4462
+ const normalizedToBlock = Number(toBlock);
4463
+ expect(
4464
+ Number.isInteger(normalizedToBlock) && normalizedToBlock >= fromBlock - 1,
4465
+ "Wallet recovery event scan target block is invalid.",
4466
+ );
4506
4467
  const registeredTopic = context.channelManager.interface.getEvent("ChannelTokenVaultIdentityRegistered").topicHash;
4507
4468
  const exitedTopic = context.channelManager.interface.getEvent("ChannelTokenVaultIdentityExited").topicHash;
4508
4469
  const normalizedRegisteredTopic = normalizeBytes32Hex(registeredTopic);
@@ -4519,7 +4480,7 @@ async function scanWalletRecoveryEvents({ context, provider, l1Address, progress
4519
4480
  address: context.workspace.channelManager,
4520
4481
  topics: [[registeredTopic, exitedTopic, NOTE_VALUE_ENCRYPTED_TOPIC, CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC]],
4521
4482
  fromBlock,
4522
- toBlock,
4483
+ toBlock: normalizedToBlock,
4523
4484
  collectLogs: false,
4524
4485
  onProgress: progressAction
4525
4486
  ? createRpcLogScanProgress({ action: progressAction, label: "wallet-recovery events" })
@@ -4564,7 +4525,7 @@ async function scanWalletRecoveryEvents({ context, provider, l1Address, progress
4564
4525
  storageObservationLogs,
4565
4526
  scanRange: {
4566
4527
  fromBlock,
4567
- toBlock,
4528
+ toBlock: normalizedToBlock,
4568
4529
  },
4569
4530
  };
4570
4531
  }
@@ -5268,7 +5229,8 @@ async function handleWalletGetNotes({ args, provider }) {
5268
5229
  })
5269
5230
  : {
5270
5231
  nextBlock: wallet.wallet.noteReceiveLastScannedBlock,
5271
- latestBlock: await provider.getBlockNumber(),
5232
+ targetBlock: walletNoteReceiveTargetBlock(context),
5233
+ targetNextBlock: channelWorkspaceRecoveryTargetNextBlock(context),
5272
5234
  recoveredWalletWorkspace: false,
5273
5235
  recoveredDeliveryState: null,
5274
5236
  };
@@ -5320,7 +5282,8 @@ async function handleWalletGetNotes({ args, provider }) {
5320
5282
  spentTotalTokens: spentTotal === null ? null : ethers.formatUnits(spentTotal, canonicalAssetDecimals),
5321
5283
  bridgeStatusMismatches: [...unusedNotes, ...spentNotes].filter((note) => !note.walletStatusMatchesBridge).length,
5322
5284
  noteReceiveLastScannedBlock: noteReceiveFreshness.nextBlock,
5323
- latestBlock: noteReceiveFreshness.latestBlock,
5285
+ noteReceiveTargetBlock: noteReceiveFreshness.targetBlock,
5286
+ noteReceiveTargetNextBlock: noteReceiveFreshness.targetNextBlock,
5324
5287
  viewingKeyAvailable: Boolean(wallet.wallet.noteReceivePrivateKey),
5325
5288
  recoveredWalletWorkspace: noteReceiveFreshness.recoveredWalletWorkspace,
5326
5289
  recoveredFromLogs: noteReceiveFreshness.recoveredDeliveryState?.importedNotes ?? 0,
@@ -6581,6 +6544,7 @@ async function recoverWalletReceivedNotes({
6581
6544
  provider,
6582
6545
  signer,
6583
6546
  noteReceiveKeyMaterial = null,
6547
+ toBlock = null,
6584
6548
  progressAction = null,
6585
6549
  fromGenesis = false,
6586
6550
  }) {
@@ -6594,6 +6558,7 @@ async function recoverWalletReceivedNotes({
6594
6558
  context,
6595
6559
  provider,
6596
6560
  noteReceivePrivateKey: resolvedNoteReceiveKeyMaterial.privateKey,
6561
+ toBlock,
6597
6562
  progressAction,
6598
6563
  fromGenesis,
6599
6564
  });
@@ -6608,18 +6573,23 @@ async function recoverDeliveredNotesFromEventLogs({
6608
6573
  context,
6609
6574
  provider,
6610
6575
  noteReceivePrivateKey,
6576
+ toBlock = null,
6611
6577
  storageObservationLogs = null,
6612
6578
  progressAction = null,
6613
6579
  fromGenesis = false,
6614
6580
  }) {
6615
- 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
+ );
6616
6586
  const scanStartBlock = fromGenesis
6617
6587
  ? Number(context.workspace.genesisBlockNumber)
6618
- : requireUsableWalletNoteReceiveRecoveryIndex({
6588
+ : walletNoteReceiveCursorDelta({
6619
6589
  walletContext,
6620
6590
  context,
6621
- latestBlock,
6622
- });
6591
+ targetNextBlock: latestBlock + 1,
6592
+ }).localNextBlock;
6623
6593
  const scanRange = {
6624
6594
  fromBlock: scanStartBlock,
6625
6595
  toBlock: latestBlock,
@@ -6636,7 +6606,10 @@ async function recoverDeliveredNotesFromEventLogs({
6636
6606
  context,
6637
6607
  storageObservationLogs: storageObservationLogs ?? [],
6638
6608
  });
6639
- walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
6609
+ walletContext.wallet.noteReceiveLastScannedBlock = Math.max(
6610
+ Number(walletContext.wallet.noteReceiveLastScannedBlock),
6611
+ latestBlock + 1,
6612
+ );
6640
6613
  persistWallet(walletContext);
6641
6614
  return {
6642
6615
  importedNotes: [],
@@ -6746,7 +6719,10 @@ async function recoverDeliveredNotesFromCollectedLogs({
6746
6719
  context,
6747
6720
  storageObservationLogs,
6748
6721
  });
6749
- walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
6722
+ walletContext.wallet.noteReceiveLastScannedBlock = Math.max(
6723
+ Number(walletContext.wallet.noteReceiveLastScannedBlock),
6724
+ latestBlock + 1,
6725
+ );
6750
6726
  persistWallet(walletContext);
6751
6727
  return {
6752
6728
  importedNotes: [],
@@ -6873,40 +6849,88 @@ async function recoverDeliveredNoteCandidatesFromLogs({
6873
6849
  return importedCandidates;
6874
6850
  }
6875
6851
 
6876
- function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, latestBlock }) {
6877
- const nextBlock = Number(walletContext.wallet.noteReceiveLastScannedBlock);
6852
+ function channelWorkspaceRecoveryTargetNextBlock(context) {
6853
+ const targetNextBlock = Number(context.workspace.recoveryLastScannedBlock);
6878
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);
6879
6875
  if (
6880
- !Number.isInteger(nextBlock)
6881
- || nextBlock < genesisBlockNumber
6882
- || nextBlock > Number(latestBlock) + 1
6876
+ !Number.isInteger(normalizedLocalNextBlock)
6877
+ || !Number.isInteger(normalizedTargetNextBlock)
6878
+ || !Number.isInteger(normalizedGenesisBlockNumber)
6879
+ || normalizedLocalNextBlock < normalizedGenesisBlockNumber
6880
+ || normalizedTargetNextBlock < normalizedGenesisBlockNumber
6883
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) {
6884
6911
  throw new Error([
6885
6912
  `Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
6886
- `Expected noteReceiveLastScannedBlock to be an integer between ${genesisBlockNumber} and ${Number(latestBlock) + 1}.`,
6913
+ `Expected noteReceiveLastScannedBlock to be an integer greater than or equal to ${genesisBlockNumber}.`,
6887
6914
  "Run wallet recover-workspace --from-genesis to restart received-note scanning from channel genesis.",
6915
+ `Details: ${error.message}`,
6888
6916
  ].join(" "));
6889
6917
  }
6890
- return nextBlock;
6891
6918
  }
6892
6919
 
6893
- async function assertWalletNoteReceiveStateFresh({ walletContext, context, provider }) {
6894
- const latestBlock = await fetchFreshBlockNumber(provider);
6895
- const nextBlock = requireUsableWalletNoteReceiveRecoveryIndex({
6896
- walletContext,
6897
- context,
6898
- latestBlock,
6899
- });
6900
- if (nextBlock !== latestBlock + 1) {
6920
+ function assertWalletNoteReceiveStateFresh({ walletContext, context }) {
6921
+ const cursorDelta = walletNoteReceiveCursorDelta({ walletContext, context });
6922
+ if (!cursorDelta.fresh) {
6901
6923
  throw new Error([
6902
6924
  `Wallet note workspace is stale for wallet ${walletContext.walletName}.`,
6903
- `noteReceiveLastScannedBlock is ${nextBlock}, but latest block requires ${latestBlock + 1}.`,
6925
+ `noteReceiveLastScannedBlock is ${cursorDelta.localNextBlock}, but channel workspace recovery frontier requires ${cursorDelta.targetNextBlock}.`,
6904
6926
  "Run wallet recover-workspace before using commands that read or spend wallet notes.",
6905
6927
  ].join(" "));
6906
6928
  }
6907
6929
  return {
6908
- latestBlock,
6909
- nextBlock,
6930
+ targetBlock: cursorDelta.toBlock,
6931
+ targetNextBlock: cursorDelta.targetNextBlock,
6932
+ nextBlock: cursorDelta.localNextBlock,
6933
+ blockDelta: cursorDelta.blockDelta,
6910
6934
  };
6911
6935
  }
6912
6936
 
@@ -6918,14 +6942,9 @@ async function ensureWalletNoteReceiveStateCurrent({
6918
6942
  progressAction = null,
6919
6943
  preConsumedBlockDelta = 0,
6920
6944
  }) {
6921
- const latestBlock = await fetchFreshBlockNumber(provider);
6922
- let nextBlock;
6945
+ let cursorDelta;
6923
6946
  try {
6924
- nextBlock = requireUsableWalletNoteReceiveRecoveryIndex({
6925
- walletContext,
6926
- context,
6927
- latestBlock,
6928
- });
6947
+ cursorDelta = walletNoteReceiveCursorDelta({ walletContext, context });
6929
6948
  } catch (indexError) {
6930
6949
  throw new Error([
6931
6950
  `Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
@@ -6935,10 +6954,11 @@ async function ensureWalletNoteReceiveStateCurrent({
6935
6954
  ].join(" "));
6936
6955
  }
6937
6956
 
6938
- if (nextBlock === latestBlock + 1) {
6957
+ if (cursorDelta.fresh) {
6939
6958
  return {
6940
- latestBlock,
6941
- nextBlock,
6959
+ targetBlock: cursorDelta.toBlock,
6960
+ targetNextBlock: cursorDelta.targetNextBlock,
6961
+ nextBlock: cursorDelta.localNextBlock,
6942
6962
  recoveredWalletWorkspace: false,
6943
6963
  recoveredDeliveryState: null,
6944
6964
  autoRecoveryBlockDelta: 0,
@@ -6947,8 +6967,8 @@ async function ensureWalletNoteReceiveStateCurrent({
6947
6967
  const remainingBlockBudget = AUTO_RECOVERY_BLOCK_BUDGET - Math.max(0, Number(preConsumedBlockDelta));
6948
6968
  const autoRecoveryBlockDelta = assertAutoRecoveryBlockBudget({
6949
6969
  label: `wallet note workspace ${walletContext.walletName}`,
6950
- fromBlock: nextBlock,
6951
- toBlock: latestBlock,
6970
+ fromBlock: cursorDelta.fromBlock,
6971
+ toBlock: cursorDelta.toBlock,
6952
6972
  recoveryCommand: `wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT>`,
6953
6973
  blockBudget: remainingBlockBudget,
6954
6974
  });
@@ -6961,6 +6981,7 @@ async function ensureWalletNoteReceiveStateCurrent({
6961
6981
  context,
6962
6982
  provider,
6963
6983
  signer: resolvedSigner,
6984
+ toBlock: cursorDelta.toBlock,
6964
6985
  progressAction,
6965
6986
  fromGenesis: false,
6966
6987
  }));
@@ -6972,22 +6993,12 @@ async function ensureWalletNoteReceiveStateCurrent({
6972
6993
  `Details: ${recoveryError.message}`,
6973
6994
  ].join(" "));
6974
6995
  }
6975
- try {
6976
- const freshness = await assertWalletNoteReceiveStateFresh({ walletContext, context, provider });
6977
- return {
6978
- ...freshness,
6979
- recoveredWalletWorkspace: true,
6980
- recoveredDeliveryState,
6981
- autoRecoveryBlockDelta,
6982
- };
6983
- } catch (postRecoveryError) {
6984
- throw new Error([
6985
- `Wallet workspace is still stale after recovery-index sync for wallet ${walletContext.walletName}.`,
6986
- "Automatic wallet recovery will not replay from genesis.",
6987
- `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.`,
6988
- `Details: ${postRecoveryError.message}`,
6989
- ].join(" "));
6990
- }
6996
+ return {
6997
+ ...assertWalletNoteReceiveStateFresh({ walletContext, context }),
6998
+ recoveredWalletWorkspace: true,
6999
+ recoveredDeliveryState,
7000
+ autoRecoveryBlockDelta,
7001
+ };
6991
7002
  }
6992
7003
 
6993
7004
  function extractEncryptedNoteValueFromBridgeLog(log) {
@@ -7511,13 +7522,19 @@ async function requireChannelWorkspaceRecoveryIndexForAutoRefresh({
7511
7522
  if (!recoveryIndex) {
7512
7523
  fail(`Channel workspace recovery index is unusable for ${channelName} on ${networkName}.`);
7513
7524
  }
7514
- if (Number(recoveryIndex.nextBlock) > Number(latestBlock)) {
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) {
7515
7532
  fail(`Channel workspace recovery index has already scanned through block ${recoveryIndex.nextBlock - 1}, but the local snapshot is not current.`);
7516
7533
  }
7517
7534
  const autoRecoveryBlockDelta = assertAutoRecoveryBlockBudget({
7518
7535
  label: `channel workspace ${channelName} on ${networkName}`,
7519
- fromBlock: recoveryIndex.nextBlock,
7520
- toBlock: latestBlock,
7536
+ fromBlock: cursorDelta.fromBlock,
7537
+ toBlock: cursorDelta.toBlock,
7521
7538
  recoveryCommand: `channel recover-workspace --channel-name ${channelName} --network ${networkName}`,
7522
7539
  });
7523
7540
  return { alreadyCurrent: false, autoRecoveryBlockDelta };
@@ -7585,19 +7602,19 @@ async function recoverLocalWorkspacesAfterAcceptedNoteTransaction({
7585
7602
  walletContext: wallet,
7586
7603
  context,
7587
7604
  provider,
7605
+ toBlock: walletNoteReceiveTargetBlock(context),
7588
7606
  progressAction,
7589
7607
  fromGenesis: false,
7590
7608
  });
7591
7609
  const freshness = await assertWalletNoteReceiveStateFresh({
7592
7610
  walletContext: wallet,
7593
7611
  context,
7594
- provider,
7595
7612
  });
7596
7613
  return {
7597
7614
  channelRecoveryLastScannedBlock: context.workspace.recoveryLastScannedBlock,
7598
7615
  channelRecoveryScanRange: context.workspace.recoveryScanRange,
7599
7616
  walletNoteReceiveNextBlock: freshness.nextBlock,
7600
- walletLatestBlock: freshness.latestBlock,
7617
+ walletTargetBlock: freshness.targetBlock,
7601
7618
  recoveredFromLogs: recoveredDeliveryState.importedNotes,
7602
7619
  scannedDeliveryLogs: recoveredDeliveryState.scannedLogs,
7603
7620
  linkedEvidence: recoveredDeliveryState.linkedEvidence,
@@ -10268,15 +10285,19 @@ function assertAllowedCommandKeys(args, commandName, allowedKeys, acceptedUsage)
10268
10285
  `${commandName} only accepts ${acceptedUsage}. Unsupported option(s): ${unsupported.join(", ")}.`,
10269
10286
  );
10270
10287
  }
10271
- if (args.json !== undefined && args.json !== true) {
10272
- throw new Error(`${commandName} option --json does not accept a value.`);
10273
- }
10288
+ assertBooleanFlag(args, "json", `${commandName} option --json`);
10274
10289
  expect(
10275
10290
  (args.positional ?? []).length === 1,
10276
10291
  `${commandName} does not accept positional arguments beyond the command name.`,
10277
10292
  );
10278
10293
  }
10279
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
+
10280
10301
  function assertWalletChannelMoveArgs(args, commandName) {
10281
10302
  assertAllowedCommandSchema(args, commandName);
10282
10303
  assertActionImpactArg(args, COMMAND_ARG_SCHEMAS[commandName]?.label ?? commandName);
@@ -10284,9 +10305,7 @@ function assertWalletChannelMoveArgs(args, commandName) {
10284
10305
 
10285
10306
  function assertInstallZkEvmArgs(args) {
10286
10307
  assertAllowedCommandSchema(args, "install");
10287
- if (args.readOnly !== undefined && args.readOnly !== true) {
10288
- throw new Error("install option --read-only does not accept a value.");
10289
- }
10308
+ assertBooleanFlag(args, "readOnly", "install option --read-only");
10290
10309
  if (args.readOnly === true && args.docker !== undefined) {
10291
10310
  throw new Error("install --read-only does not accept --docker because proof runtimes are not installed.");
10292
10311
  }
@@ -10327,9 +10346,7 @@ function assertUpdateArgs(args) {
10327
10346
 
10328
10347
  function assertDoctorArgs(args) {
10329
10348
  assertAllowedCommandSchema(args, "help-doctor");
10330
- if (args.gpu !== undefined && args.gpu !== true) {
10331
- throw new Error("help doctor option --gpu does not accept a value.");
10332
- }
10349
+ assertBooleanFlag(args, "gpu", "help doctor option --gpu");
10333
10350
  }
10334
10351
 
10335
10352
  function assertGuideArgs(args) {
@@ -10405,12 +10422,7 @@ function assertTxSubmitterArg(args) {
10405
10422
  }
10406
10423
 
10407
10424
  function assertActionImpactArg(args, commandName) {
10408
- if (
10409
- args.acknowledgeActionImpact !== undefined
10410
- && args.acknowledgeActionImpact !== true
10411
- ) {
10412
- throw new Error(`${commandName} option --acknowledge-action-impact does not accept a value.`);
10413
- }
10425
+ assertBooleanFlag(args, "acknowledgeActionImpact", `${commandName} option --acknowledge-action-impact`);
10414
10426
  if (args.acknowledgeActionImpact !== true && !process.stdin.isTTY) {
10415
10427
  throw new Error(`${commandName} requires --acknowledge-action-impact after reviewing the action-impact warning.`);
10416
10428
  }
@@ -10426,12 +10438,11 @@ function assertWalletGetNotesArgs(args) {
10426
10438
  );
10427
10439
  }
10428
10440
  }
10429
- if (
10430
- args.acknowledgeFullNotePlaintextExport !== undefined
10431
- && args.acknowledgeFullNotePlaintextExport !== true
10432
- ) {
10433
- throw new Error("wallet get-notes option --acknowledge-full-note-plaintext-export does not accept a value.");
10434
- }
10441
+ assertBooleanFlag(
10442
+ args,
10443
+ "acknowledgeFullNotePlaintextExport",
10444
+ "wallet get-notes option --acknowledge-full-note-plaintext-export",
10445
+ );
10435
10446
  }
10436
10447
 
10437
10448
  function assertCreateChannelArgs(args) {
@@ -10441,12 +10452,8 @@ function assertCreateChannelArgs(args) {
10441
10452
  function assertRecoverWorkspaceArgs(args) {
10442
10453
  assertAllowedCommandSchema(args, "channel-recover-workspace");
10443
10454
  const source = resolveWorkspaceRecoverySource(args);
10444
- if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
10445
- throw new Error("channel recover-workspace option --from-genesis does not accept a value.");
10446
- }
10447
- if (args.outputRaw !== undefined && args.outputRaw !== true) {
10448
- throw new Error("channel recover-workspace option --output-raw does not accept a value.");
10449
- }
10455
+ assertBooleanFlag(args, "fromGenesis", "channel recover-workspace option --from-genesis");
10456
+ assertBooleanFlag(args, "outputRaw", "channel recover-workspace option --output-raw");
10450
10457
  if (args.outputRaw === true && source !== "rpc") {
10451
10458
  throw new Error("channel recover-workspace option --output-raw requires --source rpc.");
10452
10459
  }
@@ -10464,9 +10471,7 @@ function assertSetWorkspaceMirrorArgs(args) {
10464
10471
  function assertPublishWorkspaceMirrorArgs(args) {
10465
10472
  assertAllowedCommandSchema(args, "channel-publish-workspace-mirror");
10466
10473
  requireArg(args.output, "--output");
10467
- if (args.force !== undefined && args.force !== true) {
10468
- throw new Error("channel publish-workspace-mirror option --force does not accept a value.");
10469
- }
10474
+ assertBooleanFlag(args, "force", "channel publish-workspace-mirror option --force");
10470
10475
  }
10471
10476
 
10472
10477
  function assertDepositBridgeArgs(args) {
@@ -10480,9 +10485,7 @@ function assertAccountGetBridgeFundArgs(args) {
10480
10485
 
10481
10486
  function assertRecoverWalletArgs(args) {
10482
10487
  assertAllowedCommandSchema(args, "wallet-recover-workspace");
10483
- if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
10484
- throw new Error("wallet recover-workspace option --from-genesis does not accept a value.");
10485
- }
10488
+ assertBooleanFlag(args, "fromGenesis", "wallet recover-workspace option --from-genesis");
10486
10489
  }
10487
10490
 
10488
10491
  function assertJoinChannelArgs(args) {
@@ -11185,7 +11188,14 @@ function getUsableWorkspaceRecoveryIndex({
11185
11188
  return null;
11186
11189
  }
11187
11190
  const recoveryRootVectorHash = normalizeBytes32Hex(workspace.recoveryRootVectorHash);
11188
- if (!Number.isInteger(nextBlock) || nextBlock < Number(genesisBlockNumber) || nextBlock > Number(latestBlock) + 1) {
11191
+ try {
11192
+ computeRecoveryCursorDelta({
11193
+ localNextBlock: nextBlock,
11194
+ targetNextBlock: Number(latestBlock) + 1,
11195
+ genesisBlockNumber,
11196
+ label: "Channel workspace",
11197
+ });
11198
+ } catch {
11189
11199
  return null;
11190
11200
  }
11191
11201
  if (recoveryRootVectorHash === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Command-line client for the Tokamak private-state DApp.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "Tokamak Network",