@cogcoin/client 0.5.11 → 0.5.13
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 +3 -1
- package/dist/app-paths.d.ts +1 -0
- package/dist/app-paths.js +3 -0
- package/dist/bitcoind/bootstrap/controller.d.ts +3 -0
- package/dist/bitcoind/bootstrap/controller.js +7 -5
- package/dist/bitcoind/client/factory.d.ts +8 -0
- package/dist/bitcoind/client/factory.js +43 -6
- package/dist/bitcoind/client/managed-client.js +19 -10
- package/dist/bitcoind/client/sync-engine.js +35 -4
- package/dist/bitcoind/progress/formatting.js +1 -1
- package/dist/bitcoind/testing.d.ts +1 -0
- package/dist/bitcoind/testing.js +1 -0
- package/dist/cli/commands/follow.js +47 -14
- package/dist/cli/commands/sync.js +48 -15
- package/dist/cli/context.js +5 -1
- package/dist/cli/output.js +11 -0
- package/dist/cli/runner.js +2 -0
- package/dist/cli/signals.d.ts +1 -1
- package/dist/cli/signals.js +17 -4
- package/dist/cli/types.d.ts +4 -0
- package/dist/cli/update-notifier.d.ts +2 -0
- package/dist/cli/update-notifier.js +276 -0
- package/dist/client/default-client.d.ts +1 -1
- package/dist/client/default-client.js +7 -1
- package/dist/client/factory.js +6 -1
- package/dist/sqlite/store.js +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/wallet/archive.js +10 -8
- package/dist/wallet/coin-control.d.ts +41 -0
- package/dist/wallet/coin-control.js +365 -0
- package/dist/wallet/lifecycle.js +39 -2
- package/dist/wallet/mining/runner.js +46 -44
- package/dist/wallet/read/context.js +15 -6
- package/dist/wallet/reset.js +2 -0
- package/dist/wallet/state/storage.js +5 -4
- package/dist/wallet/tx/anchor.js +36 -51
- package/dist/wallet/tx/cog.js +19 -12
- package/dist/wallet/tx/common.d.ts +41 -10
- package/dist/wallet/tx/common.js +112 -5
- package/dist/wallet/tx/domain-admin.js +13 -8
- package/dist/wallet/tx/domain-market.js +19 -12
- package/dist/wallet/tx/field.js +21 -18
- package/dist/wallet/tx/register.js +17 -12
- package/dist/wallet/tx/reputation.js +13 -8
- package/dist/wallet/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@0.5.
|
|
3
|
+
`@cogcoin/client@0.5.13` is the store-backed Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -81,6 +81,8 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
|
|
|
81
81
|
- mining and hook commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, `mine setup`, and `hooks status`
|
|
82
82
|
|
|
83
83
|
The CLI also supports stable `--output json` and `--output preview-json` envelopes on the commands that advertise machine-readable output.
|
|
84
|
+
Interactive text invocations periodically check the npm registry for newer `@cogcoin/client` releases and print `npm install -g @cogcoin/client` when a newer version is available.
|
|
85
|
+
Set `COGCOIN_DISABLE_UPDATE_CHECK=1` to disable the CLI update notice entirely.
|
|
84
86
|
Ordinary `sync`, `follow`, and wallet-aware read/status flows detach from the managed Bitcoin and indexer services on exit instead of stopping them.
|
|
85
87
|
Use the explicit `bitcoin ...` and `indexer ...` commands when you want direct service inspection or start/stop control.
|
|
86
88
|
For provider-backed local wallets, normal reads, mutations, export, and mining setup flows auto-materialize a local unlock session when the wallet is not explicitly locked.
|
package/dist/app-paths.d.ts
CHANGED
|
@@ -39,3 +39,4 @@ export declare function resolveCogcoinAppRootForTesting(resolution?: CogcoinPath
|
|
|
39
39
|
export declare function resolveDefaultBitcoindDataDirForTesting(resolution?: CogcoinPathResolution): string;
|
|
40
40
|
export declare function resolveDefaultClientDatabasePathForTesting(resolution?: CogcoinPathResolution): string;
|
|
41
41
|
export declare function resolveDefaultClientDatabaseDirectoryForTesting(resolution?: CogcoinPathResolution): string;
|
|
42
|
+
export declare function resolveDefaultUpdateCheckStatePathForTesting(resolution?: CogcoinPathResolution): string;
|
package/dist/app-paths.js
CHANGED
|
@@ -125,3 +125,6 @@ export function resolveDefaultClientDatabaseDirectoryForTesting(resolution = {})
|
|
|
125
125
|
const dbPath = resolveDefaultClientDatabasePathForTesting(resolution);
|
|
126
126
|
return platform === "win32" ? win32Path.dirname(dbPath) : dirname(dbPath);
|
|
127
127
|
}
|
|
128
|
+
export function resolveDefaultUpdateCheckStatePathForTesting(resolution = {}) {
|
|
129
|
+
return joinForPlatform(resolution.platform ?? process.platform, resolveCogcoinPathsForTesting(resolution).stateRoot, "update-check.json");
|
|
130
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ManagedProgressController } from "../progress.js";
|
|
|
3
3
|
import { BitcoinRpcClient } from "../rpc.js";
|
|
4
4
|
import { type ManagedRpcRetryState } from "../retryable-rpc.js";
|
|
5
5
|
import type { BootstrapPhase, SnapshotMetadata, SnapshotChunkManifest } from "../types.js";
|
|
6
|
+
type ResumeDisplayMode = "sync" | "follow";
|
|
6
7
|
export declare class AssumeUtxoBootstrapController {
|
|
7
8
|
#private;
|
|
8
9
|
constructor(options: {
|
|
@@ -18,6 +19,7 @@ export declare class AssumeUtxoBootstrapController {
|
|
|
18
19
|
ensureReady(indexedTip: ClientTip | null, expectedChain: "main" | "regtest", options?: {
|
|
19
20
|
signal?: AbortSignal;
|
|
20
21
|
retryState?: ManagedRpcRetryState;
|
|
22
|
+
resumeDisplayMode?: ResumeDisplayMode;
|
|
21
23
|
}): Promise<void>;
|
|
22
24
|
getStateForTesting(): Promise<{
|
|
23
25
|
metadataVersion: number;
|
|
@@ -35,3 +37,4 @@ export declare class AssumeUtxoBootstrapController {
|
|
|
35
37
|
updatedAt: number;
|
|
36
38
|
}>;
|
|
37
39
|
}
|
|
40
|
+
export {};
|
|
@@ -44,11 +44,13 @@ export class AssumeUtxoBootstrapController {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
if (indexedTip !== null) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
if ((options.resumeDisplayMode ?? "sync") === "follow") {
|
|
48
|
+
await this.#progress.setPhase("follow_tip", {
|
|
49
|
+
blocks: indexedTip.height,
|
|
50
|
+
targetHeight: indexedTip.height,
|
|
51
|
+
message: "Resuming from the persisted Cogcoin indexed tip.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
56
|
const { state, snapshotIdentity } = await this.#loadStateRecord();
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { stopIndexerDaemonService, type IndexerDaemonClient } from "../indexer-daemon.js";
|
|
1
2
|
import type { InternalManagedBitcoindOptions, ManagedBitcoindClient } from "../types.js";
|
|
3
|
+
export declare function pauseIndexerDaemonForForegroundClientForTesting(options: {
|
|
4
|
+
daemon: IndexerDaemonClient;
|
|
5
|
+
dataDir: string;
|
|
6
|
+
walletRootId: string;
|
|
7
|
+
shutdownTimeoutMs?: number;
|
|
8
|
+
stopDaemon?: typeof stopIndexerDaemonService;
|
|
9
|
+
}): Promise<IndexerDaemonClient | null>;
|
|
2
10
|
export declare function openManagedBitcoindClient(options: Omit<InternalManagedBitcoindOptions, "chain" | "startHeight">): Promise<ManagedBitcoindClient>;
|
|
3
11
|
export declare function openManagedBitcoindClientInternal(options: InternalManagedBitcoindOptions): Promise<ManagedBitcoindClient>;
|
|
@@ -2,13 +2,46 @@ import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
|
2
2
|
import { resolveDefaultBitcoindDataDirForTesting } from "../../app-paths.js";
|
|
3
3
|
import { openClient } from "../../client.js";
|
|
4
4
|
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting, } from "../bootstrap.js";
|
|
5
|
-
import { attachOrStartIndexerDaemon } from "../indexer-daemon.js";
|
|
5
|
+
import { attachOrStartIndexerDaemon, stopIndexerDaemonService, } from "../indexer-daemon.js";
|
|
6
6
|
import { createRpcClient } from "../node.js";
|
|
7
7
|
import { assertCogcoinProcessingStartHeight, resolveCogcoinProcessingStartHeight, } from "../processing-start-height.js";
|
|
8
8
|
import { ManagedProgressController } from "../progress.js";
|
|
9
9
|
import { attachOrStartManagedBitcoindService, } from "../service.js";
|
|
10
10
|
import { DefaultManagedBitcoindClient } from "./managed-client.js";
|
|
11
11
|
const DEFAULT_SYNC_DEBOUNCE_MS = 250;
|
|
12
|
+
function isRecoverableIndexerDaemonPauseError(error) {
|
|
13
|
+
if (!(error instanceof Error)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (error.message === "indexer_daemon_request_timeout"
|
|
17
|
+
|| error.message === "indexer_daemon_connection_closed"
|
|
18
|
+
|| error.message === "indexer_daemon_protocol_error") {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if ("code" in error) {
|
|
22
|
+
const code = error.code;
|
|
23
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET";
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export async function pauseIndexerDaemonForForegroundClientForTesting(options) {
|
|
28
|
+
try {
|
|
29
|
+
await options.daemon.pauseBackgroundFollow();
|
|
30
|
+
return options.daemon;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
await options.daemon.close().catch(() => undefined);
|
|
34
|
+
if (!isRecoverableIndexerDaemonPauseError(error)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
await (options.stopDaemon ?? stopIndexerDaemonService)({
|
|
38
|
+
dataDir: options.dataDir,
|
|
39
|
+
walletRootId: options.walletRootId,
|
|
40
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
41
|
+
});
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
12
45
|
async function createManagedBitcoindClient(options) {
|
|
13
46
|
const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
|
|
14
47
|
assertCogcoinProcessingStartHeight({
|
|
@@ -51,14 +84,18 @@ async function createManagedBitcoindClient(options) {
|
|
|
51
84
|
snapshotInterval: options.snapshotInterval,
|
|
52
85
|
});
|
|
53
86
|
const indexerDaemon = options.databasePath
|
|
54
|
-
? await
|
|
87
|
+
? await pauseIndexerDaemonForForegroundClientForTesting({
|
|
88
|
+
daemon: await attachOrStartIndexerDaemon({
|
|
89
|
+
dataDir,
|
|
90
|
+
databasePath: options.databasePath,
|
|
91
|
+
walletRootId: options.walletRootId,
|
|
92
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
93
|
+
}),
|
|
55
94
|
dataDir,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
startupTimeoutMs: options.startupTimeoutMs,
|
|
95
|
+
walletRootId,
|
|
96
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
59
97
|
})
|
|
60
98
|
: null;
|
|
61
|
-
await indexerDaemon?.pauseBackgroundFollow();
|
|
62
99
|
// The persistent service may already exist from a non-processing attach path
|
|
63
100
|
// that used startHeight 0. Cogcoin replay still begins at the requested
|
|
64
101
|
// processing boundary for this managed client.
|
|
@@ -292,7 +292,10 @@ export class DefaultManagedBitcoindClient {
|
|
|
292
292
|
async #ensureBootstrapReady(signal) {
|
|
293
293
|
await this.#node.validate();
|
|
294
294
|
const indexedTipBeforeBootstrap = await this.#client.getTip();
|
|
295
|
-
await this.#bootstrap.ensureReady(indexedTipBeforeBootstrap, this.#node.expectedChain, {
|
|
295
|
+
await this.#bootstrap.ensureReady(indexedTipBeforeBootstrap, this.#node.expectedChain, {
|
|
296
|
+
signal,
|
|
297
|
+
resumeDisplayMode: this.#following ? "follow" : "sync",
|
|
298
|
+
});
|
|
296
299
|
}
|
|
297
300
|
async #restartManagedNodeWithRange(readyRange, abortSignal) {
|
|
298
301
|
if (abortSignal.aborted) {
|
|
@@ -372,19 +375,25 @@ export class DefaultManagedBitcoindClient {
|
|
|
372
375
|
}
|
|
373
376
|
}
|
|
374
377
|
async #resumeIndexerBackgroundFollow() {
|
|
375
|
-
if (this.#indexerDaemon === null) {
|
|
378
|
+
if (this.#indexerDaemon === null && this.#reattachIndexerDaemon === null) {
|
|
376
379
|
return;
|
|
377
380
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
catch (error) {
|
|
383
|
-
if (this.#reattachIndexerDaemon === null) {
|
|
384
|
-
throw error;
|
|
381
|
+
if (this.#indexerDaemon !== null) {
|
|
382
|
+
try {
|
|
383
|
+
await this.#indexerDaemon.resumeBackgroundFollow();
|
|
384
|
+
return;
|
|
385
385
|
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (this.#reattachIndexerDaemon === null) {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const reattachIndexerDaemon = this.#reattachIndexerDaemon;
|
|
393
|
+
if (reattachIndexerDaemon === null) {
|
|
394
|
+
return;
|
|
386
395
|
}
|
|
387
|
-
const replacementDaemon = await
|
|
396
|
+
const replacementDaemon = await reattachIndexerDaemon();
|
|
388
397
|
this.#indexerDaemon = replacementDaemon;
|
|
389
398
|
await replacementDaemon?.resumeBackgroundFollow();
|
|
390
399
|
}
|
|
@@ -4,6 +4,7 @@ import { normalizeRpcBlock } from "../normalize.js";
|
|
|
4
4
|
import { MANAGED_RPC_RETRY_MESSAGE, consumeManagedRpcRetryDelayMs, createManagedRpcRetryState, describeManagedRpcRetryError, isRetryableManagedRpcError, resetManagedRpcRetryState, } from "../retryable-rpc.js";
|
|
5
5
|
import { estimateEtaSeconds } from "./rate-tracker.js";
|
|
6
6
|
const DEFAULT_SYNC_CATCH_UP_POLL_MS = 2_000;
|
|
7
|
+
const BITCOIN_SYNC_PHASE_DEBOUNCE_MS = DEFAULT_SYNC_CATCH_UP_POLL_MS * 3;
|
|
7
8
|
function createAbortError(signal) {
|
|
8
9
|
const reason = signal?.reason;
|
|
9
10
|
if (reason instanceof Error) {
|
|
@@ -53,6 +54,22 @@ async function setBitcoinSyncProgress(dependencies, info, targetHeightCap) {
|
|
|
53
54
|
: "Reading blocks from the managed Bitcoin node.",
|
|
54
55
|
});
|
|
55
56
|
}
|
|
57
|
+
function resolveIndexedHeightForReplayWindow(tip, startHeight) {
|
|
58
|
+
return tip?.height ?? (startHeight - 1);
|
|
59
|
+
}
|
|
60
|
+
function hasPendingCogcoinReplay(tip, startHeight, bestHeight) {
|
|
61
|
+
if (bestHeight < startHeight) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return resolveIndexedHeightForReplayWindow(tip, startHeight) < bestHeight;
|
|
65
|
+
}
|
|
66
|
+
function shouldPreserveCogcoinSyncPhase(dependencies) {
|
|
67
|
+
const status = dependencies.progress.getStatusSnapshot();
|
|
68
|
+
// Keep the TTY on Cogcoin replay through brief idle gaps so it does not
|
|
69
|
+
// bounce back to Bitcoin sync between closely spaced replay passes.
|
|
70
|
+
return status.bootstrapPhase === "cogcoin_sync"
|
|
71
|
+
&& Date.now() - status.bootstrapProgress.updatedAt < BITCOIN_SYNC_PHASE_DEBOUNCE_MS;
|
|
72
|
+
}
|
|
56
73
|
async function setRetryingProgress(dependencies, error) {
|
|
57
74
|
const status = dependencies.progress.getStatusSnapshot();
|
|
58
75
|
const { phase: _phase, updatedAt: _updatedAt, ...progress } = status.bootstrapProgress;
|
|
@@ -152,6 +169,7 @@ export async function syncToTip(dependencies) {
|
|
|
152
169
|
await runRpc(() => dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain, {
|
|
153
170
|
signal: dependencies.abortSignal,
|
|
154
171
|
retryState,
|
|
172
|
+
resumeDisplayMode: dependencies.isFollowing() ? "follow" : "sync",
|
|
155
173
|
}));
|
|
156
174
|
if (dependencies.node.expectedChain === "main"
|
|
157
175
|
&& dependencies.node.getblockArchiveEndHeight !== null) {
|
|
@@ -175,7 +193,11 @@ export async function syncToTip(dependencies) {
|
|
|
175
193
|
const cappedBestHeight = dependencies.targetHeightCap === null || dependencies.targetHeightCap === undefined
|
|
176
194
|
? startInfo.blocks
|
|
177
195
|
: Math.min(startInfo.blocks, dependencies.targetHeightCap);
|
|
178
|
-
|
|
196
|
+
const tipBeforePass = await dependencies.client.getTip();
|
|
197
|
+
if (!hasPendingCogcoinReplay(tipBeforePass, dependencies.startHeight, cappedBestHeight)
|
|
198
|
+
&& !shouldPreserveCogcoinSyncPhase(dependencies)) {
|
|
199
|
+
await setBitcoinSyncProgress(dependencies, startInfo, dependencies.targetHeightCap ?? null);
|
|
200
|
+
}
|
|
179
201
|
const pass = await syncAgainstBestHeight(dependencies, cappedBestHeight, runRpc);
|
|
180
202
|
aggregate.appliedBlocks += pass.appliedBlocks;
|
|
181
203
|
aggregate.rewoundBlocks += pass.rewoundBlocks;
|
|
@@ -193,8 +215,15 @@ export async function syncToTip(dependencies) {
|
|
|
193
215
|
aggregate.endingHeight = finalTip?.height ?? null;
|
|
194
216
|
aggregate.bestHeight = endBestHeight;
|
|
195
217
|
aggregate.bestHashHex = endInfo.bestblockhash;
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
const reachedTargetHeightCap = dependencies.targetHeightCap !== null
|
|
219
|
+
&& dependencies.targetHeightCap !== undefined
|
|
220
|
+
&& endBestHeight >= dependencies.targetHeightCap;
|
|
221
|
+
if (reachedTargetHeightCap && caughtUpCogcoin) {
|
|
222
|
+
return aggregate;
|
|
223
|
+
}
|
|
224
|
+
if (dependencies.targetHeightCap === null
|
|
225
|
+
&& endInfo.blocks === endInfo.headers
|
|
226
|
+
&& caughtUpCogcoin) {
|
|
198
227
|
if (dependencies.isFollowing()) {
|
|
199
228
|
dependencies.progress.replaceFollowBlockTimes(await runRpc(() => dependencies.loadVisibleFollowBlockTimes(finalTip)));
|
|
200
229
|
}
|
|
@@ -209,10 +238,12 @@ export async function syncToTip(dependencies) {
|
|
|
209
238
|
});
|
|
210
239
|
return aggregate;
|
|
211
240
|
}
|
|
212
|
-
await setBitcoinSyncProgress(dependencies, endInfo, dependencies.targetHeightCap ?? null);
|
|
213
241
|
if (endBestHeight >= dependencies.startHeight && finalTip?.height !== endBestHeight) {
|
|
214
242
|
continue;
|
|
215
243
|
}
|
|
244
|
+
if (!shouldPreserveCogcoinSyncPhase(dependencies)) {
|
|
245
|
+
await setBitcoinSyncProgress(dependencies, endInfo, dependencies.targetHeightCap ?? null);
|
|
246
|
+
}
|
|
216
247
|
await sleep(DEFAULT_SYNC_CATCH_UP_POLL_MS, dependencies.abortSignal);
|
|
217
248
|
}
|
|
218
249
|
}
|
|
@@ -144,7 +144,7 @@ export function resolveStatusFieldText(progress, snapshotHeight, now = 0) {
|
|
|
144
144
|
case "follow_tip":
|
|
145
145
|
return `Waiting for next block to be mined${animateStatusEllipsis(now)}`;
|
|
146
146
|
case "complete":
|
|
147
|
-
return `
|
|
147
|
+
return `Sync complete${animateStatusEllipsis(now)}`;
|
|
148
148
|
case "error":
|
|
149
149
|
return progress.lastError === null
|
|
150
150
|
? progress.message
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { openManagedBitcoindClientInternal } from "./client.js";
|
|
2
2
|
export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
|
|
3
|
+
export { pauseIndexerDaemonForForegroundClientForTesting } from "./client/factory.js";
|
|
3
4
|
export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
|
|
4
5
|
export { normalizeRpcBlock } from "./normalize.js";
|
|
5
6
|
export { BitcoinRpcClient } from "./rpc.js";
|
package/dist/bitcoind/testing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { openManagedBitcoindClientInternal } from "./client.js";
|
|
2
2
|
export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
|
|
3
|
+
export { pauseIndexerDaemonForForegroundClientForTesting } from "./client/factory.js";
|
|
3
4
|
export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
|
|
4
5
|
export { normalizeRpcBlock } from "./normalize.js";
|
|
5
6
|
export { BitcoinRpcClient } from "./rpc.js";
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
+
import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
|
|
2
3
|
import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
|
|
3
4
|
import { usesTtyProgress, writeLine } from "../io.js";
|
|
4
|
-
import { classifyCliError } from "../output.js";
|
|
5
|
+
import { classifyCliError, formatCliTextError } from "../output.js";
|
|
5
6
|
import { createStopSignalWatcher } from "../signals.js";
|
|
6
7
|
export async function runFollowCommand(parsed, context) {
|
|
7
8
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
8
9
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
|
|
13
|
-
loadUnlockSession: context.loadUnlockSession,
|
|
14
|
-
loadWalletExplicitLock: context.loadWalletExplicitLock,
|
|
15
|
-
});
|
|
16
|
-
await context.ensureDirectory(dirname(dbPath));
|
|
17
|
-
const store = await context.openSqliteStore({ filename: dbPath });
|
|
10
|
+
const runtimePaths = context.resolveWalletRuntimePaths();
|
|
11
|
+
let controlLock = null;
|
|
12
|
+
let store = null;
|
|
18
13
|
let storeOwned = true;
|
|
19
14
|
try {
|
|
15
|
+
const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
|
|
16
|
+
paths: runtimePaths,
|
|
17
|
+
provider: context.walletSecretProvider,
|
|
18
|
+
loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
|
|
19
|
+
loadUnlockSession: context.loadUnlockSession,
|
|
20
|
+
loadWalletExplicitLock: context.loadWalletExplicitLock,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
controlLock = await acquireFileLock(runtimePaths.walletControlLockPath, {
|
|
24
|
+
purpose: "managed-follow",
|
|
25
|
+
walletRootId: walletRoot.walletRootId,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof FileLockBusyError) {
|
|
30
|
+
throw new Error("wallet_control_lock_busy");
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
await context.ensureDirectory(dirname(dbPath));
|
|
35
|
+
store = await context.openSqliteStore({ filename: dbPath });
|
|
20
36
|
const client = await context.openManagedBitcoindClient({
|
|
21
37
|
store,
|
|
22
38
|
databasePath: dbPath,
|
|
@@ -25,7 +41,7 @@ export async function runFollowCommand(parsed, context) {
|
|
|
25
41
|
progressOutput: parsed.progressOutput,
|
|
26
42
|
});
|
|
27
43
|
storeOwned = false;
|
|
28
|
-
const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit);
|
|
44
|
+
const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit, [runtimePaths.walletControlLockPath]);
|
|
29
45
|
try {
|
|
30
46
|
await client.startFollowingTip();
|
|
31
47
|
if (!usesTtyProgress(parsed.progressOutput, context.stderr)) {
|
|
@@ -43,10 +59,27 @@ export async function runFollowCommand(parsed, context) {
|
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
catch (error) {
|
|
46
|
-
|
|
47
|
-
if (
|
|
62
|
+
const classified = classifyCliError(error);
|
|
63
|
+
if (classified.errorCode === "wallet_control_lock_busy") {
|
|
64
|
+
const formatted = formatCliTextError(error);
|
|
65
|
+
if (formatted !== null) {
|
|
66
|
+
for (const line of formatted) {
|
|
67
|
+
writeLine(context.stderr, line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
writeLine(context.stderr, classified.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
writeLine(context.stderr, `follow failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
if (storeOwned && store !== null) {
|
|
48
78
|
await store.close().catch(() => undefined);
|
|
49
79
|
}
|
|
50
|
-
return
|
|
80
|
+
return classified.exitCode;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await controlLock?.release().catch(() => undefined);
|
|
51
84
|
}
|
|
52
85
|
}
|
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
2
|
import { formatManagedSyncErrorMessage } from "../../bitcoind/errors.js";
|
|
3
|
+
import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
|
|
3
4
|
import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
|
|
4
5
|
import { writeLine } from "../io.js";
|
|
5
|
-
import { classifyCliError } from "../output.js";
|
|
6
|
+
import { classifyCliError, formatCliTextError } from "../output.js";
|
|
6
7
|
import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
|
|
7
8
|
export async function runSyncCommand(parsed, context) {
|
|
8
9
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
9
10
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
|
|
14
|
-
loadUnlockSession: context.loadUnlockSession,
|
|
15
|
-
loadWalletExplicitLock: context.loadWalletExplicitLock,
|
|
16
|
-
});
|
|
17
|
-
await context.ensureDirectory(dirname(dbPath));
|
|
18
|
-
const store = await context.openSqliteStore({ filename: dbPath });
|
|
11
|
+
const runtimePaths = context.resolveWalletRuntimePaths();
|
|
12
|
+
let controlLock = null;
|
|
13
|
+
let store = null;
|
|
19
14
|
let storeOwned = true;
|
|
20
15
|
try {
|
|
16
|
+
const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
|
|
17
|
+
paths: runtimePaths,
|
|
18
|
+
provider: context.walletSecretProvider,
|
|
19
|
+
loadRawWalletStateEnvelope: context.loadRawWalletStateEnvelope,
|
|
20
|
+
loadUnlockSession: context.loadUnlockSession,
|
|
21
|
+
loadWalletExplicitLock: context.loadWalletExplicitLock,
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
controlLock = await acquireFileLock(runtimePaths.walletControlLockPath, {
|
|
25
|
+
purpose: "managed-sync",
|
|
26
|
+
walletRootId: walletRoot.walletRootId,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof FileLockBusyError) {
|
|
31
|
+
throw new Error("wallet_control_lock_busy");
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
await context.ensureDirectory(dirname(dbPath));
|
|
36
|
+
store = await context.openSqliteStore({ filename: dbPath });
|
|
21
37
|
const client = await context.openManagedBitcoindClient({
|
|
22
38
|
store,
|
|
23
39
|
databasePath: dbPath,
|
|
@@ -26,7 +42,7 @@ export async function runSyncCommand(parsed, context) {
|
|
|
26
42
|
progressOutput: parsed.progressOutput,
|
|
27
43
|
});
|
|
28
44
|
storeOwned = false;
|
|
29
|
-
const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit);
|
|
45
|
+
const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit, [runtimePaths.walletControlLockPath]);
|
|
30
46
|
try {
|
|
31
47
|
const syncOutcome = await waitForCompletionOrStop(client.syncToTip(), stopWatcher);
|
|
32
48
|
if (syncOutcome.kind === "stopped") {
|
|
@@ -51,11 +67,28 @@ export async function runSyncCommand(parsed, context) {
|
|
|
51
67
|
}
|
|
52
68
|
}
|
|
53
69
|
catch (error) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
const classified = classifyCliError(error);
|
|
71
|
+
if (classified.errorCode === "wallet_control_lock_busy") {
|
|
72
|
+
const formatted = formatCliTextError(error);
|
|
73
|
+
if (formatted !== null) {
|
|
74
|
+
for (const line of formatted) {
|
|
75
|
+
writeLine(context.stderr, line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
writeLine(context.stderr, classified.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const message = formatManagedSyncErrorMessage(error instanceof Error ? error.message : String(error));
|
|
84
|
+
writeLine(context.stderr, `sync failed: ${message}`);
|
|
85
|
+
}
|
|
86
|
+
if (storeOwned && store !== null) {
|
|
57
87
|
await store.close().catch(() => undefined);
|
|
58
88
|
}
|
|
59
|
-
return
|
|
89
|
+
return classified.exitCode;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await controlLock?.release().catch(() => undefined);
|
|
60
93
|
}
|
|
61
94
|
}
|
package/dist/cli/context.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { attachOrStartIndexerDaemon, probeIndexerDaemon, readObservedIndexerDaemonStatus, stopIndexerDaemonService, } from "../bitcoind/indexer-daemon.js";
|
|
3
3
|
import { createRpcClient } from "../bitcoind/node.js";
|
|
4
4
|
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../bitcoind/service.js";
|
|
5
|
-
import { resolveDefaultBitcoindDataDirForTesting, resolveDefaultClientDatabasePathForTesting, } from "../app-paths.js";
|
|
5
|
+
import { resolveDefaultBitcoindDataDirForTesting, resolveDefaultClientDatabasePathForTesting, resolveDefaultUpdateCheckStatePathForTesting, } from "../app-paths.js";
|
|
6
6
|
import { openManagedBitcoindClient } from "../bitcoind/index.js";
|
|
7
7
|
import { inspectPassiveClientStatus } from "../passive-status.js";
|
|
8
8
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
@@ -26,10 +26,13 @@ export function createDefaultContext(overrides = {}) {
|
|
|
26
26
|
stdout: overrides.stdout ?? process.stdout,
|
|
27
27
|
stderr: overrides.stderr ?? process.stderr,
|
|
28
28
|
stdin: overrides.stdin ?? process.stdin,
|
|
29
|
+
env: overrides.env ?? process.env,
|
|
30
|
+
now: overrides.now ?? (() => Date.now()),
|
|
29
31
|
signalSource: overrides.signalSource ?? process,
|
|
30
32
|
forceExit: overrides.forceExit ?? ((code) => {
|
|
31
33
|
process.exit(code);
|
|
32
34
|
}),
|
|
35
|
+
fetchImpl: overrides.fetchImpl ?? fetch,
|
|
33
36
|
openSqliteStore: overrides.openSqliteStore ?? openSqliteStore,
|
|
34
37
|
openManagedBitcoindClient: overrides.openManagedBitcoindClient ?? openManagedBitcoindClient,
|
|
35
38
|
inspectPassiveClientStatus: overrides.inspectPassiveClientStatus ?? inspectPassiveClientStatus,
|
|
@@ -96,6 +99,7 @@ export function createDefaultContext(overrides = {}) {
|
|
|
96
99
|
loadWalletExplicitLock: overrides.loadWalletExplicitLock ?? loadWalletExplicitLock,
|
|
97
100
|
resolveDefaultBitcoindDataDir: overrides.resolveDefaultBitcoindDataDir ?? resolveDefaultBitcoindDataDirForTesting,
|
|
98
101
|
resolveDefaultClientDatabasePath: overrides.resolveDefaultClientDatabasePath ?? resolveDefaultClientDatabasePathForTesting,
|
|
102
|
+
resolveUpdateCheckStatePath: overrides.resolveUpdateCheckStatePath ?? resolveDefaultUpdateCheckStatePathForTesting,
|
|
99
103
|
resolveWalletRuntimePaths: overrides.resolveWalletRuntimePaths ?? ((seedName) => resolveWalletRuntimePathsForTesting({ seedName })),
|
|
100
104
|
};
|
|
101
105
|
}
|
package/dist/cli/output.js
CHANGED
|
@@ -187,6 +187,10 @@ export function classifyCliError(error) {
|
|
|
187
187
|
return { exitCode: 5, errorCode: message, message };
|
|
188
188
|
}
|
|
189
189
|
function isBlockedError(message) {
|
|
190
|
+
if (message === "wallet_control_lock_busy"
|
|
191
|
+
|| message.startsWith("file_lock_busy_")) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
190
194
|
if (message === "wallet_locked"
|
|
191
195
|
|| message === "wallet_uninitialized"
|
|
192
196
|
|| message === "local-state-corrupt"
|
|
@@ -239,6 +243,13 @@ export function createCliErrorPresentation(errorCode, fallbackMessage) {
|
|
|
239
243
|
next: "Run `cogcoin unlock --for 15m` and retry.",
|
|
240
244
|
};
|
|
241
245
|
}
|
|
246
|
+
if (errorCode === "wallet_control_lock_busy") {
|
|
247
|
+
return {
|
|
248
|
+
what: "Another Cogcoin command is already controlling this wallet.",
|
|
249
|
+
why: "Commands that sync, follow, or mutate the local index take an exclusive wallet control lock so they do not write the same sqlite store concurrently.",
|
|
250
|
+
next: "Wait for the other Cogcoin command to finish, or stop it cleanly before retrying.",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
242
253
|
if (errorCode === "reset_wallet_choice_invalid") {
|
|
243
254
|
return {
|
|
244
255
|
what: "Wallet reset choice is invalid.",
|
package/dist/cli/runner.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runSyncCommand } from "./commands/sync.js";
|
|
|
12
12
|
import { runWalletAdminCommand } from "./commands/wallet-admin.js";
|
|
13
13
|
import { runWalletMutationCommand } from "./commands/wallet-mutation.js";
|
|
14
14
|
import { runWalletReadCommand } from "./commands/wallet-read.js";
|
|
15
|
+
import { maybeNotifyAboutCliUpdate } from "./update-notifier.js";
|
|
15
16
|
import { findWalletSeedRecord, loadWalletSeedIndex } from "../wallet/state/seed-index.js";
|
|
16
17
|
function commandUsesExistingWalletSeed(parsed) {
|
|
17
18
|
return parsed.seedName !== null
|
|
@@ -48,6 +49,7 @@ export async function runCli(argv, contextOverrides = {}) {
|
|
|
48
49
|
writeLine(context.stdout, HELP_TEXT.trimEnd());
|
|
49
50
|
return parsed.help ? 0 : 2;
|
|
50
51
|
}
|
|
52
|
+
await maybeNotifyAboutCliUpdate(parsed, context);
|
|
51
53
|
try {
|
|
52
54
|
if (commandUsesExistingWalletSeed(parsed)) {
|
|
53
55
|
const mainPaths = context.resolveWalletRuntimePaths("main");
|
package/dist/cli/signals.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { InterruptibleOutcome, ManagedClientLike, SignalSource, StopSignalWatcher, WritableLike } from "./types.js";
|
|
2
|
-
export declare function createStopSignalWatcher(signalSource: SignalSource, stderr: WritableLike, client: ManagedClientLike, forceExit: (code: number) => never | void): StopSignalWatcher;
|
|
2
|
+
export declare function createStopSignalWatcher(signalSource: SignalSource, stderr: WritableLike, client: ManagedClientLike, forceExit: (code: number) => never | void, lockPaths?: readonly string[]): StopSignalWatcher;
|
|
3
3
|
export declare function createOwnedLockCleanupSignalWatcher(signalSource: SignalSource, forceExit: (code: number) => never | void, lockPaths: readonly string[]): StopSignalWatcher;
|
|
4
4
|
export declare function waitForCompletionOrStop<T>(promise: Promise<T>, stopWatcher: StopSignalWatcher): Promise<InterruptibleOutcome<T>>;
|
package/dist/cli/signals.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeLine } from "./io.js";
|
|
2
2
|
import { clearLockIfOwnedByCurrentProcess } from "../wallet/fs/lock.js";
|
|
3
|
-
export function createStopSignalWatcher(signalSource, stderr, client, forceExit) {
|
|
3
|
+
export function createStopSignalWatcher(signalSource, stderr, client, forceExit, lockPaths = []) {
|
|
4
4
|
let closing = false;
|
|
5
5
|
let resolved = false;
|
|
6
6
|
let onSignal = () => { };
|
|
@@ -17,16 +17,23 @@ export function createStopSignalWatcher(signalSource, stderr, client, forceExit)
|
|
|
17
17
|
cleanup();
|
|
18
18
|
resolve(code);
|
|
19
19
|
};
|
|
20
|
+
const releaseOwnedLocks = async () => {
|
|
21
|
+
await Promise.allSettled([...new Set(lockPaths)].map(async (lockPath) => {
|
|
22
|
+
await clearLockIfOwnedByCurrentProcess(lockPath);
|
|
23
|
+
}));
|
|
24
|
+
};
|
|
20
25
|
const onFirstSignal = () => {
|
|
21
26
|
closing = true;
|
|
22
27
|
writeLine(stderr, "Detaching from managed Cogcoin client and resuming background indexer follow...");
|
|
23
|
-
void client.close().then(() => {
|
|
28
|
+
void client.close().then(async () => {
|
|
29
|
+
await releaseOwnedLocks();
|
|
24
30
|
if (resolved) {
|
|
25
31
|
return;
|
|
26
32
|
}
|
|
27
33
|
writeLine(stderr, "Detached cleanly; background indexer follow resumed.");
|
|
28
34
|
settle(0);
|
|
29
|
-
}, () => {
|
|
35
|
+
}, async () => {
|
|
36
|
+
await releaseOwnedLocks();
|
|
30
37
|
if (resolved) {
|
|
31
38
|
return;
|
|
32
39
|
}
|
|
@@ -40,7 +47,13 @@ export function createStopSignalWatcher(signalSource, stderr, client, forceExit)
|
|
|
40
47
|
return;
|
|
41
48
|
}
|
|
42
49
|
settle(130);
|
|
43
|
-
|
|
50
|
+
if (lockPaths.length === 0) {
|
|
51
|
+
forceExit(130);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
void releaseOwnedLocks().finally(() => {
|
|
55
|
+
forceExit(130);
|
|
56
|
+
});
|
|
44
57
|
};
|
|
45
58
|
});
|
|
46
59
|
signalSource.on("SIGINT", onSignal);
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -87,8 +87,11 @@ export interface CliRunnerContext {
|
|
|
87
87
|
stdout?: WritableLike;
|
|
88
88
|
stderr?: WritableLike;
|
|
89
89
|
stdin?: ReadableLike;
|
|
90
|
+
env?: NodeJS.ProcessEnv;
|
|
91
|
+
now?: () => number;
|
|
90
92
|
signalSource?: SignalSource;
|
|
91
93
|
forceExit?: (code: number) => never | void;
|
|
94
|
+
fetchImpl?: typeof fetch;
|
|
92
95
|
openSqliteStore?: typeof openSqliteStore;
|
|
93
96
|
openManagedBitcoindClient?: (options: {
|
|
94
97
|
store: ClientStoreAdapter;
|
|
@@ -163,6 +166,7 @@ export interface CliRunnerContext {
|
|
|
163
166
|
readPackageVersion?: () => Promise<string>;
|
|
164
167
|
resolveDefaultBitcoindDataDir?: () => string;
|
|
165
168
|
resolveDefaultClientDatabasePath?: () => string;
|
|
169
|
+
resolveUpdateCheckStatePath?: () => string;
|
|
166
170
|
resolveWalletRuntimePaths?: (seedName?: string | null) => WalletRuntimePaths;
|
|
167
171
|
}
|
|
168
172
|
export interface StopSignalWatcher {
|