@cogcoin/client 1.0.1 → 1.1.0
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 +4 -2
- package/dist/bitcoind/client/factory.d.ts +0 -8
- package/dist/bitcoind/client/factory.js +1 -59
- package/dist/bitcoind/client/managed-client.d.ts +1 -3
- package/dist/bitcoind/client/managed-client.js +3 -47
- package/dist/bitcoind/indexer-daemon-main.js +173 -28
- package/dist/bitcoind/indexer-daemon.d.ts +14 -3
- package/dist/bitcoind/indexer-daemon.js +145 -29
- package/dist/bitcoind/indexer-monitor.d.ts +12 -0
- package/dist/bitcoind/indexer-monitor.js +89 -0
- package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
- package/dist/bitcoind/progress/follow-scene.js +87 -4
- package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
- package/dist/bitcoind/progress/tty-renderer.js +2 -0
- package/dist/bitcoind/retryable-rpc.js +3 -0
- package/dist/bitcoind/service.d.ts +1 -0
- package/dist/bitcoind/service.js +31 -9
- package/dist/bitcoind/testing.d.ts +0 -1
- package/dist/bitcoind/testing.js +0 -1
- package/dist/bitcoind/types.d.ts +5 -2
- package/dist/cli/commands/follow.js +44 -49
- package/dist/cli/commands/mining-admin.js +65 -2
- package/dist/cli/commands/mining-read.js +43 -3
- package/dist/cli/commands/mining-runtime.js +91 -73
- package/dist/cli/commands/service-runtime.js +42 -2
- package/dist/cli/commands/status.js +3 -1
- package/dist/cli/commands/sync.js +50 -90
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/commands/wallet-admin.js +21 -3
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +36 -1
- package/dist/cli/managed-indexer-observer.d.ts +33 -0
- package/dist/cli/managed-indexer-observer.js +163 -0
- package/dist/cli/mining-format.d.ts +3 -1
- package/dist/cli/mining-format.js +63 -0
- package/dist/cli/mining-json.d.ts +11 -1
- package/dist/cli/mining-json.js +15 -0
- package/dist/cli/output.js +74 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +28 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +26 -1
- package/dist/cli/read-json.js +48 -0
- package/dist/cli/runner.js +8 -2
- package/dist/cli/signals.d.ts +12 -0
- package/dist/cli/signals.js +31 -13
- package/dist/cli/types.d.ts +13 -4
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +34 -0
- package/dist/cli/update-service.js +152 -0
- package/dist/client/initialization.js +5 -0
- package/dist/semver.d.ts +12 -0
- package/dist/semver.js +68 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/mining/config.js +64 -3
- package/dist/wallet/mining/control.d.ts +5 -1
- package/dist/wallet/mining/control.js +269 -26
- package/dist/wallet/mining/domain-prompts.d.ts +17 -0
- package/dist/wallet/mining/domain-prompts.js +130 -0
- package/dist/wallet/mining/index.d.ts +2 -1
- package/dist/wallet/mining/index.js +1 -0
- package/dist/wallet/mining/provider-model.d.ts +30 -0
- package/dist/wallet/mining/provider-model.js +134 -0
- package/dist/wallet/mining/runner.d.ts +156 -5
- package/dist/wallet/mining/runner.js +1019 -399
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +32 -6
- package/dist/wallet/mining/types.d.ts +35 -1
- package/dist/wallet/mining/visualizer.d.ts +3 -0
- package/dist/wallet/mining/visualizer.js +132 -15
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +15 -7
- package/dist/wallet/state/client-password-agent.js +4 -1
- package/dist/wallet/state/client-password.js +15 -8
- package/dist/wallet/tx/common.js +1 -1
- package/package.json +3 -2
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
+
import { DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting } from "../../bitcoind/bootstrap.js";
|
|
2
3
|
import { formatManagedSyncErrorMessage } from "../../bitcoind/errors.js";
|
|
3
|
-
import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
|
|
4
4
|
import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
|
|
5
5
|
import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
|
|
6
|
+
import { ManagedIndexerProgressObserver, pollManagedIndexerUntilCaughtUp, } from "../managed-indexer-observer.js";
|
|
6
7
|
import { usesTtyProgress, writeLine } from "../io.js";
|
|
7
|
-
import { classifyCliError
|
|
8
|
+
import { classifyCliError } from "../output.js";
|
|
8
9
|
import { createTerminalPrompter } from "../prompt.js";
|
|
9
|
-
import {
|
|
10
|
+
import { createCloseSignalWatcher, waitForCompletionOrStop } from "../signals.js";
|
|
10
11
|
import { createSyncProgressReporter } from "../sync-progress.js";
|
|
11
12
|
import { formatBalanceReport } from "../wallet-format.js";
|
|
12
13
|
async function writePostSyncBalanceReport(options) {
|
|
@@ -15,7 +16,7 @@ async function writePostSyncBalanceReport(options) {
|
|
|
15
16
|
dataDir: options.dataDir,
|
|
16
17
|
databasePath: options.databasePath,
|
|
17
18
|
secretProvider: provider,
|
|
18
|
-
|
|
19
|
+
expectedIndexerBinaryVersion: options.expectedIndexerBinaryVersion,
|
|
19
20
|
paths: options.runtimePaths,
|
|
20
21
|
});
|
|
21
22
|
try {
|
|
@@ -28,38 +29,27 @@ async function writePostSyncBalanceReport(options) {
|
|
|
28
29
|
export async function runSyncCommand(parsed, context) {
|
|
29
30
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
30
31
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
32
|
+
const packageVersion = await context.readPackageVersion();
|
|
31
33
|
const runtimePaths = context.resolveWalletRuntimePaths();
|
|
32
34
|
const ttyProgressActive = usesTtyProgress(parsed.progressOutput, context.stderr);
|
|
33
|
-
let
|
|
34
|
-
let
|
|
35
|
-
let storeOwned = true;
|
|
36
|
-
let client = null;
|
|
37
|
-
let clientClosed = false;
|
|
35
|
+
let monitor = null;
|
|
36
|
+
let observer = null;
|
|
38
37
|
try {
|
|
39
38
|
const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
|
|
40
39
|
paths: runtimePaths,
|
|
41
40
|
provider: context.walletSecretProvider,
|
|
42
41
|
loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
|
|
43
42
|
});
|
|
44
|
-
try {
|
|
45
|
-
controlLock = await acquireFileLock(runtimePaths.walletControlLockPath, {
|
|
46
|
-
purpose: "managed-sync",
|
|
47
|
-
walletRootId: walletRoot.walletRootId,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error instanceof FileLockBusyError) {
|
|
52
|
-
throw new Error("wallet_control_lock_busy");
|
|
53
|
-
}
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
43
|
await context.ensureDirectory(dirname(dbPath));
|
|
57
|
-
|
|
58
|
-
client = await context.openManagedBitcoindClient({
|
|
59
|
-
store,
|
|
60
|
-
databasePath: dbPath,
|
|
44
|
+
monitor = await context.openManagedIndexerMonitor({
|
|
61
45
|
dataDir,
|
|
46
|
+
databasePath: dbPath,
|
|
62
47
|
walletRootId: walletRoot.walletRootId,
|
|
48
|
+
expectedBinaryVersion: packageVersion,
|
|
49
|
+
});
|
|
50
|
+
observer = new ManagedIndexerProgressObserver({
|
|
51
|
+
quoteStatePath: resolveBootstrapPathsForTesting(dataDir, DEFAULT_SNAPSHOT_METADATA).quoteStatePath,
|
|
52
|
+
stream: context.stderr,
|
|
63
53
|
progressOutput: parsed.progressOutput,
|
|
64
54
|
onProgress: ttyProgressActive ? undefined : createSyncProgressReporter({
|
|
65
55
|
progressOutput: parsed.progressOutput,
|
|
@@ -68,82 +58,52 @@ export async function runSyncCommand(parsed, context) {
|
|
|
68
58
|
},
|
|
69
59
|
}),
|
|
70
60
|
});
|
|
71
|
-
|
|
72
|
-
const stopWatcher =
|
|
61
|
+
const abortController = new AbortController();
|
|
62
|
+
const stopWatcher = createCloseSignalWatcher({
|
|
63
|
+
signalSource: context.signalSource,
|
|
64
|
+
stderr: context.stderr,
|
|
65
|
+
closeable: {
|
|
66
|
+
close: async () => {
|
|
67
|
+
abortController.abort(new Error("managed_indexer_observer_aborted"));
|
|
68
|
+
await observer?.close().catch(() => undefined);
|
|
69
|
+
await monitor?.close().catch(() => undefined);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
forceExit: context.forceExit,
|
|
73
|
+
firstMessage: "Stopping managed Cogcoin sync observation...",
|
|
74
|
+
successMessage: "Stopped observing managed Cogcoin sync.",
|
|
75
|
+
failureMessage: "Managed Cogcoin sync observation cleanup failed.",
|
|
76
|
+
});
|
|
73
77
|
try {
|
|
74
|
-
const syncOutcome = await waitForCompletionOrStop(
|
|
78
|
+
const syncOutcome = await waitForCompletionOrStop(pollManagedIndexerUntilCaughtUp({
|
|
79
|
+
monitor,
|
|
80
|
+
observer,
|
|
81
|
+
signal: abortController.signal,
|
|
82
|
+
}), stopWatcher);
|
|
75
83
|
if (syncOutcome.kind === "stopped") {
|
|
76
84
|
return syncOutcome.code;
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
stopWatcher.cleanup();
|
|
81
|
-
const detachPromise = typeof client.detachToBackgroundFollow === "function"
|
|
82
|
-
? client.detachToBackgroundFollow()
|
|
83
|
-
: Promise.resolve();
|
|
84
|
-
if (typeof client.playSyncCompletionScene === "function") {
|
|
85
|
-
await client.playSyncCompletionScene().catch(() => undefined);
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
await detachPromise;
|
|
89
|
-
await client.close();
|
|
90
|
-
clientClosed = true;
|
|
91
|
-
writeLine(context.stderr, "Detached cleanly; background indexer follow resumed.");
|
|
92
|
-
await writePostSyncBalanceReport({
|
|
93
|
-
context,
|
|
94
|
-
dataDir,
|
|
95
|
-
databasePath: dbPath,
|
|
96
|
-
runtimePaths,
|
|
97
|
-
}).catch(() => undefined);
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
writeLine(context.stderr, "Detach failed before background indexer follow was confirmed.");
|
|
102
|
-
return 1;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (typeof client.playSyncCompletionScene === "function") {
|
|
106
|
-
const completionOutcome = await waitForCompletionOrStop(client.playSyncCompletionScene().catch(() => undefined), stopWatcher);
|
|
107
|
-
if (completionOutcome.kind === "stopped") {
|
|
108
|
-
return completionOutcome.code;
|
|
109
|
-
}
|
|
86
|
+
if (ttyProgressActive) {
|
|
87
|
+
await observer.playCompletionScene().catch(() => undefined);
|
|
110
88
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
89
|
+
await writePostSyncBalanceReport({
|
|
90
|
+
context,
|
|
91
|
+
dataDir,
|
|
92
|
+
databasePath: dbPath,
|
|
93
|
+
expectedIndexerBinaryVersion: packageVersion,
|
|
94
|
+
runtimePaths,
|
|
95
|
+
}).catch(() => undefined);
|
|
115
96
|
return 0;
|
|
116
97
|
}
|
|
117
98
|
finally {
|
|
118
99
|
stopWatcher.cleanup();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
100
|
+
await observer?.close().catch(() => undefined);
|
|
101
|
+
await monitor?.close().catch(() => undefined);
|
|
122
102
|
}
|
|
123
103
|
}
|
|
124
104
|
catch (error) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (formatted !== null) {
|
|
129
|
-
for (const line of formatted) {
|
|
130
|
-
writeLine(context.stderr, line);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
writeLine(context.stderr, classified.message);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
const message = formatManagedSyncErrorMessage(error instanceof Error ? error.message : String(error));
|
|
139
|
-
writeLine(context.stderr, `sync failed: ${message}`);
|
|
140
|
-
}
|
|
141
|
-
if (storeOwned && store !== null) {
|
|
142
|
-
await store.close().catch(() => undefined);
|
|
143
|
-
}
|
|
144
|
-
return classified.exitCode;
|
|
145
|
-
}
|
|
146
|
-
finally {
|
|
147
|
-
await controlLock?.release().catch(() => undefined);
|
|
105
|
+
const message = formatManagedSyncErrorMessage(error instanceof Error ? error.message : String(error));
|
|
106
|
+
writeLine(context.stderr, `sync failed: ${message}`);
|
|
107
|
+
return classifyCliError(error).exitCode;
|
|
148
108
|
}
|
|
149
109
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { writeLine } from "../io.js";
|
|
2
|
+
import { createMutationSuccessEnvelope, describeCanonicalCommand, resolveStableMutationJsonSchema, writeHandledCliError, writeJsonValue, } from "../output.js";
|
|
3
|
+
import { CLI_INSTALL_COMMAND, EXPLICIT_UPDATE_CHECK_TIMEOUT_MS, applyUpdateCheckResult, compareSemver, createEmptyUpdateCheckCache, fetchLatestPublishedVersion, loadUpdateCheckCache, persistUpdateCheckCache, } from "../update-service.js";
|
|
4
|
+
function createUpdateResult(currentVersion, latestVersion, status, applied) {
|
|
5
|
+
return {
|
|
6
|
+
currentVersion,
|
|
7
|
+
latestVersion,
|
|
8
|
+
installCommand: CLI_INSTALL_COMMAND,
|
|
9
|
+
status,
|
|
10
|
+
applied,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function writeVersionSummary(context, currentVersion, latestVersion) {
|
|
14
|
+
writeLine(context.stdout, `Current version: ${currentVersion}`);
|
|
15
|
+
writeLine(context.stdout, `Latest version: ${latestVersion}`);
|
|
16
|
+
writeLine(context.stdout, `Run: ${CLI_INSTALL_COMMAND}`);
|
|
17
|
+
}
|
|
18
|
+
async function confirmApplyUpdate(context) {
|
|
19
|
+
const prompter = context.createPrompter();
|
|
20
|
+
if (!prompter.isInteractive) {
|
|
21
|
+
throw new Error("cli_update_requires_tty");
|
|
22
|
+
}
|
|
23
|
+
while (true) {
|
|
24
|
+
const answer = (await prompter.prompt("Install update now? [Y/n]: ")).trim().toLowerCase();
|
|
25
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (answer === "n" || answer === "no") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
prompter.writeLine("Enter \"y\" to continue or \"n\" to cancel.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function writeSuccessJson(parsed, context, result) {
|
|
35
|
+
writeJsonValue(context.stdout, createMutationSuccessEnvelope(resolveStableMutationJsonSchema(parsed), describeCanonicalCommand(parsed), result.status, result));
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
export async function runUpdateCommand(parsed, context) {
|
|
39
|
+
try {
|
|
40
|
+
const currentVersion = await context.readPackageVersion();
|
|
41
|
+
const cachePath = context.resolveUpdateCheckStatePath();
|
|
42
|
+
const now = context.now();
|
|
43
|
+
const cache = await loadUpdateCheckCache(cachePath) ?? createEmptyUpdateCheckCache();
|
|
44
|
+
const updateResult = await fetchLatestPublishedVersion(context.fetchImpl, {
|
|
45
|
+
timeoutMs: EXPLICIT_UPDATE_CHECK_TIMEOUT_MS,
|
|
46
|
+
});
|
|
47
|
+
if (updateResult.kind !== "success") {
|
|
48
|
+
throw new Error("cli_update_registry_unavailable");
|
|
49
|
+
}
|
|
50
|
+
await persistUpdateCheckCache(cachePath, applyUpdateCheckResult(cache, updateResult, now));
|
|
51
|
+
const latestVersion = updateResult.latestVersion;
|
|
52
|
+
const comparison = compareSemver(latestVersion, currentVersion);
|
|
53
|
+
if (comparison === null) {
|
|
54
|
+
throw new Error("cli_update_registry_unavailable");
|
|
55
|
+
}
|
|
56
|
+
if (comparison <= 0) {
|
|
57
|
+
const result = createUpdateResult(currentVersion, latestVersion, "up-to-date", false);
|
|
58
|
+
if (parsed.outputMode === "json") {
|
|
59
|
+
return writeSuccessJson(parsed, context, result);
|
|
60
|
+
}
|
|
61
|
+
writeVersionSummary(context, currentVersion, latestVersion);
|
|
62
|
+
writeLine(context.stdout, "Cogcoin is already up to date.");
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (!parsed.assumeYes) {
|
|
66
|
+
if (parsed.outputMode !== "text") {
|
|
67
|
+
throw new Error("cli_update_requires_tty");
|
|
68
|
+
}
|
|
69
|
+
writeVersionSummary(context, currentVersion, latestVersion);
|
|
70
|
+
if (!(await confirmApplyUpdate(context))) {
|
|
71
|
+
writeLine(context.stdout, "Update canceled.");
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (parsed.outputMode === "text") {
|
|
76
|
+
writeVersionSummary(context, currentVersion, latestVersion);
|
|
77
|
+
}
|
|
78
|
+
if (parsed.outputMode === "text") {
|
|
79
|
+
writeLine(context.stdout, "Installing update...");
|
|
80
|
+
}
|
|
81
|
+
await context.runGlobalClientUpdateInstall({
|
|
82
|
+
stdout: parsed.outputMode === "json" ? context.stderr : context.stdout,
|
|
83
|
+
stderr: context.stderr,
|
|
84
|
+
env: context.env,
|
|
85
|
+
});
|
|
86
|
+
const result = createUpdateResult(currentVersion, latestVersion, "updated", true);
|
|
87
|
+
if (parsed.outputMode === "json") {
|
|
88
|
+
return writeSuccessJson(parsed, context, result);
|
|
89
|
+
}
|
|
90
|
+
writeLine(context.stdout, "Update completed. The next cogcoin invocation will use the new install.");
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return writeHandledCliError({
|
|
95
|
+
parsed,
|
|
96
|
+
stdout: context.stdout,
|
|
97
|
+
stderr: context.stderr,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -29,6 +29,19 @@ function writeSetupUnlockGuidance(stdout) {
|
|
|
29
29
|
writeLine(stdout, line);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
function writeWelcomeArtBlock(stdout) {
|
|
33
|
+
writeLine(stdout, "");
|
|
34
|
+
writeLine(stdout, loadWelcomeArtText());
|
|
35
|
+
writeLine(stdout, "");
|
|
36
|
+
}
|
|
37
|
+
function assertInitTextPreflight(options) {
|
|
38
|
+
if (!options.prompter.isInteractive) {
|
|
39
|
+
throw new Error("wallet_init_requires_tty");
|
|
40
|
+
}
|
|
41
|
+
if (options.runtimePaths.selectedSeedName !== "main") {
|
|
42
|
+
throw new Error("wallet_init_seed_not_supported");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
32
45
|
function getResetNextSteps(result) {
|
|
33
46
|
return result.walletAction === "deleted" || result.walletAction === "not-present"
|
|
34
47
|
? ["Run `cogcoin init` to create a new wallet."]
|
|
@@ -169,6 +182,13 @@ export async function runWalletAdminCommand(parsed, context) {
|
|
|
169
182
|
if (parsed.command === "init" || parsed.command === "wallet-init") {
|
|
170
183
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
171
184
|
const prompter = createCommandPrompter(parsed, context);
|
|
185
|
+
if (parsed.outputMode === "text") {
|
|
186
|
+
assertInitTextPreflight({
|
|
187
|
+
prompter,
|
|
188
|
+
runtimePaths,
|
|
189
|
+
});
|
|
190
|
+
writeWelcomeArtBlock(context.stdout);
|
|
191
|
+
}
|
|
172
192
|
const interactiveProvider = withInteractiveWalletSecretProvider(provider, prompter);
|
|
173
193
|
const result = await context.initializeWallet({
|
|
174
194
|
dataDir,
|
|
@@ -184,9 +204,7 @@ export async function runWalletAdminCommand(parsed, context) {
|
|
|
184
204
|
}));
|
|
185
205
|
return 0;
|
|
186
206
|
}
|
|
187
|
-
|
|
188
|
-
writeLine(context.stdout, loadWelcomeArtText());
|
|
189
|
-
writeLine(context.stdout, "");
|
|
207
|
+
writeWelcomeArtBlock(context.stdout);
|
|
190
208
|
writeLine(context.stdout, result.walletAction === "already-initialized"
|
|
191
209
|
? "Wallet already initialized."
|
|
192
210
|
: "Wallet initialized.");
|
|
@@ -40,6 +40,7 @@ function emitJson(context, parsed, schema, result) {
|
|
|
40
40
|
export async function runWalletReadCommand(parsed, context) {
|
|
41
41
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
42
42
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
43
|
+
const packageVersion = await context.readPackageVersion();
|
|
43
44
|
const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
|
|
44
45
|
await context.ensureDirectory(dirname(dbPath));
|
|
45
46
|
const provider = parsed.outputMode === "text"
|
|
@@ -49,6 +50,7 @@ export async function runWalletReadCommand(parsed, context) {
|
|
|
49
50
|
dataDir,
|
|
50
51
|
databasePath: dbPath,
|
|
51
52
|
secretProvider: provider,
|
|
53
|
+
expectedIndexerBinaryVersion: packageVersion,
|
|
52
54
|
paths: runtimePaths,
|
|
53
55
|
});
|
|
54
56
|
try {
|
package/dist/cli/context.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { mkdir, readFile } from "node:fs/promises";
|
|
2
3
|
import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, stopIndexerDaemonService, } from "../bitcoind/indexer-daemon.js";
|
|
3
4
|
import { createRpcClient } from "../bitcoind/node.js";
|
|
4
5
|
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../bitcoind/service.js";
|
|
5
6
|
import { resolveDefaultBitcoindDataDirForTesting, resolveDefaultClientDatabasePathForTesting, resolveDefaultUpdateCheckStatePathForTesting, } from "../app-paths.js";
|
|
6
7
|
import { openManagedBitcoindClient } from "../bitcoind/index.js";
|
|
8
|
+
import { openManagedIndexerMonitor } from "../bitcoind/indexer-monitor.js";
|
|
7
9
|
import { inspectPassiveClientStatus } from "../passive-status.js";
|
|
8
10
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
9
11
|
import { initializeWallet, deleteImportedWalletSeed, previewResetWallet, repairWallet, resetWallet, restoreWalletFromMnemonic, showWalletMnemonic, } from "../wallet/lifecycle.js";
|
|
10
12
|
import { resolveWalletRuntimePathsForTesting } from "../wallet/runtime.js";
|
|
11
13
|
import { openWalletReadContext } from "../wallet/read/index.js";
|
|
12
14
|
import { loadRawWalletStateEnvelope, loadWalletState } from "../wallet/state/storage.js";
|
|
13
|
-
import { ensureBuiltInMiningSetupIfNeeded, followMiningLog, inspectMiningControlPlane, readMiningLog, runForegroundMining, setupBuiltInMining, startBackgroundMining, stopBackgroundMining, } from "../wallet/mining/index.js";
|
|
15
|
+
import { ensureBuiltInMiningSetupIfNeeded, followMiningLog, inspectMiningControlPlane, inspectMiningDomainPromptState, readMiningLog, runForegroundMining, setupBuiltInMining, startBackgroundMining, stopBackgroundMining, updateMiningDomainPrompt, } from "../wallet/mining/index.js";
|
|
14
16
|
import { createLazyDefaultWalletSecretProvider } from "../wallet/state/provider.js";
|
|
15
17
|
import { anchorDomain, transferBitcoin, buyDomain, claimCogLock, clearDomainDelegate, clearDomainEndpoint, clearDomainMiner, clearField, createField, giveReputation, lockCogToDomain, registerDomain, reclaimCogLock, revokeReputation, sendCog, setField, setDomainCanonical, setDomainDelegate, setDomainEndpoint, setDomainMiner, sellDomain, transferDomain, } from "../wallet/tx/index.js";
|
|
16
18
|
import { createTerminalPrompter } from "./prompt.js";
|
|
@@ -36,6 +38,35 @@ export async function readPackageVersionFromDisk() {
|
|
|
36
38
|
}
|
|
37
39
|
return "0.0.0";
|
|
38
40
|
}
|
|
41
|
+
async function runGlobalClientUpdateInstall(options) {
|
|
42
|
+
const binary = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn(binary, ["install", "-g", "@cogcoin/client"], {
|
|
45
|
+
env: options.env,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
child.stdout?.on("data", (chunk) => {
|
|
49
|
+
options.stdout.write(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
50
|
+
});
|
|
51
|
+
child.stderr?.on("data", (chunk) => {
|
|
52
|
+
options.stderr.write(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
53
|
+
});
|
|
54
|
+
child.on("error", (error) => {
|
|
55
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
56
|
+
reject(new Error("cli_update_npm_not_found"));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
reject(new Error("cli_update_install_failed"));
|
|
60
|
+
});
|
|
61
|
+
child.on("close", (code) => {
|
|
62
|
+
if (code === 0) {
|
|
63
|
+
resolve();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
reject(new Error("cli_update_install_failed"));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
39
70
|
export function createDefaultContext(overrides = {}) {
|
|
40
71
|
return {
|
|
41
72
|
stdout: overrides.stdout ?? process.stdout,
|
|
@@ -48,8 +79,10 @@ export function createDefaultContext(overrides = {}) {
|
|
|
48
79
|
process.exit(code);
|
|
49
80
|
}),
|
|
50
81
|
fetchImpl: overrides.fetchImpl ?? fetch,
|
|
82
|
+
runGlobalClientUpdateInstall: overrides.runGlobalClientUpdateInstall ?? runGlobalClientUpdateInstall,
|
|
51
83
|
openSqliteStore: overrides.openSqliteStore ?? openSqliteStore,
|
|
52
84
|
openManagedBitcoindClient: overrides.openManagedBitcoindClient ?? openManagedBitcoindClient,
|
|
85
|
+
openManagedIndexerMonitor: overrides.openManagedIndexerMonitor ?? openManagedIndexerMonitor,
|
|
53
86
|
inspectPassiveClientStatus: overrides.inspectPassiveClientStatus ?? inspectPassiveClientStatus,
|
|
54
87
|
openWalletReadContext: overrides.openWalletReadContext ?? openWalletReadContext,
|
|
55
88
|
initializeWallet: overrides.initializeWallet ?? initializeWallet,
|
|
@@ -80,11 +113,13 @@ export function createDefaultContext(overrides = {}) {
|
|
|
80
113
|
giveReputation: overrides.giveReputation ?? giveReputation,
|
|
81
114
|
revokeReputation: overrides.revokeReputation ?? revokeReputation,
|
|
82
115
|
inspectMiningControlPlane: overrides.inspectMiningControlPlane ?? inspectMiningControlPlane,
|
|
116
|
+
inspectMiningDomainPromptState: overrides.inspectMiningDomainPromptState ?? inspectMiningDomainPromptState,
|
|
83
117
|
ensureBuiltInMiningSetupIfNeeded: overrides.ensureBuiltInMiningSetupIfNeeded ?? ensureBuiltInMiningSetupIfNeeded,
|
|
84
118
|
runForegroundMining: overrides.runForegroundMining ?? runForegroundMining,
|
|
85
119
|
startBackgroundMining: overrides.startBackgroundMining ?? startBackgroundMining,
|
|
86
120
|
stopBackgroundMining: overrides.stopBackgroundMining ?? stopBackgroundMining,
|
|
87
121
|
setupBuiltInMining: overrides.setupBuiltInMining ?? setupBuiltInMining,
|
|
122
|
+
updateMiningDomainPrompt: overrides.updateMiningDomainPrompt ?? updateMiningDomainPrompt,
|
|
88
123
|
readMiningLog: overrides.readMiningLog ?? readMiningLog,
|
|
89
124
|
followMiningLog: overrides.followMiningLog ?? followMiningLog,
|
|
90
125
|
repairWallet: overrides.repairWallet ?? repairWallet,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ManagedBitcoindProgressEvent, ManagedIndexerDaemonObservedStatus } from "../bitcoind/types.js";
|
|
2
|
+
import type { ProgressOutput, WritableLike } from "./types.js";
|
|
3
|
+
export declare function isManagedIndexerCaughtUp(status: ManagedIndexerDaemonObservedStatus): boolean;
|
|
4
|
+
export declare function assertManagedIndexerStatusRecoverable(status: ManagedIndexerDaemonObservedStatus): void;
|
|
5
|
+
export declare class ManagedIndexerProgressObserver {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(options: {
|
|
8
|
+
quoteStatePath: string;
|
|
9
|
+
stream: WritableLike;
|
|
10
|
+
progressOutput: ProgressOutput;
|
|
11
|
+
followVisualMode?: boolean;
|
|
12
|
+
onProgress?: (event: ManagedBitcoindProgressEvent) => void;
|
|
13
|
+
});
|
|
14
|
+
applyStatus(status: ManagedIndexerDaemonObservedStatus): Promise<void>;
|
|
15
|
+
playCompletionScene(): Promise<void>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function pollManagedIndexerUntilCaughtUp(options: {
|
|
19
|
+
monitor: {
|
|
20
|
+
getStatus(): Promise<ManagedIndexerDaemonObservedStatus>;
|
|
21
|
+
};
|
|
22
|
+
observer: ManagedIndexerProgressObserver;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
pollIntervalMs?: number;
|
|
25
|
+
}): Promise<ManagedIndexerDaemonObservedStatus>;
|
|
26
|
+
export declare function followManagedIndexerStatus(options: {
|
|
27
|
+
monitor: {
|
|
28
|
+
getStatus(): Promise<ManagedIndexerDaemonObservedStatus>;
|
|
29
|
+
};
|
|
30
|
+
observer: ManagedIndexerProgressObserver;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
pollIntervalMs?: number;
|
|
33
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { DEFAULT_SNAPSHOT_METADATA } from "../bitcoind/bootstrap.js";
|
|
2
|
+
import { ManagedProgressController } from "../bitcoind/progress.js";
|
|
3
|
+
import { createBootstrapProgress, createDefaultMessage, } from "../bitcoind/progress/formatting.js";
|
|
4
|
+
const INDEXER_MONITOR_POLL_INTERVAL_MS = 500;
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
setTimeout(resolve, ms);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function isTransientIndexerFailureMessage(message) {
|
|
11
|
+
if (message === null) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return message === "managed_bitcoind_runtime_config_unavailable"
|
|
15
|
+
|| message.includes("cookie file is unavailable")
|
|
16
|
+
|| message.includes("ECONNREFUSED")
|
|
17
|
+
|| message.includes("ECONNRESET")
|
|
18
|
+
|| message.includes("socket hang up");
|
|
19
|
+
}
|
|
20
|
+
function deriveFallbackPhase(status) {
|
|
21
|
+
switch (status.state) {
|
|
22
|
+
case "synced":
|
|
23
|
+
return "follow_tip";
|
|
24
|
+
case "catching-up":
|
|
25
|
+
case "reorging":
|
|
26
|
+
return "cogcoin_sync";
|
|
27
|
+
case "failed":
|
|
28
|
+
return "error";
|
|
29
|
+
case "starting":
|
|
30
|
+
return "paused";
|
|
31
|
+
case "stopping":
|
|
32
|
+
return "paused";
|
|
33
|
+
case "schema-mismatch":
|
|
34
|
+
case "service-version-mismatch":
|
|
35
|
+
return "error";
|
|
36
|
+
default:
|
|
37
|
+
return "paused";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function normalizeBootstrapProgress(status) {
|
|
41
|
+
const phase = status.bootstrapPhase ?? deriveFallbackPhase(status);
|
|
42
|
+
const sourceProgress = status.bootstrapProgress ?? createBootstrapProgress(phase, DEFAULT_SNAPSHOT_METADATA);
|
|
43
|
+
const progress = {
|
|
44
|
+
...sourceProgress,
|
|
45
|
+
phase,
|
|
46
|
+
message: sourceProgress.message || createDefaultMessage(phase),
|
|
47
|
+
blocks: sourceProgress.blocks,
|
|
48
|
+
headers: sourceProgress.headers ?? status.coreBestHeight,
|
|
49
|
+
targetHeight: sourceProgress.targetHeight ?? status.coreBestHeight,
|
|
50
|
+
lastError: status.lastError ?? sourceProgress.lastError,
|
|
51
|
+
updatedAt: sourceProgress.updatedAt ?? status.updatedAtUnixMs,
|
|
52
|
+
};
|
|
53
|
+
const cogcoinSyncHeight = status.cogcoinSyncHeight ?? status.appliedTipHeight ?? null;
|
|
54
|
+
const cogcoinSyncTargetHeight = status.cogcoinSyncTargetHeight ?? status.coreBestHeight ?? null;
|
|
55
|
+
if (phase === "cogcoin_sync") {
|
|
56
|
+
progress.blocks = progress.blocks ?? cogcoinSyncHeight;
|
|
57
|
+
progress.headers = progress.headers ?? cogcoinSyncTargetHeight;
|
|
58
|
+
progress.targetHeight = progress.targetHeight ?? cogcoinSyncTargetHeight;
|
|
59
|
+
}
|
|
60
|
+
else if (phase === "follow_tip") {
|
|
61
|
+
progress.blocks = status.coreBestHeight ?? progress.blocks;
|
|
62
|
+
progress.headers = status.coreBestHeight ?? progress.headers;
|
|
63
|
+
progress.targetHeight = status.coreBestHeight ?? progress.targetHeight;
|
|
64
|
+
}
|
|
65
|
+
else if (phase === "error" && progress.message === createDefaultMessage("error") && status.lastError !== null) {
|
|
66
|
+
progress.message = status.lastError;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
phase,
|
|
70
|
+
progress,
|
|
71
|
+
cogcoinSyncHeight,
|
|
72
|
+
cogcoinSyncTargetHeight,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function isManagedIndexerCaughtUp(status) {
|
|
76
|
+
return status.state === "synced"
|
|
77
|
+
&& status.coreBestHeight !== null
|
|
78
|
+
&& status.appliedTipHeight === status.coreBestHeight
|
|
79
|
+
&& (status.coreBestHash === null
|
|
80
|
+
|| status.appliedTipHash === null
|
|
81
|
+
|| status.coreBestHash === status.appliedTipHash);
|
|
82
|
+
}
|
|
83
|
+
export function assertManagedIndexerStatusRecoverable(status) {
|
|
84
|
+
if (status.state === "schema-mismatch" || status.state === "service-version-mismatch") {
|
|
85
|
+
throw new Error(status.lastError ?? `indexer_daemon_${status.state}`);
|
|
86
|
+
}
|
|
87
|
+
if (status.state === "failed" && !isTransientIndexerFailureMessage(status.lastError)) {
|
|
88
|
+
throw new Error(status.lastError ?? "indexer_daemon_failed");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export class ManagedIndexerProgressObserver {
|
|
92
|
+
#progress;
|
|
93
|
+
#followVisualMode;
|
|
94
|
+
#started = false;
|
|
95
|
+
#followVisualModeEnabled = false;
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.#followVisualMode = options.followVisualMode ?? false;
|
|
98
|
+
this.#progress = new ManagedProgressController({
|
|
99
|
+
onProgress: options.onProgress,
|
|
100
|
+
progressOutput: options.progressOutput,
|
|
101
|
+
snapshot: DEFAULT_SNAPSHOT_METADATA,
|
|
102
|
+
quoteStatePath: options.quoteStatePath,
|
|
103
|
+
stream: options.stream,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async applyStatus(status) {
|
|
107
|
+
if (!this.#started) {
|
|
108
|
+
await this.#progress.start();
|
|
109
|
+
this.#started = true;
|
|
110
|
+
}
|
|
111
|
+
if (this.#followVisualMode && !this.#followVisualModeEnabled) {
|
|
112
|
+
await this.#progress.enableFollowVisualMode(status.appliedTipHeight ?? null);
|
|
113
|
+
this.#followVisualModeEnabled = true;
|
|
114
|
+
}
|
|
115
|
+
const normalized = normalizeBootstrapProgress(status);
|
|
116
|
+
if (normalized.phase === "cogcoin_sync"
|
|
117
|
+
|| (normalized.phase === "follow_tip" && this.#followVisualMode)) {
|
|
118
|
+
await this.#progress.setCogcoinSync(normalized.cogcoinSyncHeight, normalized.cogcoinSyncTargetHeight, normalized.progress.etaSeconds);
|
|
119
|
+
}
|
|
120
|
+
const { phase: _phase, updatedAt: _updatedAt, ...patch } = normalized.progress;
|
|
121
|
+
await this.#progress.setPhase(normalized.phase, patch);
|
|
122
|
+
}
|
|
123
|
+
async playCompletionScene() {
|
|
124
|
+
if (!this.#started) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await this.#progress.playCompletionScene();
|
|
128
|
+
}
|
|
129
|
+
async close() {
|
|
130
|
+
if (!this.#started) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await this.#progress.close();
|
|
134
|
+
this.#started = false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function pollManagedIndexerUntilCaughtUp(options) {
|
|
138
|
+
while (true) {
|
|
139
|
+
if (options.signal?.aborted) {
|
|
140
|
+
throw options.signal.reason instanceof Error
|
|
141
|
+
? options.signal.reason
|
|
142
|
+
: new Error("managed_indexer_observer_aborted");
|
|
143
|
+
}
|
|
144
|
+
const status = await options.monitor.getStatus();
|
|
145
|
+
await options.observer.applyStatus(status);
|
|
146
|
+
assertManagedIndexerStatusRecoverable(status);
|
|
147
|
+
if (isManagedIndexerCaughtUp(status)) {
|
|
148
|
+
return status;
|
|
149
|
+
}
|
|
150
|
+
await sleep(options.pollIntervalMs ?? INDEXER_MONITOR_POLL_INTERVAL_MS);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function followManagedIndexerStatus(options) {
|
|
154
|
+
while (true) {
|
|
155
|
+
if (options.signal?.aborted) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const status = await options.monitor.getStatus();
|
|
159
|
+
await options.observer.applyStatus(status);
|
|
160
|
+
assertManagedIndexerStatusRecoverable(status);
|
|
161
|
+
await sleep(options.pollIntervalMs ?? INDEXER_MONITOR_POLL_INTERVAL_MS);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { MiningControlPlaneView, MiningEventRecord } from "../wallet/mining/index.js";
|
|
1
|
+
import type { MiningControlPlaneView, MiningDomainPromptListResult, MiningDomainPromptMutationResult, MiningEventRecord } from "../wallet/mining/index.js";
|
|
2
2
|
export declare function formatMiningSummaryLine(mining: MiningControlPlaneView): string;
|
|
3
3
|
export declare function formatMineStatusReport(mining: MiningControlPlaneView): string;
|
|
4
4
|
export declare function formatMiningEventRecord(event: MiningEventRecord): string;
|
|
5
|
+
export declare function formatMiningPromptMutationReport(result: MiningDomainPromptMutationResult): string;
|
|
6
|
+
export declare function formatMiningPromptListReport(result: MiningDomainPromptListResult): string;
|