@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.
- package/README.md +2 -1
- package/dist/bitcoind/indexer-daemon.d.ts +3 -0
- package/dist/bitcoind/indexer-daemon.js +58 -8
- package/dist/bitcoind/retryable-rpc.js +3 -0
- package/dist/bitcoind/service.d.ts +1 -0
- package/dist/bitcoind/service.js +31 -9
- package/dist/cli/commands/mining-admin.js +9 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/context.js +31 -0
- package/dist/cli/mining-format.js +28 -0
- package/dist/cli/mining-json.js +6 -0
- package/dist/cli/output.js +50 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +5 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +13 -0
- package/dist/cli/read-json.js +17 -0
- package/dist/cli/runner.js +4 -0
- package/dist/cli/types.d.ts +6 -1
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +44 -0
- package/dist/cli/update-service.js +218 -0
- package/dist/client/initialization.js +5 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/lifecycle.js +6 -0
- package/dist/wallet/mining/config.js +13 -3
- package/dist/wallet/mining/control.d.ts +2 -1
- package/dist/wallet/mining/control.js +143 -19
- package/dist/wallet/mining/index.d.ts +1 -1
- package/dist/wallet/mining/provider-model.d.ts +30 -0
- package/dist/wallet/mining/provider-model.js +134 -0
- package/dist/wallet/mining/runner.d.ts +98 -3
- package/dist/wallet/mining/runner.js +493 -95
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +25 -2
- package/dist/wallet/mining/types.d.ts +9 -1
- package/dist/wallet/mining/visualizer.js +28 -5
- package/dist/wallet/read/context.js +3 -0
- package/dist/wallet/reset.js +1 -0
- package/dist/wallet/tx/anchor.js +1 -0
- package/dist/wallet/tx/bitcoin-transfer.js +1 -0
- package/dist/wallet/tx/cog.js +3 -0
- package/dist/wallet/tx/domain-admin.js +1 -0
- package/dist/wallet/tx/domain-market.js +3 -0
- package/dist/wallet/tx/field.js +1 -0
- package/dist/wallet/tx/register.js +1 -0
- package/dist/wallet/tx/reputation.js +1 -0
- 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 {
|
package/dist/wallet/lifecycle.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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";
|