@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 +2 -1
- package/dist/bitcoind/client/follow-loop.d.ts +3 -4
- package/dist/bitcoind/client/follow-loop.js +16 -2
- package/dist/bitcoind/client/internal-types.d.ts +10 -2
- package/dist/bitcoind/client/managed-client.js +1 -1
- package/dist/wallet/descriptor-normalization.d.ts +42 -0
- package/dist/wallet/descriptor-normalization.js +108 -0
- package/dist/wallet/lifecycle.d.ts +3 -0
- package/dist/wallet/lifecycle.js +58 -32
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +56 -6
- package/dist/wallet/state/crypto.js +12 -17
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@0.5.
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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;
|
package/dist/wallet/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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 ===
|
|
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:
|
|
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:
|
|
681
|
-
publicExternal:
|
|
682
|
-
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:
|
|
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
|
-
|
|
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
|
-
...
|
|
807
|
-
miningState: normalizeMiningStateRecord(
|
|
827
|
+
...state,
|
|
828
|
+
miningState: normalizeMiningStateRecord(state.miningState),
|
|
808
829
|
},
|
|
809
|
-
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 {
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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": {
|