@cogcoin/client 1.0.2 → 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.
Files changed (76) hide show
  1. package/README.md +3 -2
  2. package/dist/bitcoind/client/factory.d.ts +0 -8
  3. package/dist/bitcoind/client/factory.js +1 -59
  4. package/dist/bitcoind/client/managed-client.d.ts +1 -3
  5. package/dist/bitcoind/client/managed-client.js +3 -47
  6. package/dist/bitcoind/indexer-daemon-main.js +173 -28
  7. package/dist/bitcoind/indexer-daemon.d.ts +11 -3
  8. package/dist/bitcoind/indexer-daemon.js +123 -57
  9. package/dist/bitcoind/indexer-monitor.d.ts +12 -0
  10. package/dist/bitcoind/indexer-monitor.js +89 -0
  11. package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
  12. package/dist/bitcoind/progress/follow-scene.js +87 -4
  13. package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
  14. package/dist/bitcoind/progress/tty-renderer.js +2 -0
  15. package/dist/bitcoind/testing.d.ts +0 -1
  16. package/dist/bitcoind/testing.js +0 -1
  17. package/dist/bitcoind/types.d.ts +5 -2
  18. package/dist/cli/commands/follow.js +44 -49
  19. package/dist/cli/commands/mining-admin.js +56 -2
  20. package/dist/cli/commands/mining-read.js +43 -3
  21. package/dist/cli/commands/mining-runtime.js +91 -73
  22. package/dist/cli/commands/service-runtime.js +42 -2
  23. package/dist/cli/commands/status.js +3 -1
  24. package/dist/cli/commands/sync.js +50 -90
  25. package/dist/cli/commands/wallet-admin.js +21 -3
  26. package/dist/cli/commands/wallet-read.js +2 -0
  27. package/dist/cli/context.js +5 -1
  28. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  29. package/dist/cli/managed-indexer-observer.js +163 -0
  30. package/dist/cli/mining-format.d.ts +3 -1
  31. package/dist/cli/mining-format.js +35 -0
  32. package/dist/cli/mining-json.d.ts +11 -1
  33. package/dist/cli/mining-json.js +9 -0
  34. package/dist/cli/output.js +24 -0
  35. package/dist/cli/parse.d.ts +1 -1
  36. package/dist/cli/parse.js +23 -0
  37. package/dist/cli/read-json.d.ts +13 -1
  38. package/dist/cli/read-json.js +31 -0
  39. package/dist/cli/runner.js +4 -2
  40. package/dist/cli/signals.d.ts +12 -0
  41. package/dist/cli/signals.js +31 -13
  42. package/dist/cli/types.d.ts +8 -4
  43. package/dist/cli/update-service.d.ts +2 -12
  44. package/dist/cli/update-service.js +2 -68
  45. package/dist/semver.d.ts +12 -0
  46. package/dist/semver.js +68 -0
  47. package/dist/wallet/lifecycle.js +0 -6
  48. package/dist/wallet/mining/config.js +54 -3
  49. package/dist/wallet/mining/control.d.ts +5 -2
  50. package/dist/wallet/mining/control.js +153 -34
  51. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  52. package/dist/wallet/mining/domain-prompts.js +130 -0
  53. package/dist/wallet/mining/index.d.ts +2 -1
  54. package/dist/wallet/mining/index.js +1 -0
  55. package/dist/wallet/mining/runner.d.ts +58 -2
  56. package/dist/wallet/mining/runner.js +553 -331
  57. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  58. package/dist/wallet/mining/sentences.js +7 -4
  59. package/dist/wallet/mining/types.d.ts +26 -0
  60. package/dist/wallet/mining/visualizer.d.ts +3 -0
  61. package/dist/wallet/mining/visualizer.js +106 -12
  62. package/dist/wallet/read/context.d.ts +1 -0
  63. package/dist/wallet/read/context.js +15 -10
  64. package/dist/wallet/reset.js +0 -1
  65. package/dist/wallet/state/client-password-agent.js +4 -1
  66. package/dist/wallet/state/client-password.js +15 -8
  67. package/dist/wallet/tx/anchor.js +0 -1
  68. package/dist/wallet/tx/bitcoin-transfer.js +0 -1
  69. package/dist/wallet/tx/cog.js +0 -3
  70. package/dist/wallet/tx/common.js +1 -1
  71. package/dist/wallet/tx/domain-admin.js +0 -1
  72. package/dist/wallet/tx/domain-market.js +0 -3
  73. package/dist/wallet/tx/field.js +0 -1
  74. package/dist/wallet/tx/register.js +0 -1
  75. package/dist/wallet/tx/reputation.js +0 -1
  76. package/package.json +1 -1
@@ -1,83 +1,78 @@
1
1
  import { dirname } from "node:path";
2
- import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
2
+ import { DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting } from "../../bitcoind/bootstrap.js";
3
3
  import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
4
+ import { followManagedIndexerStatus, ManagedIndexerProgressObserver, } from "../managed-indexer-observer.js";
4
5
  import { usesTtyProgress, writeLine } from "../io.js";
5
- import { classifyCliError, formatCliTextError } from "../output.js";
6
- import { createStopSignalWatcher } from "../signals.js";
6
+ import { classifyCliError } from "../output.js";
7
+ import { createCloseSignalWatcher, waitForCompletionOrStop } from "../signals.js";
7
8
  export async function runFollowCommand(parsed, context) {
8
9
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
9
10
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
11
+ const packageVersion = await context.readPackageVersion();
10
12
  const runtimePaths = context.resolveWalletRuntimePaths();
11
- let controlLock = null;
12
- let store = null;
13
- let storeOwned = true;
13
+ let monitor = null;
14
+ let observer = null;
14
15
  try {
15
16
  const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
16
17
  paths: runtimePaths,
17
18
  provider: context.walletSecretProvider,
18
19
  loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
19
20
  });
20
- try {
21
- controlLock = await acquireFileLock(runtimePaths.walletControlLockPath, {
22
- purpose: "managed-follow",
23
- walletRootId: walletRoot.walletRootId,
24
- });
25
- }
26
- catch (error) {
27
- if (error instanceof FileLockBusyError) {
28
- throw new Error("wallet_control_lock_busy");
29
- }
30
- throw error;
31
- }
32
21
  await context.ensureDirectory(dirname(dbPath));
33
- store = await context.openSqliteStore({ filename: dbPath });
34
- const client = await context.openManagedBitcoindClient({
35
- store,
36
- databasePath: dbPath,
22
+ monitor = await context.openManagedIndexerMonitor({
37
23
  dataDir,
24
+ databasePath: dbPath,
38
25
  walletRootId: walletRoot.walletRootId,
26
+ expectedBinaryVersion: packageVersion,
27
+ });
28
+ observer = new ManagedIndexerProgressObserver({
29
+ quoteStatePath: resolveBootstrapPathsForTesting(dataDir, DEFAULT_SNAPSHOT_METADATA).quoteStatePath,
30
+ stream: context.stderr,
39
31
  progressOutput: parsed.progressOutput,
32
+ followVisualMode: true,
33
+ });
34
+ const abortController = new AbortController();
35
+ const stopWatcher = createCloseSignalWatcher({
36
+ signalSource: context.signalSource,
37
+ stderr: context.stderr,
38
+ closeable: {
39
+ close: async () => {
40
+ abortController.abort(new Error("managed_indexer_follow_aborted"));
41
+ await observer?.close().catch(() => undefined);
42
+ await monitor?.close().catch(() => undefined);
43
+ },
44
+ },
45
+ forceExit: context.forceExit,
46
+ firstMessage: "Stopping managed Cogcoin tip observation...",
47
+ successMessage: "Stopped observing managed Cogcoin tip.",
48
+ failureMessage: "Managed Cogcoin tip observation cleanup failed.",
40
49
  });
41
- storeOwned = false;
42
- const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit, [runtimePaths.walletControlLockPath]);
43
50
  try {
44
- await client.startFollowingTip();
45
51
  if (!usesTtyProgress(parsed.progressOutput, context.stderr)) {
46
52
  writeLine(context.stdout, "Following managed Cogcoin tip. Press Ctrl-C to stop.");
47
53
  }
48
- return await stopWatcher.promise;
54
+ const followOutcome = await waitForCompletionOrStop(followManagedIndexerStatus({
55
+ monitor,
56
+ observer,
57
+ signal: abortController.signal,
58
+ }), stopWatcher);
59
+ if (followOutcome.kind === "stopped") {
60
+ return followOutcome.code;
61
+ }
62
+ return 0;
49
63
  }
50
64
  catch (error) {
51
65
  writeLine(context.stderr, `follow failed: ${error instanceof Error ? error.message : String(error)}`);
52
- await client.close().catch(() => undefined);
53
66
  return classifyCliError(error).exitCode;
54
67
  }
55
68
  finally {
56
69
  stopWatcher.cleanup();
70
+ await observer?.close().catch(() => undefined);
71
+ await monitor?.close().catch(() => undefined);
57
72
  }
58
73
  }
59
74
  catch (error) {
60
- const classified = classifyCliError(error);
61
- if (classified.errorCode === "wallet_control_lock_busy") {
62
- const formatted = formatCliTextError(error);
63
- if (formatted !== null) {
64
- for (const line of formatted) {
65
- writeLine(context.stderr, line);
66
- }
67
- }
68
- else {
69
- writeLine(context.stderr, classified.message);
70
- }
71
- }
72
- else {
73
- writeLine(context.stderr, `follow failed: ${error instanceof Error ? error.message : String(error)}`);
74
- }
75
- if (storeOwned && store !== null) {
76
- await store.close().catch(() => undefined);
77
- }
78
- return classified.exitCode;
79
- }
80
- finally {
81
- await controlLock?.release().catch(() => undefined);
75
+ writeLine(context.stderr, `follow failed: ${error instanceof Error ? error.message : String(error)}`);
76
+ return classifyCliError(error).exitCode;
82
77
  }
83
78
  }
@@ -1,9 +1,11 @@
1
- import { buildMineSetupData, } from "../mining-json.js";
1
+ import { dirname } from "node:path";
2
+ import { buildMinePromptData, buildMineSetupData, } from "../mining-json.js";
2
3
  import { buildMineSetupPreviewData } from "../preview-json.js";
4
+ import { formatMiningPromptMutationReport } from "../mining-format.js";
3
5
  import { writeLine } from "../io.js";
4
6
  import { createTerminalPrompter } from "../prompt.js";
5
7
  import { createPreviewSuccessEnvelope, createMutationSuccessEnvelope, describeCanonicalCommand, resolvePreviewJsonSchema, resolveStableMiningControlJsonSchema, writeHandledCliError, writeJsonValue, } from "../output.js";
6
- import { formatNextStepLines, getMineSetupNextSteps, } from "../workflow-hints.js";
8
+ import { formatNextStepLines, getMineSetupNextSteps } from "../workflow-hints.js";
7
9
  import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
8
10
  function createCommandPrompter(parsed, context) {
9
11
  return parsed.outputMode !== "text"
@@ -13,6 +15,9 @@ function createCommandPrompter(parsed, context) {
13
15
  export async function runMiningAdminCommand(parsed, context) {
14
16
  try {
15
17
  const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
18
+ const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
19
+ const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
20
+ const packageVersion = await context.readPackageVersion();
16
21
  if (parsed.command === "mine-setup") {
17
22
  const prompter = createCommandPrompter(parsed, context);
18
23
  const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, prompter);
@@ -50,6 +55,55 @@ export async function runMiningAdminCommand(parsed, context) {
50
55
  }
51
56
  return 0;
52
57
  }
58
+ if (parsed.command === "mine-prompt") {
59
+ const prompter = createCommandPrompter(parsed, context);
60
+ if (!prompter.isInteractive) {
61
+ throw new Error("mine_prompt_requires_tty");
62
+ }
63
+ const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, prompter);
64
+ await context.ensureDirectory(dirname(dbPath));
65
+ const readContext = await context.openWalletReadContext({
66
+ dataDir,
67
+ databasePath: dbPath,
68
+ secretProvider: provider,
69
+ expectedIndexerBinaryVersion: packageVersion,
70
+ paths: runtimePaths,
71
+ });
72
+ try {
73
+ const targetDomain = parsed.args[0].trim().toLowerCase();
74
+ const promptState = await context.inspectMiningDomainPromptState({
75
+ paths: runtimePaths,
76
+ provider,
77
+ readContext,
78
+ });
79
+ const currentEntry = promptState.prompts.find((entry) => entry.domain.name === targetDomain);
80
+ if (currentEntry === undefined) {
81
+ throw new Error("mine_prompt_domain_not_mineable");
82
+ }
83
+ if (parsed.outputMode === "text") {
84
+ writeLine(context.stdout, `Domain: ${currentEntry.domain.name}`);
85
+ writeLine(context.stdout, `Current domain prompt: ${currentEntry.prompt ?? "none"}`);
86
+ writeLine(context.stdout, `Global fallback prompt: ${promptState.fallbackPromptConfigured ? "configured" : "not configured"}`);
87
+ }
88
+ const nextPrompt = await prompter.prompt("Domain prompt (blank to clear and use the global fallback): ");
89
+ const result = await context.updateMiningDomainPrompt({
90
+ paths: runtimePaths,
91
+ provider,
92
+ readContext,
93
+ domainName: targetDomain,
94
+ prompt: nextPrompt,
95
+ });
96
+ if (parsed.outputMode === "json") {
97
+ writeJsonValue(context.stdout, createMutationSuccessEnvelope(resolveStableMiningControlJsonSchema(parsed), describeCanonicalCommand(parsed), result.status, buildMinePromptData(result)));
98
+ return 0;
99
+ }
100
+ writeLine(context.stdout, formatMiningPromptMutationReport(result));
101
+ return 0;
102
+ }
103
+ finally {
104
+ await readContext.close();
105
+ }
106
+ }
53
107
  writeLine(context.stderr, `mining admin command not implemented: ${parsed.command}`);
54
108
  return 1;
55
109
  }
@@ -1,9 +1,10 @@
1
1
  import { dirname } from "node:path";
2
2
  import { stat } from "node:fs/promises";
3
- import { formatMineStatusReport, formatMiningEventRecord } from "../mining-format.js";
3
+ import { formatMineStatusReport, formatMiningEventRecord, formatMiningPromptListReport, } from "../mining-format.js";
4
4
  import { writeLine } from "../io.js";
5
5
  import { createErrorEnvelope, createSuccessEnvelope, describeCanonicalCommand, normalizeListPage, writeJsonValue, } from "../output.js";
6
- import { buildMineLogJson, buildMineStatusJson } from "../read-json.js";
6
+ import { buildMineLogJson, buildMinePromptListJson, buildMineStatusJson } from "../read-json.js";
7
+ import { formatNextStepLines } from "../workflow-hints.js";
7
8
  import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
8
9
  async function readRotationIndices(paths) {
9
10
  const rotation = [];
@@ -24,6 +25,7 @@ export async function runMiningReadCommand(parsed, context) {
24
25
  try {
25
26
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
26
27
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
28
+ const packageVersion = await context.readPackageVersion();
27
29
  const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
28
30
  await context.ensureDirectory(dirname(dbPath));
29
31
  if (parsed.command === "mine-log") {
@@ -88,6 +90,41 @@ export async function runMiningReadCommand(parsed, context) {
88
90
  }
89
91
  return 0;
90
92
  }
93
+ if (parsed.command === "mine-prompt-list") {
94
+ const provider = parsed.outputMode === "text"
95
+ ? withInteractiveWalletSecretProvider(context.walletSecretProvider, context.createPrompter())
96
+ : context.walletSecretProvider;
97
+ const readContext = await context.openWalletReadContext({
98
+ dataDir,
99
+ databasePath: dbPath,
100
+ secretProvider: provider,
101
+ expectedIndexerBinaryVersion: packageVersion,
102
+ paths: runtimePaths,
103
+ });
104
+ try {
105
+ const result = buildMinePromptListJson(await context.inspectMiningDomainPromptState({
106
+ paths: runtimePaths,
107
+ provider,
108
+ readContext,
109
+ }));
110
+ if (parsed.outputMode === "json") {
111
+ writeJsonValue(context.stdout, createSuccessEnvelope("cogcoin/mine-prompt-list/v1", describeCanonicalCommand(parsed), result.data, {
112
+ warnings: result.warnings,
113
+ explanations: result.explanations,
114
+ nextSteps: result.nextSteps,
115
+ }));
116
+ return 0;
117
+ }
118
+ writeLine(context.stdout, formatMiningPromptListReport(result.data));
119
+ for (const line of formatNextStepLines(result.nextSteps)) {
120
+ writeLine(context.stdout, line);
121
+ }
122
+ return 0;
123
+ }
124
+ finally {
125
+ await readContext.close();
126
+ }
127
+ }
91
128
  const provider = parsed.outputMode === "text"
92
129
  ? withInteractiveWalletSecretProvider(context.walletSecretProvider, context.createPrompter())
93
130
  : context.walletSecretProvider;
@@ -95,6 +132,7 @@ export async function runMiningReadCommand(parsed, context) {
95
132
  dataDir,
96
133
  databasePath: dbPath,
97
134
  secretProvider: provider,
135
+ expectedIndexerBinaryVersion: packageVersion,
98
136
  paths: runtimePaths,
99
137
  });
100
138
  try {
@@ -128,7 +166,9 @@ export async function runMiningReadCommand(parsed, context) {
128
166
  if (parsed.outputMode === "json") {
129
167
  writeJsonValue(context.stdout, createErrorEnvelope(parsed.command === "mine-log"
130
168
  ? "cogcoin/mine-log/v1"
131
- : "cogcoin/mine-status/v1", describeCanonicalCommand(parsed), message, message));
169
+ : parsed.command === "mine-prompt-list"
170
+ ? "cogcoin/mine-prompt-list/v1"
171
+ : "cogcoin/mine-status/v1", describeCanonicalCommand(parsed), message, message));
132
172
  return 5;
133
173
  }
134
174
  writeLine(context.stderr, message);
@@ -1,15 +1,17 @@
1
1
  import { dirname } from "node:path";
2
- import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
2
+ import { DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting } from "../../bitcoind/bootstrap.js";
3
3
  import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
4
4
  import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
5
+ import { ManagedIndexerProgressObserver, pollManagedIndexerUntilCaughtUp, } from "../managed-indexer-observer.js";
5
6
  import { buildMineStartData, buildMineStopData, } from "../mining-json.js";
6
7
  import { buildMineStartPreviewData, buildMineStopPreviewData, } from "../preview-json.js";
7
8
  import { usesTtyProgress, writeLine } from "../io.js";
8
9
  import { createTerminalPrompter } from "../prompt.js";
9
10
  import { createPreviewSuccessEnvelope, createMutationSuccessEnvelope, describeCanonicalCommand, resolvePreviewJsonSchema, resolveStableMiningControlJsonSchema, writeHandledCliError, writeJsonValue, } from "../output.js";
10
11
  import { formatNextStepLines, getMineStopNextSteps, } from "../workflow-hints.js";
11
- import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
12
+ import { createCloseSignalWatcher, waitForCompletionOrStop } from "../signals.js";
12
13
  import { createSyncProgressReporter } from "../sync-progress.js";
14
+ import { PASSIVE_UPDATE_CHECK_TIMEOUT_MS, applyUpdateCheckResult, compareSemver, createEmptyUpdateCheckCache, fetchLatestPublishedVersion, isUpdateCheckDisabled, loadUpdateCheckCache, persistUpdateCheckCache, shouldRefreshUpdateCheck, } from "../update-service.js";
13
15
  function createCommandPrompter(parsed, context) {
14
16
  return parsed.outputMode !== "text"
15
17
  ? createTerminalPrompter(context.stdin, context.stderr)
@@ -27,89 +29,98 @@ async function ensureMiningProviderSetup(options) {
27
29
  }
28
30
  async function syncManagedMiningReadiness(options) {
29
31
  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;
32
+ let monitor = null;
33
+ let observer = null;
34
+ const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
35
+ paths: options.runtimePaths,
36
+ provider: options.provider,
37
+ loadRawWalletStateEnvelope: options.context.loadRawWalletStateEnvelope,
38
+ });
39
+ await options.context.ensureDirectory(dirname(options.databasePath));
40
+ monitor = await options.context.openManagedIndexerMonitor({
41
+ dataDir: options.dataDir,
42
+ databasePath: options.databasePath,
43
+ walletRootId: walletRoot.walletRootId,
44
+ expectedBinaryVersion: options.expectedBinaryVersion,
45
+ });
46
+ observer = new ManagedIndexerProgressObserver({
47
+ quoteStatePath: resolveBootstrapPathsForTesting(options.dataDir, DEFAULT_SNAPSHOT_METADATA).quoteStatePath,
48
+ stream: options.context.stderr,
49
+ progressOutput: options.parsed.progressOutput,
50
+ onProgress: ttyProgressActive ? undefined : createSyncProgressReporter({
51
+ progressOutput: options.parsed.progressOutput,
52
+ write: (line) => {
53
+ writeLine(options.context.stderr, line);
54
+ },
55
+ }),
56
+ });
57
+ const abortController = new AbortController();
58
+ const stopWatcher = createCloseSignalWatcher({
59
+ signalSource: options.context.signalSource,
60
+ stderr: options.context.stderr,
61
+ closeable: {
62
+ close: async () => {
63
+ abortController.abort(new Error("managed_indexer_preflight_aborted"));
64
+ await observer?.close().catch(() => undefined);
65
+ await monitor?.close().catch(() => undefined);
66
+ },
67
+ },
68
+ forceExit: options.context.forceExit,
69
+ firstMessage: "Stopping managed mining readiness observation...",
70
+ successMessage: "Stopped observing managed mining readiness.",
71
+ failureMessage: "Managed mining readiness observation cleanup failed.",
72
+ });
35
73
  try {
36
- const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
37
- paths: options.runtimePaths,
38
- provider: options.provider,
39
- loadRawWalletStateEnvelope: options.context.loadRawWalletStateEnvelope,
40
- });
41
- try {
42
- controlLock = await acquireFileLock(options.runtimePaths.walletControlLockPath, {
43
- purpose: "managed-sync",
44
- walletRootId: walletRoot.walletRootId,
45
- });
74
+ const syncOutcome = await waitForCompletionOrStop(pollManagedIndexerUntilCaughtUp({
75
+ monitor,
76
+ observer,
77
+ signal: abortController.signal,
78
+ }), stopWatcher);
79
+ if (syncOutcome.kind === "stopped") {
80
+ return syncOutcome.code;
46
81
  }
47
- catch (error) {
48
- if (error instanceof FileLockBusyError) {
49
- throw new Error("wallet_control_lock_busy");
50
- }
51
- throw error;
82
+ return null;
83
+ }
84
+ finally {
85
+ stopWatcher.cleanup();
86
+ await observer?.close().catch(() => undefined);
87
+ await monitor?.close().catch(() => undefined);
88
+ }
89
+ }
90
+ async function resolveMineUpdateAvailable(currentVersion, context) {
91
+ if (isUpdateCheckDisabled(context.env)) {
92
+ return false;
93
+ }
94
+ try {
95
+ const cachePath = context.resolveUpdateCheckStatePath();
96
+ const now = context.now();
97
+ let cache = await loadUpdateCheckCache(cachePath) ?? createEmptyUpdateCheckCache();
98
+ let cacheChanged = false;
99
+ if (shouldRefreshUpdateCheck(cache, now)) {
100
+ const updateResult = await fetchLatestPublishedVersion(context.fetchImpl, {
101
+ timeoutMs: PASSIVE_UPDATE_CHECK_TIMEOUT_MS,
102
+ });
103
+ cache = applyUpdateCheckResult(cache, updateResult, now);
104
+ cacheChanged = true;
52
105
  }
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.");
106
+ if (cacheChanged) {
107
+ await persistUpdateCheckCache(cachePath, cache);
94
108
  }
95
- finally {
96
- stopWatcher.cleanup();
97
- if (!clientClosed) {
98
- await client.close();
99
- }
109
+ if (cache.latestVersion === null) {
110
+ return false;
100
111
  }
112
+ const comparison = compareSemver(cache.latestVersion, currentVersion);
113
+ return comparison !== null && comparison > 0;
101
114
  }
102
- finally {
103
- if (storeOwned && store !== null) {
104
- await store.close().catch(() => undefined);
105
- }
106
- await controlLock?.release().catch(() => undefined);
115
+ catch {
116
+ return false;
107
117
  }
108
118
  }
109
119
  export async function runMiningRuntimeCommand(parsed, context) {
110
120
  try {
111
121
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
112
122
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
123
+ const packageVersion = await context.readPackageVersion();
113
124
  const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
114
125
  if (parsed.command === "mine") {
115
126
  const prompter = context.createPrompter();
@@ -125,12 +136,16 @@ export async function runMiningRuntimeCommand(parsed, context) {
125
136
  context,
126
137
  dataDir,
127
138
  databasePath: dbPath,
139
+ expectedBinaryVersion: packageVersion,
128
140
  provider,
129
141
  runtimePaths,
130
142
  });
131
143
  if (preflightCode !== null) {
132
144
  return preflightCode;
133
145
  }
146
+ const updateAvailable = usesTtyProgress(parsed.progressOutput, context.stderr)
147
+ ? await resolveMineUpdateAvailable(packageVersion, context)
148
+ : false;
134
149
  const abortController = new AbortController();
135
150
  const onStop = () => {
136
151
  abortController.abort();
@@ -139,6 +154,8 @@ export async function runMiningRuntimeCommand(parsed, context) {
139
154
  context.signalSource.on("SIGTERM", onStop);
140
155
  try {
141
156
  await context.runForegroundMining({
157
+ clientVersion: packageVersion,
158
+ updateAvailable,
142
159
  dataDir,
143
160
  databasePath: dbPath,
144
161
  provider,
@@ -171,6 +188,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
171
188
  context,
172
189
  dataDir,
173
190
  databasePath: dbPath,
191
+ expectedBinaryVersion: packageVersion,
174
192
  provider,
175
193
  runtimePaths,
176
194
  });
@@ -2,6 +2,7 @@ import { dirname } from "node:path";
2
2
  import { loadBundledGenesisParameters } from "@cogcoin/indexer";
3
3
  import { resolveCogcoinProcessingStartHeight } from "../../bitcoind/processing-start-height.js";
4
4
  import { UNINITIALIZED_WALLET_ROOT_ID, resolveManagedServicePaths } from "../../bitcoind/service-paths.js";
5
+ import { INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, } from "../../bitcoind/indexer-daemon.js";
5
6
  import { resolveWalletRootIdFromLocalArtifacts, } from "../../wallet/root-resolution.js";
6
7
  import { writeLine } from "../io.js";
7
8
  import { createSuccessEnvelope, describeCanonicalCommand, writeHandledCliError, writeJsonValue, } from "../output.js";
@@ -86,13 +87,49 @@ async function inspectManagedBitcoindStatus(dataDir, context) {
86
87
  nodeError,
87
88
  };
88
89
  }
89
- async function inspectManagedIndexerStatus(dataDir, context) {
90
+ async function inspectManagedIndexerStatus(dataDir, databasePath, expectedBinaryVersion, context) {
90
91
  const resolution = await resolveEffectiveWalletRootId(context);
91
92
  const runtimeRoot = resolveManagedServicePaths(dataDir, resolution.walletRootId).walletRuntimeRoot;
92
93
  const probe = await context.probeIndexerDaemon({
93
94
  dataDir,
94
95
  walletRootId: resolution.walletRootId,
95
96
  });
97
+ await probe.client?.close().catch(() => undefined);
98
+ if (probe.compatibility === "compatible" || probe.compatibility === "unreachable") {
99
+ try {
100
+ const daemonClient = await context.attachIndexerDaemon({
101
+ dataDir,
102
+ databasePath,
103
+ walletRootId: resolution.walletRootId,
104
+ ensureBackgroundFollow: true,
105
+ expectedBinaryVersion,
106
+ });
107
+ try {
108
+ const daemon = await daemonClient.getStatus();
109
+ return {
110
+ dataDir,
111
+ walletRootId: resolution.walletRootId,
112
+ walletRootSource: resolution.source,
113
+ compatibility: "compatible",
114
+ source: "probe",
115
+ daemon: {
116
+ ...daemon,
117
+ runtimeRoot,
118
+ },
119
+ };
120
+ }
121
+ finally {
122
+ await daemonClient.close().catch(() => undefined);
123
+ }
124
+ }
125
+ catch (error) {
126
+ if (error instanceof Error
127
+ && error.message === INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED) {
128
+ throw error;
129
+ }
130
+ // Preserve the status-file fallback when the refresh path fails.
131
+ }
132
+ }
96
133
  let source = "probe";
97
134
  let daemon = probe.status;
98
135
  if (probe.compatibility === "unreachable") {
@@ -291,7 +328,10 @@ export async function runServiceRuntimeCommand(parsed, context) {
291
328
  return 0;
292
329
  }
293
330
  if (parsed.command === "indexer-status") {
294
- const payload = await inspectManagedIndexerStatus(dataDir, context);
331
+ const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
332
+ const packageVersion = await context.readPackageVersion();
333
+ await context.ensureDirectory(dirname(dbPath));
334
+ const payload = await inspectManagedIndexerStatus(dataDir, dbPath, packageVersion, context);
295
335
  const messages = buildStatusMessages(payload);
296
336
  if (parsed.outputMode === "json") {
297
337
  writeJsonValue(context.stdout, createSuccessEnvelope("cogcoin/indexer-status/v1", describeCanonicalCommand(parsed), payload, messages));
@@ -8,6 +8,7 @@ import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider
8
8
  export async function runStatusCommand(parsed, context) {
9
9
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
10
10
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
11
+ const packageVersion = await context.readPackageVersion();
11
12
  const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
12
13
  await context.ensureDirectory(dirname(dbPath));
13
14
  const provider = parsed.outputMode === "text"
@@ -17,6 +18,7 @@ export async function runStatusCommand(parsed, context) {
17
18
  dataDir,
18
19
  databasePath: dbPath,
19
20
  secretProvider: provider,
21
+ expectedIndexerBinaryVersion: packageVersion,
20
22
  paths: runtimePaths,
21
23
  });
22
24
  try {
@@ -29,7 +31,7 @@ export async function runStatusCommand(parsed, context) {
29
31
  }));
30
32
  return 0;
31
33
  }
32
- writeLine(context.stdout, formatWalletOverviewReport(readContext, await context.readPackageVersion()));
34
+ writeLine(context.stdout, formatWalletOverviewReport(readContext, packageVersion));
33
35
  writeLine(context.stdout, formatBalanceReport(readContext));
34
36
  return 0;
35
37
  }