@cogcoin/client 1.1.10 → 1.1.11

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 (44) hide show
  1. package/dist/bitcoind/client/managed-client.d.ts +2 -0
  2. package/dist/bitcoind/client/managed-client.js +6 -0
  3. package/dist/bitcoind/indexer-daemon/background-follow.d.ts +23 -0
  4. package/dist/bitcoind/indexer-daemon/background-follow.js +132 -0
  5. package/dist/bitcoind/indexer-daemon/client.d.ts +12 -0
  6. package/dist/bitcoind/indexer-daemon/client.js +137 -0
  7. package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +30 -0
  8. package/dist/bitcoind/indexer-daemon/lifecycle.js +153 -0
  9. package/dist/bitcoind/indexer-daemon/process.d.ts +35 -0
  10. package/dist/bitcoind/indexer-daemon/process.js +140 -0
  11. package/dist/bitcoind/indexer-daemon/runtime.d.ts +23 -0
  12. package/dist/bitcoind/indexer-daemon/runtime.js +204 -0
  13. package/dist/bitcoind/indexer-daemon/server.d.ts +12 -0
  14. package/dist/bitcoind/indexer-daemon/server.js +87 -0
  15. package/dist/bitcoind/indexer-daemon/snapshot-leases.d.ts +23 -0
  16. package/dist/bitcoind/indexer-daemon/snapshot-leases.js +139 -0
  17. package/dist/bitcoind/indexer-daemon/status.d.ts +23 -0
  18. package/dist/bitcoind/indexer-daemon/status.js +282 -0
  19. package/dist/bitcoind/indexer-daemon/types.d.ts +141 -0
  20. package/dist/bitcoind/indexer-daemon/types.js +1 -0
  21. package/dist/bitcoind/indexer-daemon-main.js +14 -665
  22. package/dist/bitcoind/indexer-daemon.d.ts +4 -132
  23. package/dist/bitcoind/indexer-daemon.js +2 -417
  24. package/dist/bitcoind/managed-bitcoind-service-config.d.ts +18 -1
  25. package/dist/bitcoind/managed-bitcoind-service-config.js +38 -1
  26. package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +30 -24
  27. package/dist/bitcoind/managed-bitcoind-service-status.js +0 -8
  28. package/dist/cli/mining-format.js +6 -1
  29. package/dist/cli/wallet-format/balance.js +1 -1
  30. package/dist/client/default-client.d.ts +3 -1
  31. package/dist/client/default-client.js +22 -0
  32. package/dist/types.d.ts +13 -1
  33. package/dist/wallet/fs/atomic.d.ts +11 -2
  34. package/dist/wallet/fs/atomic.js +45 -5
  35. package/dist/wallet/mining/cycle.js +4 -4
  36. package/dist/wallet/mining/projection.d.ts +1 -0
  37. package/dist/wallet/mining/projection.js +15 -1
  38. package/dist/wallet/mining/visualizer-sync.js +7 -9
  39. package/dist/wallet/mining/visualizer.js +2 -1
  40. package/dist/wallet/read/context.js +3 -2
  41. package/dist/wallet/read/local-state.d.ts +8 -0
  42. package/dist/wallet/read/local-state.js +32 -6
  43. package/dist/wallet/read/types.d.ts +1 -0
  44. package/package.json +1 -1
@@ -9,7 +9,7 @@ import { attachOrStartManagedBitcoindRuntime, probeManagedBitcoindRuntime } from
9
9
  import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
10
10
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
11
11
  import { DEFAULT_MANAGED_BITCOIND_FOLLOW_POLL_INTERVAL_MS, } from "./types.js";
12
- import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
12
+ import { buildManagedServiceArgsForTesting, LOCAL_HOST, resolveManagedBitcoindRuntimeConfig, SUPPORTED_BITCOIND_VERSION, verifyManagedBitcoindVersion, writeManagedBitcoindRuntimeConfigFile, writeManagedBitcoindRuntimeConfigFileFromStatus, writeBitcoinConfForTesting, } from "./managed-bitcoind-service-config.js";
13
13
  import { DEFAULT_MANAGED_BITCOIND_SHUTDOWN_TIMEOUT_MS, DEFAULT_MANAGED_BITCOIND_STARTUP_TIMEOUT_MS, FileLockBusyError, acquireManagedBitcoindFileLockWithRetry, isManagedBitcoindProcessAlive, waitForManagedBitcoindProcessExit, sleep, } from "./managed-bitcoind-service-process.js";
14
14
  import { createManagedWalletReplica, loadManagedWalletReplicaIfPresent, } from "./managed-bitcoind-service-replica.js";
15
15
  import { clearManagedBitcoindRuntimeArtifacts, createBitcoindServiceStatus, createManagedBitcoindNodeHandle, probeManagedBitcoindStatusCandidate, refreshManagedBitcoindStatus, waitForManagedBitcoindRpcReady, writeManagedBitcoindStatus, } from "./managed-bitcoind-service-status.js";
@@ -109,6 +109,7 @@ async function tryAttachExistingManagedBitcoindService(options) {
109
109
  return null;
110
110
  }
111
111
  const refreshed = await refreshManagedBitcoindStatus(probe.status, paths, options);
112
+ await writeManagedBitcoindRuntimeConfigFileFromStatus(paths.bitcoindRuntimeConfigPath, refreshed);
112
113
  return createManagedBitcoindNodeHandle({
113
114
  status: refreshed,
114
115
  paths,
@@ -207,42 +208,47 @@ export async function attachOrStartManagedBitcoindService(options) {
207
208
  }
208
209
  const nowUnixMs = Date.now();
209
210
  const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, startOptions.walletRootId, startOptions.dataDir);
210
- return createBitcoindServiceStatus({
211
- binaryVersion,
212
- serviceInstanceId: randomBytes(16).toString("hex"),
213
- state: "ready",
214
- processId: child.pid ?? null,
215
- walletRootId: startOptions.walletRootId,
216
- chain: startOptions.chain,
217
- dataDir: startOptions.dataDir,
218
- runtimeRoot: paths.walletRuntimeRoot,
219
- startHeight: startOptions.startHeight,
220
- rpc: rpcConfig,
221
- zmq: zmqConfig,
222
- p2pPort: runtimeConfig.p2pPort,
223
- getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
224
- getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
225
- walletReplica,
226
- startedAtUnixMs: nowUnixMs,
227
- heartbeatAtUnixMs: nowUnixMs,
228
- lastError: walletReplica.message ?? null,
229
- });
211
+ return {
212
+ runtimeConfig,
213
+ status: createBitcoindServiceStatus({
214
+ binaryVersion,
215
+ serviceInstanceId: randomBytes(16).toString("hex"),
216
+ state: "ready",
217
+ processId: child.pid ?? null,
218
+ walletRootId: startOptions.walletRootId,
219
+ chain: startOptions.chain,
220
+ dataDir: startOptions.dataDir,
221
+ runtimeRoot: paths.walletRuntimeRoot,
222
+ startHeight: startOptions.startHeight,
223
+ rpc: rpcConfig,
224
+ zmq: zmqConfig,
225
+ p2pPort: runtimeConfig.p2pPort,
226
+ getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
227
+ getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
228
+ walletReplica,
229
+ startedAtUnixMs: nowUnixMs,
230
+ heartbeatAtUnixMs: nowUnixMs,
231
+ lastError: walletReplica.message ?? null,
232
+ }),
233
+ };
230
234
  };
235
+ let runtimeConfig;
231
236
  let status;
232
237
  try {
233
- status = await startManagedProcess(runtimeOptions);
238
+ ({ runtimeConfig, status } = await startManagedProcess(runtimeOptions));
234
239
  }
235
240
  catch (error) {
236
241
  if (runtimeOptions.getblockArchivePath === undefined || runtimeOptions.getblockArchivePath === null) {
237
242
  throw error;
238
243
  }
239
- status = await startManagedProcess({
244
+ ({ runtimeConfig, status } = await startManagedProcess({
240
245
  ...runtimeOptions,
241
246
  getblockArchivePath: null,
242
247
  getblockArchiveEndHeight: null,
243
248
  getblockArchiveSha256: null,
244
- });
249
+ }));
245
250
  }
251
+ await writeManagedBitcoindRuntimeConfigFile(paths.bitcoindRuntimeConfigPath, runtimeConfig);
246
252
  await writeManagedBitcoindStatus(paths, status);
247
253
  return createManagedBitcoindNodeHandle({
248
254
  status,
@@ -83,14 +83,6 @@ export async function writeManagedBitcoindStatus(paths, status) {
83
83
  await writeFileAtomic(paths.bitcoindPidPath, `${status.processId ?? ""}\n`, { mode: 0o600 });
84
84
  await writeFileAtomic(paths.bitcoindReadyPath, `${status.updatedAtUnixMs}\n`, { mode: 0o600 });
85
85
  await writeRuntimeStatusFile(paths.bitcoindWalletStatusPath, status.walletReplica ?? createMissingManagedWalletReplicaStatus(status.walletRootId, "Managed Core wallet replica is missing."));
86
- await writeRuntimeStatusFile(paths.bitcoindRuntimeConfigPath, {
87
- chain: status.chain,
88
- rpc: status.rpc,
89
- zmqPort: status.zmq.port,
90
- p2pPort: status.p2pPort,
91
- getblockArchiveEndHeight: status.getblockArchiveEndHeight,
92
- getblockArchiveSha256: status.getblockArchiveSha256,
93
- });
94
86
  }
95
87
  export async function clearManagedBitcoindRuntimeArtifacts(paths) {
96
88
  await rm(paths.bitcoindStatusPath, { force: true }).catch(() => undefined);
@@ -1,3 +1,4 @@
1
+ import { resolveWaitingProviderNote } from "../wallet/mining/projection.js";
1
2
  function formatMaybeIso(unixMs) {
2
3
  return unixMs === null ? "none" : new Date(unixMs).toISOString();
3
4
  }
@@ -33,7 +34,11 @@ function resolveInsufficientFundsNextStep() {
33
34
  function resolveMiningRuntimeNote(mining) {
34
35
  return mining.runtime.currentPublishDecision === "publish-paused-insufficient-funds"
35
36
  ? "Insufficient BTC to mine."
36
- : mining.runtime.note;
37
+ : mining.runtime.note !== null
38
+ ? mining.runtime.note
39
+ : mining.runtime.currentPhase === "waiting-provider"
40
+ ? resolveWaitingProviderNote(mining.runtime.providerState)
41
+ : null;
37
42
  }
38
43
  export function formatMiningSummaryLine(mining) {
39
44
  const provider = mining.provider.configured
@@ -34,7 +34,7 @@ function renderBalanceArtCard(context) {
34
34
  return renderBalanceArtLine(line, "Funding address:", ` ${fundingAddress}`);
35
35
  }
36
36
  if (line.includes("Bitcoin Balance:")) {
37
- return renderBalanceArtLine(line, "Bitcoin Balance:", ` ${formatBitcoinAmount(context.fundingSpendableSats ?? null)}`);
37
+ return renderBalanceArtLine(line, "Bitcoin Balance:", ` ${formatBitcoinAmount(context.fundingDisplaySats ?? null)}`);
38
38
  }
39
39
  if (line.includes("Cogcoin Balance:")) {
40
40
  return renderBalanceArtLine(line, "Cogcoin Balance:", ` ${formatCogAmount(spendableCog)}`);
@@ -1,10 +1,12 @@
1
1
  import type { BitcoinBlock, GenesisParameters, IndexerState } from "@cogcoin/indexer/types";
2
- import type { ApplyBlockResult, Client, ClientCheckpoint, ClientStoreAdapter, ClientTip } from "../types.js";
2
+ import type { ApplyBlockResult, Client, ClientCheckpoint, ClientMirrorDelta, ClientMirrorSnapshot, ClientStoreAdapter, ClientTip } from "../types.js";
3
3
  export declare class DefaultClient implements Client {
4
4
  #private;
5
5
  constructor(store: ClientStoreAdapter, genesisParameters: GenesisParameters, state: IndexerState, tip: ClientTip | null, snapshotInterval: number, blockRecordRetention: number);
6
6
  getTip(): Promise<ClientTip | null>;
7
7
  getState(): Promise<IndexerState>;
8
+ readMirrorSnapshot(): Promise<ClientMirrorSnapshot>;
9
+ readMirrorDelta(afterHeight: number): Promise<ClientMirrorDelta>;
8
10
  applyBlock(block: BitcoinBlock): Promise<ApplyBlockResult>;
9
11
  rewindToHeight(height: number): Promise<ClientTip | null>;
10
12
  restoreCheckpoint(checkpoint: ClientCheckpoint): Promise<ClientTip>;
@@ -26,6 +26,28 @@ export class DefaultClient {
26
26
  await this.#queue;
27
27
  return this.#state;
28
28
  }
29
+ async readMirrorSnapshot() {
30
+ return this.#enqueue(async () => {
31
+ this.#assertOpen();
32
+ return {
33
+ tip: this.#tip === null ? null : { ...this.#tip },
34
+ stateBytes: serializeIndexerState(this.#state),
35
+ };
36
+ });
37
+ }
38
+ async readMirrorDelta(afterHeight) {
39
+ return this.#enqueue(async () => {
40
+ this.#assertOpen();
41
+ const blockRecords = await this.#store.loadBlockRecordsAfter(afterHeight);
42
+ return {
43
+ tip: this.#tip === null ? null : { ...this.#tip },
44
+ blockRecords: blockRecords.map((record) => ({
45
+ ...record,
46
+ recordBytes: new Uint8Array(record.recordBytes),
47
+ })),
48
+ };
49
+ });
50
+ }
29
51
  async applyBlock(block) {
30
52
  return this.#enqueue(async () => {
31
53
  this.#assertOpen();
package/dist/types.d.ts CHANGED
@@ -19,6 +19,18 @@ export interface StoredBlockRecord {
19
19
  recordBytes: Uint8Array;
20
20
  createdAt: number;
21
21
  }
22
+ export interface ClientMirrorSnapshot {
23
+ tip: ClientTip | null;
24
+ stateBytes: Uint8Array;
25
+ }
26
+ export interface ClientMirrorDelta {
27
+ tip: ClientTip | null;
28
+ blockRecords: StoredBlockRecord[];
29
+ }
30
+ export interface ClientMirrorReader {
31
+ readMirrorSnapshot(): Promise<ClientMirrorSnapshot>;
32
+ readMirrorDelta(afterHeight: number): Promise<ClientMirrorDelta>;
33
+ }
22
34
  export interface WriteAppliedBlockEntry {
23
35
  tip: ClientTip | null;
24
36
  stateBytes: Uint8Array | null;
@@ -48,7 +60,7 @@ export interface ApplyBlockResult {
48
60
  checkpoint: ClientCheckpoint | null;
49
61
  applied: AppliedBlock;
50
62
  }
51
- export interface Client {
63
+ export interface Client extends ClientMirrorReader {
52
64
  getTip(): Promise<ClientTip | null>;
53
65
  getState(): Promise<IndexerState>;
54
66
  applyBlock(block: BitcoinBlock): Promise<ApplyBlockResult>;
@@ -1,6 +1,15 @@
1
+ import { rename, rm } from "node:fs/promises";
1
2
  export interface AtomicWriteOptions {
2
3
  mode?: number;
3
4
  encoding?: BufferEncoding;
4
5
  }
5
- export declare function writeFileAtomic(filePath: string, data: string | Uint8Array, options?: AtomicWriteOptions): Promise<void>;
6
- export declare function writeJsonFileAtomic(filePath: string, value: unknown, options?: AtomicWriteOptions): Promise<void>;
6
+ export interface AtomicWriteDependencies {
7
+ platform?: NodeJS.Platform;
8
+ rename?: typeof rename;
9
+ rm?: typeof rm;
10
+ sleep?: (ms: number) => Promise<void>;
11
+ now?: () => number;
12
+ }
13
+ export declare function replaceFileAtomicWithRetryForTesting(tempPath: string, filePath: string, dependencies?: AtomicWriteDependencies): Promise<void>;
14
+ export declare function writeFileAtomic(filePath: string, data: string | Uint8Array, options?: AtomicWriteOptions, dependencies?: AtomicWriteDependencies): Promise<void>;
15
+ export declare function writeJsonFileAtomic(filePath: string, value: unknown, options?: AtomicWriteOptions, dependencies?: AtomicWriteDependencies): Promise<void>;
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, open, rename } from "node:fs/promises";
2
+ import { mkdir, open, rename, rm } from "node:fs/promises";
3
3
  import { basename, dirname, join } from "node:path";
4
+ const WINDOWS_REPLACE_RETRY_DELAY_MS = 25;
5
+ const WINDOWS_REPLACE_RETRY_TIMEOUT_MS = 1_000;
4
6
  async function fsyncDirectory(directoryPath) {
5
7
  try {
6
8
  const directoryHandle = await open(directoryPath, "r");
@@ -21,7 +23,39 @@ async function fsyncDirectory(directoryPath) {
21
23
  throw error;
22
24
  }
23
25
  }
24
- export async function writeFileAtomic(filePath, data, options = {}) {
26
+ function isRetryableWindowsReplaceError(error) {
27
+ if (!(error instanceof Error) || !("code" in error)) {
28
+ return false;
29
+ }
30
+ const code = error.code;
31
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY";
32
+ }
33
+ async function sleep(ms) {
34
+ await new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
36
+ export async function replaceFileAtomicWithRetryForTesting(tempPath, filePath, dependencies = {}) {
37
+ const renameImpl = dependencies.rename ?? rename;
38
+ if ((dependencies.platform ?? process.platform) !== "win32") {
39
+ await renameImpl(tempPath, filePath);
40
+ return;
41
+ }
42
+ const nowImpl = dependencies.now ?? Date.now;
43
+ const sleepImpl = dependencies.sleep ?? sleep;
44
+ const deadline = nowImpl() + WINDOWS_REPLACE_RETRY_TIMEOUT_MS;
45
+ while (true) {
46
+ try {
47
+ await renameImpl(tempPath, filePath);
48
+ return;
49
+ }
50
+ catch (error) {
51
+ if (!isRetryableWindowsReplaceError(error) || nowImpl() >= deadline) {
52
+ throw error;
53
+ }
54
+ await sleepImpl(WINDOWS_REPLACE_RETRY_DELAY_MS);
55
+ }
56
+ }
57
+ }
58
+ export async function writeFileAtomic(filePath, data, options = {}, dependencies = {}) {
25
59
  const directoryPath = dirname(filePath);
26
60
  const tempPath = join(directoryPath, `${basename(filePath)}.tmp-${randomUUID()}`);
27
61
  await mkdir(directoryPath, { recursive: true });
@@ -38,9 +72,15 @@ export async function writeFileAtomic(filePath, data, options = {}) {
38
72
  finally {
39
73
  await handle.close();
40
74
  }
41
- await rename(tempPath, filePath);
75
+ try {
76
+ await replaceFileAtomicWithRetryForTesting(tempPath, filePath, dependencies);
77
+ }
78
+ catch (error) {
79
+ await (dependencies.rm ?? rm)(tempPath, { force: true }).catch(() => undefined);
80
+ throw error;
81
+ }
42
82
  await fsyncDirectory(directoryPath);
43
83
  }
44
- export async function writeJsonFileAtomic(filePath, value, options = {}) {
45
- await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options);
84
+ export async function writeJsonFileAtomic(filePath, value, options = {}, dependencies = {}) {
85
+ await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options, dependencies);
46
86
  }
@@ -10,7 +10,7 @@ import { MiningProviderRequestError } from "./sentences.js";
10
10
  import { isInsufficientFundsError } from "../tx/common.js";
11
11
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
12
12
  import { createRpcClient } from "../../bitcoind/node.js";
13
- import { buildPrePublishStatusOverrides, } from "./projection.js";
13
+ import { buildPrePublishStatusOverrides, resolveWaitingProviderNote, } from "./projection.js";
14
14
  function createInitialState(options) {
15
15
  return {
16
16
  phase: "idle",
@@ -149,7 +149,7 @@ export async function runMiningPhaseMachine(options) {
149
149
  currentPublishDecision: null,
150
150
  providerState: options.loopState.providerWaitState,
151
151
  lastError: options.loopState.providerWaitLastError,
152
- note: "Mining is waiting for the sentence provider to recover.",
152
+ note: resolveWaitingProviderNote(options.loopState.providerWaitState),
153
153
  });
154
154
  return;
155
155
  }
@@ -162,7 +162,7 @@ export async function runMiningPhaseMachine(options) {
162
162
  currentPublishDecision: null,
163
163
  providerState: options.loopState.providerWaitState,
164
164
  lastError: options.loopState.providerWaitLastError,
165
- note: "Mining is waiting for the sentence provider to recover.",
165
+ note: resolveWaitingProviderNote(options.loopState.providerWaitState),
166
166
  });
167
167
  return;
168
168
  }
@@ -231,7 +231,7 @@ export async function runMiningPhaseMachine(options) {
231
231
  currentPublishDecision: null,
232
232
  providerState: options.loopState.providerWaitState ?? error.providerState,
233
233
  lastError: error.message,
234
- note: "Mining is waiting for the sentence provider to recover.",
234
+ note: resolveWaitingProviderNote(options.loopState.providerWaitState ?? error.providerState),
235
235
  });
236
236
  await options.appendEvent(createMiningEventRecord("publish-paused-provider", error.message, {
237
237
  level: "warn",
@@ -37,6 +37,7 @@ export interface MiningRuntimeStatusOverrides {
37
37
  note?: string | null;
38
38
  livePublishInMempool?: boolean | null;
39
39
  }
40
+ export declare function resolveWaitingProviderNote(providerState: MiningRuntimeStatusV1["providerState"] | null): string;
40
41
  export declare function buildPrePublishStatusOverrides(options: {
41
42
  state: WalletStateV1;
42
43
  candidate: MiningCandidate;
@@ -1,6 +1,20 @@
1
1
  import { livePublishTargetsCandidateTip } from "./engine-state.js";
2
2
  import { normalizeMiningPublishState, normalizeMiningStateRecord } from "./state.js";
3
3
  import { MINING_WORKER_API_VERSION, MINING_WORKER_HEARTBEAT_STALE_MS, } from "./constants.js";
4
+ export function resolveWaitingProviderNote(providerState) {
5
+ switch (providerState) {
6
+ case "backoff":
7
+ return "Mining is waiting because the sentence provider had a transient failure and will be retried automatically.";
8
+ case "rate-limited":
9
+ return "Mining is waiting because the sentence provider is rate limited and will be retried automatically.";
10
+ case "auth-error":
11
+ return "Mining is waiting because the sentence provider rejected the configured API key.";
12
+ case "not-found":
13
+ return "Mining is waiting because the configured sentence-provider model was not found.";
14
+ default:
15
+ return "Mining is waiting for the sentence provider to recover.";
16
+ }
17
+ }
4
18
  export function buildPrePublishStatusOverrides(options) {
5
19
  const replacing = options.state.miningState.currentTxid !== null;
6
20
  const replacingAcrossTips = replacing && !livePublishTargetsCandidateTip({
@@ -249,7 +263,7 @@ export async function buildMiningRuntimeStatusSnapshot(options) {
249
263
  : existing?.currentPhase === "resuming"
250
264
  ? "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health."
251
265
  : reuseExistingProviderWait
252
- ? "Mining is waiting for the sentence provider to recover."
266
+ ? resolveWaitingProviderNote(existing?.providerState ?? providerState)
253
267
  : existing?.currentPhase === "waiting-indexer"
254
268
  ? "Mining is waiting for Bitcoin Core and the indexer to align."
255
269
  : existing?.currentPhase === "waiting-bitcoin-network"
@@ -1,8 +1,9 @@
1
1
  import { getBalance, getBlockWinners, lookupDomainById, } from "@cogcoin/indexer/queries";
2
2
  import { displayToInternalBlockhash } from "@cogcoin/scoring";
3
3
  import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-times.js";
4
+ import { readFundingBalanceSummary } from "../read/local-state.js";
4
5
  import { buildMiningTipKey, resetMiningUiForTip } from "./engine-state.js";
5
- import { deriveMiningWordIndices, numberToSats, resolveBip39WordsFromIndices, } from "./engine-utils.js";
6
+ import { deriveMiningWordIndices, resolveBip39WordsFromIndices, } from "./engine-utils.js";
6
7
  import { createEmptyMiningFollowVisualizerState } from "./visualizer.js";
7
8
  function cloneSettledBoardEntries(entries) {
8
9
  return entries.map((entry) => ({
@@ -156,14 +157,11 @@ export function syncMiningUiForCurrentTip(options) {
156
157
  };
157
158
  }
158
159
  export async function resolveFundingDisplaySats(state, rpc) {
159
- const utxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
160
- return utxos.reduce((sum, entry) => {
161
- if (entry.scriptPubKey !== state.funding.scriptPubKeyHex
162
- || entry.spendable === false) {
163
- return sum;
164
- }
165
- return sum + numberToSats(entry.amount);
166
- }, 0n);
160
+ const summary = await readFundingBalanceSummary({
161
+ state,
162
+ rpc,
163
+ });
164
+ return summary.fundingDisplaySats ?? 0n;
167
165
  }
168
166
  export async function loadMiningVisibleFollowBlockTimes(options) {
169
167
  if (options.indexedTipHeight === null || options.indexedTipHashHex === null) {
@@ -3,6 +3,7 @@ import { centerLine, normalizeInlineText, truncateLine } from "../../bitcoind/pr
3
3
  import { advanceFollowSceneState, createFollowSceneState, replaceFollowBlockTimes, syncFollowSceneState, } from "../../bitcoind/progress/follow-scene.js";
4
4
  import { DEFAULT_RENDER_CLOCK, resolveTtyRenderPolicy, TtyRenderThrottle, } from "../../bitcoind/progress/render-policy.js";
5
5
  import { TtyProgressRenderer } from "../../bitcoind/progress/tty-renderer.js";
6
+ import { resolveWaitingProviderNote } from "./projection.js";
6
7
  const MINING_ARTWORK_COG_WIDTH = 22;
7
8
  const MINING_SENTENCE_BOARD_SIZE = 5;
8
9
  const MINING_SENTENCE_BOARD_WRAP_WIDTH = 80;
@@ -282,7 +283,7 @@ export function describeMiningVisualizerProgress(snapshot) {
282
283
  case "resuming":
283
284
  return "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.";
284
285
  case "waiting-provider":
285
- return "Mining is waiting for the sentence provider to recover.";
286
+ return resolveWaitingProviderNote(snapshot.providerState);
286
287
  case "waiting-indexer":
287
288
  return "Mining is waiting for Bitcoin Core and the indexer to align.";
288
289
  case "waiting-bitcoin-network":
@@ -5,7 +5,7 @@ import { inspectMiningControlPlane } from "../mining/index.js";
5
5
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
6
6
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
7
7
  import { openManagedWalletReadServiceBundle } from "./managed-services.js";
8
- import { inspectWalletLocalState, readFundingSpendableSats } from "./local-state.js";
8
+ import { inspectWalletLocalState, readFundingBalanceSummary } from "./local-state.js";
9
9
  import { createWalletReadModel } from "./project.js";
10
10
  const DEFAULT_SERVICE_START_TIMEOUT_MS = 60_000;
11
11
  export async function openWalletReadContext(options) {
@@ -31,7 +31,7 @@ export async function openWalletReadContext(options) {
31
31
  expectedIndexerBinaryVersion,
32
32
  now,
33
33
  });
34
- const fundingSpendableSats = await readFundingSpendableSats({
34
+ const { fundingDisplaySats, fundingSpendableSats, } = await readFundingBalanceSummary({
35
35
  state: localState.state,
36
36
  rpc: managedServices.node.rpc,
37
37
  });
@@ -58,6 +58,7 @@ export async function openWalletReadContext(options) {
58
58
  model: localState.state === null
59
59
  ? null
60
60
  : createWalletReadModel(localState.state, managedServices.snapshot),
61
+ fundingDisplaySats,
61
62
  fundingSpendableSats,
62
63
  mining,
63
64
  async close() {
@@ -7,6 +7,10 @@ type WalletLocalStateDeps = {
7
7
  attachOrStartManagedBitcoindService: typeof attachOrStartManagedBitcoindService;
8
8
  createRpcClient: typeof createRpcClient;
9
9
  };
10
+ export interface FundingBalanceSummary {
11
+ fundingDisplaySats: bigint | null;
12
+ fundingSpendableSats: bigint | null;
13
+ }
10
14
  export declare function inspectWalletLocalStateWithDependencies(options?: {
11
15
  dataDir?: string;
12
16
  secretProvider?: WalletSecretProvider;
@@ -25,4 +29,8 @@ export declare function readFundingSpendableSats(options: {
25
29
  state: WalletLocalStateStatus["state"];
26
30
  rpc: ReturnType<typeof createRpcClient> | null;
27
31
  }): Promise<bigint | null>;
32
+ export declare function readFundingBalanceSummary(options: {
33
+ state: WalletLocalStateStatus["state"];
34
+ rpc: Pick<ReturnType<typeof createRpcClient>, "listUnspent"> | null;
35
+ }): Promise<FundingBalanceSummary>;
28
36
  export {};
@@ -22,6 +22,10 @@ function isSpendableFundingUtxo(entry, fundingScriptPubKeyHex) {
22
22
  && entry.spendable !== false
23
23
  && entry.safe !== false;
24
24
  }
25
+ function isDisplayFundingUtxo(entry, fundingScriptPubKeyHex) {
26
+ return entry.scriptPubKey === fundingScriptPubKeyHex
27
+ && entry.spendable !== false;
28
+ }
25
29
  async function pathExists(path) {
26
30
  try {
27
31
  await access(path, constants.F_OK);
@@ -217,17 +221,39 @@ export async function inspectWalletLocalState(options = {}) {
217
221
  return inspectWalletLocalStateWithDependencies(options);
218
222
  }
219
223
  export async function readFundingSpendableSats(options) {
224
+ return (await readFundingBalanceSummary(options)).fundingSpendableSats;
225
+ }
226
+ export async function readFundingBalanceSummary(options) {
220
227
  if (options.state === null || options.rpc === null) {
221
- return null;
228
+ return {
229
+ fundingDisplaySats: null,
230
+ fundingSpendableSats: null,
231
+ };
222
232
  }
223
233
  const state = options.state;
224
234
  try {
225
- const utxos = await options.rpc.listUnspent(state.managedCoreWallet.walletName, 1);
226
- return utxos.reduce((sum, entry) => isSpendableFundingUtxo(entry, state.funding.scriptPubKeyHex)
227
- ? sum + btcAmountToSats(entry.amount)
228
- : sum, 0n);
235
+ const utxos = await options.rpc.listUnspent(state.managedCoreWallet.walletName, 0);
236
+ let fundingDisplaySats = 0n;
237
+ let fundingSpendableSats = 0n;
238
+ for (const entry of utxos) {
239
+ if (!isDisplayFundingUtxo(entry, state.funding.scriptPubKeyHex)) {
240
+ continue;
241
+ }
242
+ const amountSats = btcAmountToSats(entry.amount);
243
+ fundingDisplaySats += amountSats;
244
+ if (isSpendableFundingUtxo(entry, state.funding.scriptPubKeyHex)) {
245
+ fundingSpendableSats += amountSats;
246
+ }
247
+ }
248
+ return {
249
+ fundingDisplaySats,
250
+ fundingSpendableSats,
251
+ };
229
252
  }
230
253
  catch {
231
- return null;
254
+ return {
255
+ fundingDisplaySats: null,
256
+ fundingSpendableSats: null,
257
+ };
232
258
  }
233
259
  }
@@ -94,6 +94,7 @@ export interface WalletReadContext {
94
94
  indexer: WalletIndexerStatus;
95
95
  snapshot: WalletSnapshotView | null;
96
96
  model: WalletReadModel | null;
97
+ fundingDisplaySats: bigint | null;
97
98
  fundingSpendableSats: bigint | null;
98
99
  mining?: MiningControlPlaneView;
99
100
  close(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",