@cogcoin/client 1.0.0 → 1.0.2
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 +2 -1
- package/dist/bitcoind/indexer-daemon.d.ts +3 -0
- package/dist/bitcoind/indexer-daemon.js +58 -8
- 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/cli/commands/mining-admin.js +9 -0
- package/dist/cli/commands/mining-runtime.js +114 -12
- package/dist/cli/commands/sync.js +1 -91
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/context.js +33 -1
- package/dist/cli/mining-format.js +28 -0
- package/dist/cli/mining-json.js +6 -0
- package/dist/cli/output.js +50 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +5 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +13 -0
- package/dist/cli/read-json.js +17 -0
- package/dist/cli/runner.js +4 -0
- package/dist/cli/sync-progress.d.ts +6 -0
- package/dist/cli/sync-progress.js +91 -0
- package/dist/cli/types.d.ts +8 -2
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +44 -0
- package/dist/cli/update-service.js +218 -0
- package/dist/cli/wallet-format.js +3 -0
- package/dist/client/initialization.js +5 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/lifecycle.js +6 -0
- package/dist/wallet/mining/config.js +13 -3
- package/dist/wallet/mining/control.d.ts +2 -1
- package/dist/wallet/mining/control.js +143 -19
- package/dist/wallet/mining/index.d.ts +2 -2
- package/dist/wallet/mining/index.js +1 -1
- 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 +105 -3
- package/dist/wallet/mining/runner.js +490 -88
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +25 -2
- package/dist/wallet/mining/types.d.ts +9 -1
- package/dist/wallet/mining/visualizer.js +28 -5
- package/dist/wallet/read/context.js +3 -0
- package/dist/wallet/reset.js +1 -0
- package/dist/wallet/tx/anchor.js +1 -0
- package/dist/wallet/tx/bitcoin-transfer.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 +1 -0
- package/dist/wallet/tx/register.js +1 -0
- package/dist/wallet/tx/reputation.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.0.
|
|
3
|
+
`@cogcoin/client@1.0.2` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -130,6 +130,7 @@ Managed node subpath:
|
|
|
130
130
|
|
|
131
131
|
The installed `cogcoin` command covers the first-party local wallet and node workflow:
|
|
132
132
|
|
|
133
|
+
- update commands such as `update` to compare the current CLI version with the latest npm release and install it
|
|
133
134
|
- wallet lifecycle commands such as `init`, `restore`, `wallet delete`, `wallet show-mnemonic`, and `repair`
|
|
134
135
|
- sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
|
|
135
136
|
- domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
|
|
@@ -84,6 +84,7 @@ export interface CoherentIndexerSnapshotLease {
|
|
|
84
84
|
payload: IndexerSnapshotPayload;
|
|
85
85
|
status: ManagedIndexerDaemonStatus;
|
|
86
86
|
}
|
|
87
|
+
type ManagedIndexerDaemonServiceLifetime = "persistent" | "ephemeral";
|
|
87
88
|
export declare function stopIndexerDaemonServiceWithLockHeld(options: {
|
|
88
89
|
dataDir: string;
|
|
89
90
|
walletRootId?: string;
|
|
@@ -104,6 +105,8 @@ export declare function attachOrStartIndexerDaemon(options: {
|
|
|
104
105
|
databasePath: string;
|
|
105
106
|
walletRootId?: string;
|
|
106
107
|
startupTimeoutMs?: number;
|
|
108
|
+
shutdownTimeoutMs?: number;
|
|
109
|
+
serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
|
|
107
110
|
}): Promise<IndexerDaemonClient>;
|
|
108
111
|
export declare function stopIndexerDaemonService(options: {
|
|
109
112
|
dataDir: string;
|
|
@@ -80,7 +80,8 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
|
80
80
|
walletRootId,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
function createIndexerDaemonClient(socketPath) {
|
|
83
|
+
function createIndexerDaemonClient(socketPath, closeOptions = null) {
|
|
84
|
+
let closed = false;
|
|
84
85
|
async function sendRequest(request) {
|
|
85
86
|
return new Promise((resolve, reject) => {
|
|
86
87
|
const socket = net.createConnection(socketPath);
|
|
@@ -181,7 +182,18 @@ function createIndexerDaemonClient(socketPath) {
|
|
|
181
182
|
});
|
|
182
183
|
},
|
|
183
184
|
async close() {
|
|
184
|
-
|
|
185
|
+
if (closed) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
closed = true;
|
|
189
|
+
if (closeOptions === null || closeOptions.serviceLifetime !== "ephemeral" || closeOptions.ownership === "attached") {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await stopIndexerDaemonService({
|
|
193
|
+
dataDir: closeOptions.dataDir,
|
|
194
|
+
walletRootId: closeOptions.walletRootId,
|
|
195
|
+
shutdownTimeoutMs: closeOptions.shutdownTimeoutMs,
|
|
196
|
+
});
|
|
185
197
|
},
|
|
186
198
|
};
|
|
187
199
|
}
|
|
@@ -308,7 +320,8 @@ async function waitForIndexerDaemon(dataDir, walletRootId, timeoutMs) {
|
|
|
308
320
|
while (Date.now() < deadline) {
|
|
309
321
|
const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
310
322
|
if (probe.compatibility === "compatible" && probe.client !== null) {
|
|
311
|
-
|
|
323
|
+
await probe.client.close().catch(() => undefined);
|
|
324
|
+
return;
|
|
312
325
|
}
|
|
313
326
|
if (probe.compatibility !== "unreachable") {
|
|
314
327
|
throw new Error(probe.error ?? "indexer_daemon_protocol_error");
|
|
@@ -358,6 +371,7 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
358
371
|
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
359
372
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
360
373
|
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
374
|
+
const serviceLifetime = options.serviceLifetime ?? "persistent";
|
|
361
375
|
const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
362
376
|
if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
|
|
363
377
|
return existingProbe.client;
|
|
@@ -382,17 +396,46 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
382
396
|
}
|
|
383
397
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
384
398
|
const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
|
|
399
|
+
const spawnOptions = serviceLifetime === "ephemeral"
|
|
400
|
+
? {
|
|
401
|
+
stdio: "ignore",
|
|
402
|
+
}
|
|
403
|
+
: {
|
|
404
|
+
detached: true,
|
|
405
|
+
stdio: "ignore",
|
|
406
|
+
};
|
|
385
407
|
const child = spawn(process.execPath, [
|
|
386
408
|
daemonEntryPath,
|
|
387
409
|
`--data-dir=${options.dataDir}`,
|
|
388
410
|
`--database-path=${options.databasePath}`,
|
|
389
411
|
`--wallet-root-id=${walletRootId}`,
|
|
390
412
|
], {
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
...spawnOptions,
|
|
414
|
+
});
|
|
415
|
+
if (serviceLifetime !== "ephemeral") {
|
|
416
|
+
child.unref();
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (child.pid !== undefined) {
|
|
423
|
+
try {
|
|
424
|
+
process.kill(child.pid, "SIGTERM");
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// ignore shutdown failures while unwinding startup errors
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
433
|
+
dataDir: options.dataDir,
|
|
434
|
+
walletRootId,
|
|
435
|
+
serviceLifetime,
|
|
436
|
+
ownership: "started",
|
|
437
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
393
438
|
});
|
|
394
|
-
child.unref();
|
|
395
|
-
return await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
396
439
|
}
|
|
397
440
|
finally {
|
|
398
441
|
await lock.release();
|
|
@@ -400,7 +443,14 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
400
443
|
}
|
|
401
444
|
catch (error) {
|
|
402
445
|
if (error instanceof FileLockBusyError) {
|
|
403
|
-
|
|
446
|
+
await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
|
|
447
|
+
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
448
|
+
dataDir: options.dataDir,
|
|
449
|
+
walletRootId,
|
|
450
|
+
serviceLifetime,
|
|
451
|
+
ownership: "attached",
|
|
452
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
453
|
+
});
|
|
404
454
|
}
|
|
405
455
|
throw error;
|
|
406
456
|
}
|
|
@@ -19,6 +19,9 @@ export function isRetryableManagedRpcError(error) {
|
|
|
19
19
|
if (message === "bitcoind_rpc_timeout") {
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
|
+
if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
22
25
|
if (message.startsWith("The managed Bitcoin RPC request to ")) {
|
|
23
26
|
return message.includes(" failed");
|
|
24
27
|
}
|
|
@@ -24,6 +24,7 @@ type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataD
|
|
|
24
24
|
getblockArchivePath?: string | null;
|
|
25
25
|
getblockArchiveEndHeight?: number | null;
|
|
26
26
|
getblockArchiveSha256?: string | null;
|
|
27
|
+
serviceLifetime?: "persistent" | "ephemeral";
|
|
27
28
|
};
|
|
28
29
|
export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
|
|
29
30
|
export interface ManagedBitcoindServiceProbeResult {
|
package/dist/bitcoind/service.js
CHANGED
|
@@ -681,9 +681,10 @@ async function refreshManagedBitcoindStatus(status, paths, options) {
|
|
|
681
681
|
return nextStatus;
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
-
function createNodeHandle(status, paths, options) {
|
|
684
|
+
function createNodeHandle(status, paths, options, ownership) {
|
|
685
685
|
let currentStatus = status;
|
|
686
686
|
const rpc = createRpcClient(currentStatus.rpc);
|
|
687
|
+
let stopped = false;
|
|
687
688
|
return {
|
|
688
689
|
rpc: currentStatus.rpc,
|
|
689
690
|
zmq: currentStatus.zmq,
|
|
@@ -706,9 +707,20 @@ function createNodeHandle(status, paths, options) {
|
|
|
706
707
|
return currentStatus;
|
|
707
708
|
},
|
|
708
709
|
async stop() {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
710
|
+
if (stopped) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
stopped = true;
|
|
714
|
+
if (options.serviceLifetime !== "ephemeral" || ownership === "attached") {
|
|
715
|
+
// Public managed clients detach from persistent services, and ephemeral
|
|
716
|
+
// attach callers must not shut down services they did not launch.
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
await stopManagedBitcoindService({
|
|
720
|
+
dataDir: currentStatus.dataDir,
|
|
721
|
+
walletRootId: currentStatus.walletRootId,
|
|
722
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
723
|
+
});
|
|
712
724
|
},
|
|
713
725
|
};
|
|
714
726
|
}
|
|
@@ -720,7 +732,7 @@ async function tryAttachExistingManagedBitcoindService(options) {
|
|
|
720
732
|
return null;
|
|
721
733
|
}
|
|
722
734
|
const refreshed = await refreshManagedBitcoindStatus(probe.status, paths, options);
|
|
723
|
-
return createNodeHandle(refreshed, paths, options);
|
|
735
|
+
return createNodeHandle(refreshed, paths, options, "attached");
|
|
724
736
|
}
|
|
725
737
|
async function waitForManagedBitcoindService(options, timeoutMs) {
|
|
726
738
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -765,6 +777,7 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
765
777
|
const resolvedOptions = {
|
|
766
778
|
...options,
|
|
767
779
|
dataDir: options.dataDir,
|
|
780
|
+
serviceLifetime: options.serviceLifetime ?? "persistent",
|
|
768
781
|
walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
|
|
769
782
|
};
|
|
770
783
|
const startupTimeoutMs = resolvedOptions.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
@@ -815,11 +828,20 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
815
828
|
port: runtimeConfig.zmqPort,
|
|
816
829
|
pollIntervalMs: startOptions.pollIntervalMs ?? 15_000,
|
|
817
830
|
};
|
|
831
|
+
const spawnOptions = startOptions.serviceLifetime === "ephemeral"
|
|
832
|
+
? {
|
|
833
|
+
stdio: "ignore",
|
|
834
|
+
}
|
|
835
|
+
: {
|
|
836
|
+
detached: true,
|
|
837
|
+
stdio: "ignore",
|
|
838
|
+
};
|
|
818
839
|
const child = spawn(bitcoindPath, buildManagedServiceArgs(startOptions, runtimeConfig), {
|
|
819
|
-
|
|
820
|
-
stdio: "ignore",
|
|
840
|
+
...spawnOptions,
|
|
821
841
|
});
|
|
822
|
-
|
|
842
|
+
if (startOptions.serviceLifetime !== "ephemeral") {
|
|
843
|
+
child.unref();
|
|
844
|
+
}
|
|
823
845
|
const rpc = createRpcClient(rpcConfig);
|
|
824
846
|
try {
|
|
825
847
|
await waitForRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
|
|
@@ -876,7 +898,7 @@ export async function attachOrStartManagedBitcoindService(options) {
|
|
|
876
898
|
});
|
|
877
899
|
}
|
|
878
900
|
await writeBitcoindStatus(paths, status);
|
|
879
|
-
return createNodeHandle(status, paths, resolvedOptions);
|
|
901
|
+
return createNodeHandle(status, paths, resolvedOptions, "started");
|
|
880
902
|
}
|
|
881
903
|
finally {
|
|
882
904
|
await lock.release();
|
|
@@ -36,6 +36,15 @@ export async function runMiningAdminCommand(parsed, context) {
|
|
|
36
36
|
}
|
|
37
37
|
writeLine(context.stdout, "Built-in mining provider configured.");
|
|
38
38
|
writeLine(context.stdout, `Provider: ${view.provider.provider ?? "unknown"}`);
|
|
39
|
+
if (view.provider.modelId !== null) {
|
|
40
|
+
writeLine(context.stdout, `Selected model: ${view.provider.modelId}`);
|
|
41
|
+
}
|
|
42
|
+
if (view.provider.modelSelectionSource !== null) {
|
|
43
|
+
writeLine(context.stdout, `Selection source: ${view.provider.modelSelectionSource}`);
|
|
44
|
+
}
|
|
45
|
+
if (view.provider.estimatedDailyCostDisplay !== null) {
|
|
46
|
+
writeLine(context.stdout, `Approximate daily cost: ${view.provider.estimatedDailyCostDisplay}`);
|
|
47
|
+
}
|
|
39
48
|
for (const line of formatNextStepLines(nextSteps)) {
|
|
40
49
|
writeLine(context.stdout, line);
|
|
41
50
|
}
|
|
@@ -1,28 +1,109 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
+
import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
|
|
3
|
+
import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
|
|
4
|
+
import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
|
|
2
5
|
import { buildMineStartData, buildMineStopData, } from "../mining-json.js";
|
|
3
6
|
import { buildMineStartPreviewData, buildMineStopPreviewData, } from "../preview-json.js";
|
|
4
|
-
import { writeLine } from "../io.js";
|
|
7
|
+
import { usesTtyProgress, writeLine } from "../io.js";
|
|
5
8
|
import { createTerminalPrompter } from "../prompt.js";
|
|
6
9
|
import { createPreviewSuccessEnvelope, createMutationSuccessEnvelope, describeCanonicalCommand, resolvePreviewJsonSchema, resolveStableMiningControlJsonSchema, writeHandledCliError, writeJsonValue, } from "../output.js";
|
|
7
10
|
import { formatNextStepLines, getMineStopNextSteps, } from "../workflow-hints.js";
|
|
8
|
-
import {
|
|
11
|
+
import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
|
|
12
|
+
import { createSyncProgressReporter } from "../sync-progress.js";
|
|
9
13
|
function createCommandPrompter(parsed, context) {
|
|
10
14
|
return parsed.outputMode !== "text"
|
|
11
15
|
? createTerminalPrompter(context.stdin, context.stderr)
|
|
12
16
|
: context.createPrompter();
|
|
13
17
|
}
|
|
14
|
-
async function
|
|
15
|
-
|
|
18
|
+
async function ensureMiningProviderSetup(options) {
|
|
19
|
+
const setupReady = await options.context.ensureBuiltInMiningSetupIfNeeded({
|
|
20
|
+
provider: options.provider,
|
|
21
|
+
prompter: options.prompter,
|
|
22
|
+
paths: options.runtimePaths,
|
|
23
|
+
});
|
|
24
|
+
if (!setupReady) {
|
|
25
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function syncManagedMiningReadiness(options) {
|
|
29
|
+
const ttyProgressActive = usesTtyProgress(options.parsed.progressOutput, options.context.stderr);
|
|
30
|
+
let controlLock = null;
|
|
31
|
+
let store = null;
|
|
32
|
+
let storeOwned = true;
|
|
33
|
+
let client = null;
|
|
34
|
+
let clientClosed = false;
|
|
16
35
|
try {
|
|
17
|
-
|
|
18
|
-
dataDir: options.dataDir,
|
|
19
|
-
databasePath: options.databasePath,
|
|
20
|
-
secretProvider: options.provider,
|
|
36
|
+
const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
|
|
21
37
|
paths: options.runtimePaths,
|
|
38
|
+
provider: options.provider,
|
|
39
|
+
loadRawWalletStateEnvelope: options.context.loadRawWalletStateEnvelope,
|
|
22
40
|
});
|
|
41
|
+
try {
|
|
42
|
+
controlLock = await acquireFileLock(options.runtimePaths.walletControlLockPath, {
|
|
43
|
+
purpose: "managed-sync",
|
|
44
|
+
walletRootId: walletRoot.walletRootId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof FileLockBusyError) {
|
|
49
|
+
throw new Error("wallet_control_lock_busy");
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
await options.context.ensureDirectory(dirname(options.databasePath));
|
|
54
|
+
store = await options.context.openSqliteStore({ filename: options.databasePath });
|
|
55
|
+
client = await options.context.openManagedBitcoindClient({
|
|
56
|
+
store,
|
|
57
|
+
databasePath: options.databasePath,
|
|
58
|
+
dataDir: options.dataDir,
|
|
59
|
+
walletRootId: walletRoot.walletRootId,
|
|
60
|
+
progressOutput: options.parsed.progressOutput,
|
|
61
|
+
onProgress: ttyProgressActive ? undefined : createSyncProgressReporter({
|
|
62
|
+
progressOutput: options.parsed.progressOutput,
|
|
63
|
+
write: (line) => {
|
|
64
|
+
writeLine(options.context.stderr, line);
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
storeOwned = false;
|
|
69
|
+
const stopWatcher = createStopSignalWatcher(options.context.signalSource, options.context.stderr, client, options.context.forceExit, [options.runtimePaths.walletControlLockPath]);
|
|
70
|
+
try {
|
|
71
|
+
const syncOutcome = await waitForCompletionOrStop(client.syncToTip(), stopWatcher);
|
|
72
|
+
if (syncOutcome.kind === "stopped") {
|
|
73
|
+
return syncOutcome.code;
|
|
74
|
+
}
|
|
75
|
+
const result = syncOutcome.value;
|
|
76
|
+
if (result.endingHeight !== null && result.endingHeight === result.bestHeight) {
|
|
77
|
+
stopWatcher.cleanup();
|
|
78
|
+
const detachPromise = typeof client.detachToBackgroundFollow === "function"
|
|
79
|
+
? client.detachToBackgroundFollow()
|
|
80
|
+
: Promise.resolve();
|
|
81
|
+
try {
|
|
82
|
+
await detachPromise;
|
|
83
|
+
await client.close();
|
|
84
|
+
clientClosed = true;
|
|
85
|
+
writeLine(options.context.stderr, "Detached cleanly; background indexer follow resumed.");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
writeLine(options.context.stderr, "Detach failed before background indexer follow was confirmed.");
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error("Managed sync did not reach the current Bitcoin tip.");
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
stopWatcher.cleanup();
|
|
97
|
+
if (!clientClosed) {
|
|
98
|
+
await client.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
23
101
|
}
|
|
24
102
|
finally {
|
|
25
|
-
|
|
103
|
+
if (storeOwned && store !== null) {
|
|
104
|
+
await store.close().catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
await controlLock?.release().catch(() => undefined);
|
|
26
107
|
}
|
|
27
108
|
}
|
|
28
109
|
export async function runMiningRuntimeCommand(parsed, context) {
|
|
@@ -30,17 +111,26 @@ export async function runMiningRuntimeCommand(parsed, context) {
|
|
|
30
111
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
31
112
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
32
113
|
const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
|
|
33
|
-
await context.ensureDirectory(dirname(dbPath));
|
|
34
114
|
if (parsed.command === "mine") {
|
|
35
115
|
const prompter = context.createPrompter();
|
|
36
116
|
const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, prompter);
|
|
37
|
-
await
|
|
117
|
+
await ensureMiningProviderSetup({
|
|
118
|
+
context,
|
|
119
|
+
provider,
|
|
120
|
+
prompter,
|
|
121
|
+
runtimePaths,
|
|
122
|
+
});
|
|
123
|
+
const preflightCode = await syncManagedMiningReadiness({
|
|
124
|
+
parsed,
|
|
38
125
|
context,
|
|
39
126
|
dataDir,
|
|
40
127
|
databasePath: dbPath,
|
|
41
128
|
provider,
|
|
42
129
|
runtimePaths,
|
|
43
130
|
});
|
|
131
|
+
if (preflightCode !== null) {
|
|
132
|
+
return preflightCode;
|
|
133
|
+
}
|
|
44
134
|
const abortController = new AbortController();
|
|
45
135
|
const onStop = () => {
|
|
46
136
|
abortController.abort();
|
|
@@ -57,6 +147,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
|
|
|
57
147
|
stdout: context.stdout,
|
|
58
148
|
stderr: context.stderr,
|
|
59
149
|
progressOutput: parsed.progressOutput,
|
|
150
|
+
builtInSetupEnsured: true,
|
|
60
151
|
paths: runtimePaths,
|
|
61
152
|
});
|
|
62
153
|
}
|
|
@@ -69,18 +160,29 @@ export async function runMiningRuntimeCommand(parsed, context) {
|
|
|
69
160
|
if (parsed.command === "mine-start") {
|
|
70
161
|
const prompter = createCommandPrompter(parsed, context);
|
|
71
162
|
const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, prompter);
|
|
72
|
-
await
|
|
163
|
+
await ensureMiningProviderSetup({
|
|
164
|
+
context,
|
|
165
|
+
provider,
|
|
166
|
+
prompter,
|
|
167
|
+
runtimePaths,
|
|
168
|
+
});
|
|
169
|
+
const preflightCode = await syncManagedMiningReadiness({
|
|
170
|
+
parsed,
|
|
73
171
|
context,
|
|
74
172
|
dataDir,
|
|
75
173
|
databasePath: dbPath,
|
|
76
174
|
provider,
|
|
77
175
|
runtimePaths,
|
|
78
176
|
});
|
|
177
|
+
if (preflightCode !== null) {
|
|
178
|
+
return preflightCode;
|
|
179
|
+
}
|
|
79
180
|
const result = await context.startBackgroundMining({
|
|
80
181
|
dataDir,
|
|
81
182
|
databasePath: dbPath,
|
|
82
183
|
provider,
|
|
83
184
|
prompter,
|
|
185
|
+
builtInSetupEnsured: true,
|
|
84
186
|
paths: runtimePaths,
|
|
85
187
|
});
|
|
86
188
|
if (parsed.outputMode === "preview-json") {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { formatManagedSyncErrorMessage } from "../../bitcoind/errors.js";
|
|
3
|
-
import { formatBytes, formatDuration } from "../../bitcoind/progress/formatting.js";
|
|
4
3
|
import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
|
|
5
4
|
import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
|
|
6
5
|
import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
|
|
@@ -8,8 +7,8 @@ import { usesTtyProgress, writeLine } from "../io.js";
|
|
|
8
7
|
import { classifyCliError, formatCliTextError } from "../output.js";
|
|
9
8
|
import { createTerminalPrompter } from "../prompt.js";
|
|
10
9
|
import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
|
|
10
|
+
import { createSyncProgressReporter } from "../sync-progress.js";
|
|
11
11
|
import { formatBalanceReport } from "../wallet-format.js";
|
|
12
|
-
const SYNC_PROGRESS_LOG_INTERVAL_MS = 5_000;
|
|
13
12
|
async function writePostSyncBalanceReport(options) {
|
|
14
13
|
const provider = withInteractiveWalletSecretProvider(options.context.walletSecretProvider, options.context.createPrompter?.() ?? createTerminalPrompter(options.context.stdin, options.context.stdout));
|
|
15
14
|
const readContext = await options.context.openWalletReadContext({
|
|
@@ -26,95 +25,6 @@ async function writePostSyncBalanceReport(options) {
|
|
|
26
25
|
await readContext.close().catch(() => undefined);
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
|
-
function createSyncProgressReporter(options) {
|
|
30
|
-
let lastPhase = null;
|
|
31
|
-
let lastMessage = "";
|
|
32
|
-
let lastDownloadPrintedAt = 0;
|
|
33
|
-
let lastDownloadBytes = null;
|
|
34
|
-
let lastImportPrintedAt = 0;
|
|
35
|
-
let lastImportBlocks = null;
|
|
36
|
-
const infoEnabled = options.progressOutput !== "none";
|
|
37
|
-
function shouldPrintEntryMessage(message, phase) {
|
|
38
|
-
if (message === "Waiting to start managed sync." || message === "Sync complete.") {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
if (message.startsWith("Warning:")) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
if (!infoEnabled) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
if (phase === "getblock_archive_download" || phase === "getblock_archive_import") {
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
return phase === "snapshot_download"
|
|
51
|
-
|| phase === "wait_headers_for_snapshot"
|
|
52
|
-
|| phase === "load_snapshot"
|
|
53
|
-
|| phase === "bitcoin_sync"
|
|
54
|
-
|| phase === "cogcoin_sync"
|
|
55
|
-
|| message.includes("Getblock manifest")
|
|
56
|
-
|| message.startsWith("Fetching Getblock manifest.")
|
|
57
|
-
|| message.startsWith("Refreshing Getblock manifest.")
|
|
58
|
-
|| message.startsWith("Using Getblock range ");
|
|
59
|
-
}
|
|
60
|
-
function formatDownloadLine(label, event) {
|
|
61
|
-
const current = event.progress.downloadedBytes ?? 0;
|
|
62
|
-
const total = event.progress.totalBytes ?? 0;
|
|
63
|
-
const percent = event.progress.percent ?? (total > 0 ? (current / total) * 100 : 0);
|
|
64
|
-
const speed = event.progress.bytesPerSecond === null ? "--" : `${formatBytes(event.progress.bytesPerSecond)}/s`;
|
|
65
|
-
return `${label}: ${percent.toFixed(2)}% (${formatBytes(current)} / ${formatBytes(total)}, ${speed}, ETA ${formatDuration(event.progress.etaSeconds)})`;
|
|
66
|
-
}
|
|
67
|
-
return (event) => {
|
|
68
|
-
const message = event.progress.message.trim();
|
|
69
|
-
const phaseChanged = event.phase !== lastPhase;
|
|
70
|
-
const messageChanged = message !== lastMessage;
|
|
71
|
-
if ((phaseChanged || messageChanged) && shouldPrintEntryMessage(message, event.phase)) {
|
|
72
|
-
options.write(message);
|
|
73
|
-
}
|
|
74
|
-
if (infoEnabled && event.phase === "getblock_archive_download") {
|
|
75
|
-
const now = Date.now();
|
|
76
|
-
const currentBytes = event.progress.downloadedBytes ?? 0;
|
|
77
|
-
const isComplete = (event.progress.percent ?? 0) >= 100;
|
|
78
|
-
const shouldPrintMilestone = phaseChanged
|
|
79
|
-
|| lastDownloadBytes !== currentBytes && (isComplete
|
|
80
|
-
|| now - lastDownloadPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
|
|
81
|
-
if (shouldPrintMilestone) {
|
|
82
|
-
options.write(formatDownloadLine("Getblock download", event));
|
|
83
|
-
lastDownloadPrintedAt = now;
|
|
84
|
-
lastDownloadBytes = currentBytes;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else if (infoEnabled && event.phase === "snapshot_download") {
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
const currentBytes = event.progress.downloadedBytes ?? 0;
|
|
90
|
-
const isComplete = (event.progress.percent ?? 0) >= 100;
|
|
91
|
-
const shouldPrintMilestone = phaseChanged
|
|
92
|
-
|| lastDownloadBytes !== currentBytes && (isComplete
|
|
93
|
-
|| now - lastDownloadPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
|
|
94
|
-
if (shouldPrintMilestone) {
|
|
95
|
-
options.write(formatDownloadLine("Snapshot download", event));
|
|
96
|
-
lastDownloadPrintedAt = now;
|
|
97
|
-
lastDownloadBytes = currentBytes;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (infoEnabled && event.phase === "getblock_archive_import") {
|
|
101
|
-
const now = Date.now();
|
|
102
|
-
const currentBlocks = event.progress.blocks ?? 0;
|
|
103
|
-
const targetBlocks = event.progress.targetHeight ?? currentBlocks;
|
|
104
|
-
const isComplete = currentBlocks >= targetBlocks;
|
|
105
|
-
const shouldPrintMilestone = phaseChanged
|
|
106
|
-
|| lastImportBlocks !== currentBlocks && (isComplete
|
|
107
|
-
|| now - lastImportPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
|
|
108
|
-
if (shouldPrintMilestone) {
|
|
109
|
-
options.write(`Getblock import: Bitcoin ${currentBlocks.toLocaleString()} / ${targetBlocks.toLocaleString()}`);
|
|
110
|
-
lastImportPrintedAt = now;
|
|
111
|
-
lastImportBlocks = currentBlocks;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
lastPhase = event.phase;
|
|
115
|
-
lastMessage = message;
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
28
|
export async function runSyncCommand(parsed, context) {
|
|
119
29
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
120
30
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|