@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.
- package/README.md +11 -3
- package/dist/art/wallet.txt +10 -0
- package/dist/bitcoind/indexer-daemon.d.ts +9 -0
- package/dist/bitcoind/indexer-daemon.js +51 -14
- package/dist/bitcoind/service.d.ts +9 -0
- package/dist/bitcoind/service.js +65 -24
- package/dist/bitcoind/testing.d.ts +2 -2
- package/dist/bitcoind/testing.js +2 -2
- package/dist/cli/commands/service-runtime.d.ts +2 -0
- package/dist/cli/commands/service-runtime.js +432 -0
- package/dist/cli/commands/wallet-admin.js +227 -132
- package/dist/cli/commands/wallet-mutation.js +597 -580
- package/dist/cli/context.js +23 -1
- package/dist/cli/mutation-json.d.ts +17 -1
- package/dist/cli/mutation-json.js +42 -0
- package/dist/cli/output.js +112 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +65 -0
- package/dist/cli/preview-json.d.ts +19 -1
- package/dist/cli/preview-json.js +31 -0
- package/dist/cli/prompt.js +40 -12
- package/dist/cli/runner.js +12 -0
- package/dist/cli/signals.d.ts +1 -0
- package/dist/cli/signals.js +44 -0
- package/dist/cli/types.d.ts +24 -2
- package/dist/cli/types.js +6 -0
- package/dist/cli/wallet-format.js +3 -0
- package/dist/cli/workflow-hints.d.ts +1 -0
- package/dist/cli/workflow-hints.js +3 -0
- package/dist/wallet/fs/lock.d.ts +2 -0
- package/dist/wallet/fs/lock.js +32 -0
- package/dist/wallet/lifecycle.d.ts +19 -1
- package/dist/wallet/lifecycle.js +251 -3
- package/dist/wallet/material.d.ts +2 -0
- package/dist/wallet/material.js +8 -1
- package/dist/wallet/mnemonic-art.d.ts +2 -0
- package/dist/wallet/mnemonic-art.js +54 -0
- package/dist/wallet/reset.d.ts +61 -0
- package/dist/wallet/reset.js +781 -0
- package/package.json +3 -3
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { access, constants, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { DEFAULT_SNAPSHOT_METADATA } from "../bitcoind/bootstrap/constants.js";
|
|
5
|
+
import { resolveBootstrapPathsForTesting } from "../bitcoind/bootstrap/paths.js";
|
|
6
|
+
import { validateSnapshotFileForTesting } from "../bitcoind/bootstrap/snapshot-file.js";
|
|
7
|
+
import { acquireFileLock } from "./fs/lock.js";
|
|
8
|
+
import { createInternalCoreWalletPassphrase, deriveWalletIdentityMaterial, deriveWalletMaterialFromMnemonic, } from "./material.js";
|
|
9
|
+
import { loadMiningRuntimeStatus } from "./mining/runtime-artifacts.js";
|
|
10
|
+
import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
|
|
11
|
+
import { loadWalletExplicitLock } from "./state/explicit-lock.js";
|
|
12
|
+
import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
|
|
13
|
+
import { loadWalletState, saveWalletState } from "./state/storage.js";
|
|
14
|
+
import { confirmTypedAcknowledgement } from "./tx/confirm.js";
|
|
15
|
+
function sanitizeWalletName(walletRootId) {
|
|
16
|
+
return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
|
|
17
|
+
}
|
|
18
|
+
function isPathWithin(root, target) {
|
|
19
|
+
const rel = relative(root, target);
|
|
20
|
+
return rel === "" || (!rel.startsWith("..") && rel !== ".");
|
|
21
|
+
}
|
|
22
|
+
async function pathExists(path) {
|
|
23
|
+
try {
|
|
24
|
+
await access(path, constants.F_OK);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function readJsonFileOrNull(path) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
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
|
+
async function isProcessAlive(pid) {
|
|
81
|
+
if (pid === null) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function waitForProcessExit(pid, timeoutMs = 15_000) {
|
|
96
|
+
const deadline = Date.now() + timeoutMs;
|
|
97
|
+
while (Date.now() < deadline) {
|
|
98
|
+
if (!await isProcessAlive(pid)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
102
|
+
}
|
|
103
|
+
return !await isProcessAlive(pid);
|
|
104
|
+
}
|
|
105
|
+
async function terminateTrackedProcesses(trackedProcesses) {
|
|
106
|
+
const survivors = new Set();
|
|
107
|
+
for (const processInfo of trackedProcesses) {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(processInfo.pid, "SIGTERM");
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const processInfo of trackedProcesses) {
|
|
118
|
+
if (!await waitForProcessExit(processInfo.pid, 5_000)) {
|
|
119
|
+
survivors.add(processInfo.pid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const pid of survivors) {
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, "SIGKILL");
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const remaining = new Set();
|
|
133
|
+
for (const pid of survivors) {
|
|
134
|
+
if (!await waitForProcessExit(pid, 5_000)) {
|
|
135
|
+
remaining.add(pid);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (remaining.size > 0) {
|
|
139
|
+
throw new Error("reset_process_shutdown_failed");
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
managedBitcoind: trackedProcesses.filter((processInfo) => processInfo.kind === "managed-bitcoind").length,
|
|
143
|
+
indexerDaemon: trackedProcesses.filter((processInfo) => processInfo.kind === "indexer-daemon").length,
|
|
144
|
+
backgroundMining: trackedProcesses.filter((processInfo) => processInfo.kind === "background-mining").length,
|
|
145
|
+
survivors: 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function moveFile(sourcePath, destinationPath) {
|
|
149
|
+
await mkdir(dirname(destinationPath), { recursive: true });
|
|
150
|
+
try {
|
|
151
|
+
await rename(sourcePath, destinationPath);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EXDEV") {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
await copyFile(sourcePath, destinationPath);
|
|
158
|
+
await rm(sourcePath, { force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function stageArtifact(sourcePath, stagingRoot, label) {
|
|
162
|
+
if (!await pathExists(sourcePath)) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const stagedPath = join(stagingRoot, label);
|
|
166
|
+
await moveFile(sourcePath, stagedPath);
|
|
167
|
+
return {
|
|
168
|
+
originalPath: sourcePath,
|
|
169
|
+
stagedPath,
|
|
170
|
+
restorePath: sourcePath,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function restoreStagedArtifacts(artifacts) {
|
|
174
|
+
for (const artifact of artifacts) {
|
|
175
|
+
if (!await pathExists(artifact.stagedPath)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
await moveFile(artifact.stagedPath, artifact.restorePath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function collectMnemonicDerivedIdentityIndices(state) {
|
|
182
|
+
const indices = new Set();
|
|
183
|
+
indices.add(state.fundingIndex);
|
|
184
|
+
for (const identity of state.identities) {
|
|
185
|
+
if (identity.status === "funding" || identity.status === "dedicated") {
|
|
186
|
+
indices.add(identity.index);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...indices].sort((left, right) => left - right);
|
|
190
|
+
}
|
|
191
|
+
function createEntropyRetainedWalletState(previousState, nowUnixMs) {
|
|
192
|
+
const material = deriveWalletMaterialFromMnemonic(previousState.mnemonic.phrase);
|
|
193
|
+
const walletRootId = createWalletRootId();
|
|
194
|
+
const preservedIndices = collectMnemonicDerivedIdentityIndices(previousState);
|
|
195
|
+
const identities = preservedIndices.map((index) => {
|
|
196
|
+
if (index === previousState.fundingIndex) {
|
|
197
|
+
return {
|
|
198
|
+
index,
|
|
199
|
+
scriptPubKeyHex: material.funding.scriptPubKeyHex,
|
|
200
|
+
address: material.funding.address,
|
|
201
|
+
status: "funding",
|
|
202
|
+
assignedDomainNames: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const identityMaterial = deriveWalletIdentityMaterial(material.keys.accountXprv, index);
|
|
206
|
+
return {
|
|
207
|
+
index,
|
|
208
|
+
scriptPubKeyHex: identityMaterial.scriptPubKeyHex,
|
|
209
|
+
address: identityMaterial.address,
|
|
210
|
+
status: "dedicated",
|
|
211
|
+
assignedDomainNames: [],
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
stateRevision: 1,
|
|
217
|
+
lastWrittenAtUnixMs: nowUnixMs,
|
|
218
|
+
walletRootId,
|
|
219
|
+
network: previousState.network,
|
|
220
|
+
anchorValueSats: previousState.anchorValueSats,
|
|
221
|
+
nextDedicatedIndex: previousState.nextDedicatedIndex,
|
|
222
|
+
fundingIndex: previousState.fundingIndex,
|
|
223
|
+
mnemonic: {
|
|
224
|
+
phrase: previousState.mnemonic.phrase,
|
|
225
|
+
language: previousState.mnemonic.language,
|
|
226
|
+
},
|
|
227
|
+
keys: {
|
|
228
|
+
masterFingerprintHex: material.keys.masterFingerprintHex,
|
|
229
|
+
accountPath: material.keys.accountPath,
|
|
230
|
+
accountXprv: material.keys.accountXprv,
|
|
231
|
+
accountXpub: material.keys.accountXpub,
|
|
232
|
+
},
|
|
233
|
+
descriptor: {
|
|
234
|
+
privateExternal: material.descriptor.privateExternal,
|
|
235
|
+
publicExternal: material.descriptor.publicExternal,
|
|
236
|
+
checksum: null,
|
|
237
|
+
rangeEnd: previousState.descriptor.rangeEnd,
|
|
238
|
+
safetyMargin: previousState.descriptor.safetyMargin,
|
|
239
|
+
},
|
|
240
|
+
funding: {
|
|
241
|
+
address: material.funding.address,
|
|
242
|
+
scriptPubKeyHex: material.funding.scriptPubKeyHex,
|
|
243
|
+
},
|
|
244
|
+
walletBirthTime: previousState.walletBirthTime,
|
|
245
|
+
managedCoreWallet: {
|
|
246
|
+
walletName: sanitizeWalletName(walletRootId),
|
|
247
|
+
internalPassphrase: createInternalCoreWalletPassphrase(),
|
|
248
|
+
descriptorChecksum: null,
|
|
249
|
+
fundingAddress0: null,
|
|
250
|
+
fundingScriptPubKeyHex0: null,
|
|
251
|
+
proofStatus: "not-proven",
|
|
252
|
+
lastImportedAtUnixMs: null,
|
|
253
|
+
lastVerifiedAtUnixMs: null,
|
|
254
|
+
},
|
|
255
|
+
identities,
|
|
256
|
+
domains: [],
|
|
257
|
+
miningState: {
|
|
258
|
+
runMode: "stopped",
|
|
259
|
+
state: "idle",
|
|
260
|
+
pauseReason: null,
|
|
261
|
+
currentPublishState: "none",
|
|
262
|
+
currentDomain: null,
|
|
263
|
+
currentDomainId: null,
|
|
264
|
+
currentDomainIndex: null,
|
|
265
|
+
currentSenderScriptPubKeyHex: null,
|
|
266
|
+
currentTxid: null,
|
|
267
|
+
currentWtxid: null,
|
|
268
|
+
currentFeeRateSatVb: null,
|
|
269
|
+
currentAbsoluteFeeSats: null,
|
|
270
|
+
currentScore: null,
|
|
271
|
+
currentSentence: null,
|
|
272
|
+
currentEncodedSentenceBytesHex: null,
|
|
273
|
+
currentBip39WordIndices: null,
|
|
274
|
+
currentBlendSeedHex: null,
|
|
275
|
+
currentBlockTargetHeight: null,
|
|
276
|
+
currentReferencedBlockHashDisplay: null,
|
|
277
|
+
currentIntentFingerprintHex: null,
|
|
278
|
+
liveMiningFamilyInMempool: null,
|
|
279
|
+
currentPublishDecision: null,
|
|
280
|
+
replacementCount: 0,
|
|
281
|
+
currentBlockFeeSpentSats: "0",
|
|
282
|
+
sessionFeeSpentSats: "0",
|
|
283
|
+
lifetimeFeeSpentSats: "0",
|
|
284
|
+
sharedMiningConflictOutpoint: null,
|
|
285
|
+
},
|
|
286
|
+
hookClientState: {
|
|
287
|
+
mining: {
|
|
288
|
+
mode: "builtin",
|
|
289
|
+
validationState: "never",
|
|
290
|
+
lastValidationAtUnixMs: null,
|
|
291
|
+
lastValidationError: null,
|
|
292
|
+
validatedLaunchFingerprint: null,
|
|
293
|
+
validatedFullFingerprint: null,
|
|
294
|
+
fullTrustWarningAcknowledgedAtUnixMs: null,
|
|
295
|
+
consecutiveFailureCount: 0,
|
|
296
|
+
cooldownUntilUnixMs: null,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
proactiveFamilies: [],
|
|
300
|
+
pendingMutations: [],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function promptHiddenOrVisible(prompter, message) {
|
|
304
|
+
if (typeof prompter.promptHidden === "function") {
|
|
305
|
+
return await prompter.promptHidden(message);
|
|
306
|
+
}
|
|
307
|
+
return await prompter.prompt(message);
|
|
308
|
+
}
|
|
309
|
+
async function loadWalletForEntropyReset(options) {
|
|
310
|
+
if (options.wallet.rawEnvelope === null) {
|
|
311
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
312
|
+
}
|
|
313
|
+
if (options.wallet.mode === "provider-backed") {
|
|
314
|
+
try {
|
|
315
|
+
return {
|
|
316
|
+
loaded: await loadWalletState({
|
|
317
|
+
primaryPath: options.paths.walletStatePath,
|
|
318
|
+
backupPath: options.paths.walletStateBackupPath,
|
|
319
|
+
}, {
|
|
320
|
+
provider: options.provider,
|
|
321
|
+
}),
|
|
322
|
+
access: {
|
|
323
|
+
kind: "provider",
|
|
324
|
+
provider: options.provider,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (options.wallet.mode !== "passphrase-wrapped") {
|
|
333
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
334
|
+
}
|
|
335
|
+
const passphrase = (await promptHiddenOrVisible(options.prompter, "Wallet-state passphrase: ")).trim();
|
|
336
|
+
if (passphrase === "") {
|
|
337
|
+
throw new Error("reset_wallet_passphrase_required");
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
return {
|
|
341
|
+
loaded: await loadWalletState({
|
|
342
|
+
primaryPath: options.paths.walletStatePath,
|
|
343
|
+
backupPath: options.paths.walletStateBackupPath,
|
|
344
|
+
}, passphrase),
|
|
345
|
+
access: {
|
|
346
|
+
kind: "passphrase",
|
|
347
|
+
passphrase,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
throw new Error("reset_wallet_access_failed");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function collectTrackedManagedProcesses(paths) {
|
|
356
|
+
const trackedProcesses = [];
|
|
357
|
+
const trackedProcessKinds = new Set();
|
|
358
|
+
const serviceLockPaths = new Set();
|
|
359
|
+
const runtimeEntries = await readdir(paths.runtimeRoot, { withFileTypes: true }).catch((error) => {
|
|
360
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
throw error;
|
|
364
|
+
});
|
|
365
|
+
for (const entry of runtimeEntries) {
|
|
366
|
+
if (!entry.isDirectory()) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const serviceRoot = join(paths.runtimeRoot, entry.name);
|
|
370
|
+
const bitcoindStatus = await readJsonFileOrNull(join(serviceRoot, "bitcoind-status.json"));
|
|
371
|
+
if (bitcoindStatus?.processId != null && await isProcessAlive(bitcoindStatus.processId)) {
|
|
372
|
+
trackedProcesses.push({
|
|
373
|
+
kind: "managed-bitcoind",
|
|
374
|
+
pid: bitcoindStatus.processId,
|
|
375
|
+
});
|
|
376
|
+
trackedProcessKinds.add("managed-bitcoind");
|
|
377
|
+
serviceLockPaths.add(join(serviceRoot, "bitcoind.lock"));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const indexerEntries = await readdir(paths.indexerRoot, { withFileTypes: true }).catch((error) => {
|
|
381
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
});
|
|
386
|
+
for (const entry of indexerEntries) {
|
|
387
|
+
if (!entry.isDirectory()) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const status = await readJsonFileOrNull(join(paths.indexerRoot, entry.name, "status.json"));
|
|
391
|
+
if (status?.processId != null && await isProcessAlive(status.processId)) {
|
|
392
|
+
trackedProcesses.push({
|
|
393
|
+
kind: "indexer-daemon",
|
|
394
|
+
pid: status.processId,
|
|
395
|
+
});
|
|
396
|
+
trackedProcessKinds.add("indexer-daemon");
|
|
397
|
+
serviceLockPaths.add(join(paths.runtimeRoot, entry.name, "indexer-daemon.lock"));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const miningRuntime = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
|
|
401
|
+
if (miningRuntime?.backgroundWorkerPid != null
|
|
402
|
+
&& await isProcessAlive(miningRuntime.backgroundWorkerPid)) {
|
|
403
|
+
trackedProcesses.push({
|
|
404
|
+
kind: "background-mining",
|
|
405
|
+
pid: miningRuntime.backgroundWorkerPid,
|
|
406
|
+
});
|
|
407
|
+
trackedProcessKinds.add("background-mining");
|
|
408
|
+
}
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
const deduped = trackedProcesses.filter((processInfo) => {
|
|
411
|
+
const key = `${processInfo.kind}:${processInfo.pid}`;
|
|
412
|
+
if (seen.has(key)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
seen.add(key);
|
|
416
|
+
return true;
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
trackedProcesses: deduped,
|
|
420
|
+
trackedProcessKinds: [...trackedProcessKinds],
|
|
421
|
+
serviceLockPaths: [...serviceLockPaths].sort(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function resolveRemovedRoots(paths) {
|
|
425
|
+
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);
|
|
432
|
+
}
|
|
433
|
+
async function preflightReset(options) {
|
|
434
|
+
const rawEnvelope = await loadRawWalletEnvelope(options.paths);
|
|
435
|
+
const explicitLock = await loadWalletExplicitLock(options.paths.walletExplicitLockPath).catch(() => null);
|
|
436
|
+
const snapshotPaths = resolveBootstrapPathsForTesting(options.dataDir, DEFAULT_SNAPSHOT_METADATA);
|
|
437
|
+
const validateSnapshot = options.validateSnapshotFile
|
|
438
|
+
?? ((path) => validateSnapshotFileForTesting(path, DEFAULT_SNAPSHOT_METADATA));
|
|
439
|
+
const hasWalletState = await pathExists(options.paths.walletStatePath) || await pathExists(options.paths.walletStateBackupPath);
|
|
440
|
+
const hasSnapshot = await pathExists(snapshotPaths.snapshotPath);
|
|
441
|
+
const hasPartialSnapshot = await pathExists(snapshotPaths.partialSnapshotPath);
|
|
442
|
+
let snapshotStatus = "not-present";
|
|
443
|
+
if (hasSnapshot) {
|
|
444
|
+
try {
|
|
445
|
+
await validateSnapshot(snapshotPaths.snapshotPath);
|
|
446
|
+
snapshotStatus = "valid";
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
snapshotStatus = "invalid";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (hasPartialSnapshot) {
|
|
453
|
+
snapshotStatus = "invalid";
|
|
454
|
+
}
|
|
455
|
+
const tracked = await collectTrackedManagedProcesses(options.paths);
|
|
456
|
+
const secretProviderKeyId = rawEnvelope?.envelope.secretProvider?.keyId ?? null;
|
|
457
|
+
return {
|
|
458
|
+
dataRoot: options.paths.dataRoot,
|
|
459
|
+
removedRoots: resolveRemovedRoots(options.paths),
|
|
460
|
+
wallet: {
|
|
461
|
+
present: hasWalletState,
|
|
462
|
+
mode: rawEnvelope == null
|
|
463
|
+
? (hasWalletState ? "unknown" : "unknown")
|
|
464
|
+
: rawEnvelope.envelope.secretProvider != null
|
|
465
|
+
? "provider-backed"
|
|
466
|
+
: "passphrase-wrapped",
|
|
467
|
+
envelopeSource: rawEnvelope?.source ?? null,
|
|
468
|
+
secretProviderKeyId,
|
|
469
|
+
explicitLock,
|
|
470
|
+
rawEnvelope,
|
|
471
|
+
},
|
|
472
|
+
snapshot: {
|
|
473
|
+
status: snapshotStatus,
|
|
474
|
+
path: snapshotPaths.snapshotPath,
|
|
475
|
+
shouldPrompt: snapshotStatus === "valid",
|
|
476
|
+
shouldStageForPreserve: snapshotStatus === "valid"
|
|
477
|
+
&& resolveRemovedRoots(options.paths).some((root) => isPathWithin(root, snapshotPaths.snapshotPath)),
|
|
478
|
+
},
|
|
479
|
+
trackedProcesses: tracked.trackedProcesses,
|
|
480
|
+
trackedProcessKinds: tracked.trackedProcessKinds,
|
|
481
|
+
serviceLockPaths: tracked.serviceLockPaths,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async function acquireResetLocks(paths, serviceLockPaths) {
|
|
485
|
+
const lockPaths = [
|
|
486
|
+
paths.walletControlLockPath,
|
|
487
|
+
paths.miningControlLockPath,
|
|
488
|
+
...serviceLockPaths,
|
|
489
|
+
];
|
|
490
|
+
const handles = [];
|
|
491
|
+
try {
|
|
492
|
+
for (const lockPath of lockPaths) {
|
|
493
|
+
handles.push(await acquireFileLock(lockPath, {
|
|
494
|
+
purpose: "wallet-reset",
|
|
495
|
+
walletRootId: null,
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
return handles;
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
await Promise.all(handles.map(async (handle) => handle.release().catch(() => undefined)));
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function deleteRemovedRoots(roots) {
|
|
506
|
+
try {
|
|
507
|
+
for (const root of roots) {
|
|
508
|
+
await rm(root, {
|
|
509
|
+
recursive: true,
|
|
510
|
+
force: true,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
throw new Error("reset_data_root_delete_failed");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function resolveResetExecutionDecision(options) {
|
|
519
|
+
if (!options.prompter.isInteractive) {
|
|
520
|
+
throw new Error("reset_requires_tty");
|
|
521
|
+
}
|
|
522
|
+
await confirmTypedAcknowledgement(options.prompter, {
|
|
523
|
+
expected: "permanently reset",
|
|
524
|
+
prompt: "Type \"permanently reset\" to continue: ",
|
|
525
|
+
errorCode: "reset_typed_ack_required",
|
|
526
|
+
requiresTtyErrorCode: "reset_requires_tty",
|
|
527
|
+
typedAckRequiredErrorCode: "reset_typed_ack_required",
|
|
528
|
+
});
|
|
529
|
+
let walletChoice = "";
|
|
530
|
+
let loadedWalletForEntropyReset = null;
|
|
531
|
+
if (options.preflight.wallet.present) {
|
|
532
|
+
const answer = (await options.prompter.prompt("Wallet reset choice ([Enter] retain base entropy, \"skip\", or \"delete wallet\"): ")).trim();
|
|
533
|
+
if (answer !== "" && answer !== "skip" && answer !== "delete wallet") {
|
|
534
|
+
throw new Error("reset_wallet_choice_invalid");
|
|
535
|
+
}
|
|
536
|
+
walletChoice = answer;
|
|
537
|
+
if (walletChoice === "") {
|
|
538
|
+
loadedWalletForEntropyReset = await loadWalletForEntropyReset({
|
|
539
|
+
wallet: options.preflight.wallet,
|
|
540
|
+
paths: options.paths,
|
|
541
|
+
provider: options.provider,
|
|
542
|
+
prompter: options.prompter,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
let deleteSnapshot = false;
|
|
547
|
+
if (options.preflight.snapshot.shouldPrompt) {
|
|
548
|
+
const answer = (await options.prompter.prompt("Delete downloaded 910000 UTXO snapshot too? [y/N]: ")).trim().toLowerCase();
|
|
549
|
+
deleteSnapshot = answer === "y" || answer === "yes";
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
walletChoice,
|
|
553
|
+
deleteSnapshot,
|
|
554
|
+
loadedWalletForEntropyReset,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function determineWalletAction(walletPresent, walletChoice) {
|
|
558
|
+
if (!walletPresent) {
|
|
559
|
+
return "not-present";
|
|
560
|
+
}
|
|
561
|
+
if (walletChoice === "skip") {
|
|
562
|
+
return "kept-unchanged";
|
|
563
|
+
}
|
|
564
|
+
if (walletChoice === "delete wallet") {
|
|
565
|
+
return "deleted";
|
|
566
|
+
}
|
|
567
|
+
return "reset-base-entropy";
|
|
568
|
+
}
|
|
569
|
+
function determineSnapshotResultStatus(options) {
|
|
570
|
+
if (options.snapshotStatus === "not-present") {
|
|
571
|
+
return "not-present";
|
|
572
|
+
}
|
|
573
|
+
if (options.snapshotStatus === "invalid") {
|
|
574
|
+
return "invalid-removed";
|
|
575
|
+
}
|
|
576
|
+
return options.deleteSnapshot ? "deleted" : "preserved";
|
|
577
|
+
}
|
|
578
|
+
export async function previewResetWallet(options) {
|
|
579
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
580
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
581
|
+
const preflight = await preflightReset({
|
|
582
|
+
dataDir: options.dataDir,
|
|
583
|
+
provider,
|
|
584
|
+
paths,
|
|
585
|
+
validateSnapshotFile: options.validateSnapshotFile,
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
dataRoot: preflight.dataRoot,
|
|
589
|
+
confirmationPhrase: "permanently reset",
|
|
590
|
+
walletPrompt: preflight.wallet.present
|
|
591
|
+
? {
|
|
592
|
+
defaultAction: "reset-base-entropy",
|
|
593
|
+
acceptedInputs: ["", "skip", "delete wallet"],
|
|
594
|
+
entropyRetainingResetAvailable: preflight.wallet.mode !== "unknown",
|
|
595
|
+
requiresPassphrase: preflight.wallet.mode === "passphrase-wrapped",
|
|
596
|
+
envelopeSource: preflight.wallet.envelopeSource,
|
|
597
|
+
}
|
|
598
|
+
: null,
|
|
599
|
+
bootstrapSnapshot: {
|
|
600
|
+
status: preflight.snapshot.status,
|
|
601
|
+
path: preflight.snapshot.path,
|
|
602
|
+
defaultAction: preflight.snapshot.status === "valid" ? "preserve" : "delete",
|
|
603
|
+
},
|
|
604
|
+
trackedProcessKinds: preflight.trackedProcessKinds,
|
|
605
|
+
willDeleteOsSecrets: preflight.wallet.secretProviderKeyId !== null,
|
|
606
|
+
removedPaths: preflight.removedRoots,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
export async function resetWallet(options) {
|
|
610
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
611
|
+
const nowUnixMs = options.nowUnixMs ?? Date.now();
|
|
612
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
613
|
+
const preflight = await preflightReset({
|
|
614
|
+
dataDir: options.dataDir,
|
|
615
|
+
provider,
|
|
616
|
+
paths,
|
|
617
|
+
validateSnapshotFile: options.validateSnapshotFile,
|
|
618
|
+
});
|
|
619
|
+
const decision = await resolveResetExecutionDecision({
|
|
620
|
+
preflight,
|
|
621
|
+
provider,
|
|
622
|
+
prompter: options.prompter,
|
|
623
|
+
paths,
|
|
624
|
+
});
|
|
625
|
+
const walletAction = determineWalletAction(preflight.wallet.present, decision.walletChoice);
|
|
626
|
+
const snapshotResultStatus = determineSnapshotResultStatus({
|
|
627
|
+
snapshotStatus: preflight.snapshot.status,
|
|
628
|
+
deleteSnapshot: decision.deleteSnapshot,
|
|
629
|
+
});
|
|
630
|
+
const locks = await acquireResetLocks(paths, preflight.serviceLockPaths);
|
|
631
|
+
await mkdir(dirname(paths.dataRoot), { recursive: true });
|
|
632
|
+
const stagingRoot = await mkdtemp(join(dirname(paths.dataRoot), ".cogcoin-reset-"));
|
|
633
|
+
const stagedWalletArtifacts = [];
|
|
634
|
+
const stagedSnapshotArtifacts = [];
|
|
635
|
+
let stoppedProcesses = {
|
|
636
|
+
managedBitcoind: 0,
|
|
637
|
+
indexerDaemon: 0,
|
|
638
|
+
backgroundMining: 0,
|
|
639
|
+
survivors: 0,
|
|
640
|
+
};
|
|
641
|
+
let rootsDeleted = false;
|
|
642
|
+
let committed = false;
|
|
643
|
+
let newProviderKeyId = null;
|
|
644
|
+
let secretCleanupStatus = preflight.wallet.secretProviderKeyId === null
|
|
645
|
+
? "not-found"
|
|
646
|
+
: "not-found";
|
|
647
|
+
const deletedSecretRefs = [];
|
|
648
|
+
const failedSecretRefs = [];
|
|
649
|
+
const preservedSecretRefs = [];
|
|
650
|
+
let walletOldRootId = extractWalletRootIdFromSecretKeyId(preflight.wallet.secretProviderKeyId)
|
|
651
|
+
?? preflight.wallet.explicitLock?.walletRootId
|
|
652
|
+
?? null;
|
|
653
|
+
let walletNewRootId = null;
|
|
654
|
+
try {
|
|
655
|
+
stoppedProcesses = await terminateTrackedProcesses(preflight.trackedProcesses);
|
|
656
|
+
if (walletAction === "kept-unchanged" || walletAction === "reset-base-entropy") {
|
|
657
|
+
const stagedPrimary = await stageArtifact(paths.walletStatePath, stagingRoot, "wallet/wallet-state.enc");
|
|
658
|
+
const stagedBackup = await stageArtifact(paths.walletStateBackupPath, stagingRoot, "wallet/wallet-state.enc.bak");
|
|
659
|
+
const stagedExplicitLock = await stageArtifact(paths.walletExplicitLockPath, stagingRoot, "wallet/wallet-explicit-lock.json");
|
|
660
|
+
if (stagedPrimary !== null) {
|
|
661
|
+
stagedWalletArtifacts.push(stagedPrimary);
|
|
662
|
+
}
|
|
663
|
+
if (stagedBackup !== null) {
|
|
664
|
+
stagedWalletArtifacts.push(stagedBackup);
|
|
665
|
+
}
|
|
666
|
+
if (walletAction === "kept-unchanged" && stagedExplicitLock !== null) {
|
|
667
|
+
stagedWalletArtifacts.push(stagedExplicitLock);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (snapshotResultStatus === "preserved" && preflight.snapshot.shouldStageForPreserve) {
|
|
671
|
+
const stagedSnapshot = await stageArtifact(preflight.snapshot.path, stagingRoot, "snapshot/utxo-910000.dat");
|
|
672
|
+
if (stagedSnapshot !== null) {
|
|
673
|
+
stagedSnapshotArtifacts.push(stagedSnapshot);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
await deleteRemovedRoots(preflight.removedRoots);
|
|
677
|
+
rootsDeleted = true;
|
|
678
|
+
if (walletAction === "kept-unchanged") {
|
|
679
|
+
await restoreStagedArtifacts(stagedWalletArtifacts);
|
|
680
|
+
}
|
|
681
|
+
else if (walletAction === "reset-base-entropy") {
|
|
682
|
+
if (decision.loadedWalletForEntropyReset === null) {
|
|
683
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
684
|
+
}
|
|
685
|
+
const nextState = createEntropyRetainedWalletState(decision.loadedWalletForEntropyReset.loaded.state, nowUnixMs);
|
|
686
|
+
walletOldRootId = decision.loadedWalletForEntropyReset.loaded.state.walletRootId;
|
|
687
|
+
walletNewRootId = nextState.walletRootId;
|
|
688
|
+
if (decision.loadedWalletForEntropyReset.access.kind === "provider") {
|
|
689
|
+
const secretReference = createWalletSecretReference(nextState.walletRootId);
|
|
690
|
+
newProviderKeyId = secretReference.keyId;
|
|
691
|
+
await provider.storeSecret(secretReference.keyId, randomBytes(32));
|
|
692
|
+
await saveWalletState({
|
|
693
|
+
primaryPath: paths.walletStatePath,
|
|
694
|
+
backupPath: paths.walletStateBackupPath,
|
|
695
|
+
}, nextState, {
|
|
696
|
+
provider,
|
|
697
|
+
secretReference,
|
|
698
|
+
});
|
|
699
|
+
preservedSecretRefs.push(secretReference.keyId);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
await saveWalletState({
|
|
703
|
+
primaryPath: paths.walletStatePath,
|
|
704
|
+
backupPath: paths.walletStateBackupPath,
|
|
705
|
+
}, nextState, decision.loadedWalletForEntropyReset.access.passphrase);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (snapshotResultStatus === "preserved") {
|
|
709
|
+
await restoreStagedArtifacts(stagedSnapshotArtifacts);
|
|
710
|
+
}
|
|
711
|
+
committed = true;
|
|
712
|
+
if (walletAction === "deleted") {
|
|
713
|
+
if (preflight.wallet.secretProviderKeyId !== null) {
|
|
714
|
+
try {
|
|
715
|
+
await provider.deleteSecret(preflight.wallet.secretProviderKeyId);
|
|
716
|
+
deletedSecretRefs.push(preflight.wallet.secretProviderKeyId);
|
|
717
|
+
secretCleanupStatus = "deleted";
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
failedSecretRefs.push(preflight.wallet.secretProviderKeyId);
|
|
721
|
+
secretCleanupStatus = "failed";
|
|
722
|
+
throw new Error("reset_secret_cleanup_failed");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else if (walletAction === "reset-base-entropy" && preflight.wallet.secretProviderKeyId !== null) {
|
|
727
|
+
try {
|
|
728
|
+
if (preflight.wallet.secretProviderKeyId !== newProviderKeyId) {
|
|
729
|
+
await provider.deleteSecret(preflight.wallet.secretProviderKeyId);
|
|
730
|
+
deletedSecretRefs.push(preflight.wallet.secretProviderKeyId);
|
|
731
|
+
secretCleanupStatus = "deleted";
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
failedSecretRefs.push(preflight.wallet.secretProviderKeyId);
|
|
736
|
+
secretCleanupStatus = "failed";
|
|
737
|
+
throw new Error("reset_secret_cleanup_failed");
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else if (preflight.wallet.secretProviderKeyId !== null) {
|
|
741
|
+
preservedSecretRefs.push(preflight.wallet.secretProviderKeyId);
|
|
742
|
+
}
|
|
743
|
+
if (preflight.wallet.secretProviderKeyId === null && preflight.wallet.present && preflight.wallet.rawEnvelope === null) {
|
|
744
|
+
secretCleanupStatus = "unknown";
|
|
745
|
+
}
|
|
746
|
+
else if (deletedSecretRefs.length === 0 && failedSecretRefs.length === 0) {
|
|
747
|
+
secretCleanupStatus = "not-found";
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
dataRoot: preflight.dataRoot,
|
|
751
|
+
factoryResetReady: true,
|
|
752
|
+
stoppedProcesses,
|
|
753
|
+
secretCleanupStatus,
|
|
754
|
+
deletedSecretRefs,
|
|
755
|
+
failedSecretRefs,
|
|
756
|
+
preservedSecretRefs,
|
|
757
|
+
walletAction,
|
|
758
|
+
walletOldRootId,
|
|
759
|
+
walletNewRootId,
|
|
760
|
+
bootstrapSnapshot: {
|
|
761
|
+
status: snapshotResultStatus,
|
|
762
|
+
path: preflight.snapshot.path,
|
|
763
|
+
},
|
|
764
|
+
removedPaths: preflight.removedRoots,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
if (!committed && rootsDeleted) {
|
|
769
|
+
await restoreStagedArtifacts(stagedWalletArtifacts).catch(() => undefined);
|
|
770
|
+
await restoreStagedArtifacts(stagedSnapshotArtifacts).catch(() => undefined);
|
|
771
|
+
if (newProviderKeyId !== null) {
|
|
772
|
+
await provider.deleteSecret(newProviderKeyId).catch(() => undefined);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
await rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
779
|
+
await Promise.all(locks.reverse().map(async (lock) => lock.release().catch(() => undefined)));
|
|
780
|
+
}
|
|
781
|
+
}
|