@cogcoin/client 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@0.5.0` 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.2` 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
 
@@ -45,6 +45,7 @@ The published package depends on:
45
45
  - `@scure/bip32@^2.0.1`
46
46
  - `@scure/bip39@^2.0.1`
47
47
  - `better-sqlite3@12.8.0`
48
+ - `hash-wasm@^4.12.0`
48
49
  - `zeromq@6.5.0`
49
50
 
50
51
  `@cogcoin/vectors` is kept as a repository development dependency for conformance tests and is not part of the published runtime dependency surface.
@@ -1,10 +1,9 @@
1
- import { Subscriber } from "zeromq";
2
- import type { FollowLoopControlDependencies, FollowLoopResources, ScheduleSyncDependencies, StartFollowingTipLoopDependencies } from "./internal-types.js";
1
+ import type { FollowLoopControlDependencies, FollowLoopSubscriber, FollowLoopResources, ScheduleSyncDependencies, StartFollowingTipLoopDependencies } from "./internal-types.js";
3
2
  export declare function startFollowingTipLoop(dependencies: StartFollowingTipLoopDependencies): Promise<FollowLoopResources>;
4
- export declare function consumeZmq(subscriber: Subscriber, dependencies: FollowLoopControlDependencies): Promise<void>;
3
+ export declare function consumeZmq(subscriber: FollowLoopSubscriber, dependencies: FollowLoopControlDependencies): Promise<void>;
5
4
  export declare function scheduleSync(dependencies: ScheduleSyncDependencies): void;
6
5
  export declare function closeFollowLoopResources(resources: {
7
- subscriber: Subscriber | null;
6
+ subscriber: FollowLoopSubscriber | null;
8
7
  followLoop: Promise<void> | null;
9
8
  pollTimer: ReturnType<typeof setInterval> | null;
10
9
  }): Promise<void>;
@@ -1,9 +1,23 @@
1
- import { Subscriber } from "zeromq";
1
+ async function loadZeroMqModule() {
2
+ return await import("zeromq");
3
+ }
4
+ async function createSubscriber(loadZeroMq) {
5
+ try {
6
+ const zeroMq = loadZeroMq === undefined
7
+ ? await loadZeroMqModule()
8
+ : await loadZeroMq();
9
+ return new zeroMq.Subscriber();
10
+ }
11
+ catch (error) {
12
+ const detail = error instanceof Error ? error.message : String(error);
13
+ throw new Error(`Managed tip following could not initialize \`zeromq\`: ${detail}`, { cause: error instanceof Error ? error : undefined });
14
+ }
15
+ }
2
16
  export async function startFollowingTipLoop(dependencies) {
17
+ const subscriber = await createSubscriber(dependencies.loadZeroMq);
3
18
  const currentTip = await dependencies.client.getTip();
4
19
  await dependencies.progress.enableFollowVisualMode(currentTip?.height ?? null, await dependencies.loadVisibleFollowBlockTimes(currentTip));
5
20
  await dependencies.syncToTip();
6
- const subscriber = new Subscriber();
7
21
  subscriber.connect(dependencies.node.zmq.endpoint);
8
22
  subscriber.subscribe(dependencies.node.zmq.topic);
9
23
  const followLoop = consumeZmq(subscriber, {
@@ -1,4 +1,3 @@
1
- import type { Subscriber } from "zeromq";
2
1
  import type { Client, ClientStoreAdapter } from "../../types.js";
3
2
  import type { AssumeUtxoBootstrapController } from "../bootstrap.js";
4
3
  import type { ManagedProgressController } from "../progress.js";
@@ -27,8 +26,16 @@ export interface SyncEngineDependencies {
27
26
  isFollowing(): boolean;
28
27
  loadVisibleFollowBlockTimes(tip: Awaited<ReturnType<Client["getTip"]>>): Promise<Record<number, number>>;
29
28
  }
29
+ export interface FollowLoopSubscriber extends AsyncIterable<unknown> {
30
+ close(): void;
31
+ connect(endpoint: string): void;
32
+ subscribe(topic: string): void;
33
+ }
34
+ export interface ZeroMqModuleLike {
35
+ Subscriber: new () => FollowLoopSubscriber;
36
+ }
30
37
  export interface FollowLoopResources {
31
- subscriber: Subscriber;
38
+ subscriber: FollowLoopSubscriber;
32
39
  followLoop: Promise<void>;
33
40
  pollTimer: ReturnType<typeof setInterval>;
34
41
  }
@@ -40,6 +47,7 @@ export interface StartFollowingTipLoopDependencies {
40
47
  scheduleSync(): void;
41
48
  shouldContinue(): boolean;
42
49
  loadVisibleFollowBlockTimes(tip: Awaited<ReturnType<Client["getTip"]>>): Promise<Record<number, number>>;
50
+ loadZeroMq?(): Promise<ZeroMqModuleLike>;
43
51
  }
44
52
  export interface ScheduleSyncDependencies {
45
53
  syncDebounceMs: number;
@@ -1,6 +1,6 @@
1
1
  import { closeFollowLoopResources, scheduleSync, startFollowingTipLoop } from "./follow-loop.js";
2
2
  import { loadVisibleFollowBlockTimes } from "./follow-block-times.js";
3
- import { createBlockRateTracker, createInitialSyncResult } from "./internal-types.js";
3
+ import { createBlockRateTracker, createInitialSyncResult, } from "./internal-types.js";
4
4
  import { syncToTip as runManagedSync } from "./sync-engine.js";
5
5
  export class DefaultManagedBitcoindClient {
6
6
  #client;
@@ -0,0 +1,42 @@
1
+ import { type WalletStateSaveAccess } from "./state/storage.js";
2
+ import type { WalletRuntimePaths } from "./runtime.js";
3
+ import type { UnlockSessionStateV1, WalletStateV1 } from "./types.js";
4
+ export interface WalletDescriptorInfoRpc {
5
+ getDescriptorInfo(descriptor: string): Promise<{
6
+ descriptor: string;
7
+ checksum: string;
8
+ }>;
9
+ }
10
+ export interface NormalizedWalletDescriptorState {
11
+ privateExternal: string;
12
+ publicExternal: string;
13
+ checksum: string;
14
+ }
15
+ export declare function stripDescriptorChecksum(descriptor: string): string;
16
+ export declare function buildDescriptorWithChecksum(descriptor: string, checksum: string): string;
17
+ export declare function resolveNormalizedWalletDescriptorState(state: WalletStateV1, rpc: WalletDescriptorInfoRpc): Promise<NormalizedWalletDescriptorState>;
18
+ export declare function applyNormalizedWalletDescriptorState(state: WalletStateV1, normalized: NormalizedWalletDescriptorState): WalletStateV1;
19
+ export declare function normalizeWalletDescriptorState(state: WalletStateV1, rpc: WalletDescriptorInfoRpc): Promise<{
20
+ changed: boolean;
21
+ state: WalletStateV1;
22
+ }>;
23
+ export declare function persistWalletStateUpdate(options: {
24
+ state: WalletStateV1;
25
+ access: WalletStateSaveAccess;
26
+ paths: WalletRuntimePaths;
27
+ nowUnixMs: number;
28
+ replacePrimary?: boolean;
29
+ }): Promise<WalletStateV1>;
30
+ export declare function persistNormalizedWalletDescriptorStateIfNeeded(options: {
31
+ state: WalletStateV1;
32
+ access: WalletStateSaveAccess;
33
+ session?: UnlockSessionStateV1 | null;
34
+ paths: WalletRuntimePaths;
35
+ nowUnixMs: number;
36
+ replacePrimary?: boolean;
37
+ rpc: WalletDescriptorInfoRpc;
38
+ }): Promise<{
39
+ changed: boolean;
40
+ session: UnlockSessionStateV1 | null;
41
+ state: WalletStateV1;
42
+ }>;
@@ -0,0 +1,108 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { deriveWalletMaterialFromMnemonic } from "./material.js";
3
+ import { saveUnlockSession } from "./state/session.js";
4
+ import { saveWalletState } from "./state/storage.js";
5
+ export function stripDescriptorChecksum(descriptor) {
6
+ return descriptor.replace(/#[A-Za-z0-9]+$/, "");
7
+ }
8
+ export function buildDescriptorWithChecksum(descriptor, checksum) {
9
+ return `${stripDescriptorChecksum(descriptor)}#${checksum}`;
10
+ }
11
+ function assertWalletDescriptorStateRecoverable(state) {
12
+ const material = deriveWalletMaterialFromMnemonic(state.mnemonic.phrase);
13
+ if (material.keys.masterFingerprintHex !== state.keys.masterFingerprintHex
14
+ || material.keys.accountPath !== state.keys.accountPath
15
+ || material.keys.accountXprv !== state.keys.accountXprv
16
+ || material.keys.accountXpub !== state.keys.accountXpub
17
+ || material.funding.address !== state.funding.address
18
+ || material.funding.scriptPubKeyHex !== state.funding.scriptPubKeyHex) {
19
+ throw new Error("wallet_descriptor_state_unrecoverable");
20
+ }
21
+ }
22
+ export async function resolveNormalizedWalletDescriptorState(state, rpc) {
23
+ assertWalletDescriptorStateRecoverable(state);
24
+ const material = deriveWalletMaterialFromMnemonic(state.mnemonic.phrase);
25
+ const privateDescriptor = await rpc.getDescriptorInfo(stripDescriptorChecksum(material.descriptor.privateExternal));
26
+ const publicDescriptor = await rpc.getDescriptorInfo(stripDescriptorChecksum(material.descriptor.publicExternal));
27
+ return {
28
+ privateExternal: buildDescriptorWithChecksum(material.descriptor.privateExternal, privateDescriptor.checksum),
29
+ publicExternal: buildDescriptorWithChecksum(publicDescriptor.descriptor, publicDescriptor.checksum),
30
+ checksum: publicDescriptor.checksum,
31
+ };
32
+ }
33
+ export function applyNormalizedWalletDescriptorState(state, normalized) {
34
+ return {
35
+ ...state,
36
+ descriptor: {
37
+ ...state.descriptor,
38
+ privateExternal: normalized.privateExternal,
39
+ publicExternal: normalized.publicExternal,
40
+ checksum: normalized.checksum,
41
+ },
42
+ managedCoreWallet: {
43
+ ...state.managedCoreWallet,
44
+ descriptorChecksum: normalized.checksum,
45
+ },
46
+ };
47
+ }
48
+ export async function normalizeWalletDescriptorState(state, rpc) {
49
+ const normalized = await resolveNormalizedWalletDescriptorState(state, rpc);
50
+ const changed = state.descriptor.privateExternal !== normalized.privateExternal
51
+ || state.descriptor.publicExternal !== normalized.publicExternal
52
+ || state.descriptor.checksum !== normalized.checksum
53
+ || state.managedCoreWallet.descriptorChecksum !== normalized.checksum;
54
+ return {
55
+ changed,
56
+ state: changed ? applyNormalizedWalletDescriptorState(state, normalized) : state,
57
+ };
58
+ }
59
+ export async function persistWalletStateUpdate(options) {
60
+ const nextState = {
61
+ ...options.state,
62
+ stateRevision: options.state.stateRevision + 1,
63
+ lastWrittenAtUnixMs: options.nowUnixMs,
64
+ };
65
+ if (options.replacePrimary) {
66
+ await rm(options.paths.walletStatePath, { force: true }).catch(() => undefined);
67
+ }
68
+ await saveWalletState({
69
+ primaryPath: options.paths.walletStatePath,
70
+ backupPath: options.paths.walletStateBackupPath,
71
+ }, nextState, options.access);
72
+ return nextState;
73
+ }
74
+ export async function persistNormalizedWalletDescriptorStateIfNeeded(options) {
75
+ const normalized = await normalizeWalletDescriptorState(options.state, options.rpc);
76
+ if (!normalized.changed) {
77
+ return {
78
+ changed: false,
79
+ session: options.session ?? null,
80
+ state: options.state,
81
+ };
82
+ }
83
+ const nextState = await persistWalletStateUpdate({
84
+ state: normalized.state,
85
+ access: options.access,
86
+ paths: options.paths,
87
+ nowUnixMs: options.nowUnixMs,
88
+ replacePrimary: options.replacePrimary,
89
+ });
90
+ if (options.session === undefined || options.session === null) {
91
+ return {
92
+ changed: true,
93
+ session: options.session ?? null,
94
+ state: nextState,
95
+ };
96
+ }
97
+ const nextSession = {
98
+ ...options.session,
99
+ walletRootId: nextState.walletRootId,
100
+ sourceStateRevision: nextState.stateRevision,
101
+ };
102
+ await saveUnlockSession(options.paths.walletUnlockSessionPath, nextSession, options.access);
103
+ return {
104
+ changed: true,
105
+ session: nextSession,
106
+ state: nextState,
107
+ };
108
+ }
@@ -123,6 +123,9 @@ export declare function loadUnlockedWalletState(options?: {
123
123
  provider?: WalletSecretProvider;
124
124
  nowUnixMs?: number;
125
125
  paths?: WalletRuntimePaths;
126
+ dataDir?: string;
127
+ attachService?: typeof attachOrStartManagedBitcoindService;
128
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
126
129
  }): Promise<LoadedUnlockedWalletState | null>;
127
130
  export declare function initializeWallet(options: {
128
131
  dataDir: string;
@@ -8,6 +8,7 @@ import { resolveManagedServicePaths } from "../bitcoind/service-paths.js";
8
8
  import { createRpcClient } from "../bitcoind/node.js";
9
9
  import { openSqliteStore } from "../sqlite/index.js";
10
10
  import { readPortableWalletArchive, writePortableWalletArchive } from "./archive.js";
11
+ import { normalizeWalletDescriptorState, persistNormalizedWalletDescriptorStateIfNeeded, persistWalletStateUpdate, resolveNormalizedWalletDescriptorState, stripDescriptorChecksum, } from "./descriptor-normalization.js";
11
12
  import { acquireFileLock } from "./fs/lock.js";
12
13
  import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, } from "./material.js";
13
14
  import { resolveWalletRuntimePathsForTesting } from "./runtime.js";
@@ -23,9 +24,6 @@ export const DEFAULT_UNLOCK_DURATION_MS = 15 * 60 * 1000;
23
24
  function sanitizeWalletName(walletRootId) {
24
25
  return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
25
26
  }
26
- function stripDescriptorChecksum(descriptor) {
27
- return descriptor.replace(/#[A-Za-z0-9]+$/, "");
28
- }
29
27
  async function pathExists(path) {
30
28
  try {
31
29
  await access(path, constants.F_OK);
@@ -518,22 +516,16 @@ function createStoppedBackgroundRuntimeSnapshot(snapshot, nowUnixMs) {
518
516
  };
519
517
  }
520
518
  async function persistRepairState(options) {
521
- const nextState = {
522
- ...options.state,
523
- stateRevision: options.state.stateRevision + 1,
524
- lastWrittenAtUnixMs: options.nowUnixMs,
525
- };
526
- if (options.replacePrimary) {
527
- await rm(options.paths.walletStatePath, { force: true }).catch(() => undefined);
528
- }
529
- await saveWalletState({
530
- primaryPath: options.paths.walletStatePath,
531
- backupPath: options.paths.walletStateBackupPath,
532
- }, nextState, {
533
- provider: options.provider,
534
- secretReference: createWalletSecretReference(nextState.walletRootId),
519
+ return await persistWalletStateUpdate({
520
+ state: options.state,
521
+ access: {
522
+ provider: options.provider,
523
+ secretReference: createWalletSecretReference(options.state.walletRootId),
524
+ },
525
+ paths: options.paths,
526
+ nowUnixMs: options.nowUnixMs,
527
+ replacePrimary: options.replacePrimary,
535
528
  });
536
- return nextState;
537
529
  }
538
530
  async function stopBackgroundMiningForRepair(options) {
539
531
  const pid = options.snapshot.backgroundWorkerPid;
@@ -630,13 +622,12 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
630
622
  await createManagedWalletReplica(rpc, state.walletRootId, {
631
623
  managedWalletPassphrase: state.managedCoreWallet.internalPassphrase,
632
624
  });
633
- const privateDescriptor = await rpc.getDescriptorInfo(state.descriptor.privateExternal);
634
- const publicDescriptor = await rpc.getDescriptorInfo(state.descriptor.publicExternal);
625
+ const normalizedDescriptors = await resolveNormalizedWalletDescriptorState(state, rpc);
635
626
  const walletName = sanitizeWalletName(state.walletRootId);
636
627
  await rpc.walletPassphrase(walletName, state.managedCoreWallet.internalPassphrase, 10);
637
628
  try {
638
629
  const importResults = await rpc.importDescriptors(walletName, [{
639
- desc: privateDescriptor.descriptor,
630
+ desc: normalizedDescriptors.privateExternal,
640
631
  timestamp: state.walletBirthTime,
641
632
  active: false,
642
633
  internal: false,
@@ -649,12 +640,12 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
649
640
  finally {
650
641
  await rpc.walletLock(walletName).catch(() => undefined);
651
642
  }
652
- const derivedFunding = await rpc.deriveAddresses(publicDescriptor.descriptor, [0, 0]);
643
+ const derivedFunding = await rpc.deriveAddresses(normalizedDescriptors.publicExternal, [0, 0]);
653
644
  if (derivedFunding[0] !== state.funding.address) {
654
645
  throw new Error("wallet_funding_address_verification_failed");
655
646
  }
656
647
  const descriptors = await rpc.listDescriptors(walletName);
657
- const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === privateDescriptor.descriptor);
648
+ const importedDescriptor = descriptors.descriptors.find((entry) => entry.desc === normalizedDescriptors.publicExternal);
658
649
  if (importedDescriptor == null) {
659
650
  throw new Error("wallet_descriptor_not_present_after_import");
660
651
  }
@@ -666,7 +657,7 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
666
657
  privateKeysEnabled: true,
667
658
  created: false,
668
659
  proofStatus: "ready",
669
- descriptorChecksum: privateDescriptor.checksum,
660
+ descriptorChecksum: normalizedDescriptors.checksum,
670
661
  fundingAddress0: state.funding.address,
671
662
  fundingScriptPubKeyHex0: state.funding.scriptPubKeyHex,
672
663
  message: null,
@@ -677,14 +668,14 @@ async function importDescriptorIntoManagedCoreWallet(state, provider, paths, dat
677
668
  lastWrittenAtUnixMs: nowUnixMs,
678
669
  descriptor: {
679
670
  ...state.descriptor,
680
- privateExternal: privateDescriptor.descriptor,
681
- publicExternal: publicDescriptor.descriptor,
682
- checksum: privateDescriptor.checksum,
671
+ privateExternal: normalizedDescriptors.privateExternal,
672
+ publicExternal: normalizedDescriptors.publicExternal,
673
+ checksum: normalizedDescriptors.checksum,
683
674
  },
684
675
  managedCoreWallet: {
685
676
  ...state.managedCoreWallet,
686
677
  walletName,
687
- descriptorChecksum: privateDescriptor.checksum,
678
+ descriptorChecksum: normalizedDescriptors.checksum,
688
679
  fundingAddress0: verifiedReplica.fundingAddress0 ?? null,
689
680
  fundingScriptPubKeyHex0: verifiedReplica.fundingScriptPubKeyHex0 ?? null,
690
681
  proofStatus: "ready",
@@ -782,7 +773,7 @@ export async function loadUnlockedWalletState(options = {}) {
782
773
  const nowUnixMs = options.nowUnixMs ?? Date.now();
783
774
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
784
775
  try {
785
- const session = await loadUnlockSession(paths.walletUnlockSessionPath, {
776
+ let session = await loadUnlockSession(paths.walletUnlockSessionPath, {
786
777
  provider,
787
778
  });
788
779
  if (session.unlockUntilUnixMs <= nowUnixMs) {
@@ -800,13 +791,43 @@ export async function loadUnlockedWalletState(options = {}) {
800
791
  await clearUnlockSession(paths.walletUnlockSessionPath);
801
792
  return null;
802
793
  }
794
+ let state = loaded.state;
795
+ let source = loaded.source;
796
+ if (options.dataDir !== undefined) {
797
+ const node = await (options.attachService ?? attachOrStartManagedBitcoindService)({
798
+ dataDir: options.dataDir,
799
+ chain: "main",
800
+ startHeight: 0,
801
+ walletRootId: state.walletRootId,
802
+ });
803
+ try {
804
+ const normalized = await persistNormalizedWalletDescriptorStateIfNeeded({
805
+ state,
806
+ access: {
807
+ provider,
808
+ secretReference: createWalletSecretReference(state.walletRootId),
809
+ },
810
+ session,
811
+ paths,
812
+ nowUnixMs,
813
+ replacePrimary: loaded.source === "backup",
814
+ rpc: (options.rpcFactory ?? createRpcClient)(node.rpc),
815
+ });
816
+ state = normalized.state;
817
+ session = normalized.session ?? session;
818
+ source = normalized.changed ? "primary" : loaded.source;
819
+ }
820
+ finally {
821
+ await node.stop?.().catch(() => undefined);
822
+ }
823
+ }
803
824
  return {
804
825
  session,
805
826
  state: {
806
- ...loaded.state,
807
- miningState: normalizeMiningStateRecord(loaded.state.miningState),
827
+ ...state,
828
+ miningState: normalizeMiningStateRecord(state.miningState),
808
829
  },
809
- source: loaded.source,
830
+ source,
810
831
  };
811
832
  }
812
833
  catch {
@@ -1242,6 +1263,11 @@ export async function repairWallet(options) {
1242
1263
  walletRootId: repairedState.walletRootId,
1243
1264
  });
1244
1265
  const bitcoindRpc = (options.rpcFactory ?? createRpcClient)(bitcoindHandle.rpc);
1266
+ const normalizedDescriptorState = await normalizeWalletDescriptorState(repairedState, bitcoindRpc);
1267
+ if (normalizedDescriptorState.changed) {
1268
+ repairedState = normalizedDescriptorState.state;
1269
+ repairStateNeedsPersist = true;
1270
+ }
1245
1271
  let replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
1246
1272
  nodeHandle: bitcoindHandle,
1247
1273
  attachService: options.attachService,
@@ -3,6 +3,7 @@ import { type WalletSecretProvider } from "../state/provider.js";
3
3
  import type { WalletLocalStateStatus, WalletReadContext } from "./types.js";
4
4
  import type { WalletRuntimePaths } from "../runtime.js";
5
5
  declare function inspectWalletLocalState(options?: {
6
+ dataDir?: string;
6
7
  passphrase?: Uint8Array | string;
7
8
  secretProvider?: WalletSecretProvider;
8
9
  now?: number;
@@ -6,11 +6,12 @@ import { UNINITIALIZED_WALLET_ROOT_ID } from "../../bitcoind/service-paths.js";
6
6
  import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } from "../../bitcoind/service.js";
7
7
  import {} from "../../bitcoind/types.js";
8
8
  import { loadUnlockedWalletState, verifyManagedCoreWalletReplica, } from "../lifecycle.js";
9
+ import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
9
10
  import { inspectMiningControlPlane } from "../mining/index.js";
10
11
  import { normalizeMiningStateRecord } from "../mining/state.js";
11
12
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
12
13
  import { loadWalletState } from "../state/storage.js";
13
- import { createDefaultWalletSecretProvider, } from "../state/provider.js";
14
+ import { createDefaultWalletSecretProvider, createWalletSecretReference, } from "../state/provider.js";
14
15
  import { createWalletReadModel } from "./project.js";
15
16
  const DEFAULT_SERVICE_START_TIMEOUT_MS = 10_000;
16
17
  const STALE_HEARTBEAT_THRESHOLD_MS = 15_000;
@@ -23,6 +24,40 @@ async function pathExists(path) {
23
24
  return false;
24
25
  }
25
26
  }
27
+ async function normalizeLoadedWalletStateForRead(options) {
28
+ if (options.dataDir === undefined) {
29
+ return options.loaded;
30
+ }
31
+ const node = await attachOrStartManagedBitcoindService({
32
+ dataDir: options.dataDir,
33
+ chain: "main",
34
+ startHeight: 0,
35
+ walletRootId: options.loaded.state.walletRootId,
36
+ });
37
+ try {
38
+ const access = typeof options.access === "string" || options.access instanceof Uint8Array
39
+ ? options.access
40
+ : {
41
+ provider: options.access.provider,
42
+ secretReference: createWalletSecretReference(options.loaded.state.walletRootId),
43
+ };
44
+ const normalized = await persistNormalizedWalletDescriptorStateIfNeeded({
45
+ state: options.loaded.state,
46
+ access,
47
+ paths: options.paths,
48
+ nowUnixMs: options.now,
49
+ replacePrimary: options.loaded.source === "backup",
50
+ rpc: createRpcClient(node.rpc),
51
+ });
52
+ return {
53
+ source: normalized.changed ? "primary" : options.loaded.source,
54
+ state: normalized.state,
55
+ };
56
+ }
57
+ finally {
58
+ await node.stop?.().catch(() => undefined);
59
+ }
60
+ }
26
61
  async function inspectWalletLocalState(options = {}) {
27
62
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
28
63
  const now = options.now ?? Date.now();
@@ -51,15 +86,23 @@ async function inspectWalletLocalState(options = {}) {
51
86
  provider,
52
87
  nowUnixMs: now,
53
88
  paths,
89
+ dataDir: options.dataDir,
54
90
  });
55
91
  if (unlocked === null) {
56
92
  try {
57
- await loadWalletState({
93
+ const loaded = await loadWalletState({
58
94
  primaryPath: paths.walletStatePath,
59
95
  backupPath: paths.walletStateBackupPath,
60
96
  }, {
61
97
  provider,
62
98
  });
99
+ await normalizeLoadedWalletStateForRead({
100
+ loaded,
101
+ access: { provider },
102
+ dataDir: options.dataDir,
103
+ now,
104
+ paths,
105
+ });
63
106
  }
64
107
  catch (error) {
65
108
  return {
@@ -118,10 +161,16 @@ async function inspectWalletLocalState(options = {}) {
118
161
  }
119
162
  }
120
163
  try {
121
- const loaded = await loadWalletState({
122
- primaryPath: paths.walletStatePath,
123
- backupPath: paths.walletStateBackupPath,
124
- }, options.passphrase);
164
+ const loaded = await normalizeLoadedWalletStateForRead({
165
+ loaded: await loadWalletState({
166
+ primaryPath: paths.walletStatePath,
167
+ backupPath: paths.walletStateBackupPath,
168
+ }, options.passphrase),
169
+ access: options.passphrase,
170
+ dataDir: options.dataDir,
171
+ now,
172
+ paths,
173
+ });
125
174
  return {
126
175
  availability: "ready",
127
176
  walletRootId: loaded.state.walletRootId,
@@ -408,6 +457,7 @@ export async function openWalletReadContext(options) {
408
457
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_SERVICE_START_TIMEOUT_MS;
409
458
  const now = options.now ?? Date.now();
410
459
  const localState = await inspectWalletLocalState({
460
+ dataDir: options.dataDir,
411
461
  passphrase: options.walletStatePassphrase,
412
462
  secretProvider: options.secretProvider,
413
463
  now,
@@ -1,4 +1,5 @@
1
- import { argon2, createCipheriv, createDecipheriv, randomBytes, } from "node:crypto";
1
+ import { argon2id } from "hash-wasm";
2
+ import { createCipheriv, createDecipheriv, randomBytes, } from "node:crypto";
2
3
  const DEFAULT_ARGON2_MEMORY_KIB = 65_536;
3
4
  const DEFAULT_ARGON2_ITERATIONS = 3;
4
5
  const DEFAULT_ARGON2_PARALLELISM = 1;
@@ -23,23 +24,17 @@ function jsonReviver(_key, value) {
23
24
  }
24
25
  return value;
25
26
  }
26
- function deriveArgon2Key(message, nonce, memoryKib, iterations, parallelism) {
27
- return new Promise((resolve, reject) => {
28
- argon2("argon2id", {
29
- message,
30
- nonce,
31
- memory: memoryKib,
32
- passes: iterations,
33
- parallelism,
34
- tagLength: DERIVED_KEY_LENGTH,
35
- }, (error, derivedKey) => {
36
- if (error) {
37
- reject(error);
38
- return;
39
- }
40
- resolve(Buffer.from(derivedKey));
41
- });
27
+ async function deriveArgon2Key(message, nonce, memoryKib, iterations, parallelism) {
28
+ const derivedKey = await argon2id({
29
+ password: message,
30
+ salt: nonce,
31
+ memorySize: memoryKib,
32
+ iterations,
33
+ parallelism,
34
+ hashLength: DERIVED_KEY_LENGTH,
35
+ outputType: "binary",
42
36
  });
37
+ return Buffer.from(derivedKey);
43
38
  }
44
39
  export async function deriveKeyFromPassphrase(passphrase, options = {}) {
45
40
  const salt = Buffer.from(options.salt ?? randomBytes(ARGON2_SALT_BYTES));
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",
7
7
  "engines": {
8
- "node": ">=22"
8
+ "node": ">=22.0.0"
9
9
  },
10
10
  "homepage": "https://cogcoin.org",
11
11
  "repository": {
@@ -68,6 +68,7 @@
68
68
  "@scure/bip32": "^2.0.1",
69
69
  "@scure/bip39": "^2.0.1",
70
70
  "better-sqlite3": "12.8.0",
71
+ "hash-wasm": "^4.12.0",
71
72
  "zeromq": "6.5.0"
72
73
  },
73
74
  "devDependencies": {