@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.
Files changed (56) hide show
  1. package/README.md +2 -1
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -0
  3. package/dist/bitcoind/indexer-daemon.js +58 -8
  4. package/dist/bitcoind/retryable-rpc.js +3 -0
  5. package/dist/bitcoind/service.d.ts +1 -0
  6. package/dist/bitcoind/service.js +31 -9
  7. package/dist/cli/commands/mining-admin.js +9 -0
  8. package/dist/cli/commands/mining-runtime.js +114 -12
  9. package/dist/cli/commands/sync.js +1 -91
  10. package/dist/cli/commands/update.d.ts +2 -0
  11. package/dist/cli/commands/update.js +101 -0
  12. package/dist/cli/context.js +33 -1
  13. package/dist/cli/mining-format.js +28 -0
  14. package/dist/cli/mining-json.js +6 -0
  15. package/dist/cli/output.js +50 -2
  16. package/dist/cli/parse.d.ts +1 -1
  17. package/dist/cli/parse.js +5 -0
  18. package/dist/cli/prompt.js +109 -0
  19. package/dist/cli/read-json.d.ts +13 -0
  20. package/dist/cli/read-json.js +17 -0
  21. package/dist/cli/runner.js +4 -0
  22. package/dist/cli/sync-progress.d.ts +6 -0
  23. package/dist/cli/sync-progress.js +91 -0
  24. package/dist/cli/types.d.ts +8 -2
  25. package/dist/cli/update-notifier.js +7 -222
  26. package/dist/cli/update-service.d.ts +44 -0
  27. package/dist/cli/update-service.js +218 -0
  28. package/dist/cli/wallet-format.js +3 -0
  29. package/dist/client/initialization.js +5 -0
  30. package/dist/wallet/lifecycle.d.ts +10 -0
  31. package/dist/wallet/lifecycle.js +6 -0
  32. package/dist/wallet/mining/config.js +13 -3
  33. package/dist/wallet/mining/control.d.ts +2 -1
  34. package/dist/wallet/mining/control.js +143 -19
  35. package/dist/wallet/mining/index.d.ts +2 -2
  36. package/dist/wallet/mining/index.js +1 -1
  37. package/dist/wallet/mining/provider-model.d.ts +30 -0
  38. package/dist/wallet/mining/provider-model.js +134 -0
  39. package/dist/wallet/mining/runner.d.ts +105 -3
  40. package/dist/wallet/mining/runner.js +490 -88
  41. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  42. package/dist/wallet/mining/sentences.d.ts +2 -2
  43. package/dist/wallet/mining/sentences.js +25 -2
  44. package/dist/wallet/mining/types.d.ts +9 -1
  45. package/dist/wallet/mining/visualizer.js +28 -5
  46. package/dist/wallet/read/context.js +3 -0
  47. package/dist/wallet/reset.js +1 -0
  48. package/dist/wallet/tx/anchor.js +1 -0
  49. package/dist/wallet/tx/bitcoin-transfer.js +1 -0
  50. package/dist/wallet/tx/cog.js +3 -0
  51. package/dist/wallet/tx/domain-admin.js +1 -0
  52. package/dist/wallet/tx/domain-market.js +3 -0
  53. package/dist/wallet/tx/field.js +1 -0
  54. package/dist/wallet/tx/register.js +1 -0
  55. package/dist/wallet/tx/reputation.js +1 -0
  56. package/package.json +3 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.0.0` is the reference 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.
3
+ `@cogcoin/client@1.0.2` is the reference 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
 
@@ -130,6 +130,7 @@ Managed node subpath:
130
130
 
131
131
  The installed `cogcoin` command covers the first-party local wallet and node workflow:
132
132
 
133
+ - update commands such as `update` to compare the current CLI version with the latest npm release and install it
133
134
  - wallet lifecycle commands such as `init`, `restore`, `wallet delete`, `wallet show-mnemonic`, and `repair`
134
135
  - sync and service commands such as `status`, `sync`, `follow`, `bitcoin start`, `bitcoin stop`, `bitcoin status`, `indexer start`, `indexer stop`, and `indexer status`
135
136
  - domain and field commands such as `register`, `anchor`, `show`, `domains`, `fields`, `buy`, `sell`, and `transfer`
@@ -84,6 +84,7 @@ export interface CoherentIndexerSnapshotLease {
84
84
  payload: IndexerSnapshotPayload;
85
85
  status: ManagedIndexerDaemonStatus;
86
86
  }
87
+ type ManagedIndexerDaemonServiceLifetime = "persistent" | "ephemeral";
87
88
  export declare function stopIndexerDaemonServiceWithLockHeld(options: {
88
89
  dataDir: string;
89
90
  walletRootId?: string;
@@ -104,6 +105,8 @@ export declare function attachOrStartIndexerDaemon(options: {
104
105
  databasePath: string;
105
106
  walletRootId?: string;
106
107
  startupTimeoutMs?: number;
108
+ shutdownTimeoutMs?: number;
109
+ serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
107
110
  }): Promise<IndexerDaemonClient>;
108
111
  export declare function stopIndexerDaemonService(options: {
109
112
  dataDir: string;
@@ -80,7 +80,8 @@ export async function stopIndexerDaemonServiceWithLockHeld(options) {
80
80
  walletRootId,
81
81
  };
82
82
  }
83
- function createIndexerDaemonClient(socketPath) {
83
+ function createIndexerDaemonClient(socketPath, closeOptions = null) {
84
+ let closed = false;
84
85
  async function sendRequest(request) {
85
86
  return new Promise((resolve, reject) => {
86
87
  const socket = net.createConnection(socketPath);
@@ -181,7 +182,18 @@ function createIndexerDaemonClient(socketPath) {
181
182
  });
182
183
  },
183
184
  async close() {
184
- return;
185
+ if (closed) {
186
+ return;
187
+ }
188
+ closed = true;
189
+ if (closeOptions === null || closeOptions.serviceLifetime !== "ephemeral" || closeOptions.ownership === "attached") {
190
+ return;
191
+ }
192
+ await stopIndexerDaemonService({
193
+ dataDir: closeOptions.dataDir,
194
+ walletRootId: closeOptions.walletRootId,
195
+ shutdownTimeoutMs: closeOptions.shutdownTimeoutMs,
196
+ });
185
197
  },
186
198
  };
187
199
  }
@@ -308,7 +320,8 @@ async function waitForIndexerDaemon(dataDir, walletRootId, timeoutMs) {
308
320
  while (Date.now() < deadline) {
309
321
  const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
310
322
  if (probe.compatibility === "compatible" && probe.client !== null) {
311
- return probe.client;
323
+ await probe.client.close().catch(() => undefined);
324
+ return;
312
325
  }
313
326
  if (probe.compatibility !== "unreachable") {
314
327
  throw new Error(probe.error ?? "indexer_daemon_protocol_error");
@@ -358,6 +371,7 @@ export async function attachOrStartIndexerDaemon(options) {
358
371
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
359
372
  const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
360
373
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
374
+ const serviceLifetime = options.serviceLifetime ?? "persistent";
361
375
  const existingProbe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
362
376
  if (existingProbe.compatibility === "compatible" && existingProbe.client !== null) {
363
377
  return existingProbe.client;
@@ -382,17 +396,46 @@ export async function attachOrStartIndexerDaemon(options) {
382
396
  }
383
397
  await mkdir(paths.indexerServiceRoot, { recursive: true });
384
398
  const daemonEntryPath = fileURLToPath(new URL("./indexer-daemon-main.js", import.meta.url));
399
+ const spawnOptions = serviceLifetime === "ephemeral"
400
+ ? {
401
+ stdio: "ignore",
402
+ }
403
+ : {
404
+ detached: true,
405
+ stdio: "ignore",
406
+ };
385
407
  const child = spawn(process.execPath, [
386
408
  daemonEntryPath,
387
409
  `--data-dir=${options.dataDir}`,
388
410
  `--database-path=${options.databasePath}`,
389
411
  `--wallet-root-id=${walletRootId}`,
390
412
  ], {
391
- detached: true,
392
- stdio: "ignore",
413
+ ...spawnOptions,
414
+ });
415
+ if (serviceLifetime !== "ephemeral") {
416
+ child.unref();
417
+ }
418
+ try {
419
+ await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
420
+ }
421
+ catch (error) {
422
+ if (child.pid !== undefined) {
423
+ try {
424
+ process.kill(child.pid, "SIGTERM");
425
+ }
426
+ catch {
427
+ // ignore shutdown failures while unwinding startup errors
428
+ }
429
+ }
430
+ throw error;
431
+ }
432
+ return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
433
+ dataDir: options.dataDir,
434
+ walletRootId,
435
+ serviceLifetime,
436
+ ownership: "started",
437
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
393
438
  });
394
- child.unref();
395
- return await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
396
439
  }
397
440
  finally {
398
441
  await lock.release();
@@ -400,7 +443,14 @@ export async function attachOrStartIndexerDaemon(options) {
400
443
  }
401
444
  catch (error) {
402
445
  if (error instanceof FileLockBusyError) {
403
- return waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
446
+ await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
447
+ return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
448
+ dataDir: options.dataDir,
449
+ walletRootId,
450
+ serviceLifetime,
451
+ ownership: "attached",
452
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
453
+ });
404
454
  }
405
455
  throw error;
406
456
  }
@@ -19,6 +19,9 @@ export function isRetryableManagedRpcError(error) {
19
19
  if (message === "bitcoind_rpc_timeout") {
20
20
  return true;
21
21
  }
22
+ if (/^bitcoind_rpc_[^_]+_-28(?:_|$)/.test(message)) {
23
+ return true;
24
+ }
22
25
  if (message.startsWith("The managed Bitcoin RPC request to ")) {
23
26
  return message.includes(" failed");
24
27
  }
@@ -24,6 +24,7 @@ type ManagedBitcoindServiceOptions = Pick<InternalManagedBitcoindOptions, "dataD
24
24
  getblockArchivePath?: string | null;
25
25
  getblockArchiveEndHeight?: number | null;
26
26
  getblockArchiveSha256?: string | null;
27
+ serviceLifetime?: "persistent" | "ephemeral";
27
28
  };
28
29
  export type ManagedBitcoindServiceCompatibility = "compatible" | "service-version-mismatch" | "wallet-root-mismatch" | "runtime-mismatch" | "unreachable" | "protocol-error";
29
30
  export interface ManagedBitcoindServiceProbeResult {
@@ -681,9 +681,10 @@ async function refreshManagedBitcoindStatus(status, paths, options) {
681
681
  return nextStatus;
682
682
  }
683
683
  }
684
- function createNodeHandle(status, paths, options) {
684
+ function createNodeHandle(status, paths, options, ownership) {
685
685
  let currentStatus = status;
686
686
  const rpc = createRpcClient(currentStatus.rpc);
687
+ let stopped = false;
687
688
  return {
688
689
  rpc: currentStatus.rpc,
689
690
  zmq: currentStatus.zmq,
@@ -706,9 +707,20 @@ function createNodeHandle(status, paths, options) {
706
707
  return currentStatus;
707
708
  },
708
709
  async stop() {
709
- // Public managed clients detach from the persistent service instead of
710
- // shutting it down on ordinary command exit.
711
- return;
710
+ if (stopped) {
711
+ return;
712
+ }
713
+ stopped = true;
714
+ if (options.serviceLifetime !== "ephemeral" || ownership === "attached") {
715
+ // Public managed clients detach from persistent services, and ephemeral
716
+ // attach callers must not shut down services they did not launch.
717
+ return;
718
+ }
719
+ await stopManagedBitcoindService({
720
+ dataDir: currentStatus.dataDir,
721
+ walletRootId: currentStatus.walletRootId,
722
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
723
+ });
712
724
  },
713
725
  };
714
726
  }
@@ -720,7 +732,7 @@ async function tryAttachExistingManagedBitcoindService(options) {
720
732
  return null;
721
733
  }
722
734
  const refreshed = await refreshManagedBitcoindStatus(probe.status, paths, options);
723
- return createNodeHandle(refreshed, paths, options);
735
+ return createNodeHandle(refreshed, paths, options, "attached");
724
736
  }
725
737
  async function waitForManagedBitcoindService(options, timeoutMs) {
726
738
  const deadline = Date.now() + timeoutMs;
@@ -765,6 +777,7 @@ export async function attachOrStartManagedBitcoindService(options) {
765
777
  const resolvedOptions = {
766
778
  ...options,
767
779
  dataDir: options.dataDir,
780
+ serviceLifetime: options.serviceLifetime ?? "persistent",
768
781
  walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
769
782
  };
770
783
  const startupTimeoutMs = resolvedOptions.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
@@ -815,11 +828,20 @@ export async function attachOrStartManagedBitcoindService(options) {
815
828
  port: runtimeConfig.zmqPort,
816
829
  pollIntervalMs: startOptions.pollIntervalMs ?? 15_000,
817
830
  };
831
+ const spawnOptions = startOptions.serviceLifetime === "ephemeral"
832
+ ? {
833
+ stdio: "ignore",
834
+ }
835
+ : {
836
+ detached: true,
837
+ stdio: "ignore",
838
+ };
818
839
  const child = spawn(bitcoindPath, buildManagedServiceArgs(startOptions, runtimeConfig), {
819
- detached: true,
820
- stdio: "ignore",
840
+ ...spawnOptions,
821
841
  });
822
- child.unref();
842
+ if (startOptions.serviceLifetime !== "ephemeral") {
843
+ child.unref();
844
+ }
823
845
  const rpc = createRpcClient(rpcConfig);
824
846
  try {
825
847
  await waitForRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
@@ -876,7 +898,7 @@ export async function attachOrStartManagedBitcoindService(options) {
876
898
  });
877
899
  }
878
900
  await writeBitcoindStatus(paths, status);
879
- return createNodeHandle(status, paths, resolvedOptions);
901
+ return createNodeHandle(status, paths, resolvedOptions, "started");
880
902
  }
881
903
  finally {
882
904
  await lock.release();
@@ -36,6 +36,15 @@ export async function runMiningAdminCommand(parsed, context) {
36
36
  }
37
37
  writeLine(context.stdout, "Built-in mining provider configured.");
38
38
  writeLine(context.stdout, `Provider: ${view.provider.provider ?? "unknown"}`);
39
+ if (view.provider.modelId !== null) {
40
+ writeLine(context.stdout, `Selected model: ${view.provider.modelId}`);
41
+ }
42
+ if (view.provider.modelSelectionSource !== null) {
43
+ writeLine(context.stdout, `Selection source: ${view.provider.modelSelectionSource}`);
44
+ }
45
+ if (view.provider.estimatedDailyCostDisplay !== null) {
46
+ writeLine(context.stdout, `Approximate daily cost: ${view.provider.estimatedDailyCostDisplay}`);
47
+ }
39
48
  for (const line of formatNextStepLines(nextSteps)) {
40
49
  writeLine(context.stdout, line);
41
50
  }
@@ -1,28 +1,109 @@
1
1
  import { dirname } from "node:path";
2
+ import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
3
+ import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
4
+ import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
2
5
  import { buildMineStartData, buildMineStopData, } from "../mining-json.js";
3
6
  import { buildMineStartPreviewData, buildMineStopPreviewData, } from "../preview-json.js";
4
- import { writeLine } from "../io.js";
7
+ import { usesTtyProgress, writeLine } from "../io.js";
5
8
  import { createTerminalPrompter } from "../prompt.js";
6
9
  import { createPreviewSuccessEnvelope, createMutationSuccessEnvelope, describeCanonicalCommand, resolvePreviewJsonSchema, resolveStableMiningControlJsonSchema, writeHandledCliError, writeJsonValue, } from "../output.js";
7
10
  import { formatNextStepLines, getMineStopNextSteps, } from "../workflow-hints.js";
8
- import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
11
+ import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
12
+ import { createSyncProgressReporter } from "../sync-progress.js";
9
13
  function createCommandPrompter(parsed, context) {
10
14
  return parsed.outputMode !== "text"
11
15
  ? createTerminalPrompter(context.stdin, context.stderr)
12
16
  : context.createPrompter();
13
17
  }
14
- async function prestartManagedMiningServices(options) {
15
- let readContext = null;
18
+ async function ensureMiningProviderSetup(options) {
19
+ const setupReady = await options.context.ensureBuiltInMiningSetupIfNeeded({
20
+ provider: options.provider,
21
+ prompter: options.prompter,
22
+ paths: options.runtimePaths,
23
+ });
24
+ if (!setupReady) {
25
+ throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
26
+ }
27
+ }
28
+ async function syncManagedMiningReadiness(options) {
29
+ const ttyProgressActive = usesTtyProgress(options.parsed.progressOutput, options.context.stderr);
30
+ let controlLock = null;
31
+ let store = null;
32
+ let storeOwned = true;
33
+ let client = null;
34
+ let clientClosed = false;
16
35
  try {
17
- readContext = await options.context.openWalletReadContext({
18
- dataDir: options.dataDir,
19
- databasePath: options.databasePath,
20
- secretProvider: options.provider,
36
+ const walletRoot = await resolveWalletRootIdFromLocalArtifacts({
21
37
  paths: options.runtimePaths,
38
+ provider: options.provider,
39
+ loadRawWalletStateEnvelope: options.context.loadRawWalletStateEnvelope,
22
40
  });
41
+ try {
42
+ controlLock = await acquireFileLock(options.runtimePaths.walletControlLockPath, {
43
+ purpose: "managed-sync",
44
+ walletRootId: walletRoot.walletRootId,
45
+ });
46
+ }
47
+ catch (error) {
48
+ if (error instanceof FileLockBusyError) {
49
+ throw new Error("wallet_control_lock_busy");
50
+ }
51
+ throw error;
52
+ }
53
+ await options.context.ensureDirectory(dirname(options.databasePath));
54
+ store = await options.context.openSqliteStore({ filename: options.databasePath });
55
+ client = await options.context.openManagedBitcoindClient({
56
+ store,
57
+ databasePath: options.databasePath,
58
+ dataDir: options.dataDir,
59
+ walletRootId: walletRoot.walletRootId,
60
+ progressOutput: options.parsed.progressOutput,
61
+ onProgress: ttyProgressActive ? undefined : createSyncProgressReporter({
62
+ progressOutput: options.parsed.progressOutput,
63
+ write: (line) => {
64
+ writeLine(options.context.stderr, line);
65
+ },
66
+ }),
67
+ });
68
+ storeOwned = false;
69
+ const stopWatcher = createStopSignalWatcher(options.context.signalSource, options.context.stderr, client, options.context.forceExit, [options.runtimePaths.walletControlLockPath]);
70
+ try {
71
+ const syncOutcome = await waitForCompletionOrStop(client.syncToTip(), stopWatcher);
72
+ if (syncOutcome.kind === "stopped") {
73
+ return syncOutcome.code;
74
+ }
75
+ const result = syncOutcome.value;
76
+ if (result.endingHeight !== null && result.endingHeight === result.bestHeight) {
77
+ stopWatcher.cleanup();
78
+ const detachPromise = typeof client.detachToBackgroundFollow === "function"
79
+ ? client.detachToBackgroundFollow()
80
+ : Promise.resolve();
81
+ try {
82
+ await detachPromise;
83
+ await client.close();
84
+ clientClosed = true;
85
+ writeLine(options.context.stderr, "Detached cleanly; background indexer follow resumed.");
86
+ return null;
87
+ }
88
+ catch {
89
+ writeLine(options.context.stderr, "Detach failed before background indexer follow was confirmed.");
90
+ return 1;
91
+ }
92
+ }
93
+ throw new Error("Managed sync did not reach the current Bitcoin tip.");
94
+ }
95
+ finally {
96
+ stopWatcher.cleanup();
97
+ if (!clientClosed) {
98
+ await client.close();
99
+ }
100
+ }
23
101
  }
24
102
  finally {
25
- await readContext?.close();
103
+ if (storeOwned && store !== null) {
104
+ await store.close().catch(() => undefined);
105
+ }
106
+ await controlLock?.release().catch(() => undefined);
26
107
  }
27
108
  }
28
109
  export async function runMiningRuntimeCommand(parsed, context) {
@@ -30,17 +111,26 @@ export async function runMiningRuntimeCommand(parsed, context) {
30
111
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
31
112
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
32
113
  const runtimePaths = context.resolveWalletRuntimePaths(parsed.seedName);
33
- await context.ensureDirectory(dirname(dbPath));
34
114
  if (parsed.command === "mine") {
35
115
  const prompter = context.createPrompter();
36
116
  const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, prompter);
37
- await prestartManagedMiningServices({
117
+ await ensureMiningProviderSetup({
118
+ context,
119
+ provider,
120
+ prompter,
121
+ runtimePaths,
122
+ });
123
+ const preflightCode = await syncManagedMiningReadiness({
124
+ parsed,
38
125
  context,
39
126
  dataDir,
40
127
  databasePath: dbPath,
41
128
  provider,
42
129
  runtimePaths,
43
130
  });
131
+ if (preflightCode !== null) {
132
+ return preflightCode;
133
+ }
44
134
  const abortController = new AbortController();
45
135
  const onStop = () => {
46
136
  abortController.abort();
@@ -57,6 +147,7 @@ export async function runMiningRuntimeCommand(parsed, context) {
57
147
  stdout: context.stdout,
58
148
  stderr: context.stderr,
59
149
  progressOutput: parsed.progressOutput,
150
+ builtInSetupEnsured: true,
60
151
  paths: runtimePaths,
61
152
  });
62
153
  }
@@ -69,18 +160,29 @@ export async function runMiningRuntimeCommand(parsed, context) {
69
160
  if (parsed.command === "mine-start") {
70
161
  const prompter = createCommandPrompter(parsed, context);
71
162
  const provider = withInteractiveWalletSecretProvider(context.walletSecretProvider, prompter);
72
- await prestartManagedMiningServices({
163
+ await ensureMiningProviderSetup({
164
+ context,
165
+ provider,
166
+ prompter,
167
+ runtimePaths,
168
+ });
169
+ const preflightCode = await syncManagedMiningReadiness({
170
+ parsed,
73
171
  context,
74
172
  dataDir,
75
173
  databasePath: dbPath,
76
174
  provider,
77
175
  runtimePaths,
78
176
  });
177
+ if (preflightCode !== null) {
178
+ return preflightCode;
179
+ }
79
180
  const result = await context.startBackgroundMining({
80
181
  dataDir,
81
182
  databasePath: dbPath,
82
183
  provider,
83
184
  prompter,
185
+ builtInSetupEnsured: true,
84
186
  paths: runtimePaths,
85
187
  });
86
188
  if (parsed.outputMode === "preview-json") {
@@ -1,6 +1,5 @@
1
1
  import { dirname } from "node:path";
2
2
  import { formatManagedSyncErrorMessage } from "../../bitcoind/errors.js";
3
- import { formatBytes, formatDuration } from "../../bitcoind/progress/formatting.js";
4
3
  import { FileLockBusyError, acquireFileLock } from "../../wallet/fs/lock.js";
5
4
  import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolution.js";
6
5
  import { withInteractiveWalletSecretProvider } from "../../wallet/state/provider.js";
@@ -8,8 +7,8 @@ import { usesTtyProgress, writeLine } from "../io.js";
8
7
  import { classifyCliError, formatCliTextError } from "../output.js";
9
8
  import { createTerminalPrompter } from "../prompt.js";
10
9
  import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
10
+ import { createSyncProgressReporter } from "../sync-progress.js";
11
11
  import { formatBalanceReport } from "../wallet-format.js";
12
- const SYNC_PROGRESS_LOG_INTERVAL_MS = 5_000;
13
12
  async function writePostSyncBalanceReport(options) {
14
13
  const provider = withInteractiveWalletSecretProvider(options.context.walletSecretProvider, options.context.createPrompter?.() ?? createTerminalPrompter(options.context.stdin, options.context.stdout));
15
14
  const readContext = await options.context.openWalletReadContext({
@@ -26,95 +25,6 @@ async function writePostSyncBalanceReport(options) {
26
25
  await readContext.close().catch(() => undefined);
27
26
  }
28
27
  }
29
- function createSyncProgressReporter(options) {
30
- let lastPhase = null;
31
- let lastMessage = "";
32
- let lastDownloadPrintedAt = 0;
33
- let lastDownloadBytes = null;
34
- let lastImportPrintedAt = 0;
35
- let lastImportBlocks = null;
36
- const infoEnabled = options.progressOutput !== "none";
37
- function shouldPrintEntryMessage(message, phase) {
38
- if (message === "Waiting to start managed sync." || message === "Sync complete.") {
39
- return false;
40
- }
41
- if (message.startsWith("Warning:")) {
42
- return true;
43
- }
44
- if (!infoEnabled) {
45
- return false;
46
- }
47
- if (phase === "getblock_archive_download" || phase === "getblock_archive_import") {
48
- return true;
49
- }
50
- return phase === "snapshot_download"
51
- || phase === "wait_headers_for_snapshot"
52
- || phase === "load_snapshot"
53
- || phase === "bitcoin_sync"
54
- || phase === "cogcoin_sync"
55
- || message.includes("Getblock manifest")
56
- || message.startsWith("Fetching Getblock manifest.")
57
- || message.startsWith("Refreshing Getblock manifest.")
58
- || message.startsWith("Using Getblock range ");
59
- }
60
- function formatDownloadLine(label, event) {
61
- const current = event.progress.downloadedBytes ?? 0;
62
- const total = event.progress.totalBytes ?? 0;
63
- const percent = event.progress.percent ?? (total > 0 ? (current / total) * 100 : 0);
64
- const speed = event.progress.bytesPerSecond === null ? "--" : `${formatBytes(event.progress.bytesPerSecond)}/s`;
65
- return `${label}: ${percent.toFixed(2)}% (${formatBytes(current)} / ${formatBytes(total)}, ${speed}, ETA ${formatDuration(event.progress.etaSeconds)})`;
66
- }
67
- return (event) => {
68
- const message = event.progress.message.trim();
69
- const phaseChanged = event.phase !== lastPhase;
70
- const messageChanged = message !== lastMessage;
71
- if ((phaseChanged || messageChanged) && shouldPrintEntryMessage(message, event.phase)) {
72
- options.write(message);
73
- }
74
- if (infoEnabled && event.phase === "getblock_archive_download") {
75
- const now = Date.now();
76
- const currentBytes = event.progress.downloadedBytes ?? 0;
77
- const isComplete = (event.progress.percent ?? 0) >= 100;
78
- const shouldPrintMilestone = phaseChanged
79
- || lastDownloadBytes !== currentBytes && (isComplete
80
- || now - lastDownloadPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
81
- if (shouldPrintMilestone) {
82
- options.write(formatDownloadLine("Getblock download", event));
83
- lastDownloadPrintedAt = now;
84
- lastDownloadBytes = currentBytes;
85
- }
86
- }
87
- else if (infoEnabled && event.phase === "snapshot_download") {
88
- const now = Date.now();
89
- const currentBytes = event.progress.downloadedBytes ?? 0;
90
- const isComplete = (event.progress.percent ?? 0) >= 100;
91
- const shouldPrintMilestone = phaseChanged
92
- || lastDownloadBytes !== currentBytes && (isComplete
93
- || now - lastDownloadPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
94
- if (shouldPrintMilestone) {
95
- options.write(formatDownloadLine("Snapshot download", event));
96
- lastDownloadPrintedAt = now;
97
- lastDownloadBytes = currentBytes;
98
- }
99
- }
100
- else if (infoEnabled && event.phase === "getblock_archive_import") {
101
- const now = Date.now();
102
- const currentBlocks = event.progress.blocks ?? 0;
103
- const targetBlocks = event.progress.targetHeight ?? currentBlocks;
104
- const isComplete = currentBlocks >= targetBlocks;
105
- const shouldPrintMilestone = phaseChanged
106
- || lastImportBlocks !== currentBlocks && (isComplete
107
- || now - lastImportPrintedAt >= SYNC_PROGRESS_LOG_INTERVAL_MS);
108
- if (shouldPrintMilestone) {
109
- options.write(`Getblock import: Bitcoin ${currentBlocks.toLocaleString()} / ${targetBlocks.toLocaleString()}`);
110
- lastImportPrintedAt = now;
111
- lastImportBlocks = currentBlocks;
112
- }
113
- }
114
- lastPhase = event.phase;
115
- lastMessage = message;
116
- };
117
- }
118
28
  export async function runSyncCommand(parsed, context) {
119
29
  const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
120
30
  const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
@@ -0,0 +1,2 @@
1
+ import type { ParsedCliArgs, RequiredCliRunnerContext } from "../types.js";
2
+ export declare function runUpdateCommand(parsed: ParsedCliArgs, context: RequiredCliRunnerContext): Promise<number>;