@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.
- package/README.md +1 -1
- package/dist/bitcoind/service.js +1 -1
- package/dist/wallet/mining/engine-state.js +10 -0
- package/dist/wallet/mining/visualizer-sync.js +79 -15
- package/dist/wallet/read/context.js +1 -1
- package/dist/wallet/reset/artifacts.d.ts +16 -0
- package/dist/wallet/reset/artifacts.js +141 -0
- package/dist/wallet/reset/execution.d.ts +38 -0
- package/dist/wallet/reset/execution.js +458 -0
- package/dist/wallet/reset/preflight.d.ts +7 -0
- package/dist/wallet/reset/preflight.js +116 -0
- package/dist/wallet/reset/preview.d.ts +2 -0
- package/dist/wallet/reset/preview.js +50 -0
- package/dist/wallet/reset/process-cleanup.d.ts +12 -0
- package/dist/wallet/reset/process-cleanup.js +179 -0
- package/dist/wallet/reset/types.d.ts +189 -0
- package/dist/wallet/reset/types.js +1 -0
- package/dist/wallet/reset.d.ts +4 -119
- package/dist/wallet/reset.js +4 -882
- package/package.json +1 -1
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { attachOrStartManagedBitcoindService, createManagedWalletReplica, } from "../../bitcoind/service.js";
|
|
5
|
+
import { createRpcClient } from "../../bitcoind/node.js";
|
|
6
|
+
import { resolveNormalizedWalletDescriptorState } from "../descriptor-normalization.js";
|
|
7
|
+
import { createInternalCoreWalletPassphrase, deriveWalletMaterialFromMnemonic, } from "../material.js";
|
|
8
|
+
import { withUnlockedManagedCoreWallet } from "../managed-core-wallet.js";
|
|
9
|
+
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
10
|
+
import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "../state/provider.js";
|
|
11
|
+
import { extractWalletRootIdHintFromWalletStateEnvelope, loadWalletState, saveWalletState, } from "../state/storage.js";
|
|
12
|
+
import { confirmTypedAcknowledgement } from "../tx/confirm.js";
|
|
13
|
+
import { deleteBootstrapSnapshotArtifacts, deleteRemovedRoots, isDeletedByRemovalPlan, restoreStagedArtifacts, resolveRemovedRoots, stageArtifact, } from "./artifacts.js";
|
|
14
|
+
import { preflightReset } from "./preflight.js";
|
|
15
|
+
import { acquireResetLocks, terminateTrackedProcesses, } from "./process-cleanup.js";
|
|
16
|
+
function sanitizeWalletName(walletRootId) {
|
|
17
|
+
return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
|
|
18
|
+
}
|
|
19
|
+
export async function loadWalletForEntropyReset(options) {
|
|
20
|
+
if (options.wallet.rawEnvelope === null) {
|
|
21
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
22
|
+
}
|
|
23
|
+
if (options.wallet.mode === "provider-backed") {
|
|
24
|
+
try {
|
|
25
|
+
const loaded = await loadWalletState({
|
|
26
|
+
primaryPath: options.paths.walletStatePath,
|
|
27
|
+
backupPath: options.paths.walletStateBackupPath,
|
|
28
|
+
}, {
|
|
29
|
+
provider: options.provider,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
loaded,
|
|
33
|
+
access: {
|
|
34
|
+
kind: "provider",
|
|
35
|
+
provider: options.provider,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
44
|
+
}
|
|
45
|
+
export function createEntropyRetainedWalletState(previousState, nowUnixMs) {
|
|
46
|
+
const material = deriveWalletMaterialFromMnemonic(previousState.mnemonic.phrase);
|
|
47
|
+
const walletRootId = createWalletRootId();
|
|
48
|
+
return {
|
|
49
|
+
schemaVersion: 5,
|
|
50
|
+
stateRevision: 1,
|
|
51
|
+
lastWrittenAtUnixMs: nowUnixMs,
|
|
52
|
+
walletRootId,
|
|
53
|
+
network: previousState.network,
|
|
54
|
+
localScriptPubKeyHexes: [material.funding.scriptPubKeyHex],
|
|
55
|
+
mnemonic: {
|
|
56
|
+
phrase: previousState.mnemonic.phrase,
|
|
57
|
+
language: previousState.mnemonic.language,
|
|
58
|
+
},
|
|
59
|
+
keys: {
|
|
60
|
+
masterFingerprintHex: material.keys.masterFingerprintHex,
|
|
61
|
+
accountPath: material.keys.accountPath,
|
|
62
|
+
accountXprv: material.keys.accountXprv,
|
|
63
|
+
accountXpub: material.keys.accountXpub,
|
|
64
|
+
},
|
|
65
|
+
descriptor: {
|
|
66
|
+
privateExternal: material.descriptor.privateExternal,
|
|
67
|
+
publicExternal: material.descriptor.publicExternal,
|
|
68
|
+
checksum: null,
|
|
69
|
+
rangeEnd: previousState.descriptor.rangeEnd,
|
|
70
|
+
safetyMargin: previousState.descriptor.safetyMargin,
|
|
71
|
+
},
|
|
72
|
+
funding: {
|
|
73
|
+
address: material.funding.address,
|
|
74
|
+
scriptPubKeyHex: material.funding.scriptPubKeyHex,
|
|
75
|
+
},
|
|
76
|
+
walletBirthTime: previousState.walletBirthTime,
|
|
77
|
+
managedCoreWallet: {
|
|
78
|
+
walletName: sanitizeWalletName(walletRootId),
|
|
79
|
+
internalPassphrase: createInternalCoreWalletPassphrase(),
|
|
80
|
+
descriptorChecksum: null,
|
|
81
|
+
walletAddress: null,
|
|
82
|
+
walletScriptPubKeyHex: null,
|
|
83
|
+
proofStatus: "not-proven",
|
|
84
|
+
lastImportedAtUnixMs: null,
|
|
85
|
+
lastVerifiedAtUnixMs: null,
|
|
86
|
+
},
|
|
87
|
+
domains: [],
|
|
88
|
+
miningState: {
|
|
89
|
+
runMode: "stopped",
|
|
90
|
+
state: "idle",
|
|
91
|
+
pauseReason: null,
|
|
92
|
+
currentPublishState: "none",
|
|
93
|
+
currentDomain: null,
|
|
94
|
+
currentDomainId: null,
|
|
95
|
+
currentDomainIndex: null,
|
|
96
|
+
currentSenderScriptPubKeyHex: null,
|
|
97
|
+
currentTxid: null,
|
|
98
|
+
currentWtxid: null,
|
|
99
|
+
currentFeeRateSatVb: null,
|
|
100
|
+
currentAbsoluteFeeSats: null,
|
|
101
|
+
currentScore: null,
|
|
102
|
+
currentSentence: null,
|
|
103
|
+
currentEncodedSentenceBytesHex: null,
|
|
104
|
+
currentBip39WordIndices: null,
|
|
105
|
+
currentBlendSeedHex: null,
|
|
106
|
+
currentBlockTargetHeight: null,
|
|
107
|
+
currentReferencedBlockHashDisplay: null,
|
|
108
|
+
currentIntentFingerprintHex: null,
|
|
109
|
+
livePublishInMempool: null,
|
|
110
|
+
currentPublishDecision: null,
|
|
111
|
+
replacementCount: 0,
|
|
112
|
+
currentBlockFeeSpentSats: "0",
|
|
113
|
+
sessionFeeSpentSats: "0",
|
|
114
|
+
lifetimeFeeSpentSats: "0",
|
|
115
|
+
sharedMiningConflictOutpoint: null,
|
|
116
|
+
},
|
|
117
|
+
pendingMutations: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export async function recreateManagedCoreWalletReplicaForReset(options) {
|
|
121
|
+
const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
|
|
122
|
+
dataDir: options.dataDir,
|
|
123
|
+
chain: "main",
|
|
124
|
+
startHeight: 0,
|
|
125
|
+
walletRootId: options.state.walletRootId,
|
|
126
|
+
managedWalletPassphrase: options.state.managedCoreWallet.internalPassphrase,
|
|
127
|
+
});
|
|
128
|
+
const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
|
|
129
|
+
await createManagedWalletReplica(rpc, options.state.walletRootId, {
|
|
130
|
+
managedWalletPassphrase: options.state.managedCoreWallet.internalPassphrase,
|
|
131
|
+
});
|
|
132
|
+
const normalizedDescriptors = await resolveNormalizedWalletDescriptorState(options.state, rpc);
|
|
133
|
+
const walletName = sanitizeWalletName(options.state.walletRootId);
|
|
134
|
+
await withUnlockedManagedCoreWallet({
|
|
135
|
+
rpc,
|
|
136
|
+
walletName,
|
|
137
|
+
internalPassphrase: options.state.managedCoreWallet.internalPassphrase,
|
|
138
|
+
run: async () => {
|
|
139
|
+
const importResults = await rpc.importDescriptors(walletName, [{
|
|
140
|
+
desc: normalizedDescriptors.privateExternal,
|
|
141
|
+
timestamp: options.state.walletBirthTime,
|
|
142
|
+
active: false,
|
|
143
|
+
internal: false,
|
|
144
|
+
range: [0, options.state.descriptor.rangeEnd],
|
|
145
|
+
}]);
|
|
146
|
+
if (!importResults.every((result) => result.success)) {
|
|
147
|
+
throw new Error(`wallet_descriptor_import_failed_${JSON.stringify(importResults)}`);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const derivedFunding = await rpc.deriveAddresses(normalizedDescriptors.publicExternal, [0, 0]);
|
|
152
|
+
if (derivedFunding[0] !== options.state.funding.address) {
|
|
153
|
+
throw new Error("wallet_funding_address_verification_failed");
|
|
154
|
+
}
|
|
155
|
+
const descriptors = await rpc.listDescriptors(walletName);
|
|
156
|
+
const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === normalizedDescriptors.publicExternal);
|
|
157
|
+
if (importedDescriptor == null) {
|
|
158
|
+
throw new Error("wallet_descriptor_not_present_after_import");
|
|
159
|
+
}
|
|
160
|
+
const nextState = {
|
|
161
|
+
...options.state,
|
|
162
|
+
stateRevision: options.state.stateRevision + 1,
|
|
163
|
+
lastWrittenAtUnixMs: options.nowUnixMs,
|
|
164
|
+
descriptor: {
|
|
165
|
+
...options.state.descriptor,
|
|
166
|
+
privateExternal: normalizedDescriptors.privateExternal,
|
|
167
|
+
publicExternal: normalizedDescriptors.publicExternal,
|
|
168
|
+
checksum: normalizedDescriptors.checksum,
|
|
169
|
+
},
|
|
170
|
+
managedCoreWallet: {
|
|
171
|
+
...options.state.managedCoreWallet,
|
|
172
|
+
walletName,
|
|
173
|
+
descriptorChecksum: normalizedDescriptors.checksum,
|
|
174
|
+
walletAddress: options.state.funding.address,
|
|
175
|
+
walletScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
176
|
+
proofStatus: "ready",
|
|
177
|
+
lastImportedAtUnixMs: options.nowUnixMs,
|
|
178
|
+
lastVerifiedAtUnixMs: options.nowUnixMs,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
await saveWalletState({
|
|
182
|
+
primaryPath: options.paths.walletStatePath,
|
|
183
|
+
backupPath: options.paths.walletStateBackupPath,
|
|
184
|
+
}, nextState, options.access);
|
|
185
|
+
return nextState;
|
|
186
|
+
}
|
|
187
|
+
export async function resolveResetExecutionDecision(options) {
|
|
188
|
+
if (!options.prompter.isInteractive) {
|
|
189
|
+
throw new Error("reset_requires_tty");
|
|
190
|
+
}
|
|
191
|
+
await confirmTypedAcknowledgement(options.prompter, {
|
|
192
|
+
expected: "permanently reset",
|
|
193
|
+
prompt: "Type \"permanently reset\" to continue: ",
|
|
194
|
+
errorCode: "reset_typed_ack_required",
|
|
195
|
+
requiresTtyErrorCode: "reset_requires_tty",
|
|
196
|
+
typedAckRequiredErrorCode: "reset_typed_ack_required",
|
|
197
|
+
});
|
|
198
|
+
let walletChoice = "";
|
|
199
|
+
let loadedWalletForEntropyReset = null;
|
|
200
|
+
if (options.preflight.wallet.present) {
|
|
201
|
+
const answer = (await options.prompter.prompt("Wallet reset choice ([Enter] retain base entropy, \"skip\", or \"clear wallet entropy\"): ")).trim();
|
|
202
|
+
if (answer !== "" && answer !== "skip" && answer !== "clear wallet entropy") {
|
|
203
|
+
throw new Error("reset_wallet_choice_invalid");
|
|
204
|
+
}
|
|
205
|
+
walletChoice = answer;
|
|
206
|
+
if (walletChoice === "") {
|
|
207
|
+
loadedWalletForEntropyReset = await loadWalletForEntropyReset({
|
|
208
|
+
wallet: options.preflight.wallet,
|
|
209
|
+
paths: options.paths,
|
|
210
|
+
provider: options.provider,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
let deleteSnapshot = false;
|
|
215
|
+
let deleteBitcoinDataDir = false;
|
|
216
|
+
if (options.preflight.snapshot.shouldPrompt) {
|
|
217
|
+
const answer = (await options.prompter.prompt("Delete downloaded 910000 UTXO snapshot too? [y/N]: ")).trim().toLowerCase();
|
|
218
|
+
deleteSnapshot = answer === "y" || answer === "yes";
|
|
219
|
+
if (!deleteSnapshot && options.preflight.bitcoinDataDir.shouldPrompt) {
|
|
220
|
+
const bitcoindAnswer = (await options.prompter.prompt("Delete managed Bitcoin datadir too? [y/N]: ")).trim().toLowerCase();
|
|
221
|
+
deleteBitcoinDataDir = bitcoindAnswer === "y" || bitcoindAnswer === "yes";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
walletChoice,
|
|
226
|
+
deleteSnapshot,
|
|
227
|
+
deleteBitcoinDataDir,
|
|
228
|
+
loadedWalletForEntropyReset,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export function determineWalletAction(walletPresent, walletChoice) {
|
|
232
|
+
if (!walletPresent) {
|
|
233
|
+
return "not-present";
|
|
234
|
+
}
|
|
235
|
+
if (walletChoice === "skip") {
|
|
236
|
+
return "kept-unchanged";
|
|
237
|
+
}
|
|
238
|
+
if (walletChoice === "clear wallet entropy") {
|
|
239
|
+
return "deleted";
|
|
240
|
+
}
|
|
241
|
+
return "retain-mnemonic";
|
|
242
|
+
}
|
|
243
|
+
export function determineSnapshotResultStatus(options) {
|
|
244
|
+
if (options.snapshotStatus === "not-present") {
|
|
245
|
+
return "not-present";
|
|
246
|
+
}
|
|
247
|
+
if (options.snapshotStatus === "invalid") {
|
|
248
|
+
return "invalid-removed";
|
|
249
|
+
}
|
|
250
|
+
return options.deleteSnapshot ? "deleted" : "preserved";
|
|
251
|
+
}
|
|
252
|
+
export function determineBitcoinDataDirResultStatus(options) {
|
|
253
|
+
if (options.bitcoinDataDirStatus === "not-present") {
|
|
254
|
+
return "not-present";
|
|
255
|
+
}
|
|
256
|
+
if (options.bitcoinDataDirStatus === "outside-reset-scope") {
|
|
257
|
+
return "outside-reset-scope";
|
|
258
|
+
}
|
|
259
|
+
if (options.deleteSnapshot || options.deleteBitcoinDataDir) {
|
|
260
|
+
return "deleted";
|
|
261
|
+
}
|
|
262
|
+
return "preserved";
|
|
263
|
+
}
|
|
264
|
+
export async function resetWallet(options) {
|
|
265
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
266
|
+
const nowUnixMs = options.nowUnixMs ?? Date.now();
|
|
267
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
268
|
+
const preflight = await preflightReset({
|
|
269
|
+
...options,
|
|
270
|
+
provider,
|
|
271
|
+
paths,
|
|
272
|
+
});
|
|
273
|
+
const decision = await resolveResetExecutionDecision({
|
|
274
|
+
preflight,
|
|
275
|
+
provider,
|
|
276
|
+
prompter: options.prompter,
|
|
277
|
+
paths,
|
|
278
|
+
});
|
|
279
|
+
const walletAction = determineWalletAction(preflight.wallet.present, decision.walletChoice);
|
|
280
|
+
const snapshotResultStatus = determineSnapshotResultStatus({
|
|
281
|
+
snapshotStatus: preflight.snapshot.status,
|
|
282
|
+
deleteSnapshot: decision.deleteSnapshot,
|
|
283
|
+
});
|
|
284
|
+
const bitcoinDataDirResultStatus = determineBitcoinDataDirResultStatus({
|
|
285
|
+
bitcoinDataDirStatus: preflight.bitcoinDataDir.status,
|
|
286
|
+
deleteSnapshot: decision.deleteSnapshot,
|
|
287
|
+
deleteBitcoinDataDir: decision.deleteBitcoinDataDir,
|
|
288
|
+
});
|
|
289
|
+
const removedPaths = resolveRemovedRoots(paths, {
|
|
290
|
+
preserveBitcoinDataDir: bitcoinDataDirResultStatus === "preserved",
|
|
291
|
+
});
|
|
292
|
+
const locks = await acquireResetLocks(paths, preflight.serviceLockPaths, options.processCleanupDeps);
|
|
293
|
+
await mkdir(dirname(paths.dataRoot), { recursive: true });
|
|
294
|
+
const stagingRoot = await mkdtemp(join(dirname(paths.dataRoot), ".cogcoin-reset-"));
|
|
295
|
+
const stagedWalletArtifacts = [];
|
|
296
|
+
const stagedSnapshotArtifacts = [];
|
|
297
|
+
let stoppedProcesses = {
|
|
298
|
+
managedBitcoind: 0,
|
|
299
|
+
indexerDaemon: 0,
|
|
300
|
+
backgroundMining: 0,
|
|
301
|
+
survivors: 0,
|
|
302
|
+
};
|
|
303
|
+
let rootsDeleted = false;
|
|
304
|
+
let committed = false;
|
|
305
|
+
let newProviderKeyId = null;
|
|
306
|
+
let secretCleanupStatus = "not-found";
|
|
307
|
+
const deletedSecretRefs = [];
|
|
308
|
+
const failedSecretRefs = [];
|
|
309
|
+
const preservedSecretRefs = [];
|
|
310
|
+
let walletOldRootId = extractWalletRootIdHintFromWalletStateEnvelope(preflight.wallet.rawEnvelope?.envelope ?? null)
|
|
311
|
+
?? null;
|
|
312
|
+
let walletNewRootId = null;
|
|
313
|
+
try {
|
|
314
|
+
stoppedProcesses = await terminateTrackedProcesses(preflight.trackedProcesses, options.processCleanupDeps);
|
|
315
|
+
if (walletAction === "kept-unchanged" || walletAction === "retain-mnemonic") {
|
|
316
|
+
const stagedPrimary = await stageArtifact(paths.walletStatePath, stagingRoot, "wallet/wallet-state.enc", options.artifactDeps);
|
|
317
|
+
const stagedBackup = await stageArtifact(paths.walletStateBackupPath, stagingRoot, "wallet/wallet-state.enc.bak", options.artifactDeps);
|
|
318
|
+
if (stagedPrimary !== null) {
|
|
319
|
+
stagedWalletArtifacts.push(stagedPrimary);
|
|
320
|
+
}
|
|
321
|
+
if (stagedBackup !== null) {
|
|
322
|
+
stagedWalletArtifacts.push(stagedBackup);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (snapshotResultStatus === "preserved" && isDeletedByRemovalPlan(removedPaths, preflight.snapshot.path)) {
|
|
326
|
+
const stagedSnapshot = await stageArtifact(preflight.snapshot.path, stagingRoot, "snapshot/utxo-910000.dat", options.artifactDeps);
|
|
327
|
+
if (stagedSnapshot !== null) {
|
|
328
|
+
stagedSnapshotArtifacts.push(stagedSnapshot);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
await deleteRemovedRoots(removedPaths, options.artifactDeps);
|
|
332
|
+
rootsDeleted = true;
|
|
333
|
+
if ((snapshotResultStatus === "deleted" || snapshotResultStatus === "invalid-removed")
|
|
334
|
+
&& !isDeletedByRemovalPlan(removedPaths, preflight.snapshot.path)) {
|
|
335
|
+
await deleteBootstrapSnapshotArtifacts(options.dataDir, options.artifactDeps);
|
|
336
|
+
}
|
|
337
|
+
if (walletAction === "kept-unchanged") {
|
|
338
|
+
await restoreStagedArtifacts(stagedWalletArtifacts, options.artifactDeps);
|
|
339
|
+
}
|
|
340
|
+
else if (walletAction === "retain-mnemonic") {
|
|
341
|
+
if (decision.loadedWalletForEntropyReset === null) {
|
|
342
|
+
throw new Error("reset_wallet_entropy_reset_unavailable");
|
|
343
|
+
}
|
|
344
|
+
let nextState = createEntropyRetainedWalletState(decision.loadedWalletForEntropyReset.loaded.state, nowUnixMs);
|
|
345
|
+
walletOldRootId = decision.loadedWalletForEntropyReset.loaded.state.walletRootId;
|
|
346
|
+
walletNewRootId = nextState.walletRootId;
|
|
347
|
+
const secretReference = createWalletSecretReference(nextState.walletRootId);
|
|
348
|
+
newProviderKeyId = secretReference.keyId;
|
|
349
|
+
await provider.storeSecret(secretReference.keyId, randomBytes(32));
|
|
350
|
+
const nextAccess = {
|
|
351
|
+
provider,
|
|
352
|
+
secretReference,
|
|
353
|
+
};
|
|
354
|
+
await saveWalletState({
|
|
355
|
+
primaryPath: paths.walletStatePath,
|
|
356
|
+
backupPath: paths.walletStateBackupPath,
|
|
357
|
+
}, nextState, nextAccess);
|
|
358
|
+
preservedSecretRefs.push(secretReference.keyId);
|
|
359
|
+
nextState = await recreateManagedCoreWalletReplicaForReset({
|
|
360
|
+
state: nextState,
|
|
361
|
+
access: nextAccess,
|
|
362
|
+
paths,
|
|
363
|
+
dataDir: options.dataDir,
|
|
364
|
+
nowUnixMs,
|
|
365
|
+
attachService: options.attachService,
|
|
366
|
+
rpcFactory: options.rpcFactory,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (snapshotResultStatus === "preserved") {
|
|
370
|
+
await restoreStagedArtifacts(stagedSnapshotArtifacts, options.artifactDeps);
|
|
371
|
+
}
|
|
372
|
+
committed = true;
|
|
373
|
+
const deleteTrackedSecretReference = async (keyId) => {
|
|
374
|
+
try {
|
|
375
|
+
await provider.deleteSecret(keyId);
|
|
376
|
+
deletedSecretRefs.push(keyId);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
failedSecretRefs.push(keyId);
|
|
380
|
+
secretCleanupStatus = "failed";
|
|
381
|
+
throw new Error("reset_secret_cleanup_failed");
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
for (const importedSecretKeyId of preflight.wallet.importedSeedSecretProviderKeyIds) {
|
|
385
|
+
await deleteTrackedSecretReference(importedSecretKeyId);
|
|
386
|
+
}
|
|
387
|
+
if (walletAction === "deleted") {
|
|
388
|
+
if (preflight.wallet.secretProviderKeyId !== null) {
|
|
389
|
+
await deleteTrackedSecretReference(preflight.wallet.secretProviderKeyId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else if (walletAction === "retain-mnemonic" && preflight.wallet.secretProviderKeyId !== null) {
|
|
393
|
+
if (preflight.wallet.secretProviderKeyId !== newProviderKeyId) {
|
|
394
|
+
await deleteTrackedSecretReference(preflight.wallet.secretProviderKeyId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (preflight.wallet.secretProviderKeyId !== null) {
|
|
398
|
+
preservedSecretRefs.push(preflight.wallet.secretProviderKeyId);
|
|
399
|
+
}
|
|
400
|
+
if (failedSecretRefs.length > 0) {
|
|
401
|
+
secretCleanupStatus = "failed";
|
|
402
|
+
}
|
|
403
|
+
else if (deletedSecretRefs.length > 0) {
|
|
404
|
+
secretCleanupStatus = "deleted";
|
|
405
|
+
}
|
|
406
|
+
else if (provider.kind === "macos-keychain"
|
|
407
|
+
&& preflight.wallet.secretProviderKeyId === null
|
|
408
|
+
&& preflight.wallet.importedSeedSecretProviderKeyIds.length === 0
|
|
409
|
+
&& preflight.wallet.present
|
|
410
|
+
&& preflight.wallet.rawEnvelope === null) {
|
|
411
|
+
secretCleanupStatus = "unknown";
|
|
412
|
+
}
|
|
413
|
+
else if (preflight.wallet.secretProviderKeyId === null
|
|
414
|
+
&& preflight.wallet.importedSeedSecretProviderKeyIds.length === 0
|
|
415
|
+
&& preflight.wallet.present
|
|
416
|
+
&& preflight.wallet.rawEnvelope === null) {
|
|
417
|
+
secretCleanupStatus = "not-found";
|
|
418
|
+
}
|
|
419
|
+
else if (deletedSecretRefs.length === 0) {
|
|
420
|
+
secretCleanupStatus = "not-found";
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
dataRoot: preflight.dataRoot,
|
|
424
|
+
factoryResetReady: true,
|
|
425
|
+
stoppedProcesses,
|
|
426
|
+
secretCleanupStatus,
|
|
427
|
+
deletedSecretRefs,
|
|
428
|
+
failedSecretRefs,
|
|
429
|
+
preservedSecretRefs,
|
|
430
|
+
walletAction,
|
|
431
|
+
walletOldRootId,
|
|
432
|
+
walletNewRootId,
|
|
433
|
+
bootstrapSnapshot: {
|
|
434
|
+
status: snapshotResultStatus,
|
|
435
|
+
path: preflight.snapshot.path,
|
|
436
|
+
},
|
|
437
|
+
bitcoinDataDir: {
|
|
438
|
+
status: bitcoinDataDirResultStatus,
|
|
439
|
+
path: preflight.bitcoinDataDir.path,
|
|
440
|
+
},
|
|
441
|
+
removedPaths,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
if (!committed && rootsDeleted) {
|
|
446
|
+
await restoreStagedArtifacts(stagedWalletArtifacts, options.artifactDeps).catch(() => undefined);
|
|
447
|
+
await restoreStagedArtifacts(stagedSnapshotArtifacts, options.artifactDeps).catch(() => undefined);
|
|
448
|
+
if (newProviderKeyId !== null) {
|
|
449
|
+
await provider.deleteSecret(newProviderKeyId).catch(() => undefined);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
await rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
456
|
+
await Promise.all(locks.reverse().map(async (lock) => lock.release().catch(() => undefined)));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { WalletSecretProvider } from "../state/provider.js";
|
|
2
|
+
import type { WalletResetPreflight, WalletResetPreflightOptions } from "./types.js";
|
|
3
|
+
export declare function resetDeletesOsSecrets(options: {
|
|
4
|
+
provider: WalletSecretProvider;
|
|
5
|
+
preflight: WalletResetPreflight;
|
|
6
|
+
}): boolean;
|
|
7
|
+
export declare function preflightReset(options: WalletResetPreflightOptions): Promise<WalletResetPreflight>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DEFAULT_SNAPSHOT_METADATA } from "../../bitcoind/bootstrap/constants.js";
|
|
4
|
+
import { resolveBootstrapPathsForTesting } from "../../bitcoind/bootstrap/paths.js";
|
|
5
|
+
import { validateSnapshotFileForTesting } from "../../bitcoind/bootstrap/snapshot-file.js";
|
|
6
|
+
import { loadRawWalletStateEnvelope, } from "../state/storage.js";
|
|
7
|
+
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
8
|
+
import { isDeletedByRemovalPlan, pathExists, readJsonFileOrNull, resolveRemovedRoots, } from "./artifacts.js";
|
|
9
|
+
import { collectTrackedManagedProcesses } from "./process-cleanup.js";
|
|
10
|
+
function providerUsesExternalSecretStore(provider) {
|
|
11
|
+
return provider.kind === "macos-keychain";
|
|
12
|
+
}
|
|
13
|
+
export function resetDeletesOsSecrets(options) {
|
|
14
|
+
return providerUsesExternalSecretStore(options.provider)
|
|
15
|
+
&& (options.preflight.wallet.secretProviderKeyId !== null
|
|
16
|
+
|| options.preflight.wallet.importedSeedSecretProviderKeyIds.length > 0);
|
|
17
|
+
}
|
|
18
|
+
async function collectLegacyImportedSeedSecretProviderKeyIds(stateRoot, deps = {}) {
|
|
19
|
+
const seedsRoot = join(stateRoot, "seeds");
|
|
20
|
+
const entries = await readdir(seedsRoot, { withFileTypes: true }).catch((error) => {
|
|
21
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
const keyIds = new Set();
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const seedRoot = join(seedsRoot, entry.name);
|
|
32
|
+
const candidatePaths = [
|
|
33
|
+
join(seedRoot, "wallet-state.enc"),
|
|
34
|
+
join(seedRoot, "wallet-state.enc.bak"),
|
|
35
|
+
join(seedRoot, "wallet-init-pending.enc"),
|
|
36
|
+
join(seedRoot, "wallet-init-pending.enc.bak"),
|
|
37
|
+
];
|
|
38
|
+
for (const candidatePath of candidatePaths) {
|
|
39
|
+
const envelope = await readJsonFileOrNull(candidatePath, deps);
|
|
40
|
+
const keyId = envelope?.secretProvider?.keyId?.trim() ?? "";
|
|
41
|
+
if (keyId.length > 0) {
|
|
42
|
+
keyIds.add(keyId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...keyIds].sort((left, right) => left.localeCompare(right));
|
|
47
|
+
}
|
|
48
|
+
export async function preflightReset(options) {
|
|
49
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
50
|
+
const removedRoots = resolveRemovedRoots(paths);
|
|
51
|
+
const rawEnvelope = await loadRawWalletStateEnvelope({
|
|
52
|
+
primaryPath: paths.walletStatePath,
|
|
53
|
+
backupPath: paths.walletStateBackupPath,
|
|
54
|
+
});
|
|
55
|
+
const snapshotPaths = resolveBootstrapPathsForTesting(options.dataDir, DEFAULT_SNAPSHOT_METADATA);
|
|
56
|
+
const validateSnapshot = options.validateSnapshotFile
|
|
57
|
+
?? ((path) => validateSnapshotFileForTesting(path, DEFAULT_SNAPSHOT_METADATA));
|
|
58
|
+
const artifactDeps = options.artifactDeps ?? {};
|
|
59
|
+
const hasWalletState = await pathExists(paths.walletStatePath, artifactDeps)
|
|
60
|
+
|| await pathExists(paths.walletStateBackupPath, artifactDeps);
|
|
61
|
+
const hasBitcoinDataDir = await pathExists(options.dataDir, artifactDeps);
|
|
62
|
+
const bitcoinDataDirWithinResetScope = hasBitcoinDataDir
|
|
63
|
+
&& isDeletedByRemovalPlan(removedRoots, options.dataDir);
|
|
64
|
+
const hasSnapshot = await pathExists(snapshotPaths.snapshotPath, artifactDeps);
|
|
65
|
+
const hasPartialSnapshot = await pathExists(snapshotPaths.partialSnapshotPath, artifactDeps);
|
|
66
|
+
let snapshotStatus = "not-present";
|
|
67
|
+
if (hasSnapshot) {
|
|
68
|
+
try {
|
|
69
|
+
await validateSnapshot(snapshotPaths.snapshotPath);
|
|
70
|
+
snapshotStatus = "valid";
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
snapshotStatus = "invalid";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (hasPartialSnapshot) {
|
|
77
|
+
snapshotStatus = "invalid";
|
|
78
|
+
}
|
|
79
|
+
const tracked = await collectTrackedManagedProcesses(paths, options.processCleanupDeps);
|
|
80
|
+
const secretProviderKeyId = rawEnvelope?.envelope.secretProvider?.keyId ?? null;
|
|
81
|
+
const importedSeedSecretProviderKeyIds = await collectLegacyImportedSeedSecretProviderKeyIds(paths.stateRoot, artifactDeps);
|
|
82
|
+
return {
|
|
83
|
+
dataRoot: paths.dataRoot,
|
|
84
|
+
removedRoots,
|
|
85
|
+
wallet: {
|
|
86
|
+
present: hasWalletState,
|
|
87
|
+
mode: rawEnvelope == null
|
|
88
|
+
? (hasWalletState ? "unknown" : "unknown")
|
|
89
|
+
: rawEnvelope.envelope.secretProvider != null
|
|
90
|
+
? "provider-backed"
|
|
91
|
+
: "unsupported-legacy",
|
|
92
|
+
envelopeSource: rawEnvelope?.source ?? null,
|
|
93
|
+
secretProviderKeyId,
|
|
94
|
+
importedSeedSecretProviderKeyIds,
|
|
95
|
+
rawEnvelope,
|
|
96
|
+
},
|
|
97
|
+
snapshot: {
|
|
98
|
+
status: snapshotStatus,
|
|
99
|
+
path: snapshotPaths.snapshotPath,
|
|
100
|
+
shouldPrompt: snapshotStatus === "valid",
|
|
101
|
+
withinResetScope: isDeletedByRemovalPlan(removedRoots, snapshotPaths.snapshotPath),
|
|
102
|
+
},
|
|
103
|
+
bitcoinDataDir: {
|
|
104
|
+
status: !hasBitcoinDataDir
|
|
105
|
+
? "not-present"
|
|
106
|
+
: bitcoinDataDirWithinResetScope
|
|
107
|
+
? "within-reset-scope"
|
|
108
|
+
: "outside-reset-scope",
|
|
109
|
+
path: options.dataDir,
|
|
110
|
+
shouldPrompt: bitcoinDataDirWithinResetScope,
|
|
111
|
+
},
|
|
112
|
+
trackedProcesses: tracked.trackedProcesses,
|
|
113
|
+
trackedProcessKinds: tracked.trackedProcessKinds,
|
|
114
|
+
serviceLockPaths: tracked.serviceLockPaths,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createDefaultWalletSecretProvider } from "../state/provider.js";
|
|
2
|
+
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
3
|
+
import { resolveRemovedRoots } from "./artifacts.js";
|
|
4
|
+
import { preflightReset, resetDeletesOsSecrets } from "./preflight.js";
|
|
5
|
+
export async function previewResetWallet(options) {
|
|
6
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
7
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
8
|
+
const preflight = await preflightReset({
|
|
9
|
+
...options,
|
|
10
|
+
provider,
|
|
11
|
+
paths,
|
|
12
|
+
});
|
|
13
|
+
const removedPaths = resolveRemovedRoots(paths, {
|
|
14
|
+
preserveBitcoinDataDir: preflight.snapshot.status === "valid" && preflight.bitcoinDataDir.shouldPrompt,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
dataRoot: preflight.dataRoot,
|
|
18
|
+
confirmationPhrase: "permanently reset",
|
|
19
|
+
walletPrompt: preflight.wallet.present
|
|
20
|
+
? {
|
|
21
|
+
defaultAction: "retain-mnemonic",
|
|
22
|
+
acceptedInputs: ["", "skip", "clear wallet entropy"],
|
|
23
|
+
entropyRetainingResetAvailable: preflight.wallet.mode === "provider-backed",
|
|
24
|
+
envelopeSource: preflight.wallet.envelopeSource,
|
|
25
|
+
}
|
|
26
|
+
: null,
|
|
27
|
+
bootstrapSnapshot: {
|
|
28
|
+
status: preflight.snapshot.status,
|
|
29
|
+
path: preflight.snapshot.path,
|
|
30
|
+
defaultAction: preflight.snapshot.status === "valid" ? "preserve" : "delete",
|
|
31
|
+
},
|
|
32
|
+
bitcoinDataDir: {
|
|
33
|
+
status: preflight.bitcoinDataDir.status,
|
|
34
|
+
path: preflight.bitcoinDataDir.path,
|
|
35
|
+
conditionalPrompt: preflight.bitcoinDataDir.shouldPrompt
|
|
36
|
+
? {
|
|
37
|
+
prompt: "Delete managed Bitcoin datadir too? [y/N]: ",
|
|
38
|
+
defaultAction: "preserve",
|
|
39
|
+
acceptedInputs: ["", "n", "no", "y", "yes"],
|
|
40
|
+
}
|
|
41
|
+
: null,
|
|
42
|
+
},
|
|
43
|
+
trackedProcessKinds: preflight.trackedProcessKinds,
|
|
44
|
+
willDeleteOsSecrets: resetDeletesOsSecrets({
|
|
45
|
+
provider,
|
|
46
|
+
preflight,
|
|
47
|
+
}),
|
|
48
|
+
removedPaths,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FileLockHandle } from "../fs/lock.js";
|
|
2
|
+
import type { WalletRuntimePaths } from "../runtime.js";
|
|
3
|
+
import type { TrackedManagedProcess, WalletResetProcessCleanupDependencies, WalletResetResult } from "./types.js";
|
|
4
|
+
export declare function isProcessAlive(pid: number | null, deps?: WalletResetProcessCleanupDependencies): Promise<boolean>;
|
|
5
|
+
export declare function waitForProcessExit(pid: number, timeoutMs?: number, deps?: WalletResetProcessCleanupDependencies): Promise<boolean>;
|
|
6
|
+
export declare function terminateTrackedProcesses(trackedProcesses: readonly TrackedManagedProcess[], deps?: WalletResetProcessCleanupDependencies): Promise<WalletResetResult["stoppedProcesses"]>;
|
|
7
|
+
export declare function collectTrackedManagedProcesses(paths: WalletRuntimePaths, deps?: WalletResetProcessCleanupDependencies): Promise<{
|
|
8
|
+
trackedProcesses: TrackedManagedProcess[];
|
|
9
|
+
trackedProcessKinds: Array<TrackedManagedProcess["kind"]>;
|
|
10
|
+
serviceLockPaths: string[];
|
|
11
|
+
}>;
|
|
12
|
+
export declare function acquireResetLocks(paths: WalletRuntimePaths, serviceLockPaths: readonly string[], deps?: WalletResetProcessCleanupDependencies): Promise<FileLockHandle[]>;
|