@cogcoin/client 0.5.3 → 0.5.4

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 (40) hide show
  1. package/README.md +11 -3
  2. package/dist/art/wallet.txt +10 -0
  3. package/dist/bitcoind/indexer-daemon.d.ts +9 -0
  4. package/dist/bitcoind/indexer-daemon.js +51 -14
  5. package/dist/bitcoind/service.d.ts +9 -0
  6. package/dist/bitcoind/service.js +65 -24
  7. package/dist/bitcoind/testing.d.ts +2 -2
  8. package/dist/bitcoind/testing.js +2 -2
  9. package/dist/cli/commands/service-runtime.d.ts +2 -0
  10. package/dist/cli/commands/service-runtime.js +432 -0
  11. package/dist/cli/commands/wallet-admin.js +227 -132
  12. package/dist/cli/commands/wallet-mutation.js +597 -580
  13. package/dist/cli/context.js +23 -1
  14. package/dist/cli/mutation-json.d.ts +17 -1
  15. package/dist/cli/mutation-json.js +42 -0
  16. package/dist/cli/output.js +112 -1
  17. package/dist/cli/parse.d.ts +1 -1
  18. package/dist/cli/parse.js +65 -0
  19. package/dist/cli/preview-json.d.ts +19 -1
  20. package/dist/cli/preview-json.js +31 -0
  21. package/dist/cli/prompt.js +40 -12
  22. package/dist/cli/runner.js +12 -0
  23. package/dist/cli/signals.d.ts +1 -0
  24. package/dist/cli/signals.js +44 -0
  25. package/dist/cli/types.d.ts +24 -2
  26. package/dist/cli/types.js +6 -0
  27. package/dist/cli/wallet-format.js +3 -0
  28. package/dist/cli/workflow-hints.d.ts +1 -0
  29. package/dist/cli/workflow-hints.js +3 -0
  30. package/dist/wallet/fs/lock.d.ts +2 -0
  31. package/dist/wallet/fs/lock.js +32 -0
  32. package/dist/wallet/lifecycle.d.ts +19 -1
  33. package/dist/wallet/lifecycle.js +251 -3
  34. package/dist/wallet/material.d.ts +2 -0
  35. package/dist/wallet/material.js +8 -1
  36. package/dist/wallet/mnemonic-art.d.ts +2 -0
  37. package/dist/wallet/mnemonic-art.js +54 -0
  38. package/dist/wallet/reset.d.ts +61 -0
  39. package/dist/wallet/reset.js +781 -0
  40. package/package.json +3 -3
@@ -6,6 +6,7 @@ import { runFollowCommand } from "./commands/follow.js";
6
6
  import { runMiningAdminCommand } from "./commands/mining-admin.js";
7
7
  import { runMiningReadCommand } from "./commands/mining-read.js";
8
8
  import { runMiningRuntimeCommand } from "./commands/mining-runtime.js";
9
+ import { runServiceRuntimeCommand } from "./commands/service-runtime.js";
9
10
  import { runStatusCommand } from "./commands/status.js";
10
11
  import { runSyncCommand } from "./commands/sync.js";
11
12
  import { runWalletAdminCommand } from "./commands/wallet-admin.js";
@@ -49,6 +50,14 @@ export async function runCli(argv, contextOverrides = {}) {
49
50
  if (parsed.command === "status") {
50
51
  return runStatusCommand(parsed, context);
51
52
  }
53
+ if (parsed.command === "bitcoin-start"
54
+ || parsed.command === "bitcoin-stop"
55
+ || parsed.command === "bitcoin-status"
56
+ || parsed.command === "indexer-start"
57
+ || parsed.command === "indexer-stop"
58
+ || parsed.command === "indexer-status") {
59
+ return runServiceRuntimeCommand(parsed, context);
60
+ }
52
61
  if (parsed.command === "mine"
53
62
  || parsed.command === "mine-start"
54
63
  || parsed.command === "mine-stop") {
@@ -60,11 +69,14 @@ export async function runCli(argv, contextOverrides = {}) {
60
69
  return runMiningAdminCommand(parsed, context);
61
70
  }
62
71
  if (parsed.command === "init"
72
+ || parsed.command === "restore"
73
+ || parsed.command === "reset"
63
74
  || parsed.command === "repair"
64
75
  || parsed.command === "unlock"
65
76
  || parsed.command === "wallet-export"
66
77
  || parsed.command === "wallet-import"
67
78
  || parsed.command === "wallet-init"
79
+ || parsed.command === "wallet-restore"
68
80
  || parsed.command === "wallet-unlock"
69
81
  || parsed.command === "wallet-lock") {
70
82
  return runWalletAdminCommand(parsed, context);
@@ -1,3 +1,4 @@
1
1
  import type { InterruptibleOutcome, ManagedClientLike, SignalSource, StopSignalWatcher, WritableLike } from "./types.js";
2
2
  export declare function createStopSignalWatcher(signalSource: SignalSource, stderr: WritableLike, client: ManagedClientLike, forceExit: (code: number) => never | void): StopSignalWatcher;
3
+ export declare function createOwnedLockCleanupSignalWatcher(signalSource: SignalSource, forceExit: (code: number) => never | void, lockPaths: readonly string[]): StopSignalWatcher;
3
4
  export declare function waitForCompletionOrStop<T>(promise: Promise<T>, stopWatcher: StopSignalWatcher): Promise<InterruptibleOutcome<T>>;
@@ -1,4 +1,5 @@
1
1
  import { writeLine } from "./io.js";
2
+ import { clearLockIfOwnedByCurrentProcess } from "../wallet/fs/lock.js";
2
3
  export function createStopSignalWatcher(signalSource, stderr, client, forceExit) {
3
4
  let closing = false;
4
5
  let resolved = false;
@@ -42,6 +43,49 @@ export function createStopSignalWatcher(signalSource, stderr, client, forceExit)
42
43
  promise,
43
44
  };
44
45
  }
46
+ export function createOwnedLockCleanupSignalWatcher(signalSource, forceExit, lockPaths) {
47
+ let stopping = false;
48
+ let resolved = false;
49
+ let onSignal = () => { };
50
+ const cleanup = () => {
51
+ signalSource.off("SIGINT", onSignal);
52
+ signalSource.off("SIGTERM", onSignal);
53
+ };
54
+ const promise = new Promise((resolve) => {
55
+ const settle = (code) => {
56
+ if (resolved) {
57
+ return;
58
+ }
59
+ resolved = true;
60
+ cleanup();
61
+ resolve(code);
62
+ };
63
+ const releaseOwnedLocks = async () => {
64
+ await Promise.allSettled([...new Set(lockPaths)].map(async (lockPath) => {
65
+ await clearLockIfOwnedByCurrentProcess(lockPath);
66
+ }));
67
+ };
68
+ onSignal = () => {
69
+ if (stopping) {
70
+ settle(130);
71
+ forceExit(130);
72
+ return;
73
+ }
74
+ stopping = true;
75
+ settle(130);
76
+ void releaseOwnedLocks().finally(() => {
77
+ forceExit(130);
78
+ });
79
+ };
80
+ });
81
+ signalSource.on("SIGINT", onSignal);
82
+ signalSource.on("SIGTERM", onSignal);
83
+ return {
84
+ cleanup,
85
+ isStopping: () => stopping,
86
+ promise,
87
+ };
88
+ }
45
89
  export async function waitForCompletionOrStop(promise, stopWatcher) {
46
90
  const outcome = await Promise.race([
47
91
  promise.then((value) => ({ kind: "completed", value }), (error) => ({ kind: "error", error })),
@@ -1,14 +1,21 @@
1
1
  import type { inspectPassiveClientStatus } from "../passive-status.js";
2
+ import { createRpcClient } from "../bitcoind/node.js";
3
+ import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, stopIndexerDaemonService } from "../bitcoind/indexer-daemon.js";
4
+ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService } from "../bitcoind/service.js";
2
5
  import { openSqliteStore } from "../sqlite/index.js";
3
6
  import type { ClientStoreAdapter } from "../types.js";
4
- import type { exportWallet, WalletPrompter, importWallet, initializeWallet, lockWallet, repairWallet, unlockWallet } from "../wallet/lifecycle.js";
7
+ import type { WalletRuntimePaths } from "../wallet/runtime.js";
8
+ import type { exportWallet, WalletPrompter, importWallet, initializeWallet, lockWallet, previewResetWallet, repairWallet, resetWallet, restoreWalletFromMnemonic, unlockWallet } from "../wallet/lifecycle.js";
5
9
  import type { openWalletReadContext } from "../wallet/read/index.js";
10
+ import { loadWalletExplicitLock } from "../wallet/state/explicit-lock.js";
11
+ import { loadUnlockSession } from "../wallet/state/session.js";
12
+ import { loadWalletState } from "../wallet/state/storage.js";
6
13
  import type { WalletSecretProvider } from "../wallet/state/provider.js";
7
14
  import type { disableMiningHooks, enableMiningHooks, followMiningLog, inspectMiningControlPlane, readMiningLog, runForegroundMining, setupBuiltInMining, startBackgroundMining, stopBackgroundMining } from "../wallet/mining/index.js";
8
15
  import type { anchorDomain, buyDomain, claimCogLock, clearDomainDelegate, clearDomainEndpoint, clearDomainMiner, clearField, createField, giveReputation, lockCogToDomain, registerDomain, reclaimCogLock, revokeReputation, sendCog, setField, setDomainCanonical, setDomainDelegate, setDomainEndpoint, setDomainMiner, sellDomain, transferDomain } from "../wallet/tx/index.js";
9
16
  export type ProgressOutput = "auto" | "tty" | "none";
10
17
  export type OutputMode = "text" | "json" | "preview-json";
11
- export type CommandName = "init" | "repair" | "sync" | "status" | "follow" | "unlock" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "hooks-mining-enable" | "hooks-mining-disable" | "hooks-mining-status" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-export" | "wallet-import" | "wallet-init" | "wallet-lock" | "wallet-unlock" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
18
+ export type CommandName = "init" | "restore" | "reset" | "repair" | "sync" | "status" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "indexer-start" | "indexer-stop" | "indexer-status" | "unlock" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "hooks-mining-enable" | "hooks-mining-disable" | "hooks-mining-status" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-export" | "wallet-import" | "wallet-init" | "wallet-restore" | "wallet-lock" | "wallet-unlock" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
12
19
  export interface WritableLike {
13
20
  isTTY?: boolean;
14
21
  write(chunk: string): void;
@@ -88,9 +95,22 @@ export interface CliRunnerContext {
88
95
  dataDir?: string;
89
96
  progressOutput?: ProgressOutput;
90
97
  }) => Promise<ManagedClientLike>;
98
+ attachManagedBitcoindService?: typeof attachOrStartManagedBitcoindService;
99
+ probeManagedBitcoindService?: typeof probeManagedBitcoindService;
100
+ stopManagedBitcoindService?: typeof stopManagedBitcoindService;
101
+ createBitcoinRpcClient?: typeof createRpcClient;
102
+ attachIndexerDaemon?: typeof attachOrStartIndexerDaemon;
103
+ probeIndexerDaemon?: typeof probeIndexerDaemon;
104
+ readObservedIndexerDaemonStatus?: typeof readObservedIndexerDaemonStatus;
105
+ stopIndexerDaemonService?: typeof stopIndexerDaemonService;
91
106
  inspectPassiveClientStatus?: typeof inspectPassiveClientStatus;
92
107
  openWalletReadContext?: typeof openWalletReadContext;
108
+ loadWalletState?: typeof loadWalletState;
109
+ loadUnlockSession?: typeof loadUnlockSession;
110
+ loadWalletExplicitLock?: typeof loadWalletExplicitLock;
93
111
  initializeWallet?: typeof initializeWallet;
112
+ restoreWalletFromMnemonic?: typeof restoreWalletFromMnemonic;
113
+ previewResetWallet?: typeof previewResetWallet;
94
114
  exportWallet?: typeof exportWallet;
95
115
  importWallet?: typeof importWallet;
96
116
  unlockWallet?: typeof unlockWallet;
@@ -126,12 +146,14 @@ export interface CliRunnerContext {
126
146
  readMiningLog?: typeof readMiningLog;
127
147
  followMiningLog?: typeof followMiningLog;
128
148
  repairWallet?: typeof repairWallet;
149
+ resetWallet?: typeof resetWallet;
129
150
  walletSecretProvider?: WalletSecretProvider;
130
151
  createPrompter?: () => WalletPrompter;
131
152
  ensureDirectory?: (path: string) => Promise<void>;
132
153
  readPackageVersion?: () => Promise<string>;
133
154
  resolveDefaultBitcoindDataDir?: () => string;
134
155
  resolveDefaultClientDatabasePath?: () => string;
156
+ resolveWalletRuntimePaths?: () => WalletRuntimePaths;
135
157
  }
136
158
  export interface StopSignalWatcher {
137
159
  cleanup(): void;
package/dist/cli/types.js CHANGED
@@ -1,2 +1,8 @@
1
1
  import { openManagedBitcoindClient } from "../bitcoind/index.js";
2
+ import { createRpcClient } from "../bitcoind/node.js";
3
+ import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, stopIndexerDaemonService, } from "../bitcoind/indexer-daemon.js";
4
+ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../bitcoind/service.js";
2
5
  import { openSqliteStore } from "../sqlite/index.js";
6
+ import { loadWalletExplicitLock } from "../wallet/state/explicit-lock.js";
7
+ import { loadUnlockSession } from "../wallet/state/session.js";
8
+ import { loadWalletState } from "../wallet/state/storage.js";
@@ -75,6 +75,9 @@ function formatPendingMutationDomainLabel(mutation) {
75
75
  return kind;
76
76
  }
77
77
  export function getRepairRecommendation(context) {
78
+ if (context.localState.availability === "uninitialized") {
79
+ return "Run `cogcoin init` to create a new local wallet root.";
80
+ }
78
81
  if (context.localState.availability === "local-state-corrupt") {
79
82
  return "Run `cogcoin repair` to recover local wallet state.";
80
83
  }
@@ -2,6 +2,7 @@ import type { WalletIdentityView, WalletLockView, WalletReadContext } from "../w
2
2
  export declare function formatNextStepLines(nextSteps: readonly string[]): string[];
3
3
  export declare function getFundingQuickstartGuidance(): string;
4
4
  export declare function getInitNextSteps(): string[];
5
+ export declare function getRestoreNextSteps(): string[];
5
6
  export declare function getBootstrapSyncNextStep(context: Pick<WalletReadContext, "bitcoind" | "indexer" | "nodeHealth">): string | null;
6
7
  export declare function getRegisterNextSteps(domainName: string, registerKind: "root" | "subdomain"): string[];
7
8
  export declare function getAnchorNextSteps(domainName: string): string[];
@@ -10,6 +10,9 @@ export function getFundingQuickstartGuidance() {
10
10
  export function getInitNextSteps() {
11
11
  return ["cogcoin sync", "cogcoin address"];
12
12
  }
13
+ export function getRestoreNextSteps() {
14
+ return ["cogcoin sync", "cogcoin address"];
15
+ }
13
16
  function blocksSyncBootstrap(context) {
14
17
  return context.bitcoind.health === "service-version-mismatch"
15
18
  || context.bitcoind.health === "wallet-root-mismatch"
@@ -16,4 +16,6 @@ export declare class FileLockBusyError extends Error {
16
16
  constructor(lockPath: string, existingMetadata: FileLockMetadata | null);
17
17
  }
18
18
  export declare function readLockMetadata(lockPath: string): Promise<FileLockMetadata | null>;
19
+ export declare function clearLockIfOwnedByCurrentProcess(lockPath: string): Promise<boolean>;
20
+ export declare function clearOrphanedFileLock(lockPath: string, isProcessAlive: (pid: number) => Promise<boolean>): Promise<boolean>;
19
21
  export declare function acquireFileLock(lockPath: string, metadata?: Partial<FileLockMetadata>): Promise<FileLockHandle>;
@@ -22,6 +22,38 @@ export async function readLockMetadata(lockPath) {
22
22
  throw error;
23
23
  }
24
24
  }
25
+ export async function clearLockIfOwnedByCurrentProcess(lockPath) {
26
+ const metadata = await readLockMetadata(lockPath);
27
+ if (metadata === null || metadata.processId !== (process.pid ?? null)) {
28
+ return false;
29
+ }
30
+ await rm(lockPath, { force: true });
31
+ return true;
32
+ }
33
+ export async function clearOrphanedFileLock(lockPath, isProcessAlive) {
34
+ let metadata = null;
35
+ try {
36
+ metadata = await readLockMetadata(lockPath);
37
+ }
38
+ catch (error) {
39
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
40
+ return false;
41
+ }
42
+ await rm(lockPath, { force: true });
43
+ return true;
44
+ }
45
+ if (metadata === null) {
46
+ return false;
47
+ }
48
+ const processId = typeof metadata.processId === "number" && Number.isInteger(metadata.processId)
49
+ ? metadata.processId
50
+ : null;
51
+ if (processId === null || !await isProcessAlive(processId)) {
52
+ await rm(lockPath, { force: true });
53
+ return true;
54
+ }
55
+ return false;
56
+ }
25
57
  export async function acquireFileLock(lockPath, metadata = {}) {
26
58
  await mkdir(dirname(lockPath), { recursive: true });
27
59
  const fullMetadata = {
@@ -11,6 +11,7 @@ export interface WalletPrompter {
11
11
  readonly isInteractive: boolean;
12
12
  writeLine(message: string): void;
13
13
  prompt(message: string): Promise<string>;
14
+ promptHidden?(message: string): Promise<string>;
14
15
  clearSensitiveDisplay?(scope: "mnemonic-reveal"): void | Promise<void>;
15
16
  }
16
17
  export interface WalletInitializationResult {
@@ -40,6 +41,13 @@ export interface WalletImportResult {
40
41
  unlockUntilUnixMs: number;
41
42
  state: WalletStateV1;
42
43
  }
44
+ export interface WalletRestoreResult {
45
+ walletRootId: string;
46
+ fundingAddress: string;
47
+ unlockUntilUnixMs: number;
48
+ state: WalletStateV1;
49
+ warnings?: string[];
50
+ }
43
51
  export interface WalletRepairResult {
44
52
  walletRootId: string;
45
53
  recoveredFromBackup: boolean;
@@ -58,6 +66,7 @@ export interface WalletRepairResult {
58
66
  miningResumeError: string | null;
59
67
  note: string | null;
60
68
  }
69
+ export { previewResetWallet, resetWallet, type WalletResetPreview, type WalletResetResult, } from "./reset.js";
61
70
  interface WalletLifecycleRpcClient {
62
71
  getDescriptorInfo(descriptor: string): Promise<{
63
72
  descriptor: string;
@@ -189,6 +198,16 @@ export declare function importWallet(options: {
189
198
  attachIndexerDaemon?: typeof attachOrStartIndexerDaemon;
190
199
  rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
191
200
  }): Promise<WalletImportResult>;
201
+ export declare function restoreWalletFromMnemonic(options: {
202
+ dataDir: string;
203
+ provider?: WalletSecretProvider;
204
+ prompter: WalletPrompter;
205
+ nowUnixMs?: number;
206
+ unlockDurationMs?: number;
207
+ paths?: WalletRuntimePaths;
208
+ attachService?: typeof attachOrStartManagedBitcoindService;
209
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
210
+ }): Promise<WalletRestoreResult>;
192
211
  export declare function repairWallet(options: {
193
212
  dataDir: string;
194
213
  databasePath: string;
@@ -204,4 +223,3 @@ export declare function repairWallet(options: {
204
223
  requestMiningPreemption?: typeof requestMiningGenerationPreemption;
205
224
  startBackgroundMining?: typeof import("./mining/runner.js").startBackgroundMining;
206
225
  }): Promise<WalletRepairResult>;
207
- export {};
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { access, constants, mkdir, readFile, rename, rm } from "node:fs/promises";
2
+ import { access, constants, mkdir, readFile, readdir, rename, rm } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { openClient } from "../client.js";
5
5
  import { attachOrStartIndexerDaemon, probeIndexerDaemon, readSnapshotWithRetry, } from "../bitcoind/indexer-daemon.js";
@@ -9,19 +9,21 @@ import { createRpcClient } from "../bitcoind/node.js";
9
9
  import { openSqliteStore } from "../sqlite/index.js";
10
10
  import { readPortableWalletArchive, writePortableWalletArchive } from "./archive.js";
11
11
  import { normalizeWalletDescriptorState, persistNormalizedWalletDescriptorStateIfNeeded, persistWalletStateUpdate, resolveNormalizedWalletDescriptorState, stripDescriptorChecksum, } from "./descriptor-normalization.js";
12
- import { acquireFileLock } from "./fs/lock.js";
13
- import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, } from "./material.js";
12
+ import { acquireFileLock, clearOrphanedFileLock } from "./fs/lock.js";
13
+ import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, isEnglishMnemonicWord, validateEnglishMnemonic, } from "./material.js";
14
14
  import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
15
15
  import { requestMiningGenerationPreemption } from "./mining/coordination.js";
16
16
  import { loadClientConfig } from "./mining/config.js";
17
17
  import { inspectMiningHookState } from "./mining/hooks.js";
18
18
  import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
19
19
  import { normalizeMiningStateRecord } from "./mining/state.js";
20
+ import { renderWalletMnemonicRevealArt } from "./mnemonic-art.js";
20
21
  import { clearWalletExplicitLock, loadWalletExplicitLock, saveWalletExplicitLock, } from "./state/explicit-lock.js";
21
22
  import { clearUnlockSession, loadUnlockSession, saveUnlockSession } from "./state/session.js";
22
23
  import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
23
24
  import { loadWalletState, saveWalletState } from "./state/storage.js";
24
25
  export const DEFAULT_UNLOCK_DURATION_MS = 15 * 60 * 1000;
26
+ export { previewResetWallet, resetWallet, } from "./reset.js";
25
27
  function sanitizeWalletName(walletRootId) {
26
28
  return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
27
29
  }
@@ -34,6 +36,32 @@ async function pathExists(path) {
34
36
  return false;
35
37
  }
36
38
  }
39
+ async function readJsonFileOrNull(path) {
40
+ try {
41
+ return JSON.parse(await readFile(path, "utf8"));
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
45
+ return null;
46
+ }
47
+ return null;
48
+ }
49
+ }
50
+ async function loadRawWalletEnvelope(paths) {
51
+ const primary = await readJsonFileOrNull(paths.walletStatePath);
52
+ if (primary !== null) {
53
+ return primary;
54
+ }
55
+ return readJsonFileOrNull(paths.walletStateBackupPath);
56
+ }
57
+ function extractWalletRootIdFromEnvelope(envelope) {
58
+ const keyId = envelope?.secretProvider?.keyId ?? null;
59
+ const prefix = "wallet-state:";
60
+ if (keyId === null || !keyId.startsWith(prefix)) {
61
+ return null;
62
+ }
63
+ return keyId.slice(prefix.length);
64
+ }
37
65
  function createInitialWalletState(options) {
38
66
  return {
39
67
  schemaVersion: 1,
@@ -294,6 +322,12 @@ async function promptRequiredValue(prompter, message) {
294
322
  }
295
323
  return value;
296
324
  }
325
+ async function promptHiddenValue(prompter, message) {
326
+ const value = prompter.promptHidden != null
327
+ ? await prompter.promptHidden(message)
328
+ : await prompter.prompt(message);
329
+ return value.trim();
330
+ }
297
331
  async function promptForArchivePassphrase(prompter, promptPrefix) {
298
332
  const first = await promptRequiredValue(prompter, `${promptPrefix} passphrase: `);
299
333
  const second = await promptRequiredValue(prompter, `Confirm ${promptPrefix.toLowerCase()} passphrase: `);
@@ -302,12 +336,33 @@ async function promptForArchivePassphrase(prompter, promptPrefix) {
302
336
  }
303
337
  return first;
304
338
  }
339
+ async function promptForRestoreMnemonic(prompter) {
340
+ const words = [];
341
+ for (let index = 0; index < 24; index += 1) {
342
+ const word = (await promptHiddenValue(prompter, `Word ${index + 1} of 24: `)).toLowerCase();
343
+ if (!isEnglishMnemonicWord(word)) {
344
+ throw new Error("wallet_restore_mnemonic_invalid");
345
+ }
346
+ words.push(word);
347
+ }
348
+ const phrase = words.join(" ");
349
+ if (!validateEnglishMnemonic(phrase)) {
350
+ throw new Error("wallet_restore_mnemonic_invalid");
351
+ }
352
+ return phrase;
353
+ }
305
354
  async function confirmTypedAcknowledgement(prompter, expected, message) {
306
355
  const answer = (await prompter.prompt(message)).trim();
307
356
  if (answer !== expected) {
308
357
  throw new Error("wallet_typed_confirmation_rejected");
309
358
  }
310
359
  }
360
+ async function confirmRestoreReplacement(prompter) {
361
+ const answer = (await prompter.prompt("Type \"RESTORE\" to replace the existing local wallet state and managed Core wallet replica: ")).trim();
362
+ if (answer !== "RESTORE") {
363
+ throw new Error("wallet_restore_replace_confirmation_required");
364
+ }
365
+ }
311
366
  async function confirmOverwriteIfNeeded(prompter, path) {
312
367
  if (!await pathExists(path)) {
313
368
  return;
@@ -516,6 +571,86 @@ async function clearManagedBitcoindArtifacts(servicePaths) {
516
571
  await rm(servicePaths.bitcoindReadyPath, { force: true }).catch(() => undefined);
517
572
  await rm(servicePaths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
518
573
  }
574
+ async function detectExistingManagedWalletReplica(dataDir) {
575
+ try {
576
+ const entries = await readdir(join(dataDir, "wallets"), { withFileTypes: true });
577
+ return entries.some((entry) => entry.isDirectory() && entry.name.startsWith("cogcoin-"));
578
+ }
579
+ catch (error) {
580
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
581
+ return false;
582
+ }
583
+ throw error;
584
+ }
585
+ }
586
+ async function stopRecordedManagedProcess(pid, errorCode) {
587
+ if (pid === null || !await isProcessAlive(pid)) {
588
+ return;
589
+ }
590
+ try {
591
+ process.kill(pid, "SIGTERM");
592
+ }
593
+ catch (error) {
594
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
595
+ throw error;
596
+ }
597
+ }
598
+ try {
599
+ await waitForProcessExit(pid, 5_000, errorCode);
600
+ return;
601
+ }
602
+ catch {
603
+ try {
604
+ process.kill(pid, "SIGKILL");
605
+ }
606
+ catch (error) {
607
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
608
+ throw error;
609
+ }
610
+ }
611
+ }
612
+ await waitForProcessExit(pid, 5_000, errorCode);
613
+ }
614
+ async function clearOrphanedRepairLocks(lockPaths) {
615
+ for (const lockPath of lockPaths) {
616
+ await clearOrphanedFileLock(lockPath, isProcessAlive);
617
+ }
618
+ }
619
+ async function clearPreviousManagedWalletRuntime(options) {
620
+ if (options.walletRootId === null) {
621
+ return;
622
+ }
623
+ const servicePaths = resolveManagedServicePaths(options.dataDir, options.walletRootId);
624
+ const bitcoindLock = await acquireFileLock(servicePaths.bitcoindLockPath, {
625
+ purpose: "wallet-restore-cleanup",
626
+ walletRootId: options.walletRootId,
627
+ });
628
+ const indexerLock = await acquireFileLock(servicePaths.indexerDaemonLockPath, {
629
+ purpose: "wallet-restore-cleanup",
630
+ walletRootId: options.walletRootId,
631
+ });
632
+ try {
633
+ const bitcoindStatus = await readJsonFileOrNull(servicePaths.bitcoindStatusPath);
634
+ const indexerStatus = await readJsonFileOrNull(servicePaths.indexerDaemonStatusPath);
635
+ await stopRecordedManagedProcess(bitcoindStatus?.processId ?? null, "managed_bitcoind_stop_timeout");
636
+ await stopRecordedManagedProcess(indexerStatus?.processId ?? null, "indexer_daemon_stop_timeout");
637
+ await clearManagedBitcoindArtifacts(servicePaths);
638
+ await clearIndexerDaemonArtifacts(servicePaths);
639
+ await rm(servicePaths.walletRuntimeRoot, { recursive: true, force: true }).catch(() => undefined);
640
+ await rm(servicePaths.indexerServiceRoot, { recursive: true, force: true }).catch(() => undefined);
641
+ await rm(join(options.dataDir, "wallets", sanitizeWalletName(options.walletRootId)), { recursive: true, force: true }).catch(() => undefined);
642
+ }
643
+ finally {
644
+ await indexerLock.release();
645
+ await bitcoindLock.release();
646
+ }
647
+ }
648
+ function formatRestoreCleanupWarning(error) {
649
+ const reason = error instanceof Error && error.message.trim().length > 0
650
+ ? ` (${error.message})`
651
+ : "";
652
+ return `Previous managed runtime cleanup did not complete${reason}. Run \`cogcoin repair\` if status shows stale or conflicting managed services.`;
653
+ }
519
654
  function createSilentNonInteractivePrompter() {
520
655
  return {
521
656
  isInteractive: false,
@@ -949,6 +1084,11 @@ export async function initializeWallet(options) {
949
1084
  let mnemonicRevealed = false;
950
1085
  options.prompter.writeLine("Cogcoin Wallet Initialization");
951
1086
  options.prompter.writeLine("Write down this 24-word recovery phrase. It will only be shown once:");
1087
+ options.prompter.writeLine("");
1088
+ for (const line of renderWalletMnemonicRevealArt(material.mnemonic.words)) {
1089
+ options.prompter.writeLine(line);
1090
+ }
1091
+ options.prompter.writeLine("Single-line copy:");
952
1092
  options.prompter.writeLine(material.mnemonic.phrase);
953
1093
  mnemonicRevealed = true;
954
1094
  try {
@@ -1213,6 +1353,106 @@ export async function importWallet(options) {
1213
1353
  await controlLock.release();
1214
1354
  }
1215
1355
  }
1356
+ export async function restoreWalletFromMnemonic(options) {
1357
+ if (!options.prompter.isInteractive) {
1358
+ throw new Error("wallet_restore_requires_tty");
1359
+ }
1360
+ const provider = options.provider ?? createDefaultWalletSecretProvider();
1361
+ const nowUnixMs = options.nowUnixMs ?? Date.now();
1362
+ const unlockDurationMs = options.unlockDurationMs ?? DEFAULT_UNLOCK_DURATION_MS;
1363
+ const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
1364
+ const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1365
+ purpose: "wallet-restore",
1366
+ walletRootId: null,
1367
+ });
1368
+ try {
1369
+ const rawEnvelope = await loadRawWalletEnvelope(paths);
1370
+ const replacementStateExists = rawEnvelope !== null
1371
+ || await pathExists(paths.walletStatePath)
1372
+ || await pathExists(paths.walletStateBackupPath);
1373
+ const replacementCoreWalletExists = await detectExistingManagedWalletReplica(options.dataDir);
1374
+ const mnemonicPhrase = await promptForRestoreMnemonic(options.prompter);
1375
+ if (replacementStateExists || replacementCoreWalletExists) {
1376
+ await confirmRestoreReplacement(options.prompter);
1377
+ }
1378
+ let previousWalletRootId = extractWalletRootIdFromEnvelope(rawEnvelope);
1379
+ try {
1380
+ const loaded = await loadWalletState({
1381
+ primaryPath: paths.walletStatePath,
1382
+ backupPath: paths.walletStateBackupPath,
1383
+ }, {
1384
+ provider,
1385
+ });
1386
+ previousWalletRootId = loaded.state.walletRootId;
1387
+ }
1388
+ catch {
1389
+ previousWalletRootId = previousWalletRootId ?? null;
1390
+ }
1391
+ const miningLock = await acquireFileLock(paths.miningControlLockPath, {
1392
+ purpose: "wallet-restore",
1393
+ walletRootId: previousWalletRootId,
1394
+ });
1395
+ try {
1396
+ const warnings = [];
1397
+ const material = deriveWalletMaterialFromMnemonic(mnemonicPhrase);
1398
+ const walletRootId = createWalletRootId();
1399
+ const internalCoreWalletPassphrase = createInternalCoreWalletPassphrase();
1400
+ const secretReference = createWalletSecretReference(walletRootId);
1401
+ const secret = randomBytes(32);
1402
+ await provider.storeSecret(secretReference.keyId, secret);
1403
+ const initialState = createInitialWalletState({
1404
+ walletRootId,
1405
+ nowUnixMs,
1406
+ material,
1407
+ internalCoreWalletPassphrase,
1408
+ });
1409
+ await clearUnlockSession(paths.walletUnlockSessionPath);
1410
+ await clearWalletExplicitLock(paths.walletExplicitLockPath);
1411
+ await saveWalletState({
1412
+ primaryPath: paths.walletStatePath,
1413
+ backupPath: paths.walletStateBackupPath,
1414
+ }, initialState, {
1415
+ provider,
1416
+ secretReference,
1417
+ });
1418
+ const restoredState = await recreateManagedCoreWalletReplica(initialState, provider, paths, options.dataDir, nowUnixMs, {
1419
+ attachService: options.attachService,
1420
+ rpcFactory: options.rpcFactory,
1421
+ });
1422
+ const unlockUntilUnixMs = nowUnixMs + unlockDurationMs;
1423
+ await clearWalletExplicitLock(paths.walletExplicitLockPath);
1424
+ await saveUnlockSession(paths.walletUnlockSessionPath, createUnlockSession(restoredState, unlockUntilUnixMs, secretReference.keyId, nowUnixMs), {
1425
+ provider,
1426
+ secretReference,
1427
+ });
1428
+ if (previousWalletRootId !== null && previousWalletRootId !== walletRootId) {
1429
+ try {
1430
+ await clearPreviousManagedWalletRuntime({
1431
+ dataDir: options.dataDir,
1432
+ walletRootId: previousWalletRootId,
1433
+ });
1434
+ }
1435
+ catch (error) {
1436
+ warnings.push(formatRestoreCleanupWarning(error));
1437
+ }
1438
+ await provider.deleteSecret(createWalletSecretReference(previousWalletRootId).keyId).catch(() => undefined);
1439
+ }
1440
+ return {
1441
+ walletRootId,
1442
+ fundingAddress: restoredState.funding.address,
1443
+ unlockUntilUnixMs,
1444
+ state: restoredState,
1445
+ warnings,
1446
+ };
1447
+ }
1448
+ finally {
1449
+ await miningLock.release();
1450
+ }
1451
+ }
1452
+ finally {
1453
+ await controlLock.release();
1454
+ }
1455
+ }
1216
1456
  export async function repairWallet(options) {
1217
1457
  const provider = options.provider ?? createDefaultWalletSecretProvider();
1218
1458
  const nowUnixMs = options.nowUnixMs ?? Date.now();
@@ -1222,6 +1462,10 @@ export async function repairWallet(options) {
1222
1462
  const probeManagedIndexerDaemon = options.probeIndexerDaemon ?? probeIndexerDaemon;
1223
1463
  const attachManagedIndexerDaemon = options.attachIndexerDaemon ?? attachOrStartIndexerDaemon;
1224
1464
  const requestMiningPreemptionForRepair = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
1465
+ await clearOrphanedRepairLocks([
1466
+ paths.walletControlLockPath,
1467
+ paths.miningControlLockPath,
1468
+ ]);
1225
1469
  const controlLock = await acquireFileLock(paths.walletControlLockPath, {
1226
1470
  purpose: "wallet-repair",
1227
1471
  walletRootId: null,
@@ -1245,6 +1489,10 @@ export async function repairWallet(options) {
1245
1489
  let repairedState = loaded.state;
1246
1490
  let repairStateNeedsPersist = false;
1247
1491
  const servicePaths = resolveManagedServicePaths(options.dataDir, repairedState.walletRootId);
1492
+ await clearOrphanedRepairLocks([
1493
+ servicePaths.bitcoindLockPath,
1494
+ servicePaths.indexerDaemonLockPath,
1495
+ ]);
1248
1496
  const preRepairMiningRuntime = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
1249
1497
  const backgroundWorkerAlive = preRepairMiningRuntime?.runMode === "background"
1250
1498
  && preRepairMiningRuntime.backgroundWorkerPid !== null
@@ -36,6 +36,8 @@ export interface WalletDerivedIdentity {
36
36
  nostrNpub: string;
37
37
  }
38
38
  export declare function generateWalletMaterial(): WalletMaterial;
39
+ export declare function isEnglishMnemonicWord(word: string): boolean;
40
+ export declare function validateEnglishMnemonic(phrase: string): boolean;
39
41
  export declare function deriveWalletMaterialFromMnemonic(phrase: string): WalletMaterial;
40
42
  export declare function deriveWalletIdentityMaterial(accountKey: string, index: number): WalletDerivedIdentity;
41
43
  export declare function createInternalCoreWalletPassphrase(): string;