@cogcoin/client 1.0.0 → 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/mining-runtime.js +114 -12
- package/dist/cli/commands/sync.js +1 -91
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/context.js +33 -1
- 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/sync-progress.d.ts +6 -0
- package/dist/cli/sync-progress.js +91 -0
- package/dist/cli/types.d.ts +8 -2
- 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/cli/wallet-format.js +3 -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 +2 -2
- package/dist/wallet/mining/index.js +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 +105 -3
- package/dist/wallet/mining/runner.js +490 -88
- 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,91 @@
|
|
|
1
|
+
import { formatBytes, formatDuration } from "../bitcoind/progress/formatting.js";
|
|
2
|
+
const SYNC_PROGRESS_LOG_INTERVAL_MS = 5_000;
|
|
3
|
+
export function createSyncProgressReporter(options) {
|
|
4
|
+
let lastPhase = null;
|
|
5
|
+
let lastMessage = "";
|
|
6
|
+
let lastDownloadPrintedAt = 0;
|
|
7
|
+
let lastDownloadBytes = null;
|
|
8
|
+
let lastImportPrintedAt = 0;
|
|
9
|
+
let lastImportBlocks = null;
|
|
10
|
+
const infoEnabled = options.progressOutput !== "none";
|
|
11
|
+
function shouldPrintEntryMessage(message, phase) {
|
|
12
|
+
if (message === "Waiting to start managed sync." || message === "Sync complete.") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (message.startsWith("Warning:")) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (!infoEnabled) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (phase === "getblock_archive_download" || phase === "getblock_archive_import") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return phase === "snapshot_download"
|
|
25
|
+
|| phase === "wait_headers_for_snapshot"
|
|
26
|
+
|| phase === "load_snapshot"
|
|
27
|
+
|| phase === "bitcoin_sync"
|
|
28
|
+
|| phase === "cogcoin_sync"
|
|
29
|
+
|| message.includes("Getblock manifest")
|
|
30
|
+
|| message.startsWith("Fetching Getblock manifest.")
|
|
31
|
+
|| message.startsWith("Refreshing Getblock manifest.")
|
|
32
|
+
|| message.startsWith("Using Getblock range ");
|
|
33
|
+
}
|
|
34
|
+
function formatDownloadLine(label, event) {
|
|
35
|
+
const current = event.progress.downloadedBytes ?? 0;
|
|
36
|
+
const total = event.progress.totalBytes ?? 0;
|
|
37
|
+
const percent = event.progress.percent ?? (total > 0 ? (current / total) * 100 : 0);
|
|
38
|
+
const speed = event.progress.bytesPerSecond === null ? "--" : `${formatBytes(event.progress.bytesPerSecond)}/s`;
|
|
39
|
+
return `${label}: ${percent.toFixed(2)}% (${formatBytes(current)} / ${formatBytes(total)}, ${speed}, ETA ${formatDuration(event.progress.etaSeconds)})`;
|
|
40
|
+
}
|
|
41
|
+
return (event) => {
|
|
42
|
+
const message = event.progress.message.trim();
|
|
43
|
+
const phaseChanged = event.phase !== lastPhase;
|
|
44
|
+
const messageChanged = message !== lastMessage;
|
|
45
|
+
if ((phaseChanged || messageChanged) && shouldPrintEntryMessage(message, event.phase)) {
|
|
46
|
+
options.write(message);
|
|
47
|
+
}
|
|
48
|
+
if (infoEnabled && event.phase === "getblock_archive_download") {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const currentBytes = event.progress.downloadedBytes ?? 0;
|
|
51
|
+
const isComplete = (event.progress.percent ?? 0) >= 100;
|
|
52
|
+
const shouldPrintMilestone = phaseChanged
|
|
53
|
+
|| lastDownloadBytes !== currentBytes && (isComplete
|
|
54
|
+
|| now - lastDownloadPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
|
|
55
|
+
if (shouldPrintMilestone) {
|
|
56
|
+
options.write(formatDownloadLine("Getblock download", event));
|
|
57
|
+
lastDownloadPrintedAt = now;
|
|
58
|
+
lastDownloadBytes = currentBytes;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (infoEnabled && event.phase === "snapshot_download") {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const currentBytes = event.progress.downloadedBytes ?? 0;
|
|
64
|
+
const isComplete = (event.progress.percent ?? 0) >= 100;
|
|
65
|
+
const shouldPrintMilestone = phaseChanged
|
|
66
|
+
|| lastDownloadBytes !== currentBytes && (isComplete
|
|
67
|
+
|| now - lastDownloadPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
|
|
68
|
+
if (shouldPrintMilestone) {
|
|
69
|
+
options.write(formatDownloadLine("Snapshot download", event));
|
|
70
|
+
lastDownloadPrintedAt = now;
|
|
71
|
+
lastDownloadBytes = currentBytes;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (infoEnabled && event.phase === "getblock_archive_import") {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const currentBlocks = event.progress.blocks ?? 0;
|
|
77
|
+
const targetBlocks = event.progress.targetHeight ?? currentBlocks;
|
|
78
|
+
const isComplete = currentBlocks >= targetBlocks;
|
|
79
|
+
const shouldPrintMilestone = phaseChanged
|
|
80
|
+
|| lastImportBlocks !== currentBlocks && (isComplete
|
|
81
|
+
|| now - lastImportPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
|
|
82
|
+
if (shouldPrintMilestone) {
|
|
83
|
+
options.write(`Getblock import: Bitcoin ${currentBlocks.toLocaleString()} / ${targetBlocks.toLocaleString()}`);
|
|
84
|
+
lastImportPrintedAt = now;
|
|
85
|
+
lastImportBlocks = currentBlocks;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
lastPhase = event.phase;
|
|
89
|
+
lastMessage = message;
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -10,11 +10,11 @@ import type { WalletPrompter, initializeWallet, deleteImportedWalletSeed, previe
|
|
|
10
10
|
import type { openWalletReadContext } from "../wallet/read/index.js";
|
|
11
11
|
import { loadRawWalletStateEnvelope, loadWalletState } from "../wallet/state/storage.js";
|
|
12
12
|
import type { WalletSecretProvider } from "../wallet/state/provider.js";
|
|
13
|
-
import type { followMiningLog, inspectMiningControlPlane, readMiningLog, runForegroundMining, setupBuiltInMining, startBackgroundMining, stopBackgroundMining } from "../wallet/mining/index.js";
|
|
13
|
+
import type { ensureBuiltInMiningSetupIfNeeded, followMiningLog, inspectMiningControlPlane, readMiningLog, runForegroundMining, setupBuiltInMining, startBackgroundMining, stopBackgroundMining } from "../wallet/mining/index.js";
|
|
14
14
|
import type { anchorDomain, transferBitcoin, buyDomain, claimCogLock, clearDomainDelegate, clearDomainEndpoint, clearDomainMiner, clearField, createField, giveReputation, lockCogToDomain, registerDomain, reclaimCogLock, revokeReputation, sendCog, setField, setDomainCanonical, setDomainDelegate, setDomainEndpoint, setDomainMiner, sellDomain, transferDomain } from "../wallet/tx/index.js";
|
|
15
15
|
export type ProgressOutput = "auto" | "tty" | "none";
|
|
16
16
|
export type OutputMode = "text" | "json" | "preview-json";
|
|
17
|
-
export type CommandName = "init" | "restore" | "reset" | "repair" | "sync" | "status" | "client-lock" | "client-change-password" | "client-unlock" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "bitcoin-transfer" | "indexer-start" | "indexer-stop" | "indexer-status" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-init" | "wallet-delete" | "wallet-restore" | "wallet-show-mnemonic" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
|
|
17
|
+
export type CommandName = "init" | "restore" | "reset" | "repair" | "update" | "sync" | "status" | "client-lock" | "client-change-password" | "client-unlock" | "follow" | "bitcoin-start" | "bitcoin-stop" | "bitcoin-status" | "bitcoin-transfer" | "indexer-start" | "indexer-stop" | "indexer-status" | "anchor" | "domain-anchor" | "register" | "domain-register" | "transfer" | "domain-transfer" | "sell" | "domain-sell" | "unsell" | "domain-unsell" | "buy" | "domain-buy" | "domain-endpoint-set" | "domain-endpoint-clear" | "domain-delegate-set" | "domain-delegate-clear" | "domain-miner-set" | "domain-miner-clear" | "domain-canonical" | "field-list" | "field-show" | "field-create" | "field-set" | "field-clear" | "send" | "claim" | "reclaim" | "cog-send" | "cog-claim" | "cog-reclaim" | "cog-lock" | "rep-give" | "rep-revoke" | "cog-balance" | "cog-locks" | "mine" | "mine-start" | "mine-stop" | "mine-setup" | "mine-status" | "mine-log" | "wallet-init" | "wallet-delete" | "wallet-restore" | "wallet-show-mnemonic" | "wallet-status" | "wallet-address" | "wallet-ids" | "address" | "ids" | "balance" | "locks" | "domain-list" | "domains" | "domain-show" | "show" | "fields" | "field";
|
|
18
18
|
export interface WritableLike {
|
|
19
19
|
isTTY?: boolean;
|
|
20
20
|
write(chunk: string): void;
|
|
@@ -92,6 +92,11 @@ export interface CliRunnerContext {
|
|
|
92
92
|
signalSource?: SignalSource;
|
|
93
93
|
forceExit?: (code: number) => never | void;
|
|
94
94
|
fetchImpl?: typeof fetch;
|
|
95
|
+
runGlobalClientUpdateInstall?: (options: {
|
|
96
|
+
stdout: WritableLike;
|
|
97
|
+
stderr: WritableLike;
|
|
98
|
+
env: NodeJS.ProcessEnv;
|
|
99
|
+
}) => Promise<void>;
|
|
95
100
|
openSqliteStore?: typeof openSqliteStore;
|
|
96
101
|
openManagedBitcoindClient?: (options: {
|
|
97
102
|
store: ClientStoreAdapter;
|
|
@@ -145,6 +150,7 @@ export interface CliRunnerContext {
|
|
|
145
150
|
giveReputation?: typeof giveReputation;
|
|
146
151
|
revokeReputation?: typeof revokeReputation;
|
|
147
152
|
inspectMiningControlPlane?: typeof inspectMiningControlPlane;
|
|
153
|
+
ensureBuiltInMiningSetupIfNeeded?: typeof ensureBuiltInMiningSetupIfNeeded;
|
|
148
154
|
runForegroundMining?: typeof runForegroundMining;
|
|
149
155
|
startBackgroundMining?: typeof startBackgroundMining;
|
|
150
156
|
stopBackgroundMining?: typeof stopBackgroundMining;
|
|
@@ -1,143 +1,14 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { writeJsonFileAtomic } from "../wallet/fs/atomic.js";
|
|
3
1
|
import { writeLine } from "./io.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const UPDATE_CHECK_URL = "https://registry.npmjs.org/@cogcoin/client/latest";
|
|
8
|
-
function createEmptyUpdateCheckCache() {
|
|
9
|
-
return {
|
|
10
|
-
schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
|
|
11
|
-
lastCheckedAtUnixMs: 0,
|
|
12
|
-
latestVersion: null,
|
|
13
|
-
lastNotifiedCurrentVersion: null,
|
|
14
|
-
lastNotifiedLatestVersion: null,
|
|
15
|
-
lastNotifiedAtUnixMs: null,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function parseSemver(version) {
|
|
19
|
-
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());
|
|
20
|
-
if (match === null) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
const prerelease = match[4] === undefined
|
|
24
|
-
? []
|
|
25
|
-
: match[4].split(".").map((raw) => ({
|
|
26
|
-
raw,
|
|
27
|
-
numeric: /^(0|[1-9]\d*)$/.test(raw),
|
|
28
|
-
numericValue: /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : null,
|
|
29
|
-
}));
|
|
30
|
-
return {
|
|
31
|
-
major: Number(match[1]),
|
|
32
|
-
minor: Number(match[2]),
|
|
33
|
-
patch: Number(match[3]),
|
|
34
|
-
prerelease,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function compareSemver(left, right) {
|
|
38
|
-
const leftParsed = parseSemver(left);
|
|
39
|
-
const rightParsed = parseSemver(right);
|
|
40
|
-
if (leftParsed === null || rightParsed === null) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
if (leftParsed.major !== rightParsed.major) {
|
|
44
|
-
return leftParsed.major > rightParsed.major ? 1 : -1;
|
|
45
|
-
}
|
|
46
|
-
if (leftParsed.minor !== rightParsed.minor) {
|
|
47
|
-
return leftParsed.minor > rightParsed.minor ? 1 : -1;
|
|
48
|
-
}
|
|
49
|
-
if (leftParsed.patch !== rightParsed.patch) {
|
|
50
|
-
return leftParsed.patch > rightParsed.patch ? 1 : -1;
|
|
51
|
-
}
|
|
52
|
-
if (leftParsed.prerelease.length === 0 && rightParsed.prerelease.length === 0) {
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
if (leftParsed.prerelease.length === 0) {
|
|
56
|
-
return 1;
|
|
57
|
-
}
|
|
58
|
-
if (rightParsed.prerelease.length === 0) {
|
|
59
|
-
return -1;
|
|
60
|
-
}
|
|
61
|
-
const maxLength = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length);
|
|
62
|
-
for (let index = 0; index < maxLength; index += 1) {
|
|
63
|
-
const leftIdentifier = leftParsed.prerelease[index];
|
|
64
|
-
const rightIdentifier = rightParsed.prerelease[index];
|
|
65
|
-
if (leftIdentifier === undefined) {
|
|
66
|
-
return -1;
|
|
67
|
-
}
|
|
68
|
-
if (rightIdentifier === undefined) {
|
|
69
|
-
return 1;
|
|
70
|
-
}
|
|
71
|
-
if (leftIdentifier.numeric && rightIdentifier.numeric) {
|
|
72
|
-
if (leftIdentifier.numericValue !== rightIdentifier.numericValue) {
|
|
73
|
-
return leftIdentifier.numericValue > rightIdentifier.numericValue ? 1 : -1;
|
|
74
|
-
}
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (leftIdentifier.numeric !== rightIdentifier.numeric) {
|
|
78
|
-
return leftIdentifier.numeric ? -1 : 1;
|
|
79
|
-
}
|
|
80
|
-
if (leftIdentifier.raw !== rightIdentifier.raw) {
|
|
81
|
-
return leftIdentifier.raw > rightIdentifier.raw ? 1 : -1;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
86
|
-
function isUpdateCheckDisabled(env) {
|
|
87
|
-
const raw = env.COGCOIN_DISABLE_UPDATE_CHECK;
|
|
88
|
-
if (raw === undefined) {
|
|
2
|
+
import { PASSIVE_UPDATE_CHECK_TIMEOUT_MS, UPDATE_CHECK_MAX_AGE_MS, applyUpdateCheckResult, compareSemver, createEmptyUpdateCheckCache, fetchLatestPublishedVersion, isUpdateCheckDisabled, loadUpdateCheckCache, persistUpdateCheckCache, recordUpdateNotification, shouldRefreshUpdateCheck, } from "./update-service.js";
|
|
3
|
+
function isEligibleForUpdateNotification(parsed, context) {
|
|
4
|
+
if (parsed.command === "update") {
|
|
89
5
|
return false;
|
|
90
6
|
}
|
|
91
|
-
const normalized = raw.trim().toLowerCase();
|
|
92
|
-
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
93
|
-
}
|
|
94
|
-
function isEligibleForUpdateNotification(parsed, context) {
|
|
95
7
|
if (parsed.outputMode !== "text" || parsed.help || parsed.version) {
|
|
96
8
|
return false;
|
|
97
9
|
}
|
|
98
10
|
return context.stdout.isTTY === true || context.stderr.isTTY === true;
|
|
99
11
|
}
|
|
100
|
-
function normalizeUpdateCheckCache(parsed) {
|
|
101
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
const candidate = parsed;
|
|
105
|
-
if (candidate.schemaVersion !== UPDATE_CHECK_CACHE_SCHEMA_VERSION) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
|
|
110
|
-
lastCheckedAtUnixMs: typeof candidate.lastCheckedAtUnixMs === "number" ? candidate.lastCheckedAtUnixMs : 0,
|
|
111
|
-
latestVersion: typeof candidate.latestVersion === "string" ? candidate.latestVersion : null,
|
|
112
|
-
lastNotifiedCurrentVersion: typeof candidate.lastNotifiedCurrentVersion === "string"
|
|
113
|
-
? candidate.lastNotifiedCurrentVersion
|
|
114
|
-
: null,
|
|
115
|
-
lastNotifiedLatestVersion: typeof candidate.lastNotifiedLatestVersion === "string"
|
|
116
|
-
? candidate.lastNotifiedLatestVersion
|
|
117
|
-
: null,
|
|
118
|
-
lastNotifiedAtUnixMs: typeof candidate.lastNotifiedAtUnixMs === "number"
|
|
119
|
-
? candidate.lastNotifiedAtUnixMs
|
|
120
|
-
: null,
|
|
121
|
-
lastCheckErrorKind: typeof candidate.lastCheckErrorKind === "string"
|
|
122
|
-
? candidate.lastCheckErrorKind
|
|
123
|
-
: undefined,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
async function loadUpdateCheckCache(cachePath) {
|
|
127
|
-
try {
|
|
128
|
-
const raw = await readFile(cachePath, "utf8");
|
|
129
|
-
return normalizeUpdateCheckCache(JSON.parse(raw));
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function shouldRefreshUpdateCheck(cache, now) {
|
|
139
|
-
return now - cache.lastCheckedAtUnixMs >= UPDATE_CHECK_MAX_AGE_MS;
|
|
140
|
-
}
|
|
141
12
|
function shouldNotifyForVersionPair(cache, currentVersion, latestVersion, now) {
|
|
142
13
|
const versionComparison = compareSemver(latestVersion, currentVersion);
|
|
143
14
|
if (versionComparison === null || versionComparison <= 0) {
|
|
@@ -152,117 +23,31 @@ function shouldNotifyForVersionPair(cache, currentVersion, latestVersion, now) {
|
|
|
152
23
|
}
|
|
153
24
|
return now - cache.lastNotifiedAtUnixMs >= UPDATE_CHECK_MAX_AGE_MS;
|
|
154
25
|
}
|
|
155
|
-
function recordUpdateNotification(cache, currentVersion, latestVersion, now) {
|
|
156
|
-
return {
|
|
157
|
-
...cache,
|
|
158
|
-
lastNotifiedCurrentVersion: currentVersion,
|
|
159
|
-
lastNotifiedLatestVersion: latestVersion,
|
|
160
|
-
lastNotifiedAtUnixMs: now,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
async function fetchLatestPublishedVersion(fetchImpl) {
|
|
164
|
-
const controller = new AbortController();
|
|
165
|
-
const timer = setTimeout(() => {
|
|
166
|
-
controller.abort();
|
|
167
|
-
}, UPDATE_CHECK_TIMEOUT_MS);
|
|
168
|
-
try {
|
|
169
|
-
const response = await fetchImpl(UPDATE_CHECK_URL, {
|
|
170
|
-
headers: {
|
|
171
|
-
accept: "application/json",
|
|
172
|
-
},
|
|
173
|
-
signal: controller.signal,
|
|
174
|
-
});
|
|
175
|
-
if (!response.ok) {
|
|
176
|
-
return {
|
|
177
|
-
kind: "failure",
|
|
178
|
-
errorKind: `http_${response.status}`,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
let payload;
|
|
182
|
-
try {
|
|
183
|
-
payload = await response.json();
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
return {
|
|
187
|
-
kind: "failure",
|
|
188
|
-
errorKind: "invalid_json",
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
const latestVersion = typeof payload.version === "string"
|
|
192
|
-
? payload.version
|
|
193
|
-
: null;
|
|
194
|
-
if (latestVersion === null) {
|
|
195
|
-
return {
|
|
196
|
-
kind: "failure",
|
|
197
|
-
errorKind: "invalid_payload",
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
if (parseSemver(latestVersion) === null) {
|
|
201
|
-
return {
|
|
202
|
-
kind: "failure",
|
|
203
|
-
errorKind: "invalid_semver",
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
kind: "success",
|
|
208
|
-
latestVersion,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
213
|
-
return {
|
|
214
|
-
kind: "failure",
|
|
215
|
-
errorKind: "timeout",
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
kind: "failure",
|
|
220
|
-
errorKind: "network",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
finally {
|
|
224
|
-
clearTimeout(timer);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
26
|
function writeUpdateNotice(context, currentVersion, latestVersion) {
|
|
228
27
|
writeLine(context.stderr, `Update available: Cogcoin ${currentVersion} -> ${latestVersion}`);
|
|
229
28
|
writeLine(context.stderr, "Run: npm install -g @cogcoin/client");
|
|
230
29
|
}
|
|
231
|
-
async function persistUpdateCheckCache(cachePath, cache) {
|
|
232
|
-
await writeJsonFileAtomic(cachePath, cache);
|
|
233
|
-
}
|
|
234
30
|
export async function maybeNotifyAboutCliUpdate(parsed, context) {
|
|
235
31
|
try {
|
|
236
32
|
if (!isEligibleForUpdateNotification(parsed, context) || isUpdateCheckDisabled(context.env)) {
|
|
237
33
|
return;
|
|
238
34
|
}
|
|
239
35
|
const currentVersion = await context.readPackageVersion();
|
|
240
|
-
if (parseSemver(currentVersion) === null) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
36
|
const cachePath = context.resolveUpdateCheckStatePath();
|
|
244
37
|
const now = context.now();
|
|
245
38
|
let cache = await loadUpdateCheckCache(cachePath) ?? createEmptyUpdateCheckCache();
|
|
246
39
|
let cacheChanged = false;
|
|
247
40
|
if (shouldRefreshUpdateCheck(cache, now)) {
|
|
248
|
-
const updateResult = await fetchLatestPublishedVersion(context.fetchImpl
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
latestVersion: updateResult.kind === "success"
|
|
253
|
-
? updateResult.latestVersion
|
|
254
|
-
: cache.latestVersion,
|
|
255
|
-
lastCheckErrorKind: updateResult.kind === "success"
|
|
256
|
-
? undefined
|
|
257
|
-
: updateResult.errorKind,
|
|
258
|
-
};
|
|
41
|
+
const updateResult = await fetchLatestPublishedVersion(context.fetchImpl, {
|
|
42
|
+
timeoutMs: PASSIVE_UPDATE_CHECK_TIMEOUT_MS,
|
|
43
|
+
});
|
|
44
|
+
cache = applyUpdateCheckResult(cache, updateResult, now);
|
|
259
45
|
cacheChanged = true;
|
|
260
46
|
}
|
|
261
47
|
if (cache.latestVersion !== null
|
|
262
48
|
&& shouldNotifyForVersionPair(cache, currentVersion, cache.latestVersion, now)) {
|
|
263
49
|
writeUpdateNotice(context, currentVersion, cache.latestVersion);
|
|
264
50
|
cache = recordUpdateNotification(cache, currentVersion, cache.latestVersion, now);
|
|
265
|
-
cacheChanged = true;
|
|
266
51
|
await persistUpdateCheckCache(cachePath, cache);
|
|
267
52
|
return;
|
|
268
53
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const UPDATE_CHECK_CACHE_SCHEMA_VERSION = 1;
|
|
2
|
+
export declare const UPDATE_CHECK_MAX_AGE_MS: number;
|
|
3
|
+
export declare const PASSIVE_UPDATE_CHECK_TIMEOUT_MS = 500;
|
|
4
|
+
export declare const EXPLICIT_UPDATE_CHECK_TIMEOUT_MS = 5000;
|
|
5
|
+
export declare const UPDATE_CHECK_URL = "https://registry.npmjs.org/@cogcoin/client/latest";
|
|
6
|
+
export declare const CLI_INSTALL_COMMAND = "npm install -g @cogcoin/client";
|
|
7
|
+
export interface ParsedSemver {
|
|
8
|
+
major: number;
|
|
9
|
+
minor: number;
|
|
10
|
+
patch: number;
|
|
11
|
+
prerelease: Array<{
|
|
12
|
+
raw: string;
|
|
13
|
+
numeric: boolean;
|
|
14
|
+
numericValue: number | null;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface UpdateCheckCache {
|
|
18
|
+
schemaVersion: typeof UPDATE_CHECK_CACHE_SCHEMA_VERSION;
|
|
19
|
+
lastCheckedAtUnixMs: number;
|
|
20
|
+
latestVersion: string | null;
|
|
21
|
+
lastNotifiedCurrentVersion: string | null;
|
|
22
|
+
lastNotifiedLatestVersion: string | null;
|
|
23
|
+
lastNotifiedAtUnixMs: number | null;
|
|
24
|
+
lastCheckErrorKind?: string;
|
|
25
|
+
}
|
|
26
|
+
export type UpdateCheckResult = {
|
|
27
|
+
kind: "success";
|
|
28
|
+
latestVersion: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "failure";
|
|
31
|
+
errorKind: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function createEmptyUpdateCheckCache(): UpdateCheckCache;
|
|
34
|
+
export declare function parseSemver(version: string): ParsedSemver | null;
|
|
35
|
+
export declare function compareSemver(left: string, right: string): number | null;
|
|
36
|
+
export declare function isUpdateCheckDisabled(env: NodeJS.ProcessEnv): boolean;
|
|
37
|
+
export declare function loadUpdateCheckCache(cachePath: string): Promise<UpdateCheckCache | null>;
|
|
38
|
+
export declare function shouldRefreshUpdateCheck(cache: UpdateCheckCache, now: number): boolean;
|
|
39
|
+
export declare function applyUpdateCheckResult(cache: UpdateCheckCache, result: UpdateCheckResult, now: number): UpdateCheckCache;
|
|
40
|
+
export declare function recordUpdateNotification(cache: UpdateCheckCache, currentVersion: string, latestVersion: string, now: number): UpdateCheckCache;
|
|
41
|
+
export declare function fetchLatestPublishedVersion(fetchImpl: typeof fetch, options?: {
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}): Promise<UpdateCheckResult>;
|
|
44
|
+
export declare function persistUpdateCheckCache(cachePath: string, cache: UpdateCheckCache): Promise<void>;
|
|
@@ -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
|
+
}
|
|
@@ -558,6 +558,9 @@ function getBalanceNextSteps(context) {
|
|
|
558
558
|
return [`Transfer BTC to ${context.model.walletAddress ?? "this wallet address"} so your anchored root domain can keep mining.`];
|
|
559
559
|
}
|
|
560
560
|
if (context.fundingSpendableSats !== null && context.fundingSpendableSats > BALANCE_MINING_THRESHOLD_SATS) {
|
|
561
|
+
if (context.mining?.provider.status === "missing") {
|
|
562
|
+
return ["Run `cogcoin mine setup` to configure your mining provider."];
|
|
563
|
+
}
|
|
561
564
|
return ["Run `cogcoin mine` to start mining with your anchored root domain."];
|
|
562
565
|
}
|
|
563
566
|
return [];
|