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