@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,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, formatCliTextError } from "../output.js";
8
+ import { classifyCliError } from "../output.js";
8
9
  import { createTerminalPrompter } from "../prompt.js";
9
- import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
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
- walletControlLockHeld: true,
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 controlLock = null;
34
- let store = null;
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
- store = await context.openSqliteStore({ filename: dbPath });
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
- storeOwned = false;
72
- const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit, [runtimePaths.walletControlLockPath]);
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(client.syncToTip(), stopWatcher);
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
- const result = syncOutcome.value;
79
- if (result.endingHeight !== null && result.endingHeight === result.bestHeight) {
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
- writeLine(context.stdout, `Applied blocks: ${result.appliedBlocks}`);
112
- writeLine(context.stdout, `Rewound blocks: ${result.rewoundBlocks}`);
113
- writeLine(context.stdout, `Indexed ending height: ${result.endingHeight ?? "none"}`);
114
- writeLine(context.stdout, `Node best height: ${result.bestHeight}`);
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
- if (!clientClosed) {
120
- await client.close();
121
- }
100
+ await observer?.close().catch(() => undefined);
101
+ await monitor?.close().catch(() => undefined);
122
102
  }
123
103
  }
124
104
  catch (error) {
125
- const classified = classifyCliError(error);
126
- if (classified.errorCode === "wallet_control_lock_busy") {
127
- const formatted = formatCliTextError(error);
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
  }
@@ -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
- writeLine(context.stdout, "");
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 {
@@ -5,13 +5,14 @@ import { createRpcClient } from "../bitcoind/node.js";
5
5
  import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../bitcoind/service.js";
6
6
  import { resolveDefaultBitcoindDataDirForTesting, resolveDefaultClientDatabasePathForTesting, resolveDefaultUpdateCheckStatePathForTesting, } from "../app-paths.js";
7
7
  import { openManagedBitcoindClient } from "../bitcoind/index.js";
8
+ import { openManagedIndexerMonitor } from "../bitcoind/indexer-monitor.js";
8
9
  import { inspectPassiveClientStatus } from "../passive-status.js";
9
10
  import { openSqliteStore } from "../sqlite/index.js";
10
11
  import { initializeWallet, deleteImportedWalletSeed, previewResetWallet, repairWallet, resetWallet, restoreWalletFromMnemonic, showWalletMnemonic, } from "../wallet/lifecycle.js";
11
12
  import { resolveWalletRuntimePathsForTesting } from "../wallet/runtime.js";
12
13
  import { openWalletReadContext } from "../wallet/read/index.js";
13
14
  import { loadRawWalletStateEnvelope, loadWalletState } from "../wallet/state/storage.js";
14
- 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";
15
16
  import { createLazyDefaultWalletSecretProvider } from "../wallet/state/provider.js";
16
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";
17
18
  import { createTerminalPrompter } from "./prompt.js";
@@ -81,6 +82,7 @@ export function createDefaultContext(overrides = {}) {
81
82
  runGlobalClientUpdateInstall: overrides.runGlobalClientUpdateInstall ?? runGlobalClientUpdateInstall,
82
83
  openSqliteStore: overrides.openSqliteStore ?? openSqliteStore,
83
84
  openManagedBitcoindClient: overrides.openManagedBitcoindClient ?? openManagedBitcoindClient,
85
+ openManagedIndexerMonitor: overrides.openManagedIndexerMonitor ?? openManagedIndexerMonitor,
84
86
  inspectPassiveClientStatus: overrides.inspectPassiveClientStatus ?? inspectPassiveClientStatus,
85
87
  openWalletReadContext: overrides.openWalletReadContext ?? openWalletReadContext,
86
88
  initializeWallet: overrides.initializeWallet ?? initializeWallet,
@@ -111,11 +113,13 @@ export function createDefaultContext(overrides = {}) {
111
113
  giveReputation: overrides.giveReputation ?? giveReputation,
112
114
  revokeReputation: overrides.revokeReputation ?? revokeReputation,
113
115
  inspectMiningControlPlane: overrides.inspectMiningControlPlane ?? inspectMiningControlPlane,
116
+ inspectMiningDomainPromptState: overrides.inspectMiningDomainPromptState ?? inspectMiningDomainPromptState,
114
117
  ensureBuiltInMiningSetupIfNeeded: overrides.ensureBuiltInMiningSetupIfNeeded ?? ensureBuiltInMiningSetupIfNeeded,
115
118
  runForegroundMining: overrides.runForegroundMining ?? runForegroundMining,
116
119
  startBackgroundMining: overrides.startBackgroundMining ?? startBackgroundMining,
117
120
  stopBackgroundMining: overrides.stopBackgroundMining ?? stopBackgroundMining,
118
121
  setupBuiltInMining: overrides.setupBuiltInMining ?? setupBuiltInMining,
122
+ updateMiningDomainPrompt: overrides.updateMiningDomainPrompt ?? updateMiningDomainPrompt,
119
123
  readMiningLog: overrides.readMiningLog ?? readMiningLog,
120
124
  followMiningLog: overrides.followMiningLog ?? followMiningLog,
121
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;
@@ -27,6 +27,9 @@ function resolveProviderNotFoundNextStep(mining) {
27
27
  ? "Next: run `cogcoin mine setup` and clear or correct the provider model."
28
28
  : "Next: run `cogcoin mine setup` and choose a valid provider model.";
29
29
  }
30
+ function resolveInsufficientFundsNextStep() {
31
+ return "Next: wait for enough safe BTC funding to become spendable for the next publish; mining resumes automatically.";
32
+ }
30
33
  export function formatMiningSummaryLine(mining) {
31
34
  const provider = mining.provider.configured
32
35
  ? `${mining.provider.provider} configured`
@@ -133,6 +136,9 @@ export function formatMineStatusReport(mining) {
133
136
  else if (mining.runtime.providerState === "not-found") {
134
137
  lines.push(resolveProviderNotFoundNextStep(mining));
135
138
  }
139
+ else if (mining.runtime.currentPublishDecision === "publish-paused-insufficient-funds") {
140
+ lines.push(resolveInsufficientFundsNextStep());
141
+ }
136
142
  else if (mining.runtime.pauseReason === "zero-reward") {
137
143
  lines.push("Next: wait for the next positive-reward target height; mining resumes automatically.");
138
144
  }
@@ -150,3 +156,32 @@ export function formatMineStatusReport(mining) {
150
156
  export function formatMiningEventRecord(event) {
151
157
  return `${new Date(event.timestampUnixMs).toISOString()} ${event.level.toUpperCase()} ${event.kind} ${event.message}`;
152
158
  }
159
+ export function formatMiningPromptMutationReport(result) {
160
+ const lines = [
161
+ `Domain: ${result.domain.name}`,
162
+ `Domain prompt: ${result.prompt ?? "none"}`,
163
+ `Global fallback prompt: ${result.fallbackPromptConfigured ? "configured" : "not configured"}`,
164
+ ];
165
+ if (result.previousPrompt !== null) {
166
+ lines.push(`Previous domain prompt: ${result.previousPrompt}`);
167
+ }
168
+ lines.push(result.status === "updated"
169
+ ? "Per-domain mining prompt updated."
170
+ : "Per-domain mining prompt cleared.");
171
+ return lines.join("\n");
172
+ }
173
+ export function formatMiningPromptListReport(result) {
174
+ const lines = [
175
+ "Mining Prompt List",
176
+ `Global fallback prompt: ${result.fallbackPromptConfigured ? "configured" : "not configured"}`,
177
+ ];
178
+ if (result.prompts.length === 0) {
179
+ lines.push("No mineable root domains or stored per-domain mining prompts are configured.");
180
+ return lines.join("\n");
181
+ }
182
+ for (const entry of result.prompts) {
183
+ lines.push(`${entry.domain.name} domainId=${entry.domain.domainId ?? "none"} ${entry.mineable ? "mineable" : "dormant"} source=${entry.effectivePromptSource}`);
184
+ lines.push(` prompt: ${entry.prompt ?? "none"}`);
185
+ }
186
+ return lines.join("\n");
187
+ }
@@ -1,4 +1,4 @@
1
- import type { MiningControlPlaneView, MiningRuntimeStatusV1 } from "../wallet/mining/index.js";
1
+ import type { MiningControlPlaneView, MiningDomainPromptMutationResult, MiningRuntimeStatusV1 } from "../wallet/mining/index.js";
2
2
  export declare function buildMineSetupData(view: MiningControlPlaneView): {
3
3
  resultType: "state-change";
4
4
  stateChange: {
@@ -29,3 +29,13 @@ export declare function buildMineStopData(snapshot: MiningRuntimeStatusV1 | null
29
29
  };
30
30
  state: Record<string, unknown>;
31
31
  };
32
+ export declare function buildMinePromptData(result: MiningDomainPromptMutationResult): {
33
+ domain: {
34
+ name: string;
35
+ domainId: number | null;
36
+ };
37
+ previousPrompt: string | null;
38
+ prompt: string | null;
39
+ status: "updated" | "cleared";
40
+ fallbackPromptConfigured: boolean;
41
+ };
@@ -58,3 +58,12 @@ export function buildMineStopData(snapshot) {
58
58
  after,
59
59
  });
60
60
  }
61
+ export function buildMinePromptData(result) {
62
+ return {
63
+ domain: result.domain,
64
+ previousPrompt: result.previousPrompt,
65
+ prompt: result.prompt,
66
+ status: result.status,
67
+ fallbackPromptConfigured: result.fallbackPromptConfigured,
68
+ };
69
+ }
@@ -185,6 +185,8 @@ function isBlockedError(message) {
185
185
  || message === "indexer_daemon_wallet_root_mismatch"
186
186
  || message === "indexer_daemon_schema_mismatch"
187
187
  || message === "mine_setup_requires_tty"
188
+ || message === "mine_prompt_requires_tty"
189
+ || message === "mine_prompt_domain_not_mineable"
188
190
  || message === "mining_preemption_timeout"
189
191
  || message === "wallet_client_password_setup_required"
190
192
  || message === "wallet_client_password_migration_required"
@@ -523,6 +525,13 @@ export function createCliErrorPresentation(errorCode, fallbackMessage, error) {
523
525
  next: "Rerun `cogcoin mine setup` when you are ready to choose a provider model.",
524
526
  };
525
527
  }
528
+ if (errorCode === "mine_prompt_domain_not_mineable") {
529
+ return {
530
+ what: "A new mining prompt override can only target a mineable anchored root domain.",
531
+ why: "Cogcoin only creates new domain prompt overrides for locally controlled anchored root domains that are currently mineable. Existing stored prompt entries can still be edited or cleared by name even when they are dormant.",
532
+ next: "Run `cogcoin domains --mineable` to see eligible domains, or rerun `cogcoin mine prompt <domain>` for an existing stored prompt entry.",
533
+ };
534
+ }
526
535
  if (errorCode.endsWith("_confirmation_rejected")) {
527
536
  return {
528
537
  what: "Confirmation was declined.",
@@ -591,6 +600,13 @@ export function createCliErrorPresentation(errorCode, fallbackMessage, error) {
591
600
  next: "Check `cogcoin status`, wait for services to settle, and retry. If the state stays degraded, run `cogcoin repair`.",
592
601
  };
593
602
  }
603
+ if (errorCode === "indexer_daemon_background_follow_recovery_failed") {
604
+ return {
605
+ what: "The managed indexer daemon could not recover automatic background follow.",
606
+ why: "Cogcoin tried to resume or restart the compatible managed indexer daemon, but it still failed to enter background follow.",
607
+ next: "Run `cogcoin repair` if this persists, then retry.",
608
+ };
609
+ }
594
610
  if (errorCode === "indexer_daemon_service_version_mismatch") {
595
611
  return {
596
612
  what: "The live indexer daemon is running an incompatible service API version.",
@@ -1027,6 +1043,10 @@ export function describeCanonicalCommand(parsed) {
1027
1043
  return "cogcoin wallet status";
1028
1044
  case "mine-setup":
1029
1045
  return "cogcoin mine setup";
1046
+ case "mine-prompt":
1047
+ return `cogcoin mine prompt ${args[0] ?? "<domain>"}`;
1048
+ case "mine-prompt-list":
1049
+ return "cogcoin mine prompt";
1030
1050
  case "mine-start":
1031
1051
  return "cogcoin mine start";
1032
1052
  case "mine-stop":
@@ -1098,6 +1118,8 @@ export function resolveStableJsonSchema(parsed) {
1098
1118
  return "cogcoin/mine-status/v1";
1099
1119
  case "mine-log":
1100
1120
  return "cogcoin/mine-log/v1";
1121
+ case "mine-prompt-list":
1122
+ return "cogcoin/mine-prompt-list/v1";
1101
1123
  case "balance":
1102
1124
  case "cog-balance":
1103
1125
  return "cogcoin/balance/v1";
@@ -1205,6 +1227,8 @@ export function resolveStableMiningControlJsonSchema(parsed) {
1205
1227
  switch (parsed.command) {
1206
1228
  case "mine-setup":
1207
1229
  return "cogcoin/mine-setup/v1";
1230
+ case "mine-prompt":
1231
+ return "cogcoin/mine-prompt/v1";
1208
1232
  case "mine-start":
1209
1233
  return "cogcoin/mine-start/v1";
1210
1234
  case "mine-stop":