@cogcoin/client 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +2 -1
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -0
  3. package/dist/bitcoind/indexer-daemon.js +58 -8
  4. package/dist/bitcoind/retryable-rpc.js +3 -0
  5. package/dist/bitcoind/service.d.ts +1 -0
  6. package/dist/bitcoind/service.js +31 -9
  7. package/dist/cli/commands/mining-admin.js +9 -0
  8. package/dist/cli/commands/update.d.ts +2 -0
  9. package/dist/cli/commands/update.js +101 -0
  10. package/dist/cli/context.js +31 -0
  11. package/dist/cli/mining-format.js +28 -0
  12. package/dist/cli/mining-json.js +6 -0
  13. package/dist/cli/output.js +50 -2
  14. package/dist/cli/parse.d.ts +1 -1
  15. package/dist/cli/parse.js +5 -0
  16. package/dist/cli/prompt.js +109 -0
  17. package/dist/cli/read-json.d.ts +13 -0
  18. package/dist/cli/read-json.js +17 -0
  19. package/dist/cli/runner.js +4 -0
  20. package/dist/cli/types.d.ts +6 -1
  21. package/dist/cli/update-notifier.js +7 -222
  22. package/dist/cli/update-service.d.ts +44 -0
  23. package/dist/cli/update-service.js +218 -0
  24. package/dist/client/initialization.js +5 -0
  25. package/dist/wallet/lifecycle.d.ts +10 -0
  26. package/dist/wallet/lifecycle.js +6 -0
  27. package/dist/wallet/mining/config.js +13 -3
  28. package/dist/wallet/mining/control.d.ts +2 -1
  29. package/dist/wallet/mining/control.js +143 -19
  30. package/dist/wallet/mining/index.d.ts +1 -1
  31. package/dist/wallet/mining/provider-model.d.ts +30 -0
  32. package/dist/wallet/mining/provider-model.js +134 -0
  33. package/dist/wallet/mining/runner.d.ts +98 -3
  34. package/dist/wallet/mining/runner.js +493 -95
  35. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  36. package/dist/wallet/mining/sentences.d.ts +2 -2
  37. package/dist/wallet/mining/sentences.js +25 -2
  38. package/dist/wallet/mining/types.d.ts +9 -1
  39. package/dist/wallet/mining/visualizer.js +28 -5
  40. package/dist/wallet/read/context.js +3 -0
  41. package/dist/wallet/reset.js +1 -0
  42. package/dist/wallet/tx/anchor.js +1 -0
  43. package/dist/wallet/tx/bitcoin-transfer.js +1 -0
  44. package/dist/wallet/tx/cog.js +3 -0
  45. package/dist/wallet/tx/domain-admin.js +1 -0
  46. package/dist/wallet/tx/domain-market.js +3 -0
  47. package/dist/wallet/tx/field.js +1 -0
  48. package/dist/wallet/tx/register.js +1 -0
  49. package/dist/wallet/tx/reputation.js +1 -0
  50. package/package.json +3 -2
@@ -0,0 +1,218 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { writeJsonFileAtomic } from "../wallet/fs/atomic.js";
3
+ export const UPDATE_CHECK_CACHE_SCHEMA_VERSION = 1;
4
+ export const UPDATE_CHECK_MAX_AGE_MS = 24 * 60 * 60 * 1000;
5
+ export const PASSIVE_UPDATE_CHECK_TIMEOUT_MS = 500;
6
+ export const EXPLICIT_UPDATE_CHECK_TIMEOUT_MS = 5_000;
7
+ export const UPDATE_CHECK_URL = "https://registry.npmjs.org/@cogcoin/client/latest";
8
+ export const CLI_INSTALL_COMMAND = "npm install -g @cogcoin/client";
9
+ export function createEmptyUpdateCheckCache() {
10
+ return {
11
+ schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
12
+ lastCheckedAtUnixMs: 0,
13
+ latestVersion: null,
14
+ lastNotifiedCurrentVersion: null,
15
+ lastNotifiedLatestVersion: null,
16
+ lastNotifiedAtUnixMs: null,
17
+ };
18
+ }
19
+ export function parseSemver(version) {
20
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version.trim());
21
+ if (match === null) {
22
+ return null;
23
+ }
24
+ const prerelease = match[4] === undefined
25
+ ? []
26
+ : match[4].split(".").map((raw) => ({
27
+ raw,
28
+ numeric: /^(0|[1-9]\d*)$/.test(raw),
29
+ numericValue: /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : null,
30
+ }));
31
+ return {
32
+ major: Number(match[1]),
33
+ minor: Number(match[2]),
34
+ patch: Number(match[3]),
35
+ prerelease,
36
+ };
37
+ }
38
+ export function compareSemver(left, right) {
39
+ const leftParsed = parseSemver(left);
40
+ const rightParsed = parseSemver(right);
41
+ if (leftParsed === null || rightParsed === null) {
42
+ return null;
43
+ }
44
+ if (leftParsed.major !== rightParsed.major) {
45
+ return leftParsed.major > rightParsed.major ? 1 : -1;
46
+ }
47
+ if (leftParsed.minor !== rightParsed.minor) {
48
+ return leftParsed.minor > rightParsed.minor ? 1 : -1;
49
+ }
50
+ if (leftParsed.patch !== rightParsed.patch) {
51
+ return leftParsed.patch > rightParsed.patch ? 1 : -1;
52
+ }
53
+ if (leftParsed.prerelease.length === 0 && rightParsed.prerelease.length === 0) {
54
+ return 0;
55
+ }
56
+ if (leftParsed.prerelease.length === 0) {
57
+ return 1;
58
+ }
59
+ if (rightParsed.prerelease.length === 0) {
60
+ return -1;
61
+ }
62
+ const maxLength = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length);
63
+ for (let index = 0; index < maxLength; index += 1) {
64
+ const leftIdentifier = leftParsed.prerelease[index];
65
+ const rightIdentifier = rightParsed.prerelease[index];
66
+ if (leftIdentifier === undefined) {
67
+ return -1;
68
+ }
69
+ if (rightIdentifier === undefined) {
70
+ return 1;
71
+ }
72
+ if (leftIdentifier.numeric && rightIdentifier.numeric) {
73
+ if (leftIdentifier.numericValue !== rightIdentifier.numericValue) {
74
+ return leftIdentifier.numericValue > rightIdentifier.numericValue ? 1 : -1;
75
+ }
76
+ continue;
77
+ }
78
+ if (leftIdentifier.numeric !== rightIdentifier.numeric) {
79
+ return leftIdentifier.numeric ? -1 : 1;
80
+ }
81
+ if (leftIdentifier.raw !== rightIdentifier.raw) {
82
+ return leftIdentifier.raw > rightIdentifier.raw ? 1 : -1;
83
+ }
84
+ }
85
+ return 0;
86
+ }
87
+ export function isUpdateCheckDisabled(env) {
88
+ const raw = env.COGCOIN_DISABLE_UPDATE_CHECK;
89
+ if (raw === undefined) {
90
+ return false;
91
+ }
92
+ const normalized = raw.trim().toLowerCase();
93
+ return normalized === "1" || normalized === "true" || normalized === "yes";
94
+ }
95
+ function normalizeUpdateCheckCache(parsed) {
96
+ if (typeof parsed !== "object" || parsed === null) {
97
+ return null;
98
+ }
99
+ const candidate = parsed;
100
+ if (candidate.schemaVersion !== UPDATE_CHECK_CACHE_SCHEMA_VERSION) {
101
+ return null;
102
+ }
103
+ return {
104
+ schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
105
+ lastCheckedAtUnixMs: typeof candidate.lastCheckedAtUnixMs === "number" ? candidate.lastCheckedAtUnixMs : 0,
106
+ latestVersion: typeof candidate.latestVersion === "string" ? candidate.latestVersion : null,
107
+ lastNotifiedCurrentVersion: typeof candidate.lastNotifiedCurrentVersion === "string"
108
+ ? candidate.lastNotifiedCurrentVersion
109
+ : null,
110
+ lastNotifiedLatestVersion: typeof candidate.lastNotifiedLatestVersion === "string"
111
+ ? candidate.lastNotifiedLatestVersion
112
+ : null,
113
+ lastNotifiedAtUnixMs: typeof candidate.lastNotifiedAtUnixMs === "number"
114
+ ? candidate.lastNotifiedAtUnixMs
115
+ : null,
116
+ lastCheckErrorKind: typeof candidate.lastCheckErrorKind === "string"
117
+ ? candidate.lastCheckErrorKind
118
+ : undefined,
119
+ };
120
+ }
121
+ export async function loadUpdateCheckCache(cachePath) {
122
+ try {
123
+ const raw = await readFile(cachePath, "utf8");
124
+ return normalizeUpdateCheckCache(JSON.parse(raw));
125
+ }
126
+ catch (error) {
127
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
128
+ return null;
129
+ }
130
+ return null;
131
+ }
132
+ }
133
+ export function shouldRefreshUpdateCheck(cache, now) {
134
+ return now - cache.lastCheckedAtUnixMs >= UPDATE_CHECK_MAX_AGE_MS;
135
+ }
136
+ export function applyUpdateCheckResult(cache, result, now) {
137
+ return {
138
+ ...cache,
139
+ lastCheckedAtUnixMs: now,
140
+ latestVersion: result.kind === "success" ? result.latestVersion : cache.latestVersion,
141
+ lastCheckErrorKind: result.kind === "success" ? undefined : result.errorKind,
142
+ };
143
+ }
144
+ export function recordUpdateNotification(cache, currentVersion, latestVersion, now) {
145
+ return {
146
+ ...cache,
147
+ lastNotifiedCurrentVersion: currentVersion,
148
+ lastNotifiedLatestVersion: latestVersion,
149
+ lastNotifiedAtUnixMs: now,
150
+ };
151
+ }
152
+ export async function fetchLatestPublishedVersion(fetchImpl, options = {}) {
153
+ const controller = new AbortController();
154
+ const timer = setTimeout(() => {
155
+ controller.abort();
156
+ }, options.timeoutMs ?? PASSIVE_UPDATE_CHECK_TIMEOUT_MS);
157
+ try {
158
+ const response = await fetchImpl(UPDATE_CHECK_URL, {
159
+ headers: {
160
+ accept: "application/json",
161
+ },
162
+ signal: controller.signal,
163
+ });
164
+ if (!response.ok) {
165
+ return {
166
+ kind: "failure",
167
+ errorKind: `http_${response.status}`,
168
+ };
169
+ }
170
+ let payload;
171
+ try {
172
+ payload = await response.json();
173
+ }
174
+ catch {
175
+ return {
176
+ kind: "failure",
177
+ errorKind: "invalid_json",
178
+ };
179
+ }
180
+ const latestVersion = typeof payload.version === "string"
181
+ ? payload.version
182
+ : null;
183
+ if (latestVersion === null) {
184
+ return {
185
+ kind: "failure",
186
+ errorKind: "invalid_payload",
187
+ };
188
+ }
189
+ if (parseSemver(latestVersion) === null) {
190
+ return {
191
+ kind: "failure",
192
+ errorKind: "invalid_semver",
193
+ };
194
+ }
195
+ return {
196
+ kind: "success",
197
+ latestVersion,
198
+ };
199
+ }
200
+ catch (error) {
201
+ if (error instanceof Error && error.name === "AbortError") {
202
+ return {
203
+ kind: "failure",
204
+ errorKind: "timeout",
205
+ };
206
+ }
207
+ return {
208
+ kind: "failure",
209
+ errorKind: "network",
210
+ };
211
+ }
212
+ finally {
213
+ clearTimeout(timer);
214
+ }
215
+ }
216
+ export async function persistUpdateCheckCache(cachePath, cache) {
217
+ await writeJsonFileAtomic(cachePath, cache);
218
+ }
@@ -22,6 +22,9 @@ export async function initializeState(store, genesisParameters) {
22
22
  if (tip !== null) {
23
23
  throw new Error("client_store_tip_without_snapshot");
24
24
  }
25
+ // Repair orphaned rewind rows from previously interrupted writers so the
26
+ // next replay pass does not collide on a stale future height.
27
+ await store.deleteBlockRecordsAbove(-1);
25
28
  return {
26
29
  state: createInitialState(genesisParameters),
27
30
  tip: null,
@@ -36,6 +39,7 @@ export async function initializeState(store, genesisParameters) {
36
39
  };
37
40
  }
38
41
  if (tip === null) {
42
+ await store.deleteBlockRecordsAbove(snapshot.height);
39
43
  return {
40
44
  state,
41
45
  tip: {
@@ -49,5 +53,6 @@ export async function initializeState(store, genesisParameters) {
49
53
  if (tip.height !== snapshot.height || tip.blockHashHex !== snapshot.blockHashHex) {
50
54
  throw new Error("client_store_snapshot_tip_mismatch");
51
55
  }
56
+ await store.deleteBlockRecordsAbove(tip.height);
52
57
  return { state, tip };
53
58
  }
@@ -10,6 +10,16 @@ export interface WalletPrompter {
10
10
  writeLine(message: string): void;
11
11
  prompt(message: string): Promise<string>;
12
12
  promptHidden?(message: string): Promise<string>;
13
+ selectOption?(options: {
14
+ message: string;
15
+ options: Array<{
16
+ label: string;
17
+ description?: string | null;
18
+ value: string;
19
+ }>;
20
+ initialValue?: string | null;
21
+ footer?: string | null;
22
+ }): Promise<string>;
13
23
  clearSensitiveDisplay?(scope: "mnemonic-reveal" | "restore-mnemonic-entry"): void | Promise<void>;
14
24
  }
15
25
  export interface WalletInitializationResult {
@@ -174,6 +174,7 @@ async function normalizeLoadedWalletStateIfNeeded(options) {
174
174
  dataDir: options.dataDir,
175
175
  chain: "main",
176
176
  startHeight: 0,
177
+ serviceLifetime: "ephemeral",
177
178
  walletRootId: state.walletRootId,
178
179
  });
179
180
  try {
@@ -270,6 +271,7 @@ async function recreateManagedCoreWalletReplica(state, provider, paths, dataDir,
270
271
  dataDir,
271
272
  chain: "main",
272
273
  startHeight: 0,
274
+ serviceLifetime: "ephemeral",
273
275
  walletRootId: state.walletRootId,
274
276
  managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
275
277
  });
@@ -802,6 +804,7 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
802
804
  dataDir,
803
805
  chain: "main",
804
806
  startHeight: 0,
807
+ serviceLifetime: "ephemeral",
805
808
  walletRootId: state.walletRootId,
806
809
  managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
807
810
  });
@@ -887,6 +890,7 @@ export async function verifyManagedCoreWalletReplica(state, dataDir, dependencie
887
890
  dataDir,
888
891
  chain: "main",
889
892
  startHeight: 0,
893
+ serviceLifetime: "ephemeral",
890
894
  walletRootId: state.walletRootId,
891
895
  });
892
896
  const rpc = (dependencies.rpcFactory ?? createRpcClient)(node.rpc);
@@ -1256,6 +1260,7 @@ export async function deleteImportedWalletSeed(options) {
1256
1260
  dataDir: options.dataDir,
1257
1261
  chain: "main",
1258
1262
  startHeight: 0,
1263
+ serviceLifetime: "ephemeral",
1259
1264
  });
1260
1265
  const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1261
1266
  const walletName = sanitizeWalletName(seedRecord.walletRootId);
@@ -1418,6 +1423,7 @@ export async function repairWallet(options) {
1418
1423
  dataDir: options.dataDir,
1419
1424
  chain: "main",
1420
1425
  startHeight: 0,
1426
+ serviceLifetime: "ephemeral",
1421
1427
  walletRootId: repairedState.walletRootId,
1422
1428
  });
1423
1429
  const bitcoindRpc = (options.rpcFactory ?? createRpcClient)(bitcoindHandle.rpc);
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { writeJsonFileAtomic } from "../fs/atomic.js";
3
3
  import { decryptJsonWithSecretProvider, encryptJsonWithSecretProvider } from "../state/crypto.js";
4
+ import { normalizeMiningProviderConfigRecord } from "./provider-model.js";
4
5
  function createEmptyClientConfig() {
5
6
  return {
6
7
  schemaVersion: 1,
@@ -9,10 +10,19 @@ function createEmptyClientConfig() {
9
10
  },
10
11
  };
11
12
  }
13
+ function normalizeClientConfig(config) {
14
+ return {
15
+ ...config,
16
+ mining: {
17
+ ...config.mining,
18
+ builtIn: config.mining.builtIn === null ? null : normalizeMiningProviderConfigRecord(config.mining.builtIn),
19
+ },
20
+ };
21
+ }
12
22
  export async function loadClientConfig(options) {
13
23
  try {
14
24
  const raw = await readFile(options.path, "utf8");
15
- return await decryptJsonWithSecretProvider(JSON.parse(raw), options.provider);
25
+ return normalizeClientConfig(await decryptJsonWithSecretProvider(JSON.parse(raw), options.provider));
16
26
  }
17
27
  catch (error) {
18
28
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -22,7 +32,7 @@ export async function loadClientConfig(options) {
22
32
  }
23
33
  }
24
34
  export async function saveClientConfig(options) {
25
- const envelope = await encryptJsonWithSecretProvider(options.config, options.provider, options.secretReference, {
35
+ const envelope = await encryptJsonWithSecretProvider(normalizeClientConfig(options.config), options.provider, options.secretReference, {
26
36
  format: "cogcoin-client-config",
27
37
  });
28
38
  await writeJsonFileAtomic(options.path, envelope, { mode: 0o600 });
@@ -33,7 +43,7 @@ export async function saveBuiltInMiningProviderConfig(options) {
33
43
  provider: options.provider,
34
44
  }).catch(() => null);
35
45
  const nextConfig = existing ?? createEmptyClientConfig();
36
- nextConfig.mining.builtIn = options.config;
46
+ nextConfig.mining.builtIn = normalizeMiningProviderConfigRecord(options.config);
37
47
  await saveClientConfig({
38
48
  path: options.path,
39
49
  provider: options.provider,
@@ -2,7 +2,7 @@ import type { WalletPrompter } from "../lifecycle.js";
2
2
  import { type WalletRuntimePaths } from "../runtime.js";
3
3
  import { type WalletSecretProvider } from "../state/provider.js";
4
4
  import type { WalletBitcoindStatus, WalletIndexerStatus, WalletLocalStateStatus, WalletNodeStatus } from "../read/types.js";
5
- import type { MiningControlPlaneView, MiningEventRecord, MiningRuntimeStatusV1 } from "./types.js";
5
+ import type { MiningControlPlaneView, MiningEventRecord, MiningProviderConfigRecord, MiningRuntimeStatusV1 } from "./types.js";
6
6
  export declare function inspectMiningControlPlane(options: {
7
7
  provider?: WalletSecretProvider;
8
8
  localState: WalletLocalStateStatus;
@@ -23,6 +23,7 @@ export declare function refreshMiningRuntimeStatus(options: {
23
23
  nowUnixMs?: number;
24
24
  paths?: WalletRuntimePaths;
25
25
  }): Promise<MiningControlPlaneView>;
26
+ export declare function promptForMiningProviderConfigForTesting(prompter: WalletPrompter, eligibleRootCount: number): Promise<MiningProviderConfigRecord>;
26
27
  export declare function setupBuiltInMining(options: {
27
28
  provider?: WalletSecretProvider;
28
29
  prompter: WalletPrompter;
@@ -2,11 +2,13 @@ import { acquireFileLock } from "../fs/lock.js";
2
2
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
3
3
  import { createDefaultWalletSecretProvider, createWalletSecretReference, } from "../state/provider.js";
4
4
  import { loadWalletState } from "../state/storage.js";
5
+ import { isRootDomainName } from "../read/filter.js";
5
6
  import { appendMiningEvent, getLastMiningEventTimestamp, loadMiningRuntimeStatus, readMiningEvents, saveMiningRuntimeStatus, followMiningEvents, } from "./runtime-artifacts.js";
6
7
  import { requestMiningGenerationPreemption } from "./coordination.js";
7
8
  import { normalizeMiningPublishState, normalizeMiningStateRecord } from "./state.js";
8
9
  import { loadClientConfig, saveBuiltInMiningProviderConfig } from "./config.js";
9
10
  import { MINING_WORKER_API_VERSION, MINING_WORKER_HEARTBEAT_STALE_MS, } from "./constants.js";
11
+ import { estimateBuiltInModelDailyCost, getBuiltInProviderModelCatalog, getRecommendedBuiltInProviderModel, MINING_MODEL_DAILY_COST_ESTIMATE_ASSUMPTION, resolveBuiltInProviderSelection, } from "./provider-model.js";
10
12
  function createMiningEvent(kind, message, options = {}) {
11
13
  return {
12
14
  schemaVersion: 1,
@@ -23,8 +25,14 @@ function buildProviderInspection(options) {
23
25
  provider: null,
24
26
  status: "error",
25
27
  message: options.error,
28
+ modelId: null,
29
+ effectiveModel: null,
26
30
  modelOverride: null,
31
+ modelSelectionSource: null,
32
+ usingDefaultModel: null,
27
33
  extraPromptConfigured: false,
34
+ estimatedDailyCostUsd: null,
35
+ estimatedDailyCostDisplay: null,
28
36
  };
29
37
  }
30
38
  if (options.config === null) {
@@ -33,19 +41,50 @@ function buildProviderInspection(options) {
33
41
  provider: null,
34
42
  status: "missing",
35
43
  message: "Built-in mining provider is not configured yet.",
44
+ modelId: null,
45
+ effectiveModel: null,
36
46
  modelOverride: null,
47
+ modelSelectionSource: null,
48
+ usingDefaultModel: null,
37
49
  extraPromptConfigured: false,
50
+ estimatedDailyCostUsd: null,
51
+ estimatedDailyCostDisplay: null,
38
52
  };
39
53
  }
54
+ const selection = resolveBuiltInProviderSelection(options.config);
55
+ const estimate = options.eligibleRootCount === null
56
+ ? null
57
+ : estimateBuiltInModelDailyCost(options.config.provider, selection.modelId, options.eligibleRootCount);
40
58
  return {
41
59
  configured: true,
42
60
  provider: options.config.provider,
43
61
  status: "ready",
44
62
  message: null,
63
+ modelId: selection.modelId,
64
+ effectiveModel: selection.effectiveModel,
45
65
  modelOverride: options.config.modelOverride,
66
+ modelSelectionSource: selection.modelSelectionSource,
67
+ usingDefaultModel: selection.usingDefaultModel,
46
68
  extraPromptConfigured: options.config.extraPrompt !== null && options.config.extraPrompt.length > 0,
69
+ estimatedDailyCostUsd: estimate?.estimatedDailyCostUsd ?? null,
70
+ estimatedDailyCostDisplay: estimate?.estimatedDailyCostDisplay ?? null,
47
71
  };
48
72
  }
73
+ function countEligibleAnchoredRoots(localState) {
74
+ const state = localState.state;
75
+ if (state === null || state === undefined) {
76
+ return null;
77
+ }
78
+ let count = 0;
79
+ for (const domain of state.domains) {
80
+ if (isRootDomainName(domain.name)
81
+ && domain.canonicalChainStatus === "anchored"
82
+ && domain.currentOwnerScriptPubKeyHex === state.funding.scriptPubKeyHex) {
83
+ count += 1;
84
+ }
85
+ }
86
+ return count;
87
+ }
49
88
  async function isProcessAlive(pid) {
50
89
  if (pid === null) {
51
90
  return false;
@@ -61,10 +100,13 @@ async function isProcessAlive(pid) {
61
100
  return true;
62
101
  }
63
102
  }
64
- function mapProviderState(provider, localState) {
103
+ function mapProviderState(provider, localState, existingRuntime) {
65
104
  const miningState = localState.state?.miningState === undefined
66
105
  ? null
67
106
  : normalizeMiningStateRecord(localState.state.miningState);
107
+ if (existingRuntime?.currentPhase === "waiting-provider" && existingRuntime.providerState !== null) {
108
+ return existingRuntime.providerState;
109
+ }
68
110
  if (miningState?.state === "paused" && miningState.pauseReason?.includes("rate-limit")) {
69
111
  return "rate-limited";
70
112
  }
@@ -167,7 +209,7 @@ async function buildMiningRuntimeSnapshot(options) {
167
209
  localState: options.localState,
168
210
  nowUnixMs: options.nowUnixMs,
169
211
  });
170
- const providerState = mapProviderState(options.provider, options.localState);
212
+ const providerState = mapProviderState(options.provider, options.localState, options.existingRuntime);
171
213
  const indexerDaemonState = mapIndexerDaemonState(options.indexer);
172
214
  const corePublishState = mapCorePublishState(options.nodeHealth, options.nodeStatus);
173
215
  const existing = options.existingRuntime;
@@ -289,7 +331,10 @@ export async function inspectMiningControlPlane(options) {
289
331
  paths,
290
332
  provider,
291
333
  });
292
- const providerInspection = buildProviderInspection(providerConfig);
334
+ const providerInspection = buildProviderInspection({
335
+ ...providerConfig,
336
+ eligibleRootCount: countEligibleAnchoredRoots(options.localState),
337
+ });
293
338
  const existingRuntime = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
294
339
  const lastEventAtUnixMs = await getLastMiningEventTimestamp(paths.miningEventsPath).catch(() => null);
295
340
  const nodeBestHeight = options.nodeStatus?.nodeBestHeight ?? null;
@@ -323,6 +368,20 @@ function normalizeProviderChoice(raw) {
323
368
  const value = raw.trim().toLowerCase();
324
369
  return value === "openai" || value === "anthropic" ? value : null;
325
370
  }
371
+ function describeModelSelectionSource(source) {
372
+ switch (source) {
373
+ case "catalog":
374
+ return "catalog";
375
+ case "custom":
376
+ return "custom";
377
+ case "legacy-default":
378
+ return "legacy-default";
379
+ case "legacy-custom":
380
+ return "legacy-custom";
381
+ default:
382
+ throw new Error(`unsupported_model_selection_source:${String(source)}`);
383
+ }
384
+ }
326
385
  function writeBuiltInMiningProviderDisclosure(prompter) {
327
386
  prompter.writeLine("Built-in mining provider disclosure:");
328
387
  prompter.writeLine("The built-in mining provider will send the following to the selected provider:");
@@ -332,27 +391,83 @@ function writeBuiltInMiningProviderDisclosure(prompter) {
332
391
  prompter.writeLine("- referenced previous-block hash");
333
392
  prompter.writeLine("- optional extra prompt when configured");
334
393
  }
335
- async function promptForMiningProviderConfig(prompter) {
394
+ async function promptForMiningProviderModelSelectionFallback(prompter, options) {
395
+ prompter.writeLine(options.message);
396
+ for (const [index, option] of options.options.entries()) {
397
+ const description = option.description == null || option.description.length === 0
398
+ ? ""
399
+ : ` - ${option.description}`;
400
+ prompter.writeLine(`${index + 1}. ${option.label}${description}`);
401
+ }
402
+ if (options.footer != null && options.footer.length > 0) {
403
+ prompter.writeLine(options.footer);
404
+ }
405
+ while (true) {
406
+ const answer = (await prompter.prompt(`Choice [1-${options.options.length}]: `)).trim();
407
+ if (/^(q|quit|esc|escape)$/i.test(answer)) {
408
+ throw new Error("mining_setup_canceled");
409
+ }
410
+ const choice = Number.parseInt(answer, 10);
411
+ if (Number.isInteger(choice) && choice >= 1 && choice <= options.options.length) {
412
+ return options.options[choice - 1].value;
413
+ }
414
+ prompter.writeLine(`Enter a number from 1 to ${options.options.length}, or q to cancel.`);
415
+ }
416
+ }
417
+ async function promptForMiningProviderConfig(prompter, eligibleRootCount) {
336
418
  writeBuiltInMiningProviderDisclosure(prompter);
337
419
  const providerInput = await prompter.prompt("Provider (openai/anthropic): ");
338
420
  const provider = normalizeProviderChoice(providerInput);
339
421
  if (provider === null) {
340
422
  throw new Error("mining_setup_invalid_provider");
341
423
  }
424
+ const selectorOptions = {
425
+ message: "Choose the mining model:",
426
+ options: [
427
+ ...getBuiltInProviderModelCatalog(provider).map((entry) => {
428
+ const estimate = estimateBuiltInModelDailyCost(provider, entry.modelId, eligibleRootCount);
429
+ return {
430
+ label: entry.label,
431
+ description: `${entry.modelId} - ${estimate?.estimatedDailyCostDisplay ?? "n/a"}`,
432
+ value: entry.modelId,
433
+ };
434
+ }),
435
+ {
436
+ label: "Custom model ID...",
437
+ description: null,
438
+ value: "custom",
439
+ },
440
+ ],
441
+ initialValue: getRecommendedBuiltInProviderModel(provider),
442
+ footer: MINING_MODEL_DAILY_COST_ESTIMATE_ASSUMPTION,
443
+ };
444
+ const selectedModelId = prompter.selectOption == null
445
+ ? await promptForMiningProviderModelSelectionFallback(prompter, selectorOptions)
446
+ : await prompter.selectOption(selectorOptions);
447
+ const modelSelectionSource = selectedModelId === "custom" ? "custom" : "catalog";
448
+ const modelOverride = selectedModelId === "custom"
449
+ ? (await prompter.prompt("Custom model ID: ")).trim()
450
+ : selectedModelId;
451
+ if (modelOverride.length === 0) {
452
+ throw new Error("mining_setup_missing_model_id");
453
+ }
342
454
  const apiKey = (await prompter.prompt("API key: ")).trim();
343
455
  if (apiKey.length === 0) {
344
456
  throw new Error("mining_setup_missing_api_key");
345
457
  }
346
458
  const extraPrompt = (await prompter.prompt("Extra prompt (optional, blank for none): ")).trim();
347
- const modelOverride = (await prompter.prompt("Model override (optional, blank for default): ")).trim();
348
459
  return {
349
460
  provider,
350
461
  apiKey,
351
462
  extraPrompt: extraPrompt.length === 0 ? null : extraPrompt,
352
- modelOverride: modelOverride.length === 0 ? null : modelOverride,
463
+ modelOverride,
464
+ modelSelectionSource,
353
465
  updatedAtUnixMs: Date.now(),
354
466
  };
355
467
  }
468
+ export async function promptForMiningProviderConfigForTesting(prompter, eligibleRootCount) {
469
+ return await promptForMiningProviderConfig(prompter, eligibleRootCount);
470
+ }
356
471
  export async function setupBuiltInMining(options) {
357
472
  if (!options.prompter.isInteractive) {
358
473
  throw new Error("mine_setup_requires_tty");
@@ -375,9 +490,21 @@ export async function setupBuiltInMining(options) {
375
490
  }, {
376
491
  provider,
377
492
  });
493
+ const localState = {
494
+ availability: "ready",
495
+ clientPasswordReadiness: "ready",
496
+ unlockRequired: false,
497
+ walletRootId: loaded.state.walletRootId,
498
+ state: loaded.state,
499
+ source: loaded.source,
500
+ hasPrimaryStateFile: true,
501
+ hasBackupStateFile: true,
502
+ message: null,
503
+ };
504
+ const eligibleRootCount = countEligibleAnchoredRoots(localState) ?? 0;
378
505
  await appendMiningEvent(paths.miningEventsPath, createMiningEvent("mine-setup-started", "Started built-in mining provider setup.", { timestampUnixMs: nowUnixMs }));
379
506
  try {
380
- const config = await promptForMiningProviderConfig(options.prompter);
507
+ const config = await promptForMiningProviderConfig(options.prompter, eligibleRootCount);
381
508
  config.updatedAtUnixMs = nowUnixMs;
382
509
  await saveBuiltInMiningProviderConfig({
383
510
  path: paths.clientConfigPath,
@@ -385,20 +512,10 @@ export async function setupBuiltInMining(options) {
385
512
  secretReference: createWalletSecretReference(loaded.state.walletRootId),
386
513
  config,
387
514
  });
388
- await appendMiningEvent(paths.miningEventsPath, createMiningEvent("mine-setup-completed", `Configured the built-in ${config.provider} mining provider.`, { timestampUnixMs: nowUnixMs }));
515
+ await appendMiningEvent(paths.miningEventsPath, createMiningEvent("mine-setup-completed", `Configured the built-in ${config.provider} mining provider with model ${config.modelOverride} (${describeModelSelectionSource(config.modelSelectionSource)}).`, { timestampUnixMs: nowUnixMs }));
389
516
  return refreshMiningRuntimeStatus({
390
517
  provider,
391
- localState: {
392
- availability: "ready",
393
- clientPasswordReadiness: "ready",
394
- unlockRequired: false,
395
- walletRootId: loaded.state.walletRootId,
396
- state: loaded.state,
397
- source: loaded.source,
398
- hasPrimaryStateFile: true,
399
- hasBackupStateFile: true,
400
- message: null,
401
- },
518
+ localState,
402
519
  bitcoind: {
403
520
  health: "unavailable",
404
521
  status: null,
@@ -421,6 +538,13 @@ export async function setupBuiltInMining(options) {
421
538
  });
422
539
  }
423
540
  catch (error) {
541
+ if (error instanceof Error && error.message === "mining_setup_canceled") {
542
+ await appendMiningEvent(paths.miningEventsPath, createMiningEvent("mine-setup-canceled", "Canceled built-in mining provider setup.", {
543
+ level: "warn",
544
+ timestampUnixMs: nowUnixMs,
545
+ }));
546
+ throw error;
547
+ }
424
548
  await appendMiningEvent(paths.miningEventsPath, createMiningEvent("mine-setup-failed", error instanceof Error ? error.message : String(error), {
425
549
  level: "error",
426
550
  timestampUnixMs: nowUnixMs,
@@ -4,4 +4,4 @@ export { ensureBuiltInMiningSetupIfNeeded, runBackgroundMiningWorker, runForegro
4
4
  export { appendMiningEvent, loadMiningRuntimeStatus, readMiningEvents, resolveRotatedMiningEventsPath, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
5
5
  export type { MiningSentenceCandidateV1, MiningSentenceGenerationRequestV1, MiningSentenceGenerationResponseV1, } from "./sentence-protocol.js";
6
6
  export { loadClientConfig, saveBuiltInMiningProviderConfig, saveClientConfig, } from "./config.js";
7
- export type { ClientConfigV1, MiningControlPlaneView, MiningEventRecord, MiningProviderConfigRecord, MiningProviderInspection, MiningRuntimeStatusV1, MiningServiceHealth, } from "./types.js";
7
+ export type { ClientConfigV1, MiningControlPlaneView, MiningEventRecord, MiningModelSelectionSource, MiningProviderConfigRecord, MiningProviderInspection, MiningRuntimeStatusV1, MiningServiceHealth, } from "./types.js";