@cogcoin/client 0.5.1 → 0.5.3

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,8 +1,8 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@0.5.1` 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.3` 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
- Use Node 24.7.0 or newer.
5
+ Use Node 22 or newer.
6
6
 
7
7
  ## Links
8
8
 
@@ -32,6 +32,8 @@ npx cogcoin sync
32
32
 
33
33
  Verify the installed genesis artifacts before using the client in a production implementation.
34
34
  The installed package provides the `cogcoin` command for local wallet setup, sync, reads, writes, and mining workflows.
35
+ Provider-backed local wallets unlock on demand by default; `cogcoin wallet lock` suppresses that behavior until `cogcoin unlock` is run again.
36
+ Passphrase-wrapped wallet-state flows still require explicit passphrase-based access.
35
37
 
36
38
  ## Dependency Surface
37
39
 
@@ -45,6 +47,7 @@ The published package depends on:
45
47
  - `@scure/bip32@^2.0.1`
46
48
  - `@scure/bip39@^2.0.1`
47
49
  - `better-sqlite3@12.8.0`
50
+ - `hash-wasm@^4.12.0`
48
51
  - `zeromq@6.5.0`
49
52
 
50
53
  `@cogcoin/vectors` is kept as a repository development dependency for conformance tests and is not part of the published runtime dependency surface.
@@ -78,6 +81,7 @@ The installed `cogcoin` command covers the first-party local wallet and node wor
78
81
  - mining and hook commands such as `mine`, `mine start`, `mine stop`, `mine status`, `mine log`, `mine setup`, and `hooks status`
79
82
 
80
83
  The CLI also supports stable `--output json` and `--output preview-json` envelopes on the commands that advertise machine-readable output.
84
+ For provider-backed local wallets, normal reads, mutations, export, and mining setup flows auto-materialize a local unlock session when the wallet is not explicitly locked.
81
85
 
82
86
  ## SQLite Store
83
87
 
@@ -18,6 +18,7 @@ export interface CogcoinResolvedPaths {
18
18
  walletStatePath: string;
19
19
  walletStateBackupPath: string;
20
20
  walletUnlockSessionPath: string;
21
+ walletExplicitLockPath: string;
21
22
  walletControlLockPath: string;
22
23
  bitcoindLockPath: string;
23
24
  bitcoindStatusPath: string;
package/dist/app-paths.js CHANGED
@@ -64,6 +64,7 @@ export function resolveCogcoinPathsForTesting(resolution = {}) {
64
64
  const walletStatePath = joinForPlatform(platform, walletStateDirectory, "wallet-state.enc");
65
65
  const walletStateBackupPath = joinForPlatform(platform, walletStateDirectory, "wallet-state.enc.bak");
66
66
  const walletUnlockSessionPath = joinForPlatform(platform, runtimeRoot, "wallet-unlock-session.enc");
67
+ const walletExplicitLockPath = joinForPlatform(platform, runtimeRoot, "wallet-explicit-lock.json");
67
68
  const walletControlLockPath = joinForPlatform(platform, runtimeRoot, "wallet-control.lock");
68
69
  const bitcoindLockPath = joinForPlatform(platform, runtimeRoot, "bitcoind.lock");
69
70
  const bitcoindStatusPath = joinForPlatform(platform, runtimeRoot, "bitcoind-status.json");
@@ -91,6 +92,7 @@ export function resolveCogcoinPathsForTesting(resolution = {}) {
91
92
  walletStatePath,
92
93
  walletStateBackupPath,
93
94
  walletUnlockSessionPath,
95
+ walletExplicitLockPath,
94
96
  walletControlLockPath,
95
97
  bitcoindLockPath,
96
98
  bitcoindStatusPath,
@@ -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;
@@ -211,7 +211,7 @@ export function createCliErrorPresentation(errorCode, fallbackMessage) {
211
211
  if (errorCode === "wallet_locked") {
212
212
  return {
213
213
  what: "Wallet is locked.",
214
- why: "This command needs access to the unlocked local wallet state before it can continue.",
214
+ why: "This command needs access to the unlocked local wallet state before it can continue. Provider-backed wallets unlock on demand unless they were explicitly locked or the local secret store is unavailable.",
215
215
  next: "Run `cogcoin unlock --for 15m` and retry.",
216
216
  };
217
217
  }
@@ -1,3 +1,3 @@
1
1
  import type { ParsedCliArgs } from "./types.js";
2
- export declare const HELP_TEXT = "Usage: cogcoin <command> [options]\n\nCommands:\n status Show wallet-aware local service and chain status\n status --output json Emit the stable v1 machine-readable status envelope\n init Initialize a new local wallet root\n init --output json Emit the stable v1 machine-readable init result envelope\n repair Recover bounded wallet/indexer/runtime state\n unlock Unlock the local wallet for a limited duration\n wallet address Alias for address\n wallet ids Alias for ids\n hooks enable mining Enable a validated custom mining hook\n hooks disable mining Return to built-in mining hooks\n hooks status Show mining hook validation and trust status\n mine Run the miner in the foreground\n mine start Start the miner as a background worker\n mine stop Stop the active background miner\n mine setup Configure the built-in mining provider\n mine setup --output json\n Emit the stable v1 machine-readable mine setup result envelope\n mine status Show mining control-plane health and readiness\n mine log Show recent mining control-plane events\n anchor <domain> Anchor an owned unanchored domain with the Tx1/Tx2 family\n register <domain> [--from <identity>]\n Register a root domain or subdomain\n transfer <domain> --to <btc-target>\n Transfer an unanchored domain to another BTC identity\n sell <domain> <price> List an unanchored domain for sale in COG\n unsell <domain> Clear an active domain listing\n buy <domain> [--from <identity>]\n Buy an unanchored listed domain in COG\n send <amount> --to <btc-target>\n Send COG from one local identity to another BTC target\n claim <lock-id> --preimage <32-byte-hex>\n Claim an active COG lock before timeout\n reclaim <lock-id> Reclaim an expired COG lock as the original locker\n cog lock <amount> --to-domain <domain> (--for <blocks-or-duration> | --until-height <height>) --condition <sha256hex>\n Lock COG to an anchored recipient domain\n wallet status Show detailed wallet-local status and service health\n wallet init Initialize a new local wallet root\n wallet unlock Unlock the local wallet for a limited duration\n wallet lock Lock the local wallet immediately\n wallet export <path> Export a portable encrypted wallet archive\n wallet import <path> Import a portable encrypted wallet archive\n address Show the BTC funding identity for this wallet\n ids List locally controlled identities\n balance Show per-identity COG balances\n locks Show locally related active COG locks\n domain list Alias for domains\n domain show <domain> Alias for show <domain>\n domains [--anchored] [--listed] [--mineable]\n Show locally related domains\n show <domain> Show one domain and its local-wallet relationship\n fields <domain> List current fields on a domain\n field <domain> <field> Show one current field value\n field create <domain> <field>\n Create a new anchored field, optionally with an initial value\n field set <domain> <field>\n Update an existing anchored field value\n field clear <domain> <field>\n Clear an existing anchored field value\n rep give <source-domain> <target-domain> <amount>\n Burn COG as anchored-domain reputation support\n rep revoke <source-domain> <target-domain> <amount>\n Revoke visible support without refunding burned COG\n\nOptions:\n --db <path> Override the SQLite database path\n --data-dir <path> Override the managed bitcoin datadir\n --for <duration> Unlock duration like 15m, 2h, or 1d\n --message <text> Founding message text for anchor\n --to <btc-target> Transfer or send target as an address or spk:<hex>\n --from <identity> Resolve sender identity as id:<n>, domain:<name>, address, or spk:<hex>\n --to-domain <domain>\n Recipient domain for cog lock\n --condition <sha256hex>\n 32-byte lock condition hash\n --until-height <height>\n Absolute timeout height for cog lock\n --preimage <32-byte-hex>\n Claim preimage for an active lock\n --review <text> Optional public review text for reputation operations\n --text <utf8> UTF-8 payload text for endpoint or field writes\n --json <json> UTF-8 payload JSON text for endpoint or field writes\n --bytes <spec> Payload bytes as hex:<hex> or @<path>\n --permanent Create the field as permanent\n --format <spec> Advanced field format as raw:<u8>\n --value <spec> Advanced field value as hex:<hex>, @<path>, or utf8:<text>\n --claimable Show only currently claimable locks\n --reclaimable Show only currently reclaimable locks\n --anchored Show only anchored domains\n --listed Show only currently listed domains\n --mineable Show only locally mineable root domains\n --limit <n> Limit list rows (1..1000)\n --all Show all rows for list commands\n --verify Run full custom-hook verification\n --follow Follow mining log output\n --output <mode> Output mode: text, json, or preview-json\n --progress <mode> Progress output mode: auto, tty, or none\n --force-race Allow a visible root registration race\n --yes Approve eligible plain yes/no mutation confirmations non-interactively\n --help Show help\n --version Show package version\n\nQuickstart:\n 1. Run `cogcoin init` to create the wallet.\n 2. Run `cogcoin sync` to bootstrap assumeutxo and the managed Bitcoin/indexer state.\n 3. Run `cogcoin address`, then fund the wallet with about 0.0015 BTC so you can buy a 6+ character domain to start mining and still keep BTC available for mining transaction fees.\n\nExamples:\n cogcoin status --output json\n cogcoin init --output json\n cogcoin wallet address\n cogcoin domain list --mineable\n cogcoin register alpha-child\n cogcoin register weatherbot --from id:1\n cogcoin anchor alpha\n cogcoin buy alpha\n cogcoin buy alpha --from id:1\n cogcoin field create alpha bio --text \"hello\"\n cogcoin rep give alpha beta 10 --review \"great operator\"\n cogcoin hooks status\n cogcoin mine setup --output json\n cogcoin register alpha-child --output preview-json\n cogcoin mine status\n";
2
+ export declare const HELP_TEXT = "Usage: cogcoin <command> [options]\n\nCommands:\n status Show wallet-aware local service and chain status\n status --output json Emit the stable v1 machine-readable status envelope\n init Initialize a new local wallet root\n init --output json Emit the stable v1 machine-readable init result envelope\n repair Recover bounded wallet/indexer/runtime state\n unlock Clear an explicit wallet lock and unlock for a limited duration\n wallet address Alias for address\n wallet ids Alias for ids\n hooks enable mining Enable a validated custom mining hook\n hooks disable mining Return to built-in mining hooks\n hooks status Show mining hook validation and trust status\n mine Run the miner in the foreground\n mine start Start the miner as a background worker\n mine stop Stop the active background miner\n mine setup Configure the built-in mining provider\n mine setup --output json\n Emit the stable v1 machine-readable mine setup result envelope\n mine status Show mining control-plane health and readiness\n mine log Show recent mining control-plane events\n anchor <domain> Anchor an owned unanchored domain with the Tx1/Tx2 family\n register <domain> [--from <identity>]\n Register a root domain or subdomain\n transfer <domain> --to <btc-target>\n Transfer an unanchored domain to another BTC identity\n sell <domain> <price> List an unanchored domain for sale in COG\n unsell <domain> Clear an active domain listing\n buy <domain> [--from <identity>]\n Buy an unanchored listed domain in COG\n send <amount> --to <btc-target>\n Send COG from one local identity to another BTC target\n claim <lock-id> --preimage <32-byte-hex>\n Claim an active COG lock before timeout\n reclaim <lock-id> Reclaim an expired COG lock as the original locker\n cog lock <amount> --to-domain <domain> (--for <blocks-or-duration> | --until-height <height>) --condition <sha256hex>\n Lock COG to an anchored recipient domain\n wallet status Show detailed wallet-local status and service health\n wallet init Initialize a new local wallet root\n wallet unlock Clear an explicit wallet lock and unlock for a limited duration\n wallet lock Lock the local wallet and disable on-demand unlock\n wallet export <path> Export a portable encrypted wallet archive\n wallet import <path> Import a portable encrypted wallet archive\n address Show the BTC funding identity for this wallet\n ids List locally controlled identities\n balance Show per-identity COG balances\n locks Show locally related active COG locks\n domain list Alias for domains\n domain show <domain> Alias for show <domain>\n domains [--anchored] [--listed] [--mineable]\n Show locally related domains\n show <domain> Show one domain and its local-wallet relationship\n fields <domain> List current fields on a domain\n field <domain> <field> Show one current field value\n field create <domain> <field>\n Create a new anchored field, optionally with an initial value\n field set <domain> <field>\n Update an existing anchored field value\n field clear <domain> <field>\n Clear an existing anchored field value\n rep give <source-domain> <target-domain> <amount>\n Burn COG as anchored-domain reputation support\n rep revoke <source-domain> <target-domain> <amount>\n Revoke visible support without refunding burned COG\n\nOptions:\n --db <path> Override the SQLite database path\n --data-dir <path> Override the managed bitcoin datadir\n --for <duration> Unlock duration like 15m, 2h, or 1d when unlocking explicitly\n --message <text> Founding message text for anchor\n --to <btc-target> Transfer or send target as an address or spk:<hex>\n --from <identity> Resolve sender identity as id:<n>, domain:<name>, address, or spk:<hex>\n --to-domain <domain>\n Recipient domain for cog lock\n --condition <sha256hex>\n 32-byte lock condition hash\n --until-height <height>\n Absolute timeout height for cog lock\n --preimage <32-byte-hex>\n Claim preimage for an active lock\n --review <text> Optional public review text for reputation operations\n --text <utf8> UTF-8 payload text for endpoint or field writes\n --json <json> UTF-8 payload JSON text for endpoint or field writes\n --bytes <spec> Payload bytes as hex:<hex> or @<path>\n --permanent Create the field as permanent\n --format <spec> Advanced field format as raw:<u8>\n --value <spec> Advanced field value as hex:<hex>, @<path>, or utf8:<text>\n --claimable Show only currently claimable locks\n --reclaimable Show only currently reclaimable locks\n --anchored Show only anchored domains\n --listed Show only currently listed domains\n --mineable Show only locally mineable root domains\n --limit <n> Limit list rows (1..1000)\n --all Show all rows for list commands\n --verify Run full custom-hook verification\n --follow Follow mining log output\n --output <mode> Output mode: text, json, or preview-json\n --progress <mode> Progress output mode: auto, tty, or none\n --force-race Allow a visible root registration race\n --yes Approve eligible plain yes/no mutation confirmations non-interactively\n --help Show help\n --version Show package version\n\nQuickstart:\n 1. Run `cogcoin init` to create the wallet.\n 2. Run `cogcoin sync` to bootstrap assumeutxo and the managed Bitcoin/indexer state.\n 3. Run `cogcoin address`, then fund the wallet with about 0.0015 BTC so you can buy a 6+ character domain to start mining and still keep BTC available for mining transaction fees.\n\nExamples:\n cogcoin status --output json\n cogcoin init --output json\n cogcoin wallet address\n cogcoin domain list --mineable\n cogcoin register alpha-child\n cogcoin register weatherbot --from id:1\n cogcoin anchor alpha\n cogcoin buy alpha\n cogcoin buy alpha --from id:1\n cogcoin field create alpha bio --text \"hello\"\n cogcoin rep give alpha beta 10 --review \"great operator\"\n cogcoin hooks status\n cogcoin mine setup --output json\n cogcoin register alpha-child --output preview-json\n cogcoin mine status\n";
3
3
  export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
package/dist/cli/parse.js CHANGED
@@ -7,7 +7,7 @@ Commands:
7
7
  init Initialize a new local wallet root
8
8
  init --output json Emit the stable v1 machine-readable init result envelope
9
9
  repair Recover bounded wallet/indexer/runtime state
10
- unlock Unlock the local wallet for a limited duration
10
+ unlock Clear an explicit wallet lock and unlock for a limited duration
11
11
  wallet address Alias for address
12
12
  wallet ids Alias for ids
13
13
  hooks enable mining Enable a validated custom mining hook
@@ -39,8 +39,8 @@ Commands:
39
39
  Lock COG to an anchored recipient domain
40
40
  wallet status Show detailed wallet-local status and service health
41
41
  wallet init Initialize a new local wallet root
42
- wallet unlock Unlock the local wallet for a limited duration
43
- wallet lock Lock the local wallet immediately
42
+ wallet unlock Clear an explicit wallet lock and unlock for a limited duration
43
+ wallet lock Lock the local wallet and disable on-demand unlock
44
44
  wallet export <path> Export a portable encrypted wallet archive
45
45
  wallet import <path> Import a portable encrypted wallet archive
46
46
  address Show the BTC funding identity for this wallet
@@ -68,7 +68,7 @@ Commands:
68
68
  Options:
69
69
  --db <path> Override the SQLite database path
70
70
  --data-dir <path> Override the managed bitcoin datadir
71
- --for <duration> Unlock duration like 15m, 2h, or 1d
71
+ --for <duration> Unlock duration like 15m, 2h, or 1d when unlocking explicitly
72
72
  --message <text> Founding message text for anchor
73
73
  --to <btc-target> Transfer or send target as an address or spk:<hex>
74
74
  --from <identity> Resolve sender identity as id:<n>, domain:<name>, address, or spk:<hex>
@@ -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,19 @@ 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;
129
+ }): Promise<LoadedUnlockedWalletState | null>;
130
+ export declare function loadOrAutoUnlockWalletState(options?: {
131
+ provider?: WalletSecretProvider;
132
+ nowUnixMs?: number;
133
+ unlockDurationMs?: number;
134
+ paths?: WalletRuntimePaths;
135
+ dataDir?: string;
136
+ controlLockHeld?: boolean;
137
+ attachService?: typeof attachOrStartManagedBitcoindService;
138
+ rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;
126
139
  }): Promise<LoadedUnlockedWalletState | null>;
127
140
  export declare function initializeWallet(options: {
128
141
  dataDir: string;
@@ -143,6 +156,7 @@ export declare function unlockWallet(options?: {
143
156
  export declare function lockWallet(options: {
144
157
  dataDir: string;
145
158
  provider?: WalletSecretProvider;
159
+ nowUnixMs?: number;
146
160
  paths?: WalletRuntimePaths;
147
161
  attachService?: typeof attachOrStartManagedBitcoindService;
148
162
  rpcFactory?: (config: Parameters<typeof createRpcClient>[0]) => WalletLifecycleRpcClient;