@cogcoin/client 0.5.5 → 0.5.6

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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
  3. package/dist/bitcoind/bootstrap/chainstate.js +4 -1
  4. package/dist/bitcoind/bootstrap/controller.d.ts +4 -1
  5. package/dist/bitcoind/bootstrap/controller.js +42 -5
  6. package/dist/bitcoind/bootstrap/headers.d.ts +12 -0
  7. package/dist/bitcoind/bootstrap/headers.js +95 -10
  8. package/dist/bitcoind/client/factory.js +11 -2
  9. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  10. package/dist/bitcoind/client/managed-client.js +2 -2
  11. package/dist/bitcoind/client/sync-engine.js +48 -13
  12. package/dist/bitcoind/indexer-daemon.d.ts +7 -0
  13. package/dist/bitcoind/indexer-daemon.js +31 -22
  14. package/dist/bitcoind/processing-start-height.d.ts +7 -0
  15. package/dist/bitcoind/processing-start-height.js +9 -0
  16. package/dist/bitcoind/progress/controller.js +1 -0
  17. package/dist/bitcoind/progress/formatting.js +4 -1
  18. package/dist/bitcoind/retryable-rpc.d.ts +11 -0
  19. package/dist/bitcoind/retryable-rpc.js +30 -0
  20. package/dist/bitcoind/service.d.ts +16 -1
  21. package/dist/bitcoind/service.js +228 -115
  22. package/dist/bitcoind/testing.d.ts +1 -1
  23. package/dist/bitcoind/testing.js +1 -1
  24. package/dist/bitcoind/types.d.ts +1 -0
  25. package/dist/cli/commands/follow.js +9 -0
  26. package/dist/cli/commands/service-runtime.js +150 -134
  27. package/dist/cli/commands/sync.js +9 -0
  28. package/dist/cli/commands/wallet-admin.js +77 -21
  29. package/dist/cli/context.js +4 -2
  30. package/dist/cli/mutation-json.js +2 -0
  31. package/dist/cli/output.js +2 -0
  32. package/dist/cli/parse.d.ts +1 -1
  33. package/dist/cli/parse.js +6 -0
  34. package/dist/cli/preview-json.js +2 -0
  35. package/dist/cli/runner.js +1 -0
  36. package/dist/cli/types.d.ts +6 -3
  37. package/dist/cli/types.js +1 -1
  38. package/dist/cli/wallet-format.js +134 -14
  39. package/dist/wallet/lifecycle.d.ts +6 -0
  40. package/dist/wallet/lifecycle.js +109 -37
  41. package/dist/wallet/read/context.js +10 -4
  42. package/dist/wallet/reset.d.ts +61 -2
  43. package/dist/wallet/reset.js +208 -63
  44. package/dist/wallet/root-resolution.d.ts +20 -0
  45. package/dist/wallet/root-resolution.js +37 -0
  46. package/dist/wallet/runtime.d.ts +1 -0
  47. package/dist/wallet/runtime.js +1 -0
  48. package/dist/wallet/state/crypto.d.ts +3 -0
  49. package/dist/wallet/state/crypto.js +3 -0
  50. package/dist/wallet/state/storage.d.ts +7 -1
  51. package/dist/wallet/state/storage.js +39 -0
  52. package/dist/wallet/types.d.ts +1 -0
  53. package/package.json +1 -1
@@ -1,9 +1,12 @@
1
+ import { createRpcClient } from "../bitcoind/node.js";
2
+ import { attachOrStartManagedBitcoindService } from "../bitcoind/service.js";
1
3
  import { type WalletRuntimePaths } from "./runtime.js";
2
4
  import { type WalletSecretProvider } from "./state/provider.js";
3
5
  import type { WalletPrompter } from "./lifecycle.js";
4
- export type WalletResetAction = "not-present" | "kept-unchanged" | "reset-base-entropy" | "deleted";
6
+ export type WalletResetAction = "not-present" | "kept-unchanged" | "retain-mnemonic" | "deleted";
5
7
  export type WalletResetSecretCleanupStatus = "deleted" | "not-found" | "failed" | "unknown";
6
8
  export type WalletResetSnapshotResultStatus = "not-present" | "invalid-removed" | "deleted" | "preserved";
9
+ export type WalletResetBitcoinDataDirResultStatus = "not-present" | "preserved" | "deleted" | "outside-reset-scope";
7
10
  export interface WalletResetResult {
8
11
  dataRoot: string;
9
12
  factoryResetReady: true;
@@ -24,13 +27,17 @@ export interface WalletResetResult {
24
27
  status: WalletResetSnapshotResultStatus;
25
28
  path: string;
26
29
  };
30
+ bitcoinDataDir: {
31
+ status: WalletResetBitcoinDataDirResultStatus;
32
+ path: string;
33
+ };
27
34
  removedPaths: string[];
28
35
  }
29
36
  export interface WalletResetPreview {
30
37
  dataRoot: string;
31
38
  confirmationPhrase: "permanently reset";
32
39
  walletPrompt: null | {
33
- defaultAction: "reset-base-entropy";
40
+ defaultAction: "retain-mnemonic";
34
41
  acceptedInputs: ["", "skip", "delete wallet"];
35
42
  entropyRetainingResetAvailable: boolean;
36
43
  requiresPassphrase: boolean;
@@ -41,10 +48,59 @@ export interface WalletResetPreview {
41
48
  path: string;
42
49
  defaultAction: "preserve" | "delete";
43
50
  };
51
+ bitcoinDataDir: {
52
+ status: "not-present" | "within-reset-scope" | "outside-reset-scope";
53
+ path: string;
54
+ conditionalPrompt: null | {
55
+ prompt: "Delete managed Bitcoin datadir too? [y/N]: ";
56
+ defaultAction: "preserve";
57
+ acceptedInputs: ["", "n", "no", "y", "yes"];
58
+ };
59
+ };
44
60
  trackedProcessKinds: Array<"managed-bitcoind" | "indexer-daemon" | "background-mining">;
45
61
  willDeleteOsSecrets: boolean;
46
62
  removedPaths: string[];
47
63
  }
64
+ interface ResetWalletRpcClient {
65
+ getDescriptorInfo(descriptor: string): Promise<{
66
+ descriptor: string;
67
+ checksum: string;
68
+ }>;
69
+ createWallet(walletName: string, options: {
70
+ blank: boolean;
71
+ descriptors: boolean;
72
+ disablePrivateKeys: boolean;
73
+ loadOnStartup: boolean;
74
+ passphrase: string;
75
+ }): Promise<unknown>;
76
+ walletPassphrase(walletName: string, passphrase: string, timeoutSeconds: number): Promise<null>;
77
+ importDescriptors(walletName: string, requests: Array<{
78
+ desc: string;
79
+ timestamp: string | number;
80
+ active?: boolean;
81
+ internal?: boolean;
82
+ range?: number | [number, number];
83
+ }>): Promise<Array<{
84
+ success: boolean;
85
+ }>>;
86
+ walletLock(walletName: string): Promise<null>;
87
+ deriveAddresses(descriptor: string, range?: number | [number, number]): Promise<string[]>;
88
+ listDescriptors(walletName: string, privateOnly?: boolean): Promise<{
89
+ descriptors: Array<{
90
+ desc: string;
91
+ }>;
92
+ }>;
93
+ getWalletInfo(walletName: string): Promise<{
94
+ walletname: string;
95
+ private_keys_enabled: boolean;
96
+ descriptors: boolean;
97
+ }>;
98
+ loadWallet(walletName: string, loadOnStartup?: boolean): Promise<{
99
+ name: string;
100
+ warning: string;
101
+ }>;
102
+ listWallets(): Promise<string[]>;
103
+ }
48
104
  export declare function previewResetWallet(options: {
49
105
  dataDir: string;
50
106
  provider?: WalletSecretProvider;
@@ -58,4 +114,7 @@ export declare function resetWallet(options: {
58
114
  nowUnixMs?: number;
59
115
  paths?: WalletRuntimePaths;
60
116
  validateSnapshotFile?: (path: string) => Promise<void>;
117
+ attachService?: typeof attachOrStartManagedBitcoindService;
118
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => ResetWalletRpcClient;
61
119
  }): Promise<WalletResetResult>;
120
+ export {};
@@ -2,15 +2,18 @@ import { randomBytes } from "node:crypto";
2
2
  import { access, constants, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm } from "node:fs/promises";
3
3
  import { dirname, join, relative } from "node:path";
4
4
  import { DEFAULT_SNAPSHOT_METADATA } from "../bitcoind/bootstrap/constants.js";
5
+ import { createRpcClient } from "../bitcoind/node.js";
5
6
  import { resolveBootstrapPathsForTesting } from "../bitcoind/bootstrap/paths.js";
6
7
  import { validateSnapshotFileForTesting } from "../bitcoind/bootstrap/snapshot-file.js";
8
+ import { attachOrStartManagedBitcoindService, createManagedWalletReplica, } from "../bitcoind/service.js";
9
+ import { resolveNormalizedWalletDescriptorState } from "./descriptor-normalization.js";
7
10
  import { acquireFileLock } from "./fs/lock.js";
8
11
  import { createInternalCoreWalletPassphrase, deriveWalletIdentityMaterial, deriveWalletMaterialFromMnemonic, } from "./material.js";
9
12
  import { loadMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
10
13
  import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
11
14
  import { loadWalletExplicitLock } from "./state/explicit-lock.js";
12
15
  import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
13
- import { loadWalletState, saveWalletState } from "./state/storage.js";
16
+ import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, loadWalletState, saveWalletState, } from "./state/storage.js";
14
17
  import { confirmTypedAcknowledgement } from "./tx/confirm.js";
15
18
  function sanitizeWalletName(walletRootId) {
16
19
  return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
@@ -42,41 +45,6 @@ async function readJsonFileOrNull(path) {
42
45
  return null;
43
46
  }
44
47
  }
45
- async function readWalletEnvelope(path) {
46
- try {
47
- return JSON.parse(await readFile(path, "utf8"));
48
- }
49
- catch (error) {
50
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
51
- return null;
52
- }
53
- return null;
54
- }
55
- }
56
- async function loadRawWalletEnvelope(paths) {
57
- const primary = await readWalletEnvelope(paths.walletStatePath);
58
- if (primary !== null) {
59
- return {
60
- source: "primary",
61
- envelope: primary,
62
- };
63
- }
64
- const backup = await readWalletEnvelope(paths.walletStateBackupPath);
65
- if (backup !== null) {
66
- return {
67
- source: "backup",
68
- envelope: backup,
69
- };
70
- }
71
- return null;
72
- }
73
- function extractWalletRootIdFromSecretKeyId(keyId) {
74
- if (keyId === null) {
75
- return null;
76
- }
77
- const prefix = "wallet-state:";
78
- return keyId.startsWith(prefix) ? keyId.slice(prefix.length) : null;
79
- }
80
48
  async function isProcessAlive(pid) {
81
49
  if (pid === null) {
82
50
  return false;
@@ -300,6 +268,72 @@ function createEntropyRetainedWalletState(previousState, nowUnixMs) {
300
268
  pendingMutations: [],
301
269
  };
302
270
  }
271
+ async function recreateManagedCoreWalletReplicaForReset(options) {
272
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
273
+ dataDir: options.dataDir,
274
+ chain: "main",
275
+ startHeight: 0,
276
+ walletRootId: options.state.walletRootId,
277
+ managedWalletPassphrase: options.state.managedCoreWallet.internalPassphrase,
278
+ });
279
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
280
+ await createManagedWalletReplica(rpc, options.state.walletRootId, {
281
+ managedWalletPassphrase: options.state.managedCoreWallet.internalPassphrase,
282
+ });
283
+ const normalizedDescriptors = await resolveNormalizedWalletDescriptorState(options.state, rpc);
284
+ const walletName = sanitizeWalletName(options.state.walletRootId);
285
+ await rpc.walletPassphrase(walletName, options.state.managedCoreWallet.internalPassphrase, 10);
286
+ try {
287
+ const importResults = await rpc.importDescriptors(walletName, [{
288
+ desc: normalizedDescriptors.privateExternal,
289
+ timestamp: options.state.walletBirthTime,
290
+ active: false,
291
+ internal: false,
292
+ range: [0, options.state.descriptor.rangeEnd],
293
+ }]);
294
+ if (!importResults.every((result) => result.success)) {
295
+ throw new Error(`wallet_descriptor_import_failed_${JSON.stringify(importResults)}`);
296
+ }
297
+ }
298
+ finally {
299
+ await rpc.walletLock(walletName).catch(() => undefined);
300
+ }
301
+ const derivedFunding = await rpc.deriveAddresses(normalizedDescriptors.publicExternal, [0, 0]);
302
+ if (derivedFunding[0] !== options.state.funding.address) {
303
+ throw new Error("wallet_funding_address_verification_failed");
304
+ }
305
+ const descriptors = await rpc.listDescriptors(walletName);
306
+ const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === normalizedDescriptors.publicExternal);
307
+ if (importedDescriptor == null) {
308
+ throw new Error("wallet_descriptor_not_present_after_import");
309
+ }
310
+ const nextState = {
311
+ ...options.state,
312
+ stateRevision: options.state.stateRevision + 1,
313
+ lastWrittenAtUnixMs: options.nowUnixMs,
314
+ descriptor: {
315
+ ...options.state.descriptor,
316
+ privateExternal: normalizedDescriptors.privateExternal,
317
+ publicExternal: normalizedDescriptors.publicExternal,
318
+ checksum: normalizedDescriptors.checksum,
319
+ },
320
+ managedCoreWallet: {
321
+ ...options.state.managedCoreWallet,
322
+ walletName,
323
+ descriptorChecksum: normalizedDescriptors.checksum,
324
+ fundingAddress0: options.state.funding.address,
325
+ fundingScriptPubKeyHex0: options.state.funding.scriptPubKeyHex,
326
+ proofStatus: "ready",
327
+ lastImportedAtUnixMs: options.nowUnixMs,
328
+ lastVerifiedAtUnixMs: options.nowUnixMs,
329
+ },
330
+ };
331
+ await saveWalletState({
332
+ primaryPath: options.paths.walletStatePath,
333
+ backupPath: options.paths.walletStateBackupPath,
334
+ }, nextState, options.access);
335
+ return nextState;
336
+ }
303
337
  async function promptHiddenOrVisible(prompter, message) {
304
338
  if (typeof prompter.promptHidden === "function") {
305
339
  return await prompter.promptHidden(message);
@@ -421,22 +455,53 @@ async function collectTrackedManagedProcesses(paths) {
421
455
  serviceLockPaths: [...serviceLockPaths].sort(),
422
456
  };
423
457
  }
424
- function resolveRemovedRoots(paths) {
458
+ function dedupeSortedPaths(candidates) {
459
+ return [...new Set(candidates)].sort((left, right) => right.length - left.length);
460
+ }
461
+ function resolveDefaultRemovedRoots(paths) {
462
+ const configRoot = dirname(paths.clientConfigPath);
463
+ return dedupeSortedPaths([
464
+ paths.dataRoot,
465
+ paths.stateRoot,
466
+ paths.runtimeRoot,
467
+ configRoot,
468
+ ]);
469
+ }
470
+ function resolveBitcoindPreservingRemovedRoots(paths) {
425
471
  const configRoot = dirname(paths.clientConfigPath);
426
- return [...new Set([
427
- paths.dataRoot,
428
- paths.stateRoot,
429
- paths.runtimeRoot,
430
- configRoot,
431
- ])].sort((left, right) => right.length - left.length);
472
+ return dedupeSortedPaths([
473
+ paths.clientDataDir,
474
+ paths.indexerRoot,
475
+ paths.stateRoot,
476
+ paths.runtimeRoot,
477
+ configRoot,
478
+ paths.hooksRoot,
479
+ ]);
480
+ }
481
+ function resolveRemovedRoots(paths, options = {
482
+ preserveBitcoinDataDir: false,
483
+ }) {
484
+ return options.preserveBitcoinDataDir
485
+ ? resolveBitcoindPreservingRemovedRoots(paths)
486
+ : resolveDefaultRemovedRoots(paths);
487
+ }
488
+ function isDeletedByRemovalPlan(removedRoots, targetPath) {
489
+ return removedRoots.some((root) => isPathWithin(root, targetPath));
432
490
  }
433
491
  async function preflightReset(options) {
434
- const rawEnvelope = await loadRawWalletEnvelope(options.paths);
492
+ const removedRoots = resolveRemovedRoots(options.paths);
493
+ const rawEnvelope = await loadRawWalletStateEnvelope({
494
+ primaryPath: options.paths.walletStatePath,
495
+ backupPath: options.paths.walletStateBackupPath,
496
+ });
435
497
  const explicitLock = await loadWalletExplicitLock(options.paths.walletExplicitLockPath).catch(() => null);
436
498
  const snapshotPaths = resolveBootstrapPathsForTesting(options.dataDir, DEFAULT_SNAPSHOT_METADATA);
437
499
  const validateSnapshot = options.validateSnapshotFile
438
500
  ?? ((path) => validateSnapshotFileForTesting(path, DEFAULT_SNAPSHOT_METADATA));
439
501
  const hasWalletState = await pathExists(options.paths.walletStatePath) || await pathExists(options.paths.walletStateBackupPath);
502
+ const hasBitcoinDataDir = await pathExists(options.dataDir);
503
+ const bitcoinDataDirWithinResetScope = hasBitcoinDataDir
504
+ && isDeletedByRemovalPlan(removedRoots, options.dataDir);
440
505
  const hasSnapshot = await pathExists(snapshotPaths.snapshotPath);
441
506
  const hasPartialSnapshot = await pathExists(snapshotPaths.partialSnapshotPath);
442
507
  let snapshotStatus = "not-present";
@@ -456,7 +521,7 @@ async function preflightReset(options) {
456
521
  const secretProviderKeyId = rawEnvelope?.envelope.secretProvider?.keyId ?? null;
457
522
  return {
458
523
  dataRoot: options.paths.dataRoot,
459
- removedRoots: resolveRemovedRoots(options.paths),
524
+ removedRoots,
460
525
  wallet: {
461
526
  present: hasWalletState,
462
527
  mode: rawEnvelope == null
@@ -473,8 +538,16 @@ async function preflightReset(options) {
473
538
  status: snapshotStatus,
474
539
  path: snapshotPaths.snapshotPath,
475
540
  shouldPrompt: snapshotStatus === "valid",
476
- shouldStageForPreserve: snapshotStatus === "valid"
477
- && resolveRemovedRoots(options.paths).some((root) => isPathWithin(root, snapshotPaths.snapshotPath)),
541
+ withinResetScope: isDeletedByRemovalPlan(removedRoots, snapshotPaths.snapshotPath),
542
+ },
543
+ bitcoinDataDir: {
544
+ status: !hasBitcoinDataDir
545
+ ? "not-present"
546
+ : bitcoinDataDirWithinResetScope
547
+ ? "within-reset-scope"
548
+ : "outside-reset-scope",
549
+ path: options.dataDir,
550
+ shouldPrompt: bitcoinDataDirWithinResetScope,
478
551
  },
479
552
  trackedProcesses: tracked.trackedProcesses,
480
553
  trackedProcessKinds: tracked.trackedProcessKinds,
@@ -515,6 +588,18 @@ async function deleteRemovedRoots(roots) {
515
588
  throw new Error("reset_data_root_delete_failed");
516
589
  }
517
590
  }
591
+ async function deleteBootstrapSnapshotArtifacts(dataDir) {
592
+ const snapshotPaths = resolveBootstrapPathsForTesting(dataDir, DEFAULT_SNAPSHOT_METADATA);
593
+ await Promise.all([
594
+ snapshotPaths.snapshotPath,
595
+ snapshotPaths.partialSnapshotPath,
596
+ snapshotPaths.statePath,
597
+ snapshotPaths.quoteStatePath,
598
+ ].map(async (path) => rm(path, {
599
+ recursive: false,
600
+ force: true,
601
+ })));
602
+ }
518
603
  async function resolveResetExecutionDecision(options) {
519
604
  if (!options.prompter.isInteractive) {
520
605
  throw new Error("reset_requires_tty");
@@ -544,13 +629,19 @@ async function resolveResetExecutionDecision(options) {
544
629
  }
545
630
  }
546
631
  let deleteSnapshot = false;
632
+ let deleteBitcoinDataDir = false;
547
633
  if (options.preflight.snapshot.shouldPrompt) {
548
634
  const answer = (await options.prompter.prompt("Delete downloaded 910000 UTXO snapshot too? [y/N]: ")).trim().toLowerCase();
549
635
  deleteSnapshot = answer === "y" || answer === "yes";
636
+ if (!deleteSnapshot && options.preflight.bitcoinDataDir.shouldPrompt) {
637
+ const bitcoindAnswer = (await options.prompter.prompt("Delete managed Bitcoin datadir too? [y/N]: ")).trim().toLowerCase();
638
+ deleteBitcoinDataDir = bitcoindAnswer === "y" || bitcoindAnswer === "yes";
639
+ }
550
640
  }
551
641
  return {
552
642
  walletChoice,
553
643
  deleteSnapshot,
644
+ deleteBitcoinDataDir,
554
645
  loadedWalletForEntropyReset,
555
646
  };
556
647
  }
@@ -564,7 +655,7 @@ function determineWalletAction(walletPresent, walletChoice) {
564
655
  if (walletChoice === "delete wallet") {
565
656
  return "deleted";
566
657
  }
567
- return "reset-base-entropy";
658
+ return "retain-mnemonic";
568
659
  }
569
660
  function determineSnapshotResultStatus(options) {
570
661
  if (options.snapshotStatus === "not-present") {
@@ -575,6 +666,18 @@ function determineSnapshotResultStatus(options) {
575
666
  }
576
667
  return options.deleteSnapshot ? "deleted" : "preserved";
577
668
  }
669
+ function determineBitcoinDataDirResultStatus(options) {
670
+ if (options.bitcoinDataDirStatus === "not-present") {
671
+ return "not-present";
672
+ }
673
+ if (options.bitcoinDataDirStatus === "outside-reset-scope") {
674
+ return "outside-reset-scope";
675
+ }
676
+ if (options.deleteSnapshot || options.deleteBitcoinDataDir) {
677
+ return "deleted";
678
+ }
679
+ return "preserved";
680
+ }
578
681
  export async function previewResetWallet(options) {
579
682
  const provider = options.provider ?? createDefaultWalletSecretProvider();
580
683
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
@@ -584,12 +687,15 @@ export async function previewResetWallet(options) {
584
687
  paths,
585
688
  validateSnapshotFile: options.validateSnapshotFile,
586
689
  });
690
+ const removedPaths = resolveRemovedRoots(paths, {
691
+ preserveBitcoinDataDir: preflight.snapshot.status === "valid" && preflight.bitcoinDataDir.shouldPrompt,
692
+ });
587
693
  return {
588
694
  dataRoot: preflight.dataRoot,
589
695
  confirmationPhrase: "permanently reset",
590
696
  walletPrompt: preflight.wallet.present
591
697
  ? {
592
- defaultAction: "reset-base-entropy",
698
+ defaultAction: "retain-mnemonic",
593
699
  acceptedInputs: ["", "skip", "delete wallet"],
594
700
  entropyRetainingResetAvailable: preflight.wallet.mode !== "unknown",
595
701
  requiresPassphrase: preflight.wallet.mode === "passphrase-wrapped",
@@ -601,9 +707,20 @@ export async function previewResetWallet(options) {
601
707
  path: preflight.snapshot.path,
602
708
  defaultAction: preflight.snapshot.status === "valid" ? "preserve" : "delete",
603
709
  },
710
+ bitcoinDataDir: {
711
+ status: preflight.bitcoinDataDir.status,
712
+ path: preflight.bitcoinDataDir.path,
713
+ conditionalPrompt: preflight.bitcoinDataDir.shouldPrompt
714
+ ? {
715
+ prompt: "Delete managed Bitcoin datadir too? [y/N]: ",
716
+ defaultAction: "preserve",
717
+ acceptedInputs: ["", "n", "no", "y", "yes"],
718
+ }
719
+ : null,
720
+ },
604
721
  trackedProcessKinds: preflight.trackedProcessKinds,
605
722
  willDeleteOsSecrets: preflight.wallet.secretProviderKeyId !== null,
606
- removedPaths: preflight.removedRoots,
723
+ removedPaths,
607
724
  };
608
725
  }
609
726
  export async function resetWallet(options) {
@@ -627,6 +744,14 @@ export async function resetWallet(options) {
627
744
  snapshotStatus: preflight.snapshot.status,
628
745
  deleteSnapshot: decision.deleteSnapshot,
629
746
  });
747
+ const bitcoinDataDirResultStatus = determineBitcoinDataDirResultStatus({
748
+ bitcoinDataDirStatus: preflight.bitcoinDataDir.status,
749
+ deleteSnapshot: decision.deleteSnapshot,
750
+ deleteBitcoinDataDir: decision.deleteBitcoinDataDir,
751
+ });
752
+ const removedPaths = resolveRemovedRoots(paths, {
753
+ preserveBitcoinDataDir: bitcoinDataDirResultStatus === "preserved",
754
+ });
630
755
  const locks = await acquireResetLocks(paths, preflight.serviceLockPaths);
631
756
  await mkdir(dirname(paths.dataRoot), { recursive: true });
632
757
  const stagingRoot = await mkdtemp(join(dirname(paths.dataRoot), ".cogcoin-reset-"));
@@ -647,13 +772,13 @@ export async function resetWallet(options) {
647
772
  const deletedSecretRefs = [];
648
773
  const failedSecretRefs = [];
649
774
  const preservedSecretRefs = [];
650
- let walletOldRootId = extractWalletRootIdFromSecretKeyId(preflight.wallet.secretProviderKeyId)
775
+ let walletOldRootId = extractWalletRootIdHintFromWalletStateEnvelope(preflight.wallet.rawEnvelope?.envelope ?? null)
651
776
  ?? preflight.wallet.explicitLock?.walletRootId
652
777
  ?? null;
653
778
  let walletNewRootId = null;
654
779
  try {
655
780
  stoppedProcesses = await terminateTrackedProcesses(preflight.trackedProcesses);
656
- if (walletAction === "kept-unchanged" || walletAction === "reset-base-entropy") {
781
+ if (walletAction === "kept-unchanged" || walletAction === "retain-mnemonic") {
657
782
  const stagedPrimary = await stageArtifact(paths.walletStatePath, stagingRoot, "wallet/wallet-state.enc");
658
783
  const stagedBackup = await stageArtifact(paths.walletStateBackupPath, stagingRoot, "wallet/wallet-state.enc.bak");
659
784
  const stagedExplicitLock = await stageArtifact(paths.walletExplicitLockPath, stagingRoot, "wallet/wallet-explicit-lock.json");
@@ -667,43 +792,59 @@ export async function resetWallet(options) {
667
792
  stagedWalletArtifacts.push(stagedExplicitLock);
668
793
  }
669
794
  }
670
- if (snapshotResultStatus === "preserved" && preflight.snapshot.shouldStageForPreserve) {
795
+ if (snapshotResultStatus === "preserved" && isDeletedByRemovalPlan(removedPaths, preflight.snapshot.path)) {
671
796
  const stagedSnapshot = await stageArtifact(preflight.snapshot.path, stagingRoot, "snapshot/utxo-910000.dat");
672
797
  if (stagedSnapshot !== null) {
673
798
  stagedSnapshotArtifacts.push(stagedSnapshot);
674
799
  }
675
800
  }
676
- await deleteRemovedRoots(preflight.removedRoots);
801
+ await deleteRemovedRoots(removedPaths);
677
802
  rootsDeleted = true;
803
+ if ((snapshotResultStatus === "deleted" || snapshotResultStatus === "invalid-removed")
804
+ && !isDeletedByRemovalPlan(removedPaths, preflight.snapshot.path)) {
805
+ await deleteBootstrapSnapshotArtifacts(options.dataDir);
806
+ }
678
807
  if (walletAction === "kept-unchanged") {
679
808
  await restoreStagedArtifacts(stagedWalletArtifacts);
680
809
  }
681
- else if (walletAction === "reset-base-entropy") {
810
+ else if (walletAction === "retain-mnemonic") {
682
811
  if (decision.loadedWalletForEntropyReset === null) {
683
812
  throw new Error("reset_wallet_entropy_reset_unavailable");
684
813
  }
685
- const nextState = createEntropyRetainedWalletState(decision.loadedWalletForEntropyReset.loaded.state, nowUnixMs);
814
+ let nextState = createEntropyRetainedWalletState(decision.loadedWalletForEntropyReset.loaded.state, nowUnixMs);
686
815
  walletOldRootId = decision.loadedWalletForEntropyReset.loaded.state.walletRootId;
687
816
  walletNewRootId = nextState.walletRootId;
817
+ let nextAccess;
688
818
  if (decision.loadedWalletForEntropyReset.access.kind === "provider") {
689
819
  const secretReference = createWalletSecretReference(nextState.walletRootId);
690
820
  newProviderKeyId = secretReference.keyId;
691
821
  await provider.storeSecret(secretReference.keyId, randomBytes(32));
822
+ nextAccess = {
823
+ provider,
824
+ secretReference,
825
+ };
692
826
  await saveWalletState({
693
827
  primaryPath: paths.walletStatePath,
694
828
  backupPath: paths.walletStateBackupPath,
695
- }, nextState, {
696
- provider,
697
- secretReference,
698
- });
829
+ }, nextState, nextAccess);
699
830
  preservedSecretRefs.push(secretReference.keyId);
700
831
  }
701
832
  else {
833
+ nextAccess = decision.loadedWalletForEntropyReset.access.passphrase;
702
834
  await saveWalletState({
703
835
  primaryPath: paths.walletStatePath,
704
836
  backupPath: paths.walletStateBackupPath,
705
- }, nextState, decision.loadedWalletForEntropyReset.access.passphrase);
837
+ }, nextState, nextAccess);
706
838
  }
839
+ nextState = await recreateManagedCoreWalletReplicaForReset({
840
+ state: nextState,
841
+ access: nextAccess,
842
+ paths,
843
+ dataDir: options.dataDir,
844
+ nowUnixMs,
845
+ attachService: options.attachService,
846
+ rpcFactory: options.rpcFactory,
847
+ });
707
848
  }
708
849
  if (snapshotResultStatus === "preserved") {
709
850
  await restoreStagedArtifacts(stagedSnapshotArtifacts);
@@ -723,7 +864,7 @@ export async function resetWallet(options) {
723
864
  }
724
865
  }
725
866
  }
726
- else if (walletAction === "reset-base-entropy" && preflight.wallet.secretProviderKeyId !== null) {
867
+ else if (walletAction === "retain-mnemonic" && preflight.wallet.secretProviderKeyId !== null) {
727
868
  try {
728
869
  if (preflight.wallet.secretProviderKeyId !== newProviderKeyId) {
729
870
  await provider.deleteSecret(preflight.wallet.secretProviderKeyId);
@@ -761,7 +902,11 @@ export async function resetWallet(options) {
761
902
  status: snapshotResultStatus,
762
903
  path: preflight.snapshot.path,
763
904
  },
764
- removedPaths: preflight.removedRoots,
905
+ bitcoinDataDir: {
906
+ status: bitcoinDataDirResultStatus,
907
+ path: preflight.bitcoinDataDir.path,
908
+ },
909
+ removedPaths,
765
910
  };
766
911
  }
767
912
  catch (error) {
@@ -0,0 +1,20 @@
1
+ import type { WalletRuntimePaths } from "./runtime.js";
2
+ import { loadWalletExplicitLock } from "./state/explicit-lock.js";
3
+ import type { WalletSecretProvider } from "./state/provider.js";
4
+ import { loadUnlockSession } from "./state/session.js";
5
+ import { type RawWalletStateEnvelope } from "./state/storage.js";
6
+ export type WalletRootResolutionSource = "wallet-state" | "unlock-session" | "explicit-lock" | "default-uninitialized";
7
+ export interface WalletRootResolution {
8
+ walletRootId: string;
9
+ source: WalletRootResolutionSource;
10
+ }
11
+ export declare function resolveWalletRootIdFromLocalArtifacts(options: {
12
+ paths: WalletRuntimePaths;
13
+ provider: WalletSecretProvider;
14
+ loadRawWalletStateEnvelope?: (paths: {
15
+ primaryPath: string;
16
+ backupPath: string;
17
+ }) => Promise<RawWalletStateEnvelope | null>;
18
+ loadUnlockSession?: typeof loadUnlockSession;
19
+ loadWalletExplicitLock?: typeof loadWalletExplicitLock;
20
+ }): Promise<WalletRootResolution>;
@@ -0,0 +1,37 @@
1
+ import { UNINITIALIZED_WALLET_ROOT_ID } from "../bitcoind/service-paths.js";
2
+ import { loadWalletExplicitLock } from "./state/explicit-lock.js";
3
+ import { loadUnlockSession } from "./state/session.js";
4
+ import { extractWalletRootIdHintFromWalletStateEnvelope, loadRawWalletStateEnvelope, } from "./state/storage.js";
5
+ export async function resolveWalletRootIdFromLocalArtifacts(options) {
6
+ const rawEnvelope = await (options.loadRawWalletStateEnvelope ?? loadRawWalletStateEnvelope)({
7
+ primaryPath: options.paths.walletStatePath,
8
+ backupPath: options.paths.walletStateBackupPath,
9
+ }).catch(() => null);
10
+ const walletStateRootId = extractWalletRootIdHintFromWalletStateEnvelope(rawEnvelope?.envelope ?? null);
11
+ if (walletStateRootId !== null) {
12
+ return {
13
+ walletRootId: walletStateRootId,
14
+ source: "wallet-state",
15
+ };
16
+ }
17
+ const session = await (options.loadUnlockSession ?? loadUnlockSession)(options.paths.walletUnlockSessionPath, {
18
+ provider: options.provider,
19
+ }).catch(() => null);
20
+ if (session !== null) {
21
+ return {
22
+ walletRootId: session.walletRootId,
23
+ source: "unlock-session",
24
+ };
25
+ }
26
+ const explicitLock = await (options.loadWalletExplicitLock ?? loadWalletExplicitLock)(options.paths.walletExplicitLockPath).catch(() => null);
27
+ if (explicitLock?.walletRootId) {
28
+ return {
29
+ walletRootId: explicitLock.walletRootId,
30
+ source: "explicit-lock",
31
+ };
32
+ }
33
+ return {
34
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
35
+ source: "default-uninitialized",
36
+ };
37
+ }
@@ -1,6 +1,7 @@
1
1
  import type { CogcoinPathResolution } from "../app-paths.js";
2
2
  export interface WalletRuntimePaths {
3
3
  dataRoot: string;
4
+ clientDataDir: string;
4
5
  clientConfigPath: string;
5
6
  runtimeRoot: string;
6
7
  hooksRoot: string;
@@ -3,6 +3,7 @@ export function resolveWalletRuntimePathsForTesting(resolution = {}) {
3
3
  const paths = resolveCogcoinPathsForTesting(resolution);
4
4
  return {
5
5
  dataRoot: paths.dataRoot,
6
+ clientDataDir: paths.clientDataDir,
6
7
  clientConfigPath: paths.clientConfigPath,
7
8
  runtimeRoot: paths.runtimeRoot,
8
9
  hooksRoot: paths.hooksRoot,
@@ -15,6 +15,7 @@ export declare function rederiveKeyFromEnvelope(passphrase: Uint8Array | string,
15
15
  export declare function encryptBytesWithKey(plaintext: Uint8Array, key: Uint8Array, metadata: {
16
16
  format: string;
17
17
  wrappedBy: string;
18
+ walletRootIdHint?: string | null;
18
19
  argon2id?: Argon2EnvelopeParams | null;
19
20
  secretProvider?: WalletSecretReference | null;
20
21
  }): EncryptedEnvelopeV1;
@@ -22,10 +23,12 @@ export declare function decryptBytesWithKey(envelope: EncryptedEnvelopeV1, key:
22
23
  export declare function encryptJsonWithPassphrase<T>(value: T, passphrase: Uint8Array | string, metadata: {
23
24
  format: string;
24
25
  wrappedBy?: string;
26
+ walletRootIdHint?: string | null;
25
27
  }): Promise<EncryptedEnvelopeV1>;
26
28
  export declare function encryptJsonWithSecretProvider<T>(value: T, provider: WalletSecretProvider, secretReference: WalletSecretReference, metadata: {
27
29
  format: string;
28
30
  wrappedBy?: string;
31
+ walletRootIdHint?: string | null;
29
32
  }): Promise<EncryptedEnvelopeV1>;
30
33
  export declare function decryptJsonWithPassphrase<T>(envelope: EncryptedEnvelopeV1, passphrase: Uint8Array | string): Promise<T>;
31
34
  export declare function decryptJsonWithSecretProvider<T>(envelope: EncryptedEnvelopeV1, provider: WalletSecretProvider): Promise<T>;
@@ -76,6 +76,7 @@ export function encryptBytesWithKey(plaintext, key, metadata) {
76
76
  version: 1,
77
77
  cipher: "aes-256-gcm",
78
78
  wrappedBy: metadata.wrappedBy,
79
+ walletRootIdHint: metadata.walletRootIdHint ?? null,
79
80
  argon2id: metadata.argon2id ?? null,
80
81
  secretProvider: metadata.secretProvider ?? null,
81
82
  nonce: nonce.toString("base64"),
@@ -96,6 +97,7 @@ export async function encryptJsonWithPassphrase(value, passphrase, metadata) {
96
97
  return encryptBytesWithKey(Buffer.from(JSON.stringify(value, jsonReplacer)), derived.key, {
97
98
  format: metadata.format,
98
99
  wrappedBy: metadata.wrappedBy ?? "passphrase",
100
+ walletRootIdHint: metadata.walletRootIdHint ?? null,
99
101
  argon2id: derived.params,
100
102
  });
101
103
  }
@@ -104,6 +106,7 @@ export async function encryptJsonWithSecretProvider(value, provider, secretRefer
104
106
  return encryptBytesWithKey(Buffer.from(JSON.stringify(value, jsonReplacer)), key, {
105
107
  format: metadata.format,
106
108
  wrappedBy: metadata.wrappedBy ?? "secret-provider",
109
+ walletRootIdHint: metadata.walletRootIdHint ?? null,
107
110
  secretProvider: secretReference,
108
111
  });
109
112
  }