@cogcoin/client 1.1.7 → 1.1.8

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.
@@ -0,0 +1,179 @@
1
+ import { loadMiningRuntimeStatus } from "../mining/runtime-artifacts.js";
2
+ import { acquireFileLock } from "../fs/lock.js";
3
+ import { join } from "node:path";
4
+ import { readdir } from "node:fs/promises";
5
+ import { readJsonFileOrNull } from "./artifacts.js";
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => {
8
+ setTimeout(resolve, ms);
9
+ });
10
+ }
11
+ function resolveProcessCleanupDependencies(overrides = {}) {
12
+ return {
13
+ acquireLock: overrides.acquireLock ?? acquireFileLock,
14
+ processKill: overrides.processKill ?? process.kill.bind(process),
15
+ sleep: overrides.sleep ?? sleep,
16
+ };
17
+ }
18
+ export async function isProcessAlive(pid, deps = {}) {
19
+ const resolved = resolveProcessCleanupDependencies(deps);
20
+ if (pid === null) {
21
+ return false;
22
+ }
23
+ try {
24
+ resolved.processKill(pid, 0);
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ }
34
+ export async function waitForProcessExit(pid, timeoutMs = 15_000, deps = {}) {
35
+ const resolved = resolveProcessCleanupDependencies(deps);
36
+ const deadline = Date.now() + timeoutMs;
37
+ while (Date.now() < deadline) {
38
+ if (!await isProcessAlive(pid, resolved)) {
39
+ return true;
40
+ }
41
+ await resolved.sleep(100);
42
+ }
43
+ return !await isProcessAlive(pid, resolved);
44
+ }
45
+ export async function terminateTrackedProcesses(trackedProcesses, deps = {}) {
46
+ const resolved = resolveProcessCleanupDependencies(deps);
47
+ const survivors = new Set();
48
+ for (const processInfo of trackedProcesses) {
49
+ try {
50
+ resolved.processKill(processInfo.pid, "SIGTERM");
51
+ }
52
+ catch (error) {
53
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
54
+ throw error;
55
+ }
56
+ }
57
+ }
58
+ for (const processInfo of trackedProcesses) {
59
+ if (!await waitForProcessExit(processInfo.pid, 5_000, resolved)) {
60
+ survivors.add(processInfo.pid);
61
+ }
62
+ }
63
+ for (const pid of survivors) {
64
+ try {
65
+ resolved.processKill(pid, "SIGKILL");
66
+ }
67
+ catch (error) {
68
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
69
+ throw error;
70
+ }
71
+ }
72
+ }
73
+ const remaining = new Set();
74
+ for (const pid of survivors) {
75
+ if (!await waitForProcessExit(pid, 5_000, resolved)) {
76
+ remaining.add(pid);
77
+ }
78
+ }
79
+ if (remaining.size > 0) {
80
+ throw new Error("reset_process_shutdown_failed");
81
+ }
82
+ return {
83
+ managedBitcoind: trackedProcesses.filter((processInfo) => processInfo.kind === "managed-bitcoind").length,
84
+ indexerDaemon: trackedProcesses.filter((processInfo) => processInfo.kind === "indexer-daemon").length,
85
+ backgroundMining: trackedProcesses.filter((processInfo) => processInfo.kind === "background-mining").length,
86
+ survivors: 0,
87
+ };
88
+ }
89
+ export async function collectTrackedManagedProcesses(paths, deps = {}) {
90
+ const trackedProcesses = [];
91
+ const trackedProcessKinds = new Set();
92
+ const serviceLockPaths = new Set();
93
+ const runtimeEntries = await readdir(paths.runtimeRoot, { withFileTypes: true }).catch((error) => {
94
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
95
+ return [];
96
+ }
97
+ throw error;
98
+ });
99
+ for (const entry of runtimeEntries) {
100
+ if (!entry.isDirectory()) {
101
+ continue;
102
+ }
103
+ const serviceRoot = join(paths.runtimeRoot, entry.name);
104
+ const bitcoindStatus = await readJsonFileOrNull(join(serviceRoot, "bitcoind-status.json"));
105
+ if (bitcoindStatus?.processId != null && await isProcessAlive(bitcoindStatus.processId, deps)) {
106
+ trackedProcesses.push({
107
+ kind: "managed-bitcoind",
108
+ pid: bitcoindStatus.processId,
109
+ });
110
+ trackedProcessKinds.add("managed-bitcoind");
111
+ serviceLockPaths.add(join(serviceRoot, "bitcoind.lock"));
112
+ }
113
+ }
114
+ const indexerEntries = await readdir(paths.indexerRoot, { withFileTypes: true }).catch((error) => {
115
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
116
+ return [];
117
+ }
118
+ throw error;
119
+ });
120
+ for (const entry of indexerEntries) {
121
+ if (!entry.isDirectory()) {
122
+ continue;
123
+ }
124
+ const status = await readJsonFileOrNull(join(paths.indexerRoot, entry.name, "status.json"));
125
+ if (status?.processId != null && await isProcessAlive(status.processId, deps)) {
126
+ trackedProcesses.push({
127
+ kind: "indexer-daemon",
128
+ pid: status.processId,
129
+ });
130
+ trackedProcessKinds.add("indexer-daemon");
131
+ serviceLockPaths.add(join(paths.runtimeRoot, entry.name, "indexer-daemon.lock"));
132
+ }
133
+ }
134
+ const miningRuntime = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
135
+ if (miningRuntime?.backgroundWorkerPid != null
136
+ && await isProcessAlive(miningRuntime.backgroundWorkerPid, deps)) {
137
+ trackedProcesses.push({
138
+ kind: "background-mining",
139
+ pid: miningRuntime.backgroundWorkerPid,
140
+ });
141
+ trackedProcessKinds.add("background-mining");
142
+ }
143
+ const seen = new Set();
144
+ const deduped = trackedProcesses.filter((processInfo) => {
145
+ const key = `${processInfo.kind}:${processInfo.pid}`;
146
+ if (seen.has(key)) {
147
+ return false;
148
+ }
149
+ seen.add(key);
150
+ return true;
151
+ });
152
+ return {
153
+ trackedProcesses: deduped,
154
+ trackedProcessKinds: [...trackedProcessKinds],
155
+ serviceLockPaths: [...serviceLockPaths].sort(),
156
+ };
157
+ }
158
+ export async function acquireResetLocks(paths, serviceLockPaths, deps = {}) {
159
+ const resolved = resolveProcessCleanupDependencies(deps);
160
+ const lockPaths = [
161
+ paths.walletControlLockPath,
162
+ paths.miningControlLockPath,
163
+ ...serviceLockPaths,
164
+ ];
165
+ const handles = [];
166
+ try {
167
+ for (const lockPath of lockPaths) {
168
+ handles.push(await resolved.acquireLock(lockPath, {
169
+ purpose: "wallet-reset",
170
+ walletRootId: null,
171
+ }));
172
+ }
173
+ return handles;
174
+ }
175
+ catch (error) {
176
+ await Promise.all(handles.map(async (handle) => handle.release().catch(() => undefined)));
177
+ throw error;
178
+ }
179
+ }
@@ -0,0 +1,189 @@
1
+ import { type FileLockHandle } from "../fs/lock.js";
2
+ import type { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
3
+ import type { createRpcClient } from "../../bitcoind/node.js";
4
+ import type { WalletRuntimePaths } from "../runtime.js";
5
+ import type { WalletSecretProvider } from "../state/provider.js";
6
+ import type { RawWalletStateEnvelope } from "../state/storage.js";
7
+ import type { WalletStateV1 } from "../types.js";
8
+ import type { WalletPrompter } from "../lifecycle.js";
9
+ export type WalletResetAction = "not-present" | "kept-unchanged" | "retain-mnemonic" | "deleted";
10
+ export type WalletResetSecretCleanupStatus = "deleted" | "not-found" | "failed" | "unknown";
11
+ export type WalletResetSnapshotResultStatus = "not-present" | "invalid-removed" | "deleted" | "preserved";
12
+ export type WalletResetBitcoinDataDirResultStatus = "not-present" | "preserved" | "deleted" | "outside-reset-scope";
13
+ export interface WalletResetResult {
14
+ dataRoot: string;
15
+ factoryResetReady: true;
16
+ stoppedProcesses: {
17
+ managedBitcoind: number;
18
+ indexerDaemon: number;
19
+ backgroundMining: number;
20
+ survivors: number;
21
+ };
22
+ secretCleanupStatus: WalletResetSecretCleanupStatus;
23
+ deletedSecretRefs: string[];
24
+ failedSecretRefs: string[];
25
+ preservedSecretRefs: string[];
26
+ walletAction: WalletResetAction;
27
+ walletOldRootId: string | null;
28
+ walletNewRootId: string | null;
29
+ bootstrapSnapshot: {
30
+ status: WalletResetSnapshotResultStatus;
31
+ path: string;
32
+ };
33
+ bitcoinDataDir: {
34
+ status: WalletResetBitcoinDataDirResultStatus;
35
+ path: string;
36
+ };
37
+ removedPaths: string[];
38
+ }
39
+ export interface WalletResetPreview {
40
+ dataRoot: string;
41
+ confirmationPhrase: "permanently reset";
42
+ walletPrompt: null | {
43
+ defaultAction: "retain-mnemonic";
44
+ acceptedInputs: ["", "skip", "clear wallet entropy"];
45
+ entropyRetainingResetAvailable: boolean;
46
+ envelopeSource: "primary" | "backup" | null;
47
+ };
48
+ bootstrapSnapshot: {
49
+ status: "not-present" | "invalid" | "valid";
50
+ path: string;
51
+ defaultAction: "preserve" | "delete";
52
+ };
53
+ bitcoinDataDir: {
54
+ status: "not-present" | "within-reset-scope" | "outside-reset-scope";
55
+ path: string;
56
+ conditionalPrompt: null | {
57
+ prompt: "Delete managed Bitcoin datadir too? [y/N]: ";
58
+ defaultAction: "preserve";
59
+ acceptedInputs: ["", "n", "no", "y", "yes"];
60
+ };
61
+ };
62
+ trackedProcessKinds: Array<"managed-bitcoind" | "indexer-daemon" | "background-mining">;
63
+ willDeleteOsSecrets: boolean;
64
+ removedPaths: string[];
65
+ }
66
+ export type WalletEnvelopeMode = "provider-backed" | "unsupported-legacy" | "unknown";
67
+ export interface WalletResetPreflight {
68
+ dataRoot: string;
69
+ removedRoots: string[];
70
+ wallet: {
71
+ present: boolean;
72
+ mode: WalletEnvelopeMode;
73
+ envelopeSource: "primary" | "backup" | null;
74
+ secretProviderKeyId: string | null;
75
+ importedSeedSecretProviderKeyIds: string[];
76
+ rawEnvelope: RawWalletStateEnvelope | null;
77
+ };
78
+ snapshot: {
79
+ status: "not-present" | "invalid" | "valid";
80
+ path: string;
81
+ shouldPrompt: boolean;
82
+ withinResetScope: boolean;
83
+ };
84
+ bitcoinDataDir: {
85
+ status: "not-present" | "within-reset-scope" | "outside-reset-scope";
86
+ path: string;
87
+ shouldPrompt: boolean;
88
+ };
89
+ trackedProcesses: TrackedManagedProcess[];
90
+ trackedProcessKinds: Array<TrackedManagedProcess["kind"]>;
91
+ serviceLockPaths: string[];
92
+ }
93
+ export interface TrackedManagedProcess {
94
+ kind: "managed-bitcoind" | "indexer-daemon" | "background-mining";
95
+ pid: number;
96
+ }
97
+ export interface StagedArtifact {
98
+ originalPath: string;
99
+ stagedPath: string;
100
+ restorePath: string;
101
+ }
102
+ export interface WalletAccessForReset {
103
+ loaded: {
104
+ source: "primary" | "backup";
105
+ state: WalletStateV1;
106
+ };
107
+ access: {
108
+ kind: "provider";
109
+ provider: WalletSecretProvider;
110
+ };
111
+ }
112
+ export interface ResetWalletRpcClient {
113
+ getDescriptorInfo(descriptor: string): Promise<{
114
+ descriptor: string;
115
+ checksum: string;
116
+ }>;
117
+ createWallet(walletName: string, options: {
118
+ blank: boolean;
119
+ descriptors: boolean;
120
+ disablePrivateKeys: boolean;
121
+ loadOnStartup: boolean;
122
+ passphrase: string;
123
+ }): Promise<unknown>;
124
+ walletPassphrase(walletName: string, passphrase: string, timeoutSeconds: number): Promise<null>;
125
+ importDescriptors(walletName: string, requests: Array<{
126
+ desc: string;
127
+ timestamp: string | number;
128
+ active?: boolean;
129
+ internal?: boolean;
130
+ range?: number | [number, number];
131
+ }>): Promise<Array<{
132
+ success: boolean;
133
+ }>>;
134
+ walletLock(walletName: string): Promise<null>;
135
+ deriveAddresses(descriptor: string, range?: number | [number, number]): Promise<string[]>;
136
+ listDescriptors(walletName: string, privateOnly?: boolean): Promise<{
137
+ descriptors: Array<{
138
+ desc: string;
139
+ }>;
140
+ }>;
141
+ getWalletInfo(walletName: string): Promise<{
142
+ walletname: string;
143
+ private_keys_enabled: boolean;
144
+ descriptors: boolean;
145
+ }>;
146
+ loadWallet(walletName: string, loadOnStartup?: boolean): Promise<{
147
+ name: string;
148
+ warning: string;
149
+ }>;
150
+ listWallets(): Promise<string[]>;
151
+ }
152
+ export interface ResetExecutionDecision {
153
+ walletChoice: "" | "skip" | "clear wallet entropy";
154
+ deleteSnapshot: boolean;
155
+ deleteBitcoinDataDir: boolean;
156
+ loadedWalletForEntropyReset: WalletAccessForReset | null;
157
+ }
158
+ export interface WalletResetArtifactDependencies {
159
+ access?: typeof import("node:fs/promises").access;
160
+ copyFile?: typeof import("node:fs/promises").copyFile;
161
+ mkdir?: typeof import("node:fs/promises").mkdir;
162
+ readFile?: typeof import("node:fs/promises").readFile;
163
+ rename?: typeof import("node:fs/promises").rename;
164
+ remove?: typeof import("node:fs/promises").rm;
165
+ }
166
+ export interface WalletResetProcessCleanupDependencies {
167
+ acquireLock?: (path: string, metadata: {
168
+ purpose: string;
169
+ walletRootId: null;
170
+ }) => Promise<FileLockHandle>;
171
+ processKill?: typeof process.kill;
172
+ sleep?: (ms: number) => Promise<void>;
173
+ }
174
+ export interface WalletResetBaseOptions {
175
+ dataDir: string;
176
+ provider?: WalletSecretProvider;
177
+ paths?: WalletRuntimePaths;
178
+ }
179
+ export interface WalletResetPreflightOptions extends WalletResetBaseOptions {
180
+ validateSnapshotFile?: (path: string) => Promise<void>;
181
+ artifactDeps?: WalletResetArtifactDependencies;
182
+ processCleanupDeps?: WalletResetProcessCleanupDependencies;
183
+ }
184
+ export interface WalletResetExecutionOptions extends WalletResetPreflightOptions {
185
+ prompter: WalletPrompter;
186
+ nowUnixMs?: number;
187
+ attachService?: typeof attachOrStartManagedBitcoindService;
188
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => ResetWalletRpcClient;
189
+ }
@@ -0,0 +1 @@
1
+ import {} from "../fs/lock.js";
@@ -1,119 +1,4 @@
1
- import { createRpcClient } from "../bitcoind/node.js";
2
- import { attachOrStartManagedBitcoindService } from "../bitcoind/service.js";
3
- import { type WalletRuntimePaths } from "./runtime.js";
4
- import { type WalletSecretProvider } from "./state/provider.js";
5
- import type { WalletPrompter } from "./lifecycle.js";
6
- export type WalletResetAction = "not-present" | "kept-unchanged" | "retain-mnemonic" | "deleted";
7
- export type WalletResetSecretCleanupStatus = "deleted" | "not-found" | "failed" | "unknown";
8
- export type WalletResetSnapshotResultStatus = "not-present" | "invalid-removed" | "deleted" | "preserved";
9
- export type WalletResetBitcoinDataDirResultStatus = "not-present" | "preserved" | "deleted" | "outside-reset-scope";
10
- export interface WalletResetResult {
11
- dataRoot: string;
12
- factoryResetReady: true;
13
- stoppedProcesses: {
14
- managedBitcoind: number;
15
- indexerDaemon: number;
16
- backgroundMining: number;
17
- survivors: number;
18
- };
19
- secretCleanupStatus: WalletResetSecretCleanupStatus;
20
- deletedSecretRefs: string[];
21
- failedSecretRefs: string[];
22
- preservedSecretRefs: string[];
23
- walletAction: WalletResetAction;
24
- walletOldRootId: string | null;
25
- walletNewRootId: string | null;
26
- bootstrapSnapshot: {
27
- status: WalletResetSnapshotResultStatus;
28
- path: string;
29
- };
30
- bitcoinDataDir: {
31
- status: WalletResetBitcoinDataDirResultStatus;
32
- path: string;
33
- };
34
- removedPaths: string[];
35
- }
36
- export interface WalletResetPreview {
37
- dataRoot: string;
38
- confirmationPhrase: "permanently reset";
39
- walletPrompt: null | {
40
- defaultAction: "retain-mnemonic";
41
- acceptedInputs: ["", "skip", "clear wallet entropy"];
42
- entropyRetainingResetAvailable: boolean;
43
- envelopeSource: "primary" | "backup" | null;
44
- };
45
- bootstrapSnapshot: {
46
- status: "not-present" | "invalid" | "valid";
47
- path: string;
48
- defaultAction: "preserve" | "delete";
49
- };
50
- bitcoinDataDir: {
51
- status: "not-present" | "within-reset-scope" | "outside-reset-scope";
52
- path: string;
53
- conditionalPrompt: null | {
54
- prompt: "Delete managed Bitcoin datadir too? [y/N]: ";
55
- defaultAction: "preserve";
56
- acceptedInputs: ["", "n", "no", "y", "yes"];
57
- };
58
- };
59
- trackedProcessKinds: Array<"managed-bitcoind" | "indexer-daemon" | "background-mining">;
60
- willDeleteOsSecrets: boolean;
61
- removedPaths: string[];
62
- }
63
- interface ResetWalletRpcClient {
64
- getDescriptorInfo(descriptor: string): Promise<{
65
- descriptor: string;
66
- checksum: string;
67
- }>;
68
- createWallet(walletName: string, options: {
69
- blank: boolean;
70
- descriptors: boolean;
71
- disablePrivateKeys: boolean;
72
- loadOnStartup: boolean;
73
- passphrase: string;
74
- }): Promise<unknown>;
75
- walletPassphrase(walletName: string, passphrase: string, timeoutSeconds: number): Promise<null>;
76
- importDescriptors(walletName: string, requests: Array<{
77
- desc: string;
78
- timestamp: string | number;
79
- active?: boolean;
80
- internal?: boolean;
81
- range?: number | [number, number];
82
- }>): Promise<Array<{
83
- success: boolean;
84
- }>>;
85
- walletLock(walletName: string): Promise<null>;
86
- deriveAddresses(descriptor: string, range?: number | [number, number]): Promise<string[]>;
87
- listDescriptors(walletName: string, privateOnly?: boolean): Promise<{
88
- descriptors: Array<{
89
- desc: string;
90
- }>;
91
- }>;
92
- getWalletInfo(walletName: string): Promise<{
93
- walletname: string;
94
- private_keys_enabled: boolean;
95
- descriptors: boolean;
96
- }>;
97
- loadWallet(walletName: string, loadOnStartup?: boolean): Promise<{
98
- name: string;
99
- warning: string;
100
- }>;
101
- listWallets(): Promise<string[]>;
102
- }
103
- export declare function previewResetWallet(options: {
104
- dataDir: string;
105
- provider?: WalletSecretProvider;
106
- paths?: WalletRuntimePaths;
107
- validateSnapshotFile?: (path: string) => Promise<void>;
108
- }): Promise<WalletResetPreview>;
109
- export declare function resetWallet(options: {
110
- dataDir: string;
111
- provider?: WalletSecretProvider;
112
- prompter: WalletPrompter;
113
- nowUnixMs?: number;
114
- paths?: WalletRuntimePaths;
115
- validateSnapshotFile?: (path: string) => Promise<void>;
116
- attachService?: typeof attachOrStartManagedBitcoindService;
117
- rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => ResetWalletRpcClient;
118
- }): Promise<WalletResetResult>;
119
- export {};
1
+ export type { WalletResetAction, WalletResetBitcoinDataDirResultStatus, WalletResetPreview, WalletResetResult, WalletResetSecretCleanupStatus, WalletResetSnapshotResultStatus, } from "./reset/types.js";
2
+ import type { WalletResetExecutionOptions, WalletResetPreflightOptions, WalletResetPreview, WalletResetResult } from "./reset/types.js";
3
+ export declare function previewResetWallet(options: WalletResetPreflightOptions): Promise<WalletResetPreview>;
4
+ export declare function resetWallet(options: WalletResetExecutionOptions): Promise<WalletResetResult>;