@cogcoin/client 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -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 +14 -3
  8. package/dist/bitcoind/indexer-daemon.js +145 -29
  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/retryable-rpc.js +3 -0
  16. package/dist/bitcoind/service.d.ts +1 -0
  17. package/dist/bitcoind/service.js +31 -9
  18. package/dist/bitcoind/testing.d.ts +0 -1
  19. package/dist/bitcoind/testing.js +0 -1
  20. package/dist/bitcoind/types.d.ts +5 -2
  21. package/dist/cli/commands/follow.js +44 -49
  22. package/dist/cli/commands/mining-admin.js +65 -2
  23. package/dist/cli/commands/mining-read.js +43 -3
  24. package/dist/cli/commands/mining-runtime.js +91 -73
  25. package/dist/cli/commands/service-runtime.js +42 -2
  26. package/dist/cli/commands/status.js +3 -1
  27. package/dist/cli/commands/sync.js +50 -90
  28. package/dist/cli/commands/update.d.ts +2 -0
  29. package/dist/cli/commands/update.js +101 -0
  30. package/dist/cli/commands/wallet-admin.js +21 -3
  31. package/dist/cli/commands/wallet-read.js +2 -0
  32. package/dist/cli/context.js +36 -1
  33. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  34. package/dist/cli/managed-indexer-observer.js +163 -0
  35. package/dist/cli/mining-format.d.ts +3 -1
  36. package/dist/cli/mining-format.js +63 -0
  37. package/dist/cli/mining-json.d.ts +11 -1
  38. package/dist/cli/mining-json.js +15 -0
  39. package/dist/cli/output.js +74 -2
  40. package/dist/cli/parse.d.ts +1 -1
  41. package/dist/cli/parse.js +28 -0
  42. package/dist/cli/prompt.js +109 -0
  43. package/dist/cli/read-json.d.ts +26 -1
  44. package/dist/cli/read-json.js +48 -0
  45. package/dist/cli/runner.js +8 -2
  46. package/dist/cli/signals.d.ts +12 -0
  47. package/dist/cli/signals.js +31 -13
  48. package/dist/cli/types.d.ts +13 -4
  49. package/dist/cli/update-notifier.js +7 -222
  50. package/dist/cli/update-service.d.ts +34 -0
  51. package/dist/cli/update-service.js +152 -0
  52. package/dist/client/initialization.js +5 -0
  53. package/dist/semver.d.ts +12 -0
  54. package/dist/semver.js +68 -0
  55. package/dist/wallet/lifecycle.d.ts +10 -0
  56. package/dist/wallet/mining/config.js +64 -3
  57. package/dist/wallet/mining/control.d.ts +5 -1
  58. package/dist/wallet/mining/control.js +269 -26
  59. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  60. package/dist/wallet/mining/domain-prompts.js +130 -0
  61. package/dist/wallet/mining/index.d.ts +2 -1
  62. package/dist/wallet/mining/index.js +1 -0
  63. package/dist/wallet/mining/provider-model.d.ts +30 -0
  64. package/dist/wallet/mining/provider-model.js +134 -0
  65. package/dist/wallet/mining/runner.d.ts +156 -5
  66. package/dist/wallet/mining/runner.js +1019 -399
  67. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  68. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  69. package/dist/wallet/mining/sentences.d.ts +2 -2
  70. package/dist/wallet/mining/sentences.js +32 -6
  71. package/dist/wallet/mining/types.d.ts +35 -1
  72. package/dist/wallet/mining/visualizer.d.ts +3 -0
  73. package/dist/wallet/mining/visualizer.js +132 -15
  74. package/dist/wallet/read/context.d.ts +1 -0
  75. package/dist/wallet/read/context.js +15 -7
  76. package/dist/wallet/state/client-password-agent.js +4 -1
  77. package/dist/wallet/state/client-password.js +15 -8
  78. package/dist/wallet/tx/common.js +1 -1
  79. package/package.json +3 -2
@@ -18,6 +18,7 @@ function normalizeLegacyMiningProviderState(raw) {
18
18
  case "unavailable":
19
19
  case "rate-limited":
20
20
  case "auth-error":
21
+ case "not-found":
21
22
  return raw;
22
23
  case "hook-error":
23
24
  case "n/a":
@@ -16,6 +16,7 @@ export interface MiningSentenceGenerationRequestV1 {
16
16
  domainId: number;
17
17
  domainName: string;
18
18
  requiredWords: [string, string, string, string, string];
19
+ extraPrompt: string | null;
19
20
  }>;
20
21
  }
21
22
  export interface MiningSentenceCandidateV1 {
@@ -10,8 +10,8 @@ export interface MiningSentenceSourceOptions {
10
10
  fetchImpl?: typeof fetch;
11
11
  }
12
12
  declare class MiningProviderRequestError extends Error {
13
- readonly providerState: "unavailable" | "rate-limited" | "auth-error";
14
- constructor(providerState: "unavailable" | "rate-limited" | "auth-error", message: string);
13
+ readonly providerState: "unavailable" | "rate-limited" | "auth-error" | "not-found";
14
+ constructor(providerState: "unavailable" | "rate-limited" | "auth-error" | "not-found", message: string);
15
15
  }
16
16
  export declare function generateMiningSentences(request: MiningSentenceGenerationRequest, options: MiningSentenceSourceOptions): Promise<{
17
17
  candidates: MiningSentenceCandidateV1[];
@@ -1,6 +1,7 @@
1
1
  import { loadClientConfig } from "./config.js";
2
2
  import { MINING_BUILTIN_TIMEOUT_MS, } from "./constants.js";
3
3
  import { normalizeMiningSentenceResponse, parseStrictJsonValue, stripMarkdownCodeFence, } from "./sentence-protocol.js";
4
+ import { resolveBuiltInProviderModel } from "./provider-model.js";
4
5
  class MiningProviderRequestError extends Error {
5
6
  providerState;
6
7
  constructor(providerState, message) {
@@ -9,6 +10,14 @@ class MiningProviderRequestError extends Error {
9
10
  this.providerState = providerState;
10
11
  }
11
12
  }
13
+ function createBuiltInProviderNotFoundError(options) {
14
+ const providerName = options.provider === "anthropic" ? "Anthropic" : "OpenAI";
15
+ const providerLabel = `The built-in ${providerName} mining provider`;
16
+ const message = options.usingDefaultModel
17
+ ? `${providerLabel} returned HTTP 404 for default model "${options.model}". ${providerName} may no longer serve that model. Rerun \`cogcoin mine setup\` to choose a valid override.`
18
+ : `${providerLabel} returned HTTP 404 for model "${options.model}". The configured model override may be invalid. Rerun \`cogcoin mine setup\` to clear or correct it.`;
19
+ return new MiningProviderRequestError("not-found", message);
20
+ }
12
21
  function buildSystemPrompt(extraPrompt) {
13
22
  const lines = [
14
23
  "You are helping generate candidate Cogcoin mining sentences.",
@@ -16,9 +25,13 @@ function buildSystemPrompt(extraPrompt) {
16
25
  "Every sentence must be a single natural-language sentence.",
17
26
  "Do not add commentary, markdown, or code fences.",
18
27
  "Do not invent domain IDs or request IDs.",
28
+ "Each rootDomains entry may include an extraPrompt that applies only to that domain.",
29
+ "If rootDomains[i].extraPrompt is present, use it only for candidates for that domainId.",
30
+ "If rootDomains[i].extraPrompt is null, fall back to the request-level extraPrompt when it is present.",
31
+ "Never apply one domain's prompt to another domain's candidates.",
19
32
  ];
20
33
  if (extraPrompt !== null && extraPrompt.trim().length > 0) {
21
- lines.push(`Extra instruction: ${extraPrompt.trim()}`);
34
+ lines.push(`Request-level fallback instruction: ${extraPrompt.trim()}`);
22
35
  }
23
36
  return lines.join("\n");
24
37
  }
@@ -65,7 +78,7 @@ async function requestBuiltInSentences(options) {
65
78
  const providerSignal = createProviderSignal(options.signal, Math.min(MINING_BUILTIN_TIMEOUT_MS, options.request.limits.timeoutMs));
66
79
  try {
67
80
  if (options.provider === "openai") {
68
- const model = options.modelOverride ?? "gpt-5.4-mini";
81
+ const { effectiveModel: model, usingDefaultModel } = resolveBuiltInProviderModel(options.provider, options.modelOverride);
69
82
  const response = await fetchImpl("https://api.openai.com/v1/responses", {
70
83
  method: "POST",
71
84
  headers: {
@@ -77,7 +90,7 @@ async function requestBuiltInSentences(options) {
77
90
  input: [
78
91
  {
79
92
  role: "system",
80
- content: buildSystemPrompt(options.extraPrompt),
93
+ content: buildSystemPrompt(options.request.extraPrompt),
81
94
  },
82
95
  {
83
96
  role: "user",
@@ -93,6 +106,13 @@ async function requestBuiltInSentences(options) {
93
106
  if (response.status === 429) {
94
107
  throw new MiningProviderRequestError("rate-limited", "The built-in OpenAI mining provider is rate limited.");
95
108
  }
109
+ if (response.status === 404) {
110
+ throw createBuiltInProviderNotFoundError({
111
+ provider: options.provider,
112
+ model,
113
+ usingDefaultModel,
114
+ });
115
+ }
96
116
  if (!response.ok) {
97
117
  throw new MiningProviderRequestError("unavailable", `The built-in OpenAI mining provider returned HTTP ${response.status}.`);
98
118
  }
@@ -106,7 +126,7 @@ async function requestBuiltInSentences(options) {
106
126
  model,
107
127
  });
108
128
  }
109
- const model = options.modelOverride ?? "claude-sonnet-4-20250514";
129
+ const { effectiveModel: model, usingDefaultModel } = resolveBuiltInProviderModel(options.provider, options.modelOverride);
110
130
  const response = await fetchImpl("https://api.anthropic.com/v1/messages", {
111
131
  method: "POST",
112
132
  headers: {
@@ -117,7 +137,7 @@ async function requestBuiltInSentences(options) {
117
137
  body: JSON.stringify({
118
138
  model,
119
139
  max_tokens: 1_200,
120
- system: buildSystemPrompt(options.extraPrompt),
140
+ system: buildSystemPrompt(options.request.extraPrompt),
121
141
  messages: [
122
142
  {
123
143
  role: "user",
@@ -133,6 +153,13 @@ async function requestBuiltInSentences(options) {
133
153
  if (response.status === 429) {
134
154
  throw new MiningProviderRequestError("rate-limited", "The built-in Anthropic mining provider is rate limited.");
135
155
  }
156
+ if (response.status === 404) {
157
+ throw createBuiltInProviderNotFoundError({
158
+ provider: options.provider,
159
+ model,
160
+ usingDefaultModel,
161
+ });
162
+ }
136
163
  if (!response.ok) {
137
164
  throw new MiningProviderRequestError("unavailable", `The built-in Anthropic mining provider returned HTTP ${response.status}.`);
138
165
  }
@@ -222,7 +249,6 @@ export async function generateMiningSentences(request, options) {
222
249
  provider: builtIn.provider,
223
250
  apiKey: builtIn.apiKey,
224
251
  modelOverride: builtIn.modelOverride,
225
- extraPrompt: builtIn.extraPrompt ?? request.extraPrompt,
226
252
  request,
227
253
  fetchImpl: options.fetchImpl,
228
254
  signal: options.signal,
@@ -1,19 +1,47 @@
1
1
  import type { ManagedBitcoindHealth, ManagedIndexerTruthSource } from "../../bitcoind/types.js";
2
2
  export type MiningServiceHealth = "synced" | "catching-up" | "reorging" | "starting" | "stale-heartbeat" | "failed" | "schema-mismatch" | "service-version-mismatch" | "wallet-root-mismatch" | "unavailable";
3
3
  export type MiningProviderKind = "openai" | "anthropic";
4
+ export type MiningModelSelectionSource = "catalog" | "custom" | "legacy-default" | "legacy-custom";
4
5
  export interface MiningProviderConfigRecord {
5
6
  provider: MiningProviderKind;
6
7
  apiKey: string;
7
8
  extraPrompt: string | null;
8
9
  modelOverride: string | null;
10
+ modelSelectionSource: MiningModelSelectionSource;
9
11
  updatedAtUnixMs: number;
10
12
  }
13
+ export type MiningProviderConfigByProvider = Partial<Record<MiningProviderKind, MiningProviderConfigRecord>>;
11
14
  export interface ClientConfigV1 {
12
15
  schemaVersion: 1;
13
16
  mining: {
14
17
  builtIn: MiningProviderConfigRecord | null;
18
+ builtInByProvider?: MiningProviderConfigByProvider;
19
+ domainExtraPrompts: Record<string, string>;
15
20
  };
16
21
  }
22
+ export interface MiningDomainPromptEntry {
23
+ domain: {
24
+ name: string;
25
+ domainId: number | null;
26
+ };
27
+ mineable: boolean;
28
+ prompt: string | null;
29
+ effectivePromptSource: "domain" | "global-fallback" | "none";
30
+ }
31
+ export interface MiningDomainPromptListResult {
32
+ fallbackPromptConfigured: boolean;
33
+ prompts: MiningDomainPromptEntry[];
34
+ }
35
+ export interface MiningDomainPromptMutationResult {
36
+ domain: {
37
+ name: string;
38
+ domainId: number | null;
39
+ };
40
+ previousPrompt: string | null;
41
+ prompt: string | null;
42
+ status: "updated" | "cleared";
43
+ fallbackPromptConfigured: boolean;
44
+ }
17
45
  export interface MiningEventRecord {
18
46
  schemaVersion: 1;
19
47
  timestampUnixMs: number;
@@ -56,7 +84,7 @@ export interface MiningRuntimeStatusV1 {
56
84
  indexerReorgDepth: number | null;
57
85
  indexerTipAligned: boolean | null;
58
86
  corePublishState: "unknown" | "network-inactive" | "no-outbound-peers" | "ibd" | "mempool-loading" | "healthy" | null;
59
- providerState: "ready" | "backoff" | "unavailable" | "rate-limited" | "auth-error" | null;
87
+ providerState: "ready" | "backoff" | "unavailable" | "rate-limited" | "auth-error" | "not-found" | null;
60
88
  lastSuspendDetectedAtUnixMs: number | null;
61
89
  reconnectSettledUntilUnixMs: number | null;
62
90
  tipSettledUntilUnixMs: number | null;
@@ -103,8 +131,14 @@ export interface MiningProviderInspection {
103
131
  provider: MiningProviderKind | null;
104
132
  status: "ready" | "missing" | "error";
105
133
  message: string | null;
134
+ modelId: string | null;
135
+ effectiveModel: string | null;
106
136
  modelOverride: string | null;
137
+ modelSelectionSource: MiningModelSelectionSource | null;
138
+ usingDefaultModel: boolean | null;
107
139
  extraPromptConfigured: boolean;
140
+ estimatedDailyCostUsd: number | null;
141
+ estimatedDailyCostDisplay: string | null;
108
142
  }
109
143
  export interface MiningControlPlaneView {
110
144
  runtime: MiningRuntimeStatusV1;
@@ -11,6 +11,7 @@ export interface MiningSentenceBoardEntry {
11
11
  rank: number;
12
12
  domainName: string;
13
13
  sentence: string;
14
+ requiredWords: readonly string[];
14
15
  }
15
16
  export interface MiningProvisionalSentenceEntry {
16
17
  domainName: string | null;
@@ -44,6 +45,8 @@ export declare class MiningFollowVisualizer {
44
45
  platform?: NodeJS.Platform;
45
46
  env?: NodeJS.ProcessEnv;
46
47
  clock?: RenderClock;
48
+ clientVersion?: string | null;
49
+ updateAvailable?: boolean;
47
50
  rendererFactory?: (stream: TtyRenderStream) => VisualizerRendererLike;
48
51
  });
49
52
  update(snapshot: MiningRuntimeStatusV1, uiState?: MiningFollowVisualizerState): void;
@@ -1,9 +1,11 @@
1
1
  import { createBootstrapProgress } from "../../bitcoind/progress/formatting.js";
2
+ import { normalizeInlineText, truncateLine } from "../../bitcoind/progress/formatting.js";
2
3
  import { advanceFollowSceneState, createFollowSceneState, replaceFollowBlockTimes, syncFollowSceneState, } from "../../bitcoind/progress/follow-scene.js";
3
4
  import { DEFAULT_RENDER_CLOCK, resolveTtyRenderPolicy, TtyRenderThrottle, } from "../../bitcoind/progress/render-policy.js";
4
5
  import { TtyProgressRenderer } from "../../bitcoind/progress/tty-renderer.js";
5
6
  const MINING_ARTWORK_COG_WIDTH = 22;
6
7
  const MINING_SENTENCE_BOARD_SIZE = 5;
8
+ const MINING_SENTENCE_BOARD_WRAP_WIDTH = 80;
7
9
  function formatCogAmountWithDecimals(value, { maxFractionDigits, minFractionDigits, }) {
8
10
  const sign = value < 0n ? "-" : "";
9
11
  const absolute = value < 0n ? -value : value;
@@ -44,14 +46,86 @@ function formatCompactCogBalanceText(balanceCogtoshi) {
44
46
  function formatCompactSatBalanceText(balanceSats) {
45
47
  return balanceSats === null ? null : `${balanceSats.toString()} SAT`;
46
48
  }
49
+ function formatArtworkVersionText(clientVersion) {
50
+ if (typeof clientVersion !== "string") {
51
+ return null;
52
+ }
53
+ const normalizedVersion = normalizeInlineText(clientVersion);
54
+ if (normalizedVersion.length === 0) {
55
+ return null;
56
+ }
57
+ return normalizedVersion.startsWith("v")
58
+ ? normalizedVersion
59
+ : `v${normalizedVersion}`;
60
+ }
47
61
  function formatRewardCogAmount(value) {
48
62
  return `${formatCogAmountWithDecimals(value, {
49
63
  maxFractionDigits: 8,
50
64
  minFractionDigits: 1,
51
65
  })} COG`;
52
66
  }
53
- function formatSentenceRow(rank, domainName, sentence) {
54
- return `${rank}. @${domainName}: ${sentence}`;
67
+ function escapeRegExp(value) {
68
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69
+ }
70
+ function consumeWrappedLine(text, capacity) {
71
+ const remaining = text.trimStart();
72
+ if (remaining.length <= capacity) {
73
+ return {
74
+ line: remaining,
75
+ remaining: "",
76
+ };
77
+ }
78
+ const candidate = remaining.slice(0, capacity + 1);
79
+ const breakIndex = candidate.lastIndexOf(" ");
80
+ if (breakIndex > 0) {
81
+ return {
82
+ line: remaining.slice(0, breakIndex).trimEnd(),
83
+ remaining: remaining.slice(breakIndex + 1).trimStart(),
84
+ };
85
+ }
86
+ return {
87
+ line: remaining.slice(0, capacity),
88
+ remaining: remaining.slice(capacity).trimStart(),
89
+ };
90
+ }
91
+ function highlightRequiredWords(sentence, requiredWords) {
92
+ const uniqueWords = [...new Set(requiredWords
93
+ .map((word) => word.trim().toLowerCase())
94
+ .filter((word) => word.length > 0))].sort((left, right) => right.length - left.length);
95
+ if (uniqueWords.length === 0) {
96
+ return sentence;
97
+ }
98
+ const pattern = new RegExp(`\\b(?:${uniqueWords.map(escapeRegExp).join("|")})\\b`, "gi");
99
+ return sentence.replace(pattern, (match) => match.toUpperCase());
100
+ }
101
+ function formatSentenceSlot(prefix, sentence, requiredWords, lineCount) {
102
+ if (sentence === null) {
103
+ return [
104
+ prefix.trimEnd(),
105
+ ...Array.from({ length: Math.max(0, lineCount - 1) }, () => ""),
106
+ ];
107
+ }
108
+ const normalizedSentence = highlightRequiredWords(normalizeInlineText(sentence), requiredWords);
109
+ const continuationPrefix = " ".repeat(prefix.length);
110
+ const lines = [];
111
+ let remaining = normalizedSentence;
112
+ for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
113
+ const linePrefix = lineIndex === 0 ? prefix : continuationPrefix;
114
+ const capacity = Math.max(0, MINING_SENTENCE_BOARD_WRAP_WIDTH - linePrefix.length);
115
+ const wrapped = consumeWrappedLine(remaining, capacity);
116
+ const isLastLine = lineIndex === lineCount - 1;
117
+ const lineContent = isLastLine && wrapped.remaining.length > 0
118
+ ? truncateLine(`${wrapped.line}\u2026`, capacity)
119
+ : wrapped.line;
120
+ lines.push(lineContent.length === 0
121
+ ? ""
122
+ : `${linePrefix}${lineContent}`);
123
+ remaining = wrapped.remaining;
124
+ }
125
+ return lines;
126
+ }
127
+ function formatSentenceRow(entry) {
128
+ return formatSentenceSlot(`${entry.rank}. @${entry.domainName}: `, entry.sentence, entry.requiredWords, 2);
55
129
  }
56
130
  function formatRequiredWordsLine(words) {
57
131
  if (words.length === 0) {
@@ -59,11 +133,11 @@ function formatRequiredWordsLine(words) {
59
133
  }
60
134
  return `Required words: ${words.map((word) => word.toUpperCase()).join(", ")}`;
61
135
  }
62
- function formatProvisionalSentenceRow(entry) {
136
+ function formatProvisionalSentenceRow(entry, requiredWords) {
63
137
  if (entry.domainName === null || entry.sentence === null) {
64
- return "";
138
+ return ["", "", ""];
65
139
  }
66
- return `@${entry.domainName}: ${entry.sentence}`;
140
+ return formatSentenceSlot(`@${entry.domainName}: `, entry.sentence, requiredWords, 3);
67
141
  }
68
142
  export function createEmptyMiningFollowVisualizerState() {
69
143
  return {
@@ -82,6 +156,34 @@ export function createEmptyMiningFollowVisualizerState() {
82
156
  recentWin: null,
83
157
  };
84
158
  }
159
+ function cloneMiningRuntimeSnapshot(snapshot) {
160
+ return {
161
+ ...snapshot,
162
+ };
163
+ }
164
+ function cloneMiningFollowVisualizerState(state) {
165
+ return {
166
+ ...state,
167
+ visibleBlockTimesByHeight: { ...state.visibleBlockTimesByHeight },
168
+ settledBoardEntries: state.settledBoardEntries.map((entry) => ({
169
+ ...entry,
170
+ requiredWords: [...entry.requiredWords],
171
+ })),
172
+ provisionalRequiredWords: [...state.provisionalRequiredWords],
173
+ provisionalEntry: {
174
+ ...state.provisionalEntry,
175
+ },
176
+ recentWin: state.recentWin === null
177
+ ? null
178
+ : {
179
+ ...state.recentWin,
180
+ },
181
+ };
182
+ }
183
+ function miningFollowSceneShouldSettle(snapshot, nowUnixMs) {
184
+ return ((snapshot.tipSettledUntilUnixMs ?? 0) > nowUnixMs
185
+ || (snapshot.reconnectSettledUntilUnixMs ?? 0) > nowUnixMs);
186
+ }
85
187
  const VISUALIZER_PROGRESS_SNAPSHOT = {
86
188
  url: "",
87
189
  filename: "mining-follow-visualizer",
@@ -172,6 +274,8 @@ export function describeMiningVisualizerProgress(snapshot) {
172
274
  }
173
275
  export class MiningFollowVisualizer {
174
276
  #renderer;
277
+ #artworkStatusLeftText;
278
+ #artworkStatusRightText;
175
279
  #clock;
176
280
  #renderThrottle;
177
281
  #progress = createBootstrapProgress("follow_tip", VISUALIZER_PROGRESS_SNAPSHOT);
@@ -187,6 +291,9 @@ export class MiningFollowVisualizer {
187
291
  env: options.env,
188
292
  });
189
293
  this.#clock = options.clock ?? DEFAULT_RENDER_CLOCK;
294
+ const artworkVersionText = formatArtworkVersionText(options.clientVersion);
295
+ this.#artworkStatusLeftText = options.updateAvailable === true ? "UPDATE" : null;
296
+ this.#artworkStatusRightText = artworkVersionText;
190
297
  this.#renderer = renderPolicy.enabled
191
298
  ? options.rendererFactory?.(stream) ?? new TtyProgressRenderer(stream)
192
299
  : null;
@@ -208,17 +315,20 @@ export class MiningFollowVisualizer {
208
315
  if (this.#renderer === null) {
209
316
  return;
210
317
  }
211
- this.#latestSnapshot = snapshot;
318
+ this.#latestSnapshot = cloneMiningRuntimeSnapshot(snapshot);
212
319
  if (uiState !== undefined) {
213
- this.#latestUiState = uiState;
320
+ this.#latestUiState = cloneMiningFollowVisualizerState(uiState);
214
321
  }
215
322
  replaceFollowBlockTimes(this.#scene, this.#latestUiState.visibleBlockTimesByHeight);
216
- const indexedHeight = snapshot.indexerTipHeight ?? snapshot.coreBestHeight ?? null;
217
- const nodeHeight = snapshot.coreBestHeight ?? indexedHeight;
323
+ const indexedHeight = this.#latestSnapshot.indexerTipHeight ?? this.#latestSnapshot.coreBestHeight ?? null;
324
+ const nodeHeight = this.#latestSnapshot.coreBestHeight ?? indexedHeight;
325
+ const settleLatest = miningFollowSceneShouldSettle(this.#latestSnapshot, this.#clock.now());
218
326
  syncFollowSceneState(this.#scene, {
219
327
  indexedHeight,
220
328
  nodeHeight,
221
329
  liveActivated: true,
330
+ authoritativeTip: true,
331
+ settleLatest,
222
332
  });
223
333
  this.#renderThrottle.request();
224
334
  }
@@ -252,7 +362,7 @@ export class MiningFollowVisualizer {
252
362
  this.#progress.targetHeight = nodeHeight;
253
363
  this.#progress.etaSeconds = null;
254
364
  this.#progress.lastError = snapshot.lastError;
255
- this.#renderer.renderFollowScene(this.#progress, indexedHeight, nodeHeight, this.#scene, describeMiningVisualizerStatus(snapshot, uiState), {
365
+ const renderOptions = {
256
366
  artworkCogText: formatCompactCogBalanceText(uiState.balanceCogtoshi),
257
367
  artworkSatText: formatCompactSatBalanceText(uiState.balanceSats),
258
368
  extraLines: [
@@ -261,13 +371,20 @@ export class MiningFollowVisualizer {
261
371
  ...Array.from({ length: MINING_SENTENCE_BOARD_SIZE }, (_value, index) => {
262
372
  const entry = uiState.settledBoardEntries[index];
263
373
  return entry === undefined
264
- ? `${index + 1}.`
265
- : formatSentenceRow(entry.rank, entry.domainName, entry.sentence);
266
- }),
374
+ ? [`${index + 1}.`, ""]
375
+ : formatSentenceRow(entry);
376
+ }).flat(),
267
377
  "----------",
268
378
  formatRequiredWordsLine(uiState.provisionalRequiredWords),
269
- formatProvisionalSentenceRow(uiState.provisionalEntry),
379
+ ...formatProvisionalSentenceRow(uiState.provisionalEntry, uiState.provisionalRequiredWords),
270
380
  ],
271
- });
381
+ };
382
+ if (this.#artworkStatusLeftText !== null) {
383
+ renderOptions.artworkStatusLeftText = this.#artworkStatusLeftText;
384
+ }
385
+ if (this.#artworkStatusRightText !== null) {
386
+ renderOptions.artworkStatusRightText = this.#artworkStatusRightText;
387
+ }
388
+ this.#renderer.renderFollowScene(this.#progress, indexedHeight, nodeHeight, this.#scene, describeMiningVisualizerStatus(snapshot, uiState), renderOptions);
272
389
  }
273
390
  }
@@ -15,6 +15,7 @@ export declare function openWalletReadContext(options: {
15
15
  secretProvider?: WalletSecretProvider;
16
16
  walletControlLockHeld?: boolean;
17
17
  startupTimeoutMs?: number;
18
+ expectedIndexerBinaryVersion?: string | null;
18
19
  now?: number;
19
20
  paths?: WalletRuntimePaths;
20
21
  }): Promise<WalletReadContext>;
@@ -1,6 +1,6 @@
1
1
  import { access, constants } from "node:fs/promises";
2
2
  import { deserializeIndexerState, loadBundledGenesisParameters } from "@cogcoin/indexer";
3
- import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, readSnapshotWithRetry, } from "../../bitcoind/indexer-daemon.js";
3
+ import { attachOrStartIndexerDaemon, INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED, probeIndexerDaemon, readObservedIndexerDaemonStatus, readSnapshotWithRetry, } from "../../bitcoind/indexer-daemon.js";
4
4
  import { createRpcClient } from "../../bitcoind/node.js";
5
5
  import { UNINITIALIZED_WALLET_ROOT_ID } from "../../bitcoind/service-paths.js";
6
6
  import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } from "../../bitcoind/service.js";
@@ -245,6 +245,11 @@ function mapIndexerStartupError(message) {
245
245
  health: "unavailable",
246
246
  message: "The live indexer daemon socket responded with an invalid or incomplete protocol exchange.",
247
247
  };
248
+ case INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED:
249
+ return {
250
+ health: "failed",
251
+ message: "The managed indexer daemon could not recover automatic background follow.",
252
+ };
248
253
  default:
249
254
  return {
250
255
  health: "unavailable",
@@ -532,17 +537,15 @@ export async function openWalletReadContext(options) {
532
537
  dataDir: options.dataDir,
533
538
  walletRootId,
534
539
  });
535
- if (probe.compatibility === "compatible") {
536
- daemonClient = probe.client;
537
- observedDaemonStatus = probe.status;
538
- indexerSource = "probe";
539
- }
540
- else if (probe.compatibility === "unreachable") {
540
+ if (probe.compatibility === "compatible" || probe.compatibility === "unreachable") {
541
+ await probe.client?.close().catch(() => undefined);
541
542
  daemonClient = await attachOrStartIndexerDaemon({
542
543
  dataDir: options.dataDir,
543
544
  databasePath: options.databasePath,
544
545
  walletRootId,
545
546
  startupTimeoutMs,
547
+ ensureBackgroundFollow: true,
548
+ expectedBinaryVersion: options.expectedIndexerBinaryVersion,
546
549
  });
547
550
  }
548
551
  else {
@@ -567,6 +570,11 @@ export async function openWalletReadContext(options) {
567
570
  }
568
571
  catch (error) {
569
572
  daemonError = error instanceof Error ? error.message : String(error);
573
+ if (daemonError === INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED) {
574
+ await daemonClient?.close().catch(() => undefined);
575
+ await node.handle?.stop().catch(() => undefined);
576
+ throw error;
577
+ }
570
578
  if (observedDaemonStatus === null) {
571
579
  observedDaemonStatus = await readObservedIndexerDaemonStatus({
572
580
  dataDir: options.dataDir,
@@ -1,6 +1,9 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
2
  import net from "node:net";
3
3
  import { rm } from "node:fs/promises";
4
+ function shouldRemoveAgentEndpointPath(endpoint) {
5
+ return !endpoint.startsWith("\\\\.\\pipe\\");
6
+ }
4
7
  function zeroizeBuffer(buffer) {
5
8
  if (buffer != null) {
6
9
  buffer.fill(0);
@@ -67,7 +70,7 @@ async function main() {
67
70
  }
68
71
  zeroizeBuffer(key);
69
72
  key = Buffer.alloc(0);
70
- if (!bootstrap.endpoint.startsWith("\\\\.\\")) {
73
+ if (shouldRemoveAgentEndpointPath(bootstrap.endpoint)) {
71
74
  await rm(bootstrap.endpoint, { force: true }).catch(() => undefined);
72
75
  }
73
76
  process.exit(0);
@@ -33,13 +33,18 @@ function resolveClientPasswordStatePath(directoryPath) {
33
33
  function resolveClientPasswordRotationJournalPath(directoryPath) {
34
34
  return join(directoryPath, "client-password-rotation.json");
35
35
  }
36
- function resolveAgentEndpoint(platform, stateRoot) {
36
+ function resolveAgentEndpoint(stateRoot) {
37
37
  const hash = createHash("sha256").update(stateRoot).digest("hex").slice(0, 24);
38
- if (platform === "win32") {
38
+ // Wallet provider tests simulate foreign platforms, but the local agent transport
39
+ // still has to follow the real host runtime.
40
+ if (process.platform === "win32") {
39
41
  return `\\\\.\\pipe\\cogcoin-client-password-${hash}`;
40
42
  }
41
43
  return join(tmpdir(), `cogcoin-client-password-${hash}.sock`);
42
44
  }
45
+ function shouldRemoveAgentEndpointPath(endpoint) {
46
+ return !endpoint.startsWith("\\\\.\\pipe\\");
47
+ }
43
48
  function isMissingFileError(error) {
44
49
  return error instanceof Error
45
50
  && "code" in error
@@ -352,7 +357,7 @@ async function openAgentConnection(endpoint) {
352
357
  });
353
358
  }
354
359
  async function requestAgent(options, request) {
355
- const endpoint = resolveAgentEndpoint(options.platform, options.stateRoot);
360
+ const endpoint = resolveAgentEndpoint(options.stateRoot);
356
361
  const socket = await openAgentConnection(endpoint);
357
362
  return await new Promise((resolve, reject) => {
358
363
  let received = "";
@@ -418,8 +423,9 @@ async function requestAgentOrNull(options, request) {
418
423
  ? String(error.code ?? "")
419
424
  : "";
420
425
  if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE") {
421
- if (options.platform !== "win32") {
422
- await rm(resolveAgentEndpoint(options.platform, options.stateRoot), { force: true }).catch(() => undefined);
426
+ const endpoint = resolveAgentEndpoint(options.stateRoot);
427
+ if (shouldRemoveAgentEndpointPath(endpoint)) {
428
+ await rm(endpoint, { force: true }).catch(() => undefined);
423
429
  }
424
430
  return null;
425
431
  }
@@ -441,8 +447,9 @@ export async function readClientPasswordSessionStatus(options) {
441
447
  }
442
448
  export async function lockClientPasswordSession(options) {
443
449
  await requestAgentOrNull(options, { command: "lock" }).catch(() => null);
444
- if (options.platform !== "win32") {
445
- await rm(resolveAgentEndpoint(options.platform, options.stateRoot), { force: true }).catch(() => undefined);
450
+ const endpoint = resolveAgentEndpoint(options.stateRoot);
451
+ if (shouldRemoveAgentEndpointPath(endpoint)) {
452
+ await rm(endpoint, { force: true }).catch(() => undefined);
446
453
  }
447
454
  return {
448
455
  unlocked: false,
@@ -499,7 +506,7 @@ async function startClientPasswordSession(options) {
499
506
  }
500
507
  async function startClientPasswordSessionWithExpiry(options) {
501
508
  const unlockUntilUnixMs = options.unlockUntilUnixMs;
502
- const endpoint = resolveAgentEndpoint(options.platform, options.stateRoot);
509
+ const endpoint = resolveAgentEndpoint(options.stateRoot);
503
510
  await lockClientPasswordSession(options).catch(() => undefined);
504
511
  await mkdir(options.runtimeRoot, { recursive: true }).catch(() => undefined);
505
512
  const child = spawn(process.execPath, [fileURLToPath(new URL("./client-password-agent.js", import.meta.url)), endpoint, String(unlockUntilUnixMs)], {
@@ -435,7 +435,7 @@ export async function buildWalletMutationTransaction(options) {
435
435
  const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.fixedInputs, options.plan.outputs, 0, {
436
436
  add_inputs: true,
437
437
  include_unsafe: false,
438
- minconf: 1,
438
+ minconf: availableFundingMinConf,
439
439
  changeAddress: options.plan.changeAddress,
440
440
  ...(options.plan.changePosition == null ? {} : { changePosition: options.plan.changePosition }),
441
441
  lockUnspents: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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",
@@ -59,7 +59,8 @@
59
59
  "build": "rm -rf dist && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node ./scripts/copy-static-assets.mjs build",
60
60
  "generate:default-snapshot-chunk-manifest": "node scripts/generate-default-snapshot-chunk-manifest.mjs",
61
61
  "verify:default-snapshot-chunk-manifest": "node scripts/generate-default-snapshot-chunk-manifest.mjs --check",
62
- "test": "rm -rf .test-dist && node ./node_modules/typescript/bin/tsc -p tsconfig.test.json && node ./scripts/copy-static-assets.mjs test && node --test .test-dist/test/*.test.js"
62
+ "test": "rm -rf .test-dist && node ./node_modules/typescript/bin/tsc -p tsconfig.test.json && node ./scripts/copy-static-assets.mjs test && node --test .test-dist/test/*.test.js",
63
+ "test:mining": "rm -rf .test-dist && node ./node_modules/typescript/bin/tsc -p tsconfig.test-mining.json && node ./scripts/copy-static-assets.mjs test && node --test .test-dist/test/mining*.test.js"
63
64
  },
64
65
  "dependencies": {
65
66
  "@cogcoin/bitcoin": "30.2.0",