@cogcoin/client 0.5.2 → 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 +14 -3
- package/dist/app-paths.d.ts +1 -0
- package/dist/app-paths.js +2 -0
- 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 +113 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +69 -4
- 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 +30 -1
- package/dist/wallet/lifecycle.js +394 -40
- package/dist/wallet/material.d.ts +2 -0
- package/dist/wallet/material.js +8 -1
- package/dist/wallet/mining/control.js +4 -4
- package/dist/wallet/mining/runner.js +2 -2
- package/dist/wallet/mnemonic-art.d.ts +2 -0
- package/dist/wallet/mnemonic-art.js +54 -0
- package/dist/wallet/read/context.d.ts +2 -0
- package/dist/wallet/read/context.js +64 -17
- package/dist/wallet/reset.d.ts +61 -0
- package/dist/wallet/reset.js +781 -0
- package/dist/wallet/runtime.d.ts +1 -0
- package/dist/wallet/runtime.js +1 -0
- package/dist/wallet/state/explicit-lock.d.ts +4 -0
- package/dist/wallet/state/explicit-lock.js +19 -0
- package/dist/wallet/tx/anchor.js +1 -0
- package/dist/wallet/tx/cog.js +3 -0
- package/dist/wallet/tx/domain-admin.js +1 -0
- package/dist/wallet/tx/domain-market.js +3 -0
- package/dist/wallet/tx/field.js +2 -0
- package/dist/wallet/tx/register.js +1 -0
- package/dist/wallet/tx/reputation.js +1 -0
- package/dist/wallet/types.d.ts +5 -0
- package/package.json +3 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
const WALLET_ART_WIDTH = 80;
|
|
3
|
+
const WALLET_ART_SLOT_WIDTH = 8;
|
|
4
|
+
const WALLET_ART_PLACEHOLDER_WORD = "achieved";
|
|
5
|
+
const WALLET_ART_WORD_COUNT = 24;
|
|
6
|
+
let walletArtTemplateCache = null;
|
|
7
|
+
function normalizeWalletArtTemplate(raw) {
|
|
8
|
+
const lines = raw.replaceAll("\r\n", "\n").split("\n");
|
|
9
|
+
if (lines[lines.length - 1] === "") {
|
|
10
|
+
lines.pop();
|
|
11
|
+
}
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (line.length !== WALLET_ART_WIDTH) {
|
|
14
|
+
throw new Error(`wallet_art_template_width_invalid_${line.length}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const template = lines.join("\n");
|
|
18
|
+
for (let index = 1; index <= WALLET_ART_WORD_COUNT; index += 1) {
|
|
19
|
+
if (!template.includes(`${index}.${WALLET_ART_PLACEHOLDER_WORD}`)) {
|
|
20
|
+
throw new Error(`wallet_art_template_placeholder_missing_${index}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
25
|
+
function loadWalletArtTemplate() {
|
|
26
|
+
if (walletArtTemplateCache !== null) {
|
|
27
|
+
return walletArtTemplateCache;
|
|
28
|
+
}
|
|
29
|
+
walletArtTemplateCache = normalizeWalletArtTemplate(readFileSync(new URL("../art/wallet.txt", import.meta.url), "utf8"));
|
|
30
|
+
return walletArtTemplateCache;
|
|
31
|
+
}
|
|
32
|
+
function formatWalletArtWord(word) {
|
|
33
|
+
const normalized = word ?? "";
|
|
34
|
+
if (normalized.length > WALLET_ART_SLOT_WIDTH) {
|
|
35
|
+
throw new Error(`wallet_art_word_too_wide_${normalized.length}`);
|
|
36
|
+
}
|
|
37
|
+
return normalized.padEnd(WALLET_ART_SLOT_WIDTH, " ");
|
|
38
|
+
}
|
|
39
|
+
export function renderWalletMnemonicRevealArt(words) {
|
|
40
|
+
const rendered = loadWalletArtTemplate().map((line) => {
|
|
41
|
+
let next = line;
|
|
42
|
+
for (let index = 0; index < WALLET_ART_WORD_COUNT; index += 1) {
|
|
43
|
+
next = next.replace(`${index + 1}.${WALLET_ART_PLACEHOLDER_WORD}`, `${index + 1}.${formatWalletArtWord(words[index])}`);
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
if (rendered.some((line) => line.includes(`.${WALLET_ART_PLACEHOLDER_WORD}`))) {
|
|
48
|
+
throw new Error("wallet_art_render_placeholder_unreplaced");
|
|
49
|
+
}
|
|
50
|
+
return rendered;
|
|
51
|
+
}
|
|
52
|
+
export function loadWalletArtTemplateForTesting() {
|
|
53
|
+
return [...loadWalletArtTemplate()];
|
|
54
|
+
}
|
|
@@ -8,12 +8,14 @@ declare function inspectWalletLocalState(options?: {
|
|
|
8
8
|
secretProvider?: WalletSecretProvider;
|
|
9
9
|
now?: number;
|
|
10
10
|
paths?: WalletRuntimePaths;
|
|
11
|
+
walletControlLockHeld?: boolean;
|
|
11
12
|
}): Promise<WalletLocalStateStatus>;
|
|
12
13
|
export declare function openWalletReadContext(options: {
|
|
13
14
|
dataDir: string;
|
|
14
15
|
databasePath: string;
|
|
15
16
|
walletStatePassphrase?: Uint8Array | string;
|
|
16
17
|
secretProvider?: WalletSecretProvider;
|
|
18
|
+
walletControlLockHeld?: boolean;
|
|
17
19
|
startupTimeoutMs?: number;
|
|
18
20
|
now?: number;
|
|
19
21
|
paths?: WalletRuntimePaths;
|
|
@@ -5,11 +5,12 @@ 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
7
|
import {} from "../../bitcoind/types.js";
|
|
8
|
-
import {
|
|
8
|
+
import { loadOrAutoUnlockWalletState, verifyManagedCoreWalletReplica, } from "../lifecycle.js";
|
|
9
9
|
import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
|
|
10
10
|
import { inspectMiningControlPlane } from "../mining/index.js";
|
|
11
11
|
import { normalizeMiningStateRecord } from "../mining/state.js";
|
|
12
12
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
13
|
+
import { loadWalletExplicitLock } from "../state/explicit-lock.js";
|
|
13
14
|
import { loadWalletState } from "../state/storage.js";
|
|
14
15
|
import { createDefaultWalletSecretProvider, createWalletSecretReference, } from "../state/provider.js";
|
|
15
16
|
import { createWalletReadModel } from "./project.js";
|
|
@@ -24,6 +25,30 @@ async function pathExists(path) {
|
|
|
24
25
|
return false;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
function isLockedWalletAccessError(error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return message === "wallet_envelope_missing_secret_provider"
|
|
31
|
+
|| message.startsWith("wallet_secret_missing_")
|
|
32
|
+
|| message.startsWith("wallet_secret_provider_");
|
|
33
|
+
}
|
|
34
|
+
function describeLockedWalletMessage(options) {
|
|
35
|
+
if (options.explicitlyLocked) {
|
|
36
|
+
return "Wallet state exists but is explicitly locked until `cogcoin unlock` is run.";
|
|
37
|
+
}
|
|
38
|
+
const message = options.accessError instanceof Error ? options.accessError.message : String(options.accessError ?? "");
|
|
39
|
+
if (message === "wallet_envelope_missing_secret_provider") {
|
|
40
|
+
return "Wallet state exists but requires the local wallet-state passphrase.";
|
|
41
|
+
}
|
|
42
|
+
if (message.startsWith("wallet_secret_provider_")) {
|
|
43
|
+
return "Wallet state exists but the local secret provider is unavailable.";
|
|
44
|
+
}
|
|
45
|
+
if (message.startsWith("wallet_secret_missing_")) {
|
|
46
|
+
return "Wallet state exists but its local secret-provider material is unavailable.";
|
|
47
|
+
}
|
|
48
|
+
return options.hasUnlockSessionFile
|
|
49
|
+
? "Wallet state exists but the unlock session is expired, invalid, or belongs to a different wallet root."
|
|
50
|
+
: "Wallet state exists but is currently locked.";
|
|
51
|
+
}
|
|
27
52
|
async function normalizeLoadedWalletStateForRead(options) {
|
|
28
53
|
if (options.dataDir === undefined) {
|
|
29
54
|
return options.loaded;
|
|
@@ -82,13 +107,16 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
82
107
|
if (options.passphrase === undefined) {
|
|
83
108
|
try {
|
|
84
109
|
const provider = options.secretProvider ?? createDefaultWalletSecretProvider();
|
|
85
|
-
const unlocked = await
|
|
110
|
+
const unlocked = await loadOrAutoUnlockWalletState({
|
|
86
111
|
provider,
|
|
87
112
|
nowUnixMs: now,
|
|
88
113
|
paths,
|
|
89
114
|
dataDir: options.dataDir,
|
|
115
|
+
controlLockHeld: options.walletControlLockHeld,
|
|
90
116
|
});
|
|
91
117
|
if (unlocked === null) {
|
|
118
|
+
const explicitLock = await loadWalletExplicitLock(paths.walletExplicitLockPath);
|
|
119
|
+
const hasUnlockSessionFileNow = await pathExists(paths.walletUnlockSessionPath);
|
|
92
120
|
try {
|
|
93
121
|
const loaded = await loadWalletState({
|
|
94
122
|
primaryPath: paths.walletStatePath,
|
|
@@ -103,8 +131,39 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
103
131
|
now,
|
|
104
132
|
paths,
|
|
105
133
|
});
|
|
134
|
+
return {
|
|
135
|
+
availability: "locked",
|
|
136
|
+
walletRootId: loaded.state.walletRootId,
|
|
137
|
+
state: null,
|
|
138
|
+
source: loaded.source,
|
|
139
|
+
unlockUntilUnixMs: null,
|
|
140
|
+
hasPrimaryStateFile,
|
|
141
|
+
hasBackupStateFile,
|
|
142
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
143
|
+
message: describeLockedWalletMessage({
|
|
144
|
+
explicitlyLocked: explicitLock?.walletRootId === loaded.state.walletRootId,
|
|
145
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
106
148
|
}
|
|
107
149
|
catch (error) {
|
|
150
|
+
if (isLockedWalletAccessError(error)) {
|
|
151
|
+
return {
|
|
152
|
+
availability: "locked",
|
|
153
|
+
walletRootId: null,
|
|
154
|
+
state: null,
|
|
155
|
+
source: null,
|
|
156
|
+
unlockUntilUnixMs: null,
|
|
157
|
+
hasPrimaryStateFile,
|
|
158
|
+
hasBackupStateFile,
|
|
159
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
160
|
+
message: describeLockedWalletMessage({
|
|
161
|
+
accessError: error,
|
|
162
|
+
explicitlyLocked: false,
|
|
163
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
108
167
|
return {
|
|
109
168
|
availability: "local-state-corrupt",
|
|
110
169
|
walletRootId: null,
|
|
@@ -113,23 +172,10 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
113
172
|
unlockUntilUnixMs: null,
|
|
114
173
|
hasPrimaryStateFile,
|
|
115
174
|
hasBackupStateFile,
|
|
116
|
-
hasUnlockSessionFile,
|
|
175
|
+
hasUnlockSessionFile: hasUnlockSessionFileNow,
|
|
117
176
|
message: error instanceof Error ? error.message : String(error),
|
|
118
177
|
};
|
|
119
178
|
}
|
|
120
|
-
return {
|
|
121
|
-
availability: "locked",
|
|
122
|
-
walletRootId: null,
|
|
123
|
-
state: null,
|
|
124
|
-
source: null,
|
|
125
|
-
unlockUntilUnixMs: null,
|
|
126
|
-
hasPrimaryStateFile,
|
|
127
|
-
hasBackupStateFile,
|
|
128
|
-
hasUnlockSessionFile,
|
|
129
|
-
message: hasUnlockSessionFile
|
|
130
|
-
? "Wallet state exists but the unlock session is expired, invalid, or belongs to a different wallet root."
|
|
131
|
-
: "Wallet state exists but is currently locked.",
|
|
132
|
-
};
|
|
133
179
|
}
|
|
134
180
|
return {
|
|
135
181
|
availability: "ready",
|
|
@@ -142,7 +188,7 @@ async function inspectWalletLocalState(options = {}) {
|
|
|
142
188
|
unlockUntilUnixMs: unlocked.session.unlockUntilUnixMs,
|
|
143
189
|
hasPrimaryStateFile,
|
|
144
190
|
hasBackupStateFile,
|
|
145
|
-
hasUnlockSessionFile,
|
|
191
|
+
hasUnlockSessionFile: true,
|
|
146
192
|
message: null,
|
|
147
193
|
};
|
|
148
194
|
}
|
|
@@ -460,6 +506,7 @@ export async function openWalletReadContext(options) {
|
|
|
460
506
|
dataDir: options.dataDir,
|
|
461
507
|
passphrase: options.walletStatePassphrase,
|
|
462
508
|
secretProvider: options.secretProvider,
|
|
509
|
+
walletControlLockHeld: options.walletControlLockHeld,
|
|
463
510
|
now,
|
|
464
511
|
paths: options.paths,
|
|
465
512
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type WalletRuntimePaths } from "./runtime.js";
|
|
2
|
+
import { type WalletSecretProvider } from "./state/provider.js";
|
|
3
|
+
import type { WalletPrompter } from "./lifecycle.js";
|
|
4
|
+
export type WalletResetAction = "not-present" | "kept-unchanged" | "reset-base-entropy" | "deleted";
|
|
5
|
+
export type WalletResetSecretCleanupStatus = "deleted" | "not-found" | "failed" | "unknown";
|
|
6
|
+
export type WalletResetSnapshotResultStatus = "not-present" | "invalid-removed" | "deleted" | "preserved";
|
|
7
|
+
export interface WalletResetResult {
|
|
8
|
+
dataRoot: string;
|
|
9
|
+
factoryResetReady: true;
|
|
10
|
+
stoppedProcesses: {
|
|
11
|
+
managedBitcoind: number;
|
|
12
|
+
indexerDaemon: number;
|
|
13
|
+
backgroundMining: number;
|
|
14
|
+
survivors: number;
|
|
15
|
+
};
|
|
16
|
+
secretCleanupStatus: WalletResetSecretCleanupStatus;
|
|
17
|
+
deletedSecretRefs: string[];
|
|
18
|
+
failedSecretRefs: string[];
|
|
19
|
+
preservedSecretRefs: string[];
|
|
20
|
+
walletAction: WalletResetAction;
|
|
21
|
+
walletOldRootId: string | null;
|
|
22
|
+
walletNewRootId: string | null;
|
|
23
|
+
bootstrapSnapshot: {
|
|
24
|
+
status: WalletResetSnapshotResultStatus;
|
|
25
|
+
path: string;
|
|
26
|
+
};
|
|
27
|
+
removedPaths: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface WalletResetPreview {
|
|
30
|
+
dataRoot: string;
|
|
31
|
+
confirmationPhrase: "permanently reset";
|
|
32
|
+
walletPrompt: null | {
|
|
33
|
+
defaultAction: "reset-base-entropy";
|
|
34
|
+
acceptedInputs: ["", "skip", "delete wallet"];
|
|
35
|
+
entropyRetainingResetAvailable: boolean;
|
|
36
|
+
requiresPassphrase: boolean;
|
|
37
|
+
envelopeSource: "primary" | "backup" | null;
|
|
38
|
+
};
|
|
39
|
+
bootstrapSnapshot: {
|
|
40
|
+
status: "not-present" | "invalid" | "valid";
|
|
41
|
+
path: string;
|
|
42
|
+
defaultAction: "preserve" | "delete";
|
|
43
|
+
};
|
|
44
|
+
trackedProcessKinds: Array<"managed-bitcoind" | "indexer-daemon" | "background-mining">;
|
|
45
|
+
willDeleteOsSecrets: boolean;
|
|
46
|
+
removedPaths: string[];
|
|
47
|
+
}
|
|
48
|
+
export declare function previewResetWallet(options: {
|
|
49
|
+
dataDir: string;
|
|
50
|
+
provider?: WalletSecretProvider;
|
|
51
|
+
paths?: WalletRuntimePaths;
|
|
52
|
+
validateSnapshotFile?: (path: string) => Promise<void>;
|
|
53
|
+
}): Promise<WalletResetPreview>;
|
|
54
|
+
export declare function resetWallet(options: {
|
|
55
|
+
dataDir: string;
|
|
56
|
+
provider?: WalletSecretProvider;
|
|
57
|
+
prompter: WalletPrompter;
|
|
58
|
+
nowUnixMs?: number;
|
|
59
|
+
paths?: WalletRuntimePaths;
|
|
60
|
+
validateSnapshotFile?: (path: string) => Promise<void>;
|
|
61
|
+
}): Promise<WalletResetResult>;
|