@cogcoin/client 1.1.14 → 1.1.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.14` 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.
3
+ `@cogcoin/client@1.1.16` 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
 
@@ -12,11 +12,6 @@ Install Cogcoin:
12
12
  curl -fsSL https://cogcoin.org/install.sh | bash
13
13
  # or on Windows PowerShell:
14
14
  powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://cogcoin.org/install.ps1 | iex"
15
-
16
- # The installer provisions and uses a Cogcoin-managed Node.js runtime.
17
- # On macOS, Homebrew is only used if it is already installed and bootstrap tools are missing.
18
- # `cogcoin init` starts automatically in an interactive terminal and continues into sync.
19
- # If macOS Command Line Tools are still installing, the installer prints an exact resume command.
20
15
  cogcoin address # Send 0.0015 BTC to address
21
16
  cogcoin register <domainname> # 6+ character domain for 0.001 BTC
22
17
  cogcoin anchor <domainname> # You can leave a founding message permanently on Bitcoin!
@@ -24,6 +19,20 @@ cogcoin mine setup
24
19
  cogcoin mine # Use remaining ~0.0005 BTC for mining tx, ~1000 sats per entry (0.00001 BTC)
25
20
  ```
26
21
 
22
+ ### What The Installer Does
23
+
24
+ - Installs and uses a Cogcoin-managed Node.js runtime.
25
+ - Updates PATH for future shells so the managed `cogcoin` command stays available.
26
+ - Installs `@cogcoin/client` into a Cogcoin-managed global npm prefix.
27
+ - Starts `cogcoin init` automatically only when the installer is running in an interactive terminal.
28
+
29
+ ### If The Installer Pauses Or Exits Early
30
+
31
+ - On macOS, Homebrew is only used if it is already installed and bootstrap tools are missing.
32
+ - If the installer is noninteractive, it finishes by printing one exact `cogcoin init` follow-up command.
33
+ - If you set `COGCOIN_SKIP_INIT=1`, the installer skips `cogcoin init` and prints the exact manual command to run later.
34
+ - If macOS Command Line Tools are still installing, the installer either waits and retries automatically or prints the exact resume command.
35
+
27
36
  ## Preview
28
37
 
29
38
  ```bash
@@ -1,5 +1,6 @@
1
1
  import type { ManagedIndexerDaemonProbeResult } from "../managed-runtime/types.js";
2
2
  import type { ManagedIndexerDaemonObservedStatus } from "../types.js";
3
+ import { resolveManagedServicePaths } from "../service-paths.js";
3
4
  import type { CoherentIndexerSnapshotLease, IndexerDaemonClient, IndexerDaemonStopResult, ManagedIndexerDaemonServiceLifetime } from "./types.js";
4
5
  export type { IndexerDaemonCompatibility } from "../managed-runtime/types.js";
5
6
  export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
@@ -8,6 +9,11 @@ export declare function probeIndexerDaemon(options: {
8
9
  dataDir: string;
9
10
  walletRootId?: string;
10
11
  }): Promise<IndexerDaemonProbeResult>;
12
+ export declare function probeIndexerDaemonForStart(options: {
13
+ dataDir: string;
14
+ walletRootId: string;
15
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
16
+ }): Promise<IndexerDaemonProbeResult>;
11
17
  export declare function readSnapshotWithRetry(daemon: IndexerDaemonClient, expectedWalletRootId: string): Promise<CoherentIndexerSnapshotLease>;
12
18
  export declare function readObservedIndexerDaemonStatus(options: {
13
19
  dataDir: string;
@@ -7,7 +7,8 @@ import { attachOrStartManagedIndexerRuntime } from "../managed-runtime/indexer-r
7
7
  import { readJsonFileIfPresent } from "../managed-runtime/status.js";
8
8
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
9
9
  import { createIndexerDaemonClient, probeIndexerDaemonAtSocket, } from "./client.js";
10
- import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
10
+ import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, clearIndexerDaemonRuntimeArtifacts, isIndexerDaemonProcessAlive, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
11
+ import { openIndexerDaemonStartupLog, recordIndexerDaemonStartupFailure, waitForIndexerDaemonStartup, } from "./startup.js";
11
12
  export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
12
13
  const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
13
14
  export async function probeIndexerDaemon(options) {
@@ -15,6 +16,25 @@ export async function probeIndexerDaemon(options) {
15
16
  const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
16
17
  return probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
17
18
  }
19
+ export async function probeIndexerDaemonForStart(options) {
20
+ const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, options.walletRootId);
21
+ const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, options.walletRootId);
22
+ if (probe.compatibility === "compatible") {
23
+ return probe;
24
+ }
25
+ const status = await readJsonFileIfPresent(paths.indexerDaemonStatusPath).catch(() => null);
26
+ const processId = status?.processId ?? null;
27
+ if (processId !== null && !await isIndexerDaemonProcessAlive(processId)) {
28
+ await clearIndexerDaemonRuntimeArtifacts(paths);
29
+ return {
30
+ compatibility: "unreachable",
31
+ status: null,
32
+ client: null,
33
+ error: null,
34
+ };
35
+ }
36
+ return probe;
37
+ }
18
38
  export async function readSnapshotWithRetry(daemon, expectedWalletRootId) {
19
39
  let lastError = null;
20
40
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -69,14 +89,16 @@ export async function attachOrStartIndexerDaemon(options) {
69
89
  const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
70
90
  const startDaemon = async () => {
71
91
  await mkdir(paths.indexerServiceRoot, { recursive: true });
92
+ const startupLog = await openIndexerDaemonStartupLog(paths);
72
93
  const daemonEntryPath = fileURLToPath(new URL("../indexer-daemon-main.js", import.meta.url));
94
+ const stdio = ["ignore", startupLog.fd, startupLog.fd];
73
95
  const spawnOptions = serviceLifetime === "ephemeral"
74
96
  ? {
75
- stdio: "ignore",
97
+ stdio,
76
98
  }
77
99
  : {
78
100
  detached: true,
79
- stdio: "ignore",
101
+ stdio,
80
102
  };
81
103
  const child = spawn(process.execPath, [
82
104
  daemonEntryPath,
@@ -86,11 +108,16 @@ export async function attachOrStartIndexerDaemon(options) {
86
108
  ], {
87
109
  ...spawnOptions,
88
110
  });
111
+ await startupLog.close().catch(() => undefined);
89
112
  if (serviceLifetime !== "ephemeral") {
90
113
  child.unref();
91
114
  }
92
115
  try {
93
- await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
116
+ await waitForIndexerDaemonStartup({
117
+ child,
118
+ logPath: startupLog.logPath,
119
+ waitForReady: () => waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs),
120
+ });
94
121
  }
95
122
  catch (error) {
96
123
  if (child.pid !== undefined) {
@@ -101,6 +128,13 @@ export async function attachOrStartIndexerDaemon(options) {
101
128
  // ignore shutdown failures while unwinding startup errors
102
129
  }
103
130
  }
131
+ await recordIndexerDaemonStartupFailure({
132
+ paths,
133
+ walletRootId,
134
+ binaryVersion: expectedBinaryVersion ?? "0.0.0",
135
+ lastError: error instanceof Error ? error.message : String(error),
136
+ processId: child.pid ?? null,
137
+ }).catch(() => undefined);
104
138
  throw error;
105
139
  }
106
140
  return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
@@ -122,7 +156,11 @@ export async function attachOrStartIndexerDaemon(options) {
122
156
  expectedBinaryVersion,
123
157
  }, {
124
158
  getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
125
- probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonAtSocket(runtimePaths.indexerDaemonSocketPath, runtimeOptions.walletRootId),
159
+ probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonForStart({
160
+ dataDir: runtimeOptions.dataDir,
161
+ walletRootId: runtimeOptions.walletRootId,
162
+ paths: runtimePaths,
163
+ }),
126
164
  requestBackgroundFollow,
127
165
  closeClient: async (client) => {
128
166
  await client.close();
@@ -0,0 +1,7 @@
1
+ import { loadBetterSqlite3 } from "../../sqlite/better-sqlite3.js";
2
+ export declare const SQLITE_NATIVE_MODULE_UNAVAILABLE = "sqlite_native_module_unavailable";
3
+ type LoadBetterSqlite3 = typeof loadBetterSqlite3;
4
+ export declare function isSqliteNativeModuleLoadFailure(error: unknown): boolean;
5
+ export declare function createSqliteNativeModuleUnavailableError(error: unknown): Error;
6
+ export declare function assertIndexerDaemonNativeDependencies(loadBetterSqlite3Impl?: LoadBetterSqlite3): Promise<void>;
7
+ export {};
@@ -0,0 +1,27 @@
1
+ import { loadBetterSqlite3 } from "../../sqlite/better-sqlite3.js";
2
+ export const SQLITE_NATIVE_MODULE_UNAVAILABLE = "sqlite_native_module_unavailable";
3
+ export function isSqliteNativeModuleLoadFailure(error) {
4
+ if (!(error instanceof Error)) {
5
+ return false;
6
+ }
7
+ const message = error.message;
8
+ return message.includes("NODE_MODULE_VERSION")
9
+ || message.includes("better_sqlite3.node")
10
+ || message.includes("Cannot find module 'better-sqlite3'")
11
+ || message.includes('Cannot find module "better-sqlite3"');
12
+ }
13
+ export function createSqliteNativeModuleUnavailableError(error) {
14
+ const detail = error instanceof Error ? error.message : String(error);
15
+ return new Error(`${SQLITE_NATIVE_MODULE_UNAVAILABLE}: ${detail}`, { cause: error });
16
+ }
17
+ export async function assertIndexerDaemonNativeDependencies(loadBetterSqlite3Impl = loadBetterSqlite3) {
18
+ try {
19
+ await loadBetterSqlite3Impl();
20
+ }
21
+ catch (error) {
22
+ if (isSqliteNativeModuleLoadFailure(error)) {
23
+ throw createSqliteNativeModuleUnavailableError(error);
24
+ }
25
+ throw error;
26
+ }
27
+ }
@@ -5,6 +5,7 @@ import { createBootstrapProgress } from "../progress/formatting.js";
5
5
  import { resolveManagedServicePaths } from "../service-paths.js";
6
6
  import { UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
7
7
  import { pauseBackgroundFollow, recordBackgroundFollowFailure, resumeBackgroundFollow, withTimeout, } from "./background-follow.js";
8
+ import { assertIndexerDaemonNativeDependencies } from "./native-dependencies.js";
8
9
  import { createIndexerDaemonServer } from "./server.js";
9
10
  import { buildIndexerDaemonStatus, deriveIndexerDaemonLeaseState, observeIndexerAppliedTip, readCoreTipStatus, refreshIndexerDaemonStatus, writeIndexerDaemonStatus, } from "./status.js";
10
11
  import { closeSnapshotLease, createSnapshotHandle, loadSnapshotMaterial, pruneExpiredSnapshotLeases, readSnapshotLease, storeSnapshotLease, } from "./snapshot-leases.js";
@@ -152,6 +153,25 @@ export function createIndexerDaemonRuntime(options) {
152
153
  return;
153
154
  }
154
155
  await mkdir(paths.indexerServiceRoot, { recursive: true });
156
+ try {
157
+ await assertIndexerDaemonNativeDependencies();
158
+ }
159
+ catch (error) {
160
+ const now = Date.now();
161
+ state.state = "failed";
162
+ state.lastError = error instanceof Error ? error.message : String(error);
163
+ state.heartbeatAtUnixMs = now;
164
+ state.updatedAtUnixMs = now;
165
+ state.bootstrapPhase = "error";
166
+ state.bootstrapProgress = {
167
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
168
+ message: state.lastError,
169
+ lastError: state.lastError,
170
+ updatedAt: now,
171
+ };
172
+ await writeStatus().catch(() => undefined);
173
+ throw error;
174
+ }
155
175
  await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
156
176
  server = createIndexerDaemonServer({
157
177
  getStatus: () => buildIndexerDaemonStatus(state),
@@ -0,0 +1,44 @@
1
+ import type { ManagedServicePaths } from "../service-paths.js";
2
+ type ChildProcessLike = {
3
+ pid?: number;
4
+ once(event: "error", listener: (error: Error) => void): unknown;
5
+ once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
6
+ off(event: "error", listener: (error: Error) => void): unknown;
7
+ off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
8
+ };
9
+ export declare class IndexerDaemonStartupError extends Error {
10
+ readonly logPath: string;
11
+ readonly logTail: string | null;
12
+ readonly exitCode: number | null;
13
+ readonly signal: NodeJS.Signals | null;
14
+ constructor(message: string, options: {
15
+ logPath: string;
16
+ logTail?: string | null;
17
+ exitCode?: number | null;
18
+ signal?: NodeJS.Signals | null;
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ export declare function getIndexerDaemonStartupLogPath(error: unknown): string | null;
23
+ export declare function getIndexerDaemonStartupLogTail(error: unknown): string | null;
24
+ export declare function openIndexerDaemonStartupLog(paths: ManagedServicePaths): Promise<{
25
+ fd: number;
26
+ logPath: string;
27
+ close(): Promise<void>;
28
+ }>;
29
+ export declare function readIndexerDaemonStartupLogTail(logPath: string, maxBytes?: number): Promise<string | null>;
30
+ export declare function classifyIndexerDaemonStartupFailure(logTail: string | null): string;
31
+ export declare function waitForIndexerDaemonStartup(options: {
32
+ child: ChildProcessLike;
33
+ logPath: string;
34
+ waitForReady(): Promise<void>;
35
+ readLogTail?: (logPath: string) => Promise<string | null>;
36
+ }): Promise<void>;
37
+ export declare function recordIndexerDaemonStartupFailure(options: {
38
+ paths: ManagedServicePaths;
39
+ walletRootId: string;
40
+ binaryVersion: string;
41
+ lastError: string;
42
+ processId?: number | null;
43
+ }): Promise<void>;
44
+ export {};
@@ -0,0 +1,152 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rm } from "node:fs/promises";
3
+ import { writeRuntimeStatusFile } from "../../wallet/fs/status-file.js";
4
+ import { readJsonFileIfPresent } from "../managed-runtime/status.js";
5
+ import { SQLITE_NATIVE_MODULE_UNAVAILABLE } from "./native-dependencies.js";
6
+ import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "../types.js";
7
+ const STARTUP_LOG_TAIL_BYTES = 4_096;
8
+ export class IndexerDaemonStartupError extends Error {
9
+ logPath;
10
+ logTail;
11
+ exitCode;
12
+ signal;
13
+ constructor(message, options) {
14
+ super(message, { cause: options.cause });
15
+ this.name = "IndexerDaemonStartupError";
16
+ this.logPath = options.logPath;
17
+ this.logTail = options.logTail ?? null;
18
+ this.exitCode = options.exitCode ?? null;
19
+ this.signal = options.signal ?? null;
20
+ }
21
+ }
22
+ export function getIndexerDaemonStartupLogPath(error) {
23
+ return error instanceof IndexerDaemonStartupError ? error.logPath : null;
24
+ }
25
+ export function getIndexerDaemonStartupLogTail(error) {
26
+ return error instanceof IndexerDaemonStartupError ? error.logTail : null;
27
+ }
28
+ export async function openIndexerDaemonStartupLog(paths) {
29
+ await mkdir(paths.indexerServiceRoot, { recursive: true });
30
+ const handle = await open(paths.indexerDaemonLogPath, "w", 0o600);
31
+ await handle.writeFile([
32
+ `Cogcoin indexer daemon startup log`,
33
+ `pid=${process.pid}`,
34
+ `node=${process.version}`,
35
+ `time=${new Date().toISOString()}`,
36
+ "",
37
+ ].join("\n"));
38
+ return {
39
+ fd: handle.fd,
40
+ logPath: paths.indexerDaemonLogPath,
41
+ async close() {
42
+ await handle.close();
43
+ },
44
+ };
45
+ }
46
+ export async function readIndexerDaemonStartupLogTail(logPath, maxBytes = STARTUP_LOG_TAIL_BYTES) {
47
+ try {
48
+ const bytes = await readFile(logPath);
49
+ const tail = bytes.subarray(Math.max(0, bytes.byteLength - maxBytes));
50
+ const text = tail.toString("utf8").trim();
51
+ return text.length > 0 ? text : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export function classifyIndexerDaemonStartupFailure(logTail) {
58
+ return logTail?.includes(SQLITE_NATIVE_MODULE_UNAVAILABLE) === true
59
+ ? SQLITE_NATIVE_MODULE_UNAVAILABLE
60
+ : "indexer_daemon_start_failed";
61
+ }
62
+ export async function waitForIndexerDaemonStartup(options) {
63
+ const readLogTail = options.readLogTail ?? readIndexerDaemonStartupLogTail;
64
+ const ready = options.waitForReady();
65
+ const childFailure = new Promise((_, reject) => {
66
+ const onError = async (error) => {
67
+ const logTail = await readLogTail(options.logPath);
68
+ reject(new IndexerDaemonStartupError(classifyIndexerDaemonStartupFailure(logTail), {
69
+ logPath: options.logPath,
70
+ logTail,
71
+ cause: error,
72
+ }));
73
+ };
74
+ const onExit = async (code, signal) => {
75
+ const logTail = await readLogTail(options.logPath);
76
+ reject(new IndexerDaemonStartupError(classifyIndexerDaemonStartupFailure(logTail), {
77
+ logPath: options.logPath,
78
+ logTail,
79
+ exitCode: code,
80
+ signal,
81
+ }));
82
+ };
83
+ options.child.once("error", onError);
84
+ options.child.once("exit", onExit);
85
+ ready.then(() => {
86
+ options.child.off("error", onError);
87
+ options.child.off("exit", onExit);
88
+ }, () => {
89
+ options.child.off("error", onError);
90
+ options.child.off("exit", onExit);
91
+ });
92
+ });
93
+ try {
94
+ await Promise.race([
95
+ ready,
96
+ childFailure,
97
+ ]);
98
+ }
99
+ catch (error) {
100
+ if (error instanceof IndexerDaemonStartupError) {
101
+ throw error;
102
+ }
103
+ const logTail = await readLogTail(options.logPath);
104
+ throw new IndexerDaemonStartupError(error instanceof Error ? error.message : "indexer_daemon_start_timeout", {
105
+ logPath: options.logPath,
106
+ logTail,
107
+ cause: error,
108
+ });
109
+ }
110
+ }
111
+ export async function recordIndexerDaemonStartupFailure(options) {
112
+ await rm(options.paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
113
+ await mkdir(options.paths.indexerServiceRoot, { recursive: true });
114
+ const existing = await readJsonFileIfPresent(options.paths.indexerDaemonStatusPath).catch(() => null);
115
+ if (existing?.processId === (options.processId ?? null)
116
+ && (existing.state === "schema-mismatch"
117
+ || (existing.state === "failed" && existing.lastError !== null))) {
118
+ return;
119
+ }
120
+ const now = Date.now();
121
+ const status = {
122
+ serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
123
+ binaryVersion: options.binaryVersion,
124
+ buildId: null,
125
+ updatedAtUnixMs: now,
126
+ walletRootId: options.walletRootId,
127
+ daemonInstanceId: randomUUID(),
128
+ schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
129
+ state: "failed",
130
+ processId: options.processId ?? null,
131
+ startedAtUnixMs: now,
132
+ heartbeatAtUnixMs: now,
133
+ ipcReady: false,
134
+ rpcReachable: false,
135
+ coreBestHeight: null,
136
+ coreBestHash: null,
137
+ appliedTipHeight: null,
138
+ appliedTipHash: null,
139
+ snapshotSeq: null,
140
+ backlogBlocks: null,
141
+ reorgDepth: null,
142
+ lastAppliedAtUnixMs: null,
143
+ activeSnapshotCount: 0,
144
+ lastError: options.lastError,
145
+ backgroundFollowActive: false,
146
+ bootstrapPhase: "error",
147
+ bootstrapProgress: null,
148
+ cogcoinSyncHeight: null,
149
+ cogcoinSyncTargetHeight: null,
150
+ };
151
+ await writeRuntimeStatusFile(options.paths.indexerDaemonStatusPath, status);
152
+ }
@@ -115,6 +115,16 @@ function mapIndexerStartupError(message) {
115
115
  health: "starting",
116
116
  message: "Indexer daemon is still starting.",
117
117
  };
118
+ case "indexer_daemon_start_failed":
119
+ return {
120
+ health: "failed",
121
+ message: "The managed indexer daemon exited before it opened its local IPC socket.",
122
+ };
123
+ case "sqlite_native_module_unavailable":
124
+ return {
125
+ health: "failed",
126
+ message: "The managed indexer daemon could not load its SQLite native module.",
127
+ };
118
128
  case "indexer_daemon_service_version_mismatch":
119
129
  return {
120
130
  health: "service-version-mismatch",
@@ -15,5 +15,6 @@ export interface ManagedServicePaths {
15
15
  indexerDaemonLockPath: string;
16
16
  indexerDaemonStatusPath: string;
17
17
  indexerDaemonSocketPath: string;
18
+ indexerDaemonLogPath: string;
18
19
  }
19
20
  export declare function resolveManagedServicePaths(dataDir: string, walletRootId?: string): ManagedServicePaths;
@@ -41,5 +41,6 @@ export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED
41
41
  indexerDaemonLockPath: join(walletRuntimeRoot, "indexer-daemon.lock"),
42
42
  indexerDaemonStatusPath: join(indexerServiceRoot, "status.json"),
43
43
  indexerDaemonSocketPath: resolveIndexerDaemonSocketPath(serviceRootId),
44
+ indexerDaemonLogPath: join(indexerServiceRoot, "daemon.log"),
44
45
  };
45
46
  }
@@ -1,5 +1,19 @@
1
+ import { getIndexerDaemonStartupLogPath, getIndexerDaemonStartupLogTail, } from "../../../bitcoind/indexer-daemon/startup.js";
2
+ function formatIndexerStartupDetail(error) {
3
+ const logPath = getIndexerDaemonStartupLogPath(error);
4
+ const logTail = getIndexerDaemonStartupLogTail(error);
5
+ if (logPath === null && logTail === null) {
6
+ return null;
7
+ }
8
+ const tail = logTail === null
9
+ ? null
10
+ : logTail.replace(/\s+/g, " ").slice(0, 300);
11
+ return tail === null
12
+ ? `Startup log: ${logPath}.`
13
+ : `Startup log: ${logPath}. Last output: ${tail}`;
14
+ }
1
15
  export const serviceErrorRules = [
2
- ({ errorCode }) => {
16
+ ({ errorCode, error }) => {
3
17
  if (errorCode.endsWith("_requires_tty") && errorCode !== "cli_update_requires_tty") {
4
18
  return {
5
19
  what: "Interactive terminal input is required.",
@@ -14,6 +28,30 @@ export const serviceErrorRules = [
14
28
  next: "Check `cogcoin status`, wait for services to settle, and retry. If the state stays degraded, run `cogcoin repair`.",
15
29
  };
16
30
  }
31
+ if (errorCode === "sqlite_native_module_unavailable") {
32
+ const detail = formatIndexerStartupDetail(error);
33
+ return {
34
+ what: "The managed indexer daemon could not load its SQLite native module.",
35
+ why: `The active Node runtime is ${process.version}, but the installed native sqlite dependency appears to be missing or built for a different Node ABI.${detail === null ? "" : ` ${detail}`}`,
36
+ next: "Use the supported Node runtime for this checkout, then run `npm rebuild better-sqlite3 zeromq` or reinstall dependencies and retry.",
37
+ };
38
+ }
39
+ if (errorCode === "indexer_daemon_start_failed") {
40
+ const detail = formatIndexerStartupDetail(error);
41
+ return {
42
+ what: "The managed indexer daemon exited before it opened its local IPC socket.",
43
+ why: detail ?? "The daemon process died during startup before Cogcoin could read a service status.",
44
+ next: "Run `cogcoin repair` to clear stale managed indexer artifacts. If this was a local checkout, verify the Node version and rebuild native dependencies.",
45
+ };
46
+ }
47
+ if (errorCode === "indexer_daemon_start_timeout") {
48
+ const detail = formatIndexerStartupDetail(error);
49
+ return {
50
+ what: "The managed indexer daemon stayed alive but did not open its local IPC socket in time.",
51
+ why: detail ?? "Cogcoin could not attach to the daemon before the startup deadline.",
52
+ next: "Run `cogcoin repair` if this persists. If this was a local checkout, verify the Node version and rebuild native dependencies.",
53
+ };
54
+ }
17
55
  if (errorCode === "indexer_daemon_background_follow_recovery_failed") {
18
56
  return {
19
57
  what: "The managed indexer daemon could not recover automatic background follow.",
@@ -43,6 +43,24 @@ export function createInsufficientFundsMiningPublishWaitingNote() {
43
43
  export function createInsufficientFundsMiningPublishErrorMessage() {
44
44
  return "Bitcoin Core could not fund the next mining publish with safe BTC.";
45
45
  }
46
+ function clearAutoReconciledMiningPublish(state, currentPublishDecision) {
47
+ return {
48
+ ...state,
49
+ miningState: {
50
+ ...clearMiningPublishState(state.miningState),
51
+ currentPublishDecision,
52
+ },
53
+ };
54
+ }
55
+ async function hasConfirmedWalletConflict(options) {
56
+ for (const txid of options.conflictTxids) {
57
+ const conflictTx = await options.rpc.getTransaction(options.walletName, txid).catch(() => null);
58
+ if (conflictTx !== null && conflictTx.confirmations > 0) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
46
64
  function createMiningFundingProbeCandidate(options) {
47
65
  const referencedBlockHashInternal = Buffer.from(displayToInternalBlockhash(options.referencedBlockHashDisplay), "hex");
48
66
  const bip39WordIndices = deriveMiningWordIndices(referencedBlockHashInternal, options.domain.domainId);
@@ -193,6 +211,9 @@ export async function reconcileLiveMiningState(options) {
193
211
  };
194
212
  const currentTxid = state.miningState.currentTxid;
195
213
  if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
214
+ if (state.miningState.state === "repair-required") {
215
+ state = clearAutoReconciledMiningPublish(state, "repair-auto-cleared-empty-publish");
216
+ }
196
217
  await reconcilePersistentPolicyLocks({
197
218
  rpc: options.rpc,
198
219
  walletName: state.managedCoreWallet.walletName,
@@ -266,6 +287,24 @@ export async function reconcileLiveMiningState(options) {
266
287
  };
267
288
  }
268
289
  if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
290
+ const confirmedConflict = await hasConfirmedWalletConflict({
291
+ rpc: options.rpc,
292
+ walletName,
293
+ conflictTxids: walletTx?.walletconflicts ?? [],
294
+ });
295
+ if (confirmedConflict) {
296
+ state = clearAutoReconciledMiningPublish(state, "repair-auto-cleared-confirmed-conflict");
297
+ await reconcilePersistentPolicyLocks({
298
+ rpc: options.rpc,
299
+ walletName: state.managedCoreWallet.walletName,
300
+ state,
301
+ fixedInputs: [],
302
+ });
303
+ return {
304
+ state,
305
+ recentWin: null,
306
+ };
307
+ }
269
308
  state = defaultMiningStatePatch(state, {
270
309
  state: "repair-required",
271
310
  pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",