@cogcoin/client 0.5.7 → 0.5.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@0.5.7` 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.
3
+ `@cogcoin/client@0.5.8` 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
 
@@ -97,7 +97,15 @@ async function createManagedBitcoindClient(options) {
97
97
  // The persistent service may already exist from a non-processing attach path
98
98
  // that used startHeight 0. Cogcoin replay still begins at the requested
99
99
  // processing boundary for this managed client.
100
- return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS);
100
+ const databasePath = options.databasePath ?? null;
101
+ return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, databasePath
102
+ ? async () => attachOrStartIndexerDaemon({
103
+ dataDir,
104
+ databasePath,
105
+ walletRootId: options.walletRootId,
106
+ startupTimeoutMs: options.startupTimeoutMs,
107
+ })
108
+ : null, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS);
101
109
  }
102
110
  catch (error) {
103
111
  if (progressStarted) {
@@ -7,7 +7,7 @@ import type { BitcoinRpcClient } from "../rpc.js";
7
7
  import type { ManagedBitcoindClient, ManagedBitcoindNodeHandle, ManagedBitcoindStatus, SyncResult } from "../types.js";
8
8
  export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClient {
9
9
  #private;
10
- constructor(client: Client, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, indexerDaemon: IndexerDaemonClient | null, startHeight: number, syncDebounceMs: number);
10
+ constructor(client: Client, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, indexerDaemon: IndexerDaemonClient | null, reattachIndexerDaemon: (() => Promise<IndexerDaemonClient | null>) | null, startHeight: number, syncDebounceMs: number);
11
11
  getTip(): Promise<import("../../types.js").ClientTip | null>;
12
12
  getState(): Promise<import("@cogcoin/indexer/types").IndexerState>;
13
13
  applyBlock(block: BitcoinBlock): Promise<import("../../types.js").ApplyBlockResult>;
@@ -10,6 +10,7 @@ export class DefaultManagedBitcoindClient {
10
10
  #progress;
11
11
  #bootstrap;
12
12
  #indexerDaemon;
13
+ #reattachIndexerDaemon;
13
14
  #startHeight;
14
15
  #syncDebounceMs;
15
16
  #following = false;
@@ -22,7 +23,7 @@ export class DefaultManagedBitcoindClient {
22
23
  #syncPromise = Promise.resolve(createInitialSyncResult());
23
24
  #debounceTimer = null;
24
25
  #syncAbortControllers = new Set();
25
- constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, startHeight, syncDebounceMs) {
26
+ constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, reattachIndexerDaemon, startHeight, syncDebounceMs) {
26
27
  this.#client = client;
27
28
  this.#store = store;
28
29
  this.#node = node;
@@ -30,6 +31,7 @@ export class DefaultManagedBitcoindClient {
30
31
  this.#progress = progress;
31
32
  this.#bootstrap = bootstrap;
32
33
  this.#indexerDaemon = indexerDaemon;
34
+ this.#reattachIndexerDaemon = reattachIndexerDaemon;
33
35
  this.#startHeight = startHeight;
34
36
  this.#syncDebounceMs = syncDebounceMs;
35
37
  }
@@ -177,7 +179,7 @@ export class DefaultManagedBitcoindClient {
177
179
  await this.#progress.close();
178
180
  await this.#node.stop();
179
181
  await this.#client.close();
180
- await this.#indexerDaemon?.resumeBackgroundFollow().catch(() => undefined);
182
+ await this.#resumeIndexerBackgroundFollow();
181
183
  await this.#indexerDaemon?.close();
182
184
  }
183
185
  async playSyncCompletionScene() {
@@ -209,4 +211,21 @@ export class DefaultManagedBitcoindClient {
209
211
  throw new Error("managed_bitcoind_client_closed");
210
212
  }
211
213
  }
214
+ async #resumeIndexerBackgroundFollow() {
215
+ if (this.#indexerDaemon === null) {
216
+ return;
217
+ }
218
+ try {
219
+ await this.#indexerDaemon.resumeBackgroundFollow();
220
+ return;
221
+ }
222
+ catch (error) {
223
+ if (this.#reattachIndexerDaemon === null) {
224
+ throw error;
225
+ }
226
+ }
227
+ const replacementDaemon = await this.#reattachIndexerDaemon();
228
+ this.#indexerDaemon = replacementDaemon;
229
+ await replacementDaemon?.resumeBackgroundFollow();
230
+ }
212
231
  }
@@ -226,8 +226,7 @@ async function waitForRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
226
226
  throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
227
227
  }
228
228
  function validateManagedBitcoindStatus(status, options, runtimeRoot) {
229
- const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
230
- const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir ?? "", walletRootId).runtimeRoot, walletRootId);
229
+ const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir ?? "", options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID).runtimeRoot, status.walletRootId);
231
230
  if (status.serviceApiVersion !== MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE) {
232
231
  throw new Error("managed_bitcoind_service_version_mismatch");
233
232
  }
@@ -1,4 +1,5 @@
1
1
  export { openManagedBitcoindClientInternal } from "./client.js";
2
+ export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
2
3
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
4
  export { normalizeRpcBlock } from "./normalize.js";
4
5
  export { BitcoinRpcClient } from "./rpc.js";
@@ -1,4 +1,5 @@
1
1
  export { openManagedBitcoindClientInternal } from "./client.js";
2
+ export { DefaultManagedBitcoindClient } from "./client/managed-client.js";
2
3
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
4
  export { normalizeRpcBlock } from "./normalize.js";
4
5
  export { BitcoinRpcClient } from "./rpc.js";
@@ -169,6 +169,12 @@ export function classifyCliError(error) {
169
169
  if (message === "wallet_import_archive_not_found") {
170
170
  return { exitCode: 3, errorCode: message, message };
171
171
  }
172
+ if (message === "wallet_seed_not_found") {
173
+ return { exitCode: 3, errorCode: message, message };
174
+ }
175
+ if (message === "wallet_seed_index_invalid") {
176
+ return { exitCode: 4, errorCode: message, message };
177
+ }
172
178
  if (message === "reset_process_shutdown_failed"
173
179
  || message === "reset_data_root_delete_failed"
174
180
  || message === "reset_secret_cleanup_failed"
@@ -331,6 +337,13 @@ export function createCliErrorPresentation(errorCode, fallbackMessage) {
331
337
  next: "Check `--seed <name>` and retry.",
332
338
  };
333
339
  }
340
+ if (errorCode === "wallet_seed_index_invalid") {
341
+ return {
342
+ what: "Wallet seed registry is invalid.",
343
+ why: "Cogcoin could not parse or trust the local seed registry file, so it cannot safely decide which named wallet seed to use.",
344
+ next: "Run `cogcoin repair`, then retry the command.",
345
+ };
346
+ }
334
347
  if (errorCode === "wallet_delete_main_not_supported") {
335
348
  return {
336
349
  what: "The main wallet cannot be deleted with `wallet delete`.",
@@ -12,6 +12,14 @@ 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 { findWalletSeedRecord, loadWalletSeedIndex } from "../wallet/state/seed-index.js";
16
+ function commandUsesExistingWalletSeed(parsed) {
17
+ return parsed.seedName !== null
18
+ && parsed.seedName !== "main"
19
+ && parsed.command !== "restore"
20
+ && parsed.command !== "wallet-delete"
21
+ && parsed.command !== "wallet-restore";
22
+ }
15
23
  export async function runCli(argv, contextOverrides = {}) {
16
24
  const context = createDefaultContext(contextOverrides);
17
25
  let parsed;
@@ -41,6 +49,15 @@ export async function runCli(argv, contextOverrides = {}) {
41
49
  return parsed.help ? 0 : 2;
42
50
  }
43
51
  try {
52
+ if (commandUsesExistingWalletSeed(parsed)) {
53
+ const mainPaths = context.resolveWalletRuntimePaths("main");
54
+ const seedIndex = await loadWalletSeedIndex({
55
+ paths: mainPaths,
56
+ });
57
+ if (seedIndex.seeds.length > 0 && findWalletSeedRecord(seedIndex, parsed.seedName) === null) {
58
+ throw new Error("wallet_seed_not_found");
59
+ }
60
+ }
44
61
  if (parsed.command === "sync") {
45
62
  return runSyncCommand(parsed, context);
46
63
  }
@@ -19,7 +19,7 @@ export function createStopSignalWatcher(signalSource, stderr, client, forceExit)
19
19
  };
20
20
  const onFirstSignal = () => {
21
21
  closing = true;
22
- writeLine(stderr, "Stopping managed Cogcoin client...");
22
+ writeLine(stderr, "Detaching from managed Cogcoin client...");
23
23
  void client.close().then(() => {
24
24
  settle(0);
25
25
  }, () => {
@@ -227,6 +227,9 @@ export declare function deleteImportedWalletSeed(options: {
227
227
  assumeYes?: boolean;
228
228
  nowUnixMs?: number;
229
229
  paths?: WalletRuntimePaths;
230
+ attachService?: typeof attachOrStartManagedBitcoindService;
231
+ probeBitcoindService?: typeof probeManagedBitcoindService;
232
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
230
233
  }): Promise<WalletDeleteResult>;
231
234
  export declare function repairWallet(options: {
232
235
  dataDir: string;
@@ -1623,6 +1623,31 @@ export async function deleteImportedWalletSeed(options) {
1623
1623
  if (!options.assumeYes) {
1624
1624
  await confirmYesNo(options.prompter, `Delete imported seed "${seedName}" and release its local wallet artifacts? Type yes to continue: `);
1625
1625
  }
1626
+ const probeManagedBitcoind = options.probeBitcoindService ?? probeManagedBitcoindService;
1627
+ const managedBitcoindProbe = await probeManagedBitcoind({
1628
+ dataDir: options.dataDir,
1629
+ chain: "main",
1630
+ startHeight: 0,
1631
+ }).catch(() => ({
1632
+ compatibility: "unreachable",
1633
+ status: null,
1634
+ error: null,
1635
+ }));
1636
+ if (managedBitcoindProbe.compatibility !== "compatible" && managedBitcoindProbe.compatibility !== "unreachable") {
1637
+ throw new Error(managedBitcoindProbe.error ?? "managed_bitcoind_protocol_error");
1638
+ }
1639
+ if (managedBitcoindProbe.compatibility === "compatible") {
1640
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1641
+ dataDir: options.dataDir,
1642
+ chain: "main",
1643
+ startHeight: 0,
1644
+ });
1645
+ const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1646
+ const walletName = sanitizeWalletName(seedRecord.walletRootId);
1647
+ if (rpc.unloadWallet != null) {
1648
+ await rpc.unloadWallet(walletName, false).catch(() => undefined);
1649
+ }
1650
+ }
1626
1651
  await clearUnlockSession(paths.walletUnlockSessionPath).catch(() => undefined);
1627
1652
  await clearWalletExplicitLock(paths.walletExplicitLockPath).catch(() => undefined);
1628
1653
  await clearPendingInitialization(paths, provider).catch(() => undefined);
@@ -28,7 +28,7 @@ async function readSeedIndexFile(path) {
28
28
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
29
29
  return null;
30
30
  }
31
- throw error;
31
+ throw new Error("wallet_seed_index_invalid");
32
32
  }
33
33
  }
34
34
  export function normalizeWalletSeedName(name) {
@@ -319,7 +319,18 @@ function releaseClearedAnchorReservationState(options) {
319
319
  }
320
320
  return upsertProactiveFamily(nextState, {
321
321
  ...family,
322
+ status: "canceled",
322
323
  lastUpdatedAtUnixMs: options.nowUnixMs,
324
+ tx1: family.tx1 == null ? family.tx1 : {
325
+ ...family.tx1,
326
+ status: "canceled",
327
+ temporaryBuilderLockedOutpoints: [],
328
+ },
329
+ tx2: family.tx2 == null ? family.tx2 : {
330
+ ...family.tx2,
331
+ status: "canceled",
332
+ temporaryBuilderLockedOutpoints: [],
333
+ },
323
334
  });
324
335
  }
325
336
  function createFamilyTransactionRecord() {
@@ -1320,12 +1331,15 @@ export async function clearPendingAnchor(options) {
1320
1331
  });
1321
1332
  try {
1322
1333
  assertWalletMutationContextReady(readContext, "wallet_anchor_clear");
1334
+ const family = findActiveAnchorFamilyByDomain(readContext.localState.state, normalizedDomainName);
1323
1335
  const domain = readContext.localState.state.domains.find((entry) => entry.name === normalizedDomainName) ?? null;
1324
- if (domain === null) {
1336
+ if (domain === null && family === null) {
1325
1337
  throw new Error("wallet_anchor_clear_domain_not_found");
1326
1338
  }
1327
- const family = findActiveAnchorFamilyByDomain(readContext.localState.state, normalizedDomainName);
1328
1339
  if (family === null) {
1340
+ if (domain === null) {
1341
+ throw new Error("wallet_anchor_clear_domain_not_found");
1342
+ }
1329
1343
  if (domain.localAnchorIntent !== "none") {
1330
1344
  throw new Error("wallet_anchor_clear_inconsistent_state");
1331
1345
  }
@@ -1343,47 +1357,19 @@ export async function clearPendingAnchor(options) {
1343
1357
  if (family.status !== "draft" || family.currentStep !== "reserved") {
1344
1358
  throw new Error(`wallet_anchor_clear_not_clearable_${family.status}`);
1345
1359
  }
1346
- if (domain.localAnchorIntent !== "reserved"
1347
- || domain.dedicatedIndex === null
1348
- || family.reservedDedicatedIndex === null
1349
- || domain.dedicatedIndex !== family.reservedDedicatedIndex
1360
+ const reservedDedicatedIndex = family.reservedDedicatedIndex ?? null;
1361
+ if (reservedDedicatedIndex === null
1350
1362
  || family.tx1?.attemptedTxid !== null
1351
- || family.tx2?.attemptedTxid !== null) {
1363
+ || family.tx2?.attemptedTxid !== null
1364
+ || (domain !== null
1365
+ && (domain.localAnchorIntent !== "reserved"
1366
+ || domain.dedicatedIndex === null
1367
+ || domain.dedicatedIndex !== reservedDedicatedIndex))) {
1352
1368
  throw new Error("wallet_anchor_clear_inconsistent_state");
1353
1369
  }
1354
- await confirmAnchorClear(options.prompter, normalizedDomainName, family.reservedDedicatedIndex, options.assumeYes ?? false);
1355
- const operation = resolveAnchorOperation(readContext, normalizedDomainName, family.foundingMessageText ?? null, family.foundingMessagePayloadHex ?? null);
1356
- const targetIdentity = deriveAnchorTargetIdentityForIndex(readContext.localState.state, family.reservedDedicatedIndex);
1357
- const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
1358
- dataDir: options.dataDir,
1359
- chain: "main",
1360
- startHeight: 0,
1361
- walletRootId: readContext.localState.state.walletRootId,
1362
- });
1363
- const rpc = (options.rpcFactory ?? createRpcClient)(node.rpc);
1364
- const walletName = readContext.localState.state.managedCoreWallet.walletName;
1365
- const reconciled = await reconcileAnchorFamily({
1366
- state: readContext.localState.state,
1367
- family,
1368
- operation: {
1369
- ...operation,
1370
- targetIdentity,
1371
- },
1372
- provider,
1373
- nowUnixMs,
1374
- paths,
1375
- unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
1376
- rpc,
1377
- walletName,
1378
- });
1379
- if (reconciled.resolution !== "not-seen") {
1380
- throw new Error(reconciled.resolution === "repair-required"
1381
- ? "wallet_anchor_clear_not_clearable_repair_required"
1382
- : `wallet_anchor_clear_not_clearable_${reconciled.resolution}`);
1383
- }
1384
- const releasedDedicatedIndex = family.reservedDedicatedIndex;
1370
+ await confirmAnchorClear(options.prompter, normalizedDomainName, reservedDedicatedIndex, options.assumeYes ?? false);
1385
1371
  const releasedState = releaseClearedAnchorReservationState({
1386
- state: reconciled.state,
1372
+ state: readContext.localState.state,
1387
1373
  familyId: family.familyId,
1388
1374
  domainName: normalizedDomainName,
1389
1375
  nowUnixMs,
@@ -1404,7 +1390,7 @@ export async function clearPendingAnchor(options) {
1404
1390
  cleared: true,
1405
1391
  previousFamilyStatus: family.status,
1406
1392
  previousFamilyStep: family.currentStep ?? null,
1407
- releasedDedicatedIndex,
1393
+ releasedDedicatedIndex: reservedDedicatedIndex,
1408
1394
  };
1409
1395
  }
1410
1396
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
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",