@cogcoin/client 0.5.12 → 0.5.13

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.12` 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.13` 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
 
@@ -215,10 +215,15 @@ export async function syncToTip(dependencies) {
215
215
  aggregate.endingHeight = finalTip?.height ?? null;
216
216
  aggregate.bestHeight = endBestHeight;
217
217
  aggregate.bestHashHex = endInfo.bestblockhash;
218
- if (dependencies.targetHeightCap !== null && dependencies.targetHeightCap !== undefined && caughtUpCogcoin) {
218
+ const reachedTargetHeightCap = dependencies.targetHeightCap !== null
219
+ && dependencies.targetHeightCap !== undefined
220
+ && endBestHeight >= dependencies.targetHeightCap;
221
+ if (reachedTargetHeightCap && caughtUpCogcoin) {
219
222
  return aggregate;
220
223
  }
221
- if (endInfo.blocks === endInfo.headers && caughtUpCogcoin) {
224
+ if (dependencies.targetHeightCap === null
225
+ && endInfo.blocks === endInfo.headers
226
+ && caughtUpCogcoin) {
222
227
  if (dependencies.isFollowing()) {
223
228
  dependencies.progress.replaceFollowBlockTimes(await runRpc(() => dependencies.loadVisibleFollowBlockTimes(finalTip)));
224
229
  }
@@ -1,18 +1,20 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { writeJsonFileAtomic } from "./fs/atomic.js";
3
+ import { normalizePortableWalletArchivePayload } from "./coin-control.js";
3
4
  import { decryptJsonWithPassphrase, encryptJsonWithPassphrase, } from "./state/crypto.js";
4
5
  export const PORTABLE_WALLET_ARCHIVE_FORMAT = "cogcoin-portable-wallet-archive";
5
6
  function assertPortableWalletArchivePayload(payload) {
6
- if (payload.schemaVersion !== 1
7
- || payload.walletRootId.trim() === ""
8
- || payload.mnemonic.phrase.trim() === ""
9
- || payload.expected.accountPath.trim() === ""
10
- || payload.expected.publicExternalDescriptor.trim() === ""
11
- || payload.expected.fundingAddress0.trim() === ""
12
- || payload.expected.fundingScriptPubKeyHex0.trim() === "") {
7
+ const normalized = normalizePortableWalletArchivePayload(payload);
8
+ if (normalized.schemaVersion !== 1
9
+ || normalized.walletRootId.trim() === ""
10
+ || normalized.mnemonic.phrase.trim() === ""
11
+ || normalized.expected.accountPath.trim() === ""
12
+ || normalized.expected.publicExternalDescriptor.trim() === ""
13
+ || normalized.expected.fundingAddress0.trim() === ""
14
+ || normalized.expected.fundingScriptPubKeyHex0.trim() === "") {
13
15
  throw new Error("wallet_archive_payload_invalid");
14
16
  }
15
- return payload;
17
+ return normalized;
16
18
  }
17
19
  export async function writePortableWalletArchive(path, payload, passphrase) {
18
20
  const envelope = await encryptJsonWithPassphrase(assertPortableWalletArchivePayload(payload), passphrase, {
@@ -0,0 +1,41 @@
1
+ import type { RpcListUnspentEntry, RpcLockedUnspent } from "../bitcoind/types.js";
2
+ import { persistWalletStateUpdate } from "./descriptor-normalization.js";
3
+ import type { WalletRuntimePaths } from "./runtime.js";
4
+ import type { OutpointRecord, PortableWalletArchivePayloadV1, UnlockSessionStateV1, WalletStateV1 } from "./types.js";
5
+ export declare const DEFAULT_PROACTIVE_RESERVE_SATS = 50000;
6
+ export interface WalletCoinControlRpc {
7
+ listUnspent(walletName: string, minConf?: number): Promise<RpcListUnspentEntry[]>;
8
+ listLockUnspent(walletName: string): Promise<RpcLockedUnspent[]>;
9
+ lockUnspent(walletName: string, unlock: boolean, outputs: RpcLockedUnspent[]): Promise<boolean>;
10
+ }
11
+ export declare function outpointKey(outpoint: OutpointRecord): string;
12
+ export declare function normalizeWalletStateRecord(state: WalletStateV1): WalletStateV1;
13
+ export declare function normalizePortableWalletArchivePayload(payload: PortableWalletArchivePayloadV1): PortableWalletArchivePayloadV1;
14
+ export declare function computeDesignatedProactiveReserveOutpoints(state: WalletStateV1, spendableUtxos: readonly RpcListUnspentEntry[]): OutpointRecord[];
15
+ export declare function reconcilePersistentPolicyLocks(options: {
16
+ rpc: WalletCoinControlRpc;
17
+ walletName: string;
18
+ state: WalletStateV1;
19
+ fixedInputs?: readonly OutpointRecord[];
20
+ temporarilyUnlockedOutpoints?: readonly OutpointRecord[];
21
+ cleanupInactiveTemporaryBuilderLocks?: boolean;
22
+ spendableUtxos?: readonly RpcListUnspentEntry[];
23
+ }): Promise<{
24
+ state: WalletStateV1;
25
+ changed: boolean;
26
+ spendableUtxos: readonly RpcListUnspentEntry[];
27
+ }>;
28
+ export declare function persistWalletCoinControlStateIfNeeded(options: {
29
+ state: WalletStateV1;
30
+ access: Parameters<typeof persistWalletStateUpdate>[0]["access"];
31
+ session?: UnlockSessionStateV1 | null;
32
+ paths: WalletRuntimePaths;
33
+ nowUnixMs: number;
34
+ replacePrimary?: boolean;
35
+ rpc: WalletCoinControlRpc;
36
+ cleanupInactiveTemporaryBuilderLocks?: boolean;
37
+ }): Promise<{
38
+ changed: boolean;
39
+ session: UnlockSessionStateV1 | null;
40
+ state: WalletStateV1;
41
+ }>;
@@ -0,0 +1,365 @@
1
+ import { saveUnlockSession } from "./state/session.js";
2
+ import { persistWalletStateUpdate } from "./descriptor-normalization.js";
3
+ import { miningFamilyMayStillExist } from "./mining/state.js";
4
+ export const DEFAULT_PROACTIVE_RESERVE_SATS = 50_000;
5
+ function btcNumberToSats(value) {
6
+ return BigInt(Math.round(value * 100_000_000));
7
+ }
8
+ export function outpointKey(outpoint) {
9
+ return `${outpoint.txid}:${outpoint.vout}`;
10
+ }
11
+ function normalizeOutpointRecordList(outpoints) {
12
+ const normalized = [];
13
+ const seen = new Set();
14
+ for (const outpoint of outpoints ?? []) {
15
+ if (outpoint == null
16
+ || typeof outpoint.txid !== "string"
17
+ || outpoint.txid.length === 0
18
+ || typeof outpoint.vout !== "number"
19
+ || !Number.isInteger(outpoint.vout)
20
+ || outpoint.vout < 0) {
21
+ continue;
22
+ }
23
+ const key = outpointKey(outpoint);
24
+ if (seen.has(key)) {
25
+ continue;
26
+ }
27
+ seen.add(key);
28
+ normalized.push({ txid: outpoint.txid, vout: outpoint.vout });
29
+ }
30
+ return normalized;
31
+ }
32
+ function normalizeReserveSats(raw) {
33
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
34
+ return DEFAULT_PROACTIVE_RESERVE_SATS;
35
+ }
36
+ return Math.max(0, Math.trunc(raw));
37
+ }
38
+ function sameOutpointList(left, right) {
39
+ if (left.length !== (right?.length ?? 0)) {
40
+ return false;
41
+ }
42
+ return left.every((outpoint, index) => outpoint.txid === right?.[index]?.txid && outpoint.vout === right?.[index]?.vout);
43
+ }
44
+ export function normalizeWalletStateRecord(state) {
45
+ const proactiveReserveSats = normalizeReserveSats(state.proactiveReserveSats);
46
+ const proactiveReserveOutpoints = normalizeOutpointRecordList(state.proactiveReserveOutpoints);
47
+ const pendingMutations = state.pendingMutations ?? [];
48
+ if (proactiveReserveSats === state.proactiveReserveSats
49
+ && sameOutpointList(proactiveReserveOutpoints, state.proactiveReserveOutpoints)
50
+ && pendingMutations === state.pendingMutations) {
51
+ return state;
52
+ }
53
+ return {
54
+ ...state,
55
+ proactiveReserveSats,
56
+ proactiveReserveOutpoints,
57
+ pendingMutations,
58
+ };
59
+ }
60
+ export function normalizePortableWalletArchivePayload(payload) {
61
+ const proactiveReserveSats = normalizeReserveSats(payload.proactiveReserveSats);
62
+ const proactiveReserveOutpoints = normalizeOutpointRecordList(payload.proactiveReserveOutpoints);
63
+ if (proactiveReserveSats === payload.proactiveReserveSats
64
+ && sameOutpointList(proactiveReserveOutpoints, payload.proactiveReserveOutpoints)) {
65
+ return payload;
66
+ }
67
+ return {
68
+ ...payload,
69
+ proactiveReserveSats,
70
+ proactiveReserveOutpoints,
71
+ };
72
+ }
73
+ function isSpendableUtxo(entry) {
74
+ return entry.spendable !== false && entry.safe !== false;
75
+ }
76
+ function isConfirmedFundingUtxo(state, entry) {
77
+ return entry.scriptPubKey === state.funding.scriptPubKeyHex
78
+ && entry.confirmations >= 1
79
+ && isSpendableUtxo(entry);
80
+ }
81
+ function sortFundingEntriesForReserve(entries) {
82
+ return entries.slice().sort((left, right) => {
83
+ const amount = btcNumberToSats(right.amount) - btcNumberToSats(left.amount);
84
+ if (amount !== 0n) {
85
+ return amount > 0n ? 1 : -1;
86
+ }
87
+ const txid = left.txid.localeCompare(right.txid);
88
+ if (txid !== 0) {
89
+ return txid;
90
+ }
91
+ return left.vout - right.vout;
92
+ });
93
+ }
94
+ function isActiveTrackedTransaction(record) {
95
+ if (record == null) {
96
+ return false;
97
+ }
98
+ return record.status === "broadcasting"
99
+ || record.status === "broadcast-unknown"
100
+ || record.status === "live";
101
+ }
102
+ function isActiveTrackedStatus(status) {
103
+ return status === "broadcasting"
104
+ || status === "broadcast-unknown"
105
+ || status === "live";
106
+ }
107
+ function deriveLiveProvisionalOutpointKeys(state) {
108
+ const keys = new Set();
109
+ for (const family of state.proactiveFamilies) {
110
+ if ((family.type !== "anchor" && family.type !== "field") || family.tx1?.attemptedTxid == null) {
111
+ continue;
112
+ }
113
+ if (isActiveTrackedStatus(family.status)) {
114
+ keys.add(outpointKey({ txid: family.tx1.attemptedTxid, vout: 1 }));
115
+ }
116
+ }
117
+ return keys;
118
+ }
119
+ function deriveAuxiliaryDedicatedOutpoints(state, spendableUtxos) {
120
+ const canonicalAnchorKeys = new Set(state.domains
121
+ .map((domain) => domain.currentCanonicalAnchorOutpoint)
122
+ .filter((outpoint) => outpoint !== null)
123
+ .map((outpoint) => outpointKey(outpoint)));
124
+ const dedicatedScriptSet = new Set(state.identities
125
+ .filter((identity) => identity.status === "dedicated")
126
+ .map((identity) => identity.scriptPubKeyHex));
127
+ const liveProvisionalKeys = deriveLiveProvisionalOutpointKeys(state);
128
+ const auxiliary = [];
129
+ const seen = new Set();
130
+ for (const entry of spendableUtxos) {
131
+ if (!isSpendableUtxo(entry) || !dedicatedScriptSet.has(entry.scriptPubKey)) {
132
+ continue;
133
+ }
134
+ const outpoint = { txid: entry.txid, vout: entry.vout };
135
+ const key = outpointKey(outpoint);
136
+ if (canonicalAnchorKeys.has(key) || liveProvisionalKeys.has(key) || seen.has(key)) {
137
+ continue;
138
+ }
139
+ seen.add(key);
140
+ auxiliary.push(outpoint);
141
+ }
142
+ return auxiliary;
143
+ }
144
+ export function computeDesignatedProactiveReserveOutpoints(state, spendableUtxos) {
145
+ const normalizedState = normalizeWalletStateRecord(state);
146
+ if (normalizedState.proactiveReserveSats <= 0) {
147
+ return [];
148
+ }
149
+ const conflictKey = normalizedState.miningState.sharedMiningConflictOutpoint === null
150
+ ? null
151
+ : outpointKey(normalizedState.miningState.sharedMiningConflictOutpoint);
152
+ const eligible = sortFundingEntriesForReserve(spendableUtxos.filter((entry) => isConfirmedFundingUtxo(normalizedState, entry)
153
+ && outpointKey({ txid: entry.txid, vout: entry.vout }) !== conflictKey));
154
+ const selected = [];
155
+ let total = 0n;
156
+ const target = BigInt(normalizedState.proactiveReserveSats);
157
+ for (const entry of eligible) {
158
+ selected.push({ txid: entry.txid, vout: entry.vout });
159
+ total += btcNumberToSats(entry.amount);
160
+ if (total >= target) {
161
+ break;
162
+ }
163
+ }
164
+ return selected;
165
+ }
166
+ function syncStateWithComputedReserve(state, spendableUtxos) {
167
+ const normalizedState = normalizeWalletStateRecord(state);
168
+ const proactiveReserveOutpoints = computeDesignatedProactiveReserveOutpoints(normalizedState, spendableUtxos);
169
+ const sameLength = proactiveReserveOutpoints.length === normalizedState.proactiveReserveOutpoints.length;
170
+ const sameKeys = sameLength && proactiveReserveOutpoints.every((outpoint, index) => outpointKey(outpoint) === outpointKey(normalizedState.proactiveReserveOutpoints[index]));
171
+ if (sameKeys && normalizedState === state) {
172
+ return {
173
+ state,
174
+ changed: false,
175
+ };
176
+ }
177
+ if (sameKeys) {
178
+ return {
179
+ state: normalizedState,
180
+ changed: true,
181
+ };
182
+ }
183
+ return {
184
+ state: {
185
+ ...normalizedState,
186
+ proactiveReserveOutpoints,
187
+ },
188
+ changed: true,
189
+ };
190
+ }
191
+ function collectInactiveTemporaryBuilderLockCleanup(state) {
192
+ const normalizedState = normalizeWalletStateRecord(state);
193
+ const stale = new Map();
194
+ let familiesChanged = false;
195
+ let mutationsChanged = false;
196
+ const proactiveFamilies = normalizedState.proactiveFamilies.map((family) => {
197
+ let nextFamily = family;
198
+ for (const key of ["tx1", "tx2"]) {
199
+ const record = nextFamily[key];
200
+ if (record == null || isActiveTrackedTransaction(record) || record.temporaryBuilderLockedOutpoints.length === 0) {
201
+ continue;
202
+ }
203
+ for (const outpoint of record.temporaryBuilderLockedOutpoints) {
204
+ stale.set(outpointKey(outpoint), outpoint);
205
+ }
206
+ nextFamily = {
207
+ ...nextFamily,
208
+ [key]: {
209
+ ...record,
210
+ temporaryBuilderLockedOutpoints: [],
211
+ },
212
+ };
213
+ familiesChanged = true;
214
+ }
215
+ return nextFamily;
216
+ });
217
+ const pendingMutations = (normalizedState.pendingMutations ?? []).map((mutation) => {
218
+ if (isActiveTrackedStatus(mutation.status) || mutation.temporaryBuilderLockedOutpoints.length === 0) {
219
+ return mutation;
220
+ }
221
+ for (const outpoint of mutation.temporaryBuilderLockedOutpoints) {
222
+ stale.set(outpointKey(outpoint), outpoint);
223
+ }
224
+ mutationsChanged = true;
225
+ return {
226
+ ...mutation,
227
+ temporaryBuilderLockedOutpoints: [],
228
+ };
229
+ });
230
+ if (!familiesChanged && !mutationsChanged && normalizedState === state) {
231
+ return {
232
+ state,
233
+ staleOutpoints: [],
234
+ changed: false,
235
+ };
236
+ }
237
+ return {
238
+ state: familiesChanged || mutationsChanged
239
+ ? {
240
+ ...normalizedState,
241
+ proactiveFamilies,
242
+ pendingMutations,
243
+ }
244
+ : normalizedState,
245
+ staleOutpoints: [...stale.values()],
246
+ changed: familiesChanged || mutationsChanged || normalizedState !== state,
247
+ };
248
+ }
249
+ function collectPersistentPolicyLockedOutpoints(state, spendableUtxos) {
250
+ const outpoints = [];
251
+ const seen = new Set();
252
+ const pushUnique = (outpoint) => {
253
+ if (outpoint === null) {
254
+ return;
255
+ }
256
+ const key = outpointKey(outpoint);
257
+ if (seen.has(key)) {
258
+ return;
259
+ }
260
+ seen.add(key);
261
+ outpoints.push(outpoint);
262
+ };
263
+ for (const domain of state.domains) {
264
+ pushUnique(domain.currentCanonicalAnchorOutpoint);
265
+ }
266
+ for (const outpoint of deriveAuxiliaryDedicatedOutpoints(state, spendableUtxos)) {
267
+ pushUnique(outpoint);
268
+ }
269
+ for (const outpoint of state.proactiveReserveOutpoints) {
270
+ pushUnique(outpoint);
271
+ }
272
+ if (!miningFamilyMayStillExist(state.miningState)) {
273
+ pushUnique(state.miningState.sharedMiningConflictOutpoint);
274
+ }
275
+ return outpoints;
276
+ }
277
+ export async function reconcilePersistentPolicyLocks(options) {
278
+ let state = normalizeWalletStateRecord(options.state);
279
+ let changed = state !== options.state;
280
+ if (options.cleanupInactiveTemporaryBuilderLocks === true) {
281
+ const cleaned = collectInactiveTemporaryBuilderLockCleanup(state);
282
+ state = cleaned.state;
283
+ changed ||= cleaned.changed;
284
+ if (cleaned.staleOutpoints.length > 0) {
285
+ await options.rpc.lockUnspent(options.walletName, true, cleaned.staleOutpoints).catch(() => undefined);
286
+ }
287
+ }
288
+ const spendableUtxos = options.spendableUtxos ?? await options.rpc.listUnspent(options.walletName, 0).catch(() => []);
289
+ const reserveSynced = syncStateWithComputedReserve(state, spendableUtxos);
290
+ state = reserveSynced.state;
291
+ changed ||= reserveSynced.changed;
292
+ const protectedUniverse = collectPersistentPolicyLockedOutpoints(state, spendableUtxos);
293
+ if (protectedUniverse.length === 0) {
294
+ return {
295
+ state,
296
+ changed,
297
+ spendableUtxos,
298
+ };
299
+ }
300
+ const protectedUniverseKeys = new Set(protectedUniverse.map((outpoint) => outpointKey(outpoint)));
301
+ const fixedInputKeys = new Set((options.fixedInputs ?? []).map((outpoint) => outpointKey(outpoint)));
302
+ const temporarilyUnlockedKeys = new Set((options.temporarilyUnlockedOutpoints ?? []).map((outpoint) => outpointKey(outpoint)));
303
+ const locked = await options.rpc.listLockUnspent(options.walletName).catch(() => []);
304
+ const spendableKeys = new Set(spendableUtxos.map((entry) => outpointKey(entry)));
305
+ const expectedLocked = protectedUniverse.filter((outpoint) => {
306
+ const key = outpointKey(outpoint);
307
+ return spendableKeys.has(key) && !fixedInputKeys.has(key) && !temporarilyUnlockedKeys.has(key);
308
+ });
309
+ const expectedLockedKeys = new Set(expectedLocked.map((outpoint) => outpointKey(outpoint)));
310
+ const lockedProtected = locked.filter((outpoint) => protectedUniverseKeys.has(outpointKey(outpoint)));
311
+ const lockedProtectedKeys = new Set(lockedProtected.map((outpoint) => outpointKey(outpoint)));
312
+ const staleLocked = lockedProtected.filter((outpoint) => !expectedLockedKeys.has(outpointKey(outpoint)) || !spendableKeys.has(outpointKey(outpoint)));
313
+ const missingLocked = expectedLocked.filter((outpoint) => !lockedProtectedKeys.has(outpointKey(outpoint)));
314
+ if (staleLocked.length > 0) {
315
+ await options.rpc.lockUnspent(options.walletName, true, staleLocked).catch(() => undefined);
316
+ }
317
+ if (missingLocked.length > 0) {
318
+ await options.rpc.lockUnspent(options.walletName, false, missingLocked).catch(() => undefined);
319
+ }
320
+ return {
321
+ state,
322
+ changed,
323
+ spendableUtxos,
324
+ };
325
+ }
326
+ export async function persistWalletCoinControlStateIfNeeded(options) {
327
+ const reconciled = await reconcilePersistentPolicyLocks({
328
+ rpc: options.rpc,
329
+ walletName: options.state.managedCoreWallet.walletName,
330
+ state: options.state,
331
+ cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks ?? true,
332
+ });
333
+ if (!reconciled.changed) {
334
+ return {
335
+ changed: false,
336
+ session: options.session ?? null,
337
+ state: reconciled.state,
338
+ };
339
+ }
340
+ const nextState = await persistWalletStateUpdate({
341
+ state: reconciled.state,
342
+ access: options.access,
343
+ paths: options.paths,
344
+ nowUnixMs: options.nowUnixMs,
345
+ replacePrimary: options.replacePrimary,
346
+ });
347
+ if (options.session == null) {
348
+ return {
349
+ changed: true,
350
+ session: null,
351
+ state: nextState,
352
+ };
353
+ }
354
+ const nextSession = {
355
+ ...options.session,
356
+ walletRootId: nextState.walletRootId,
357
+ sourceStateRevision: nextState.stateRevision,
358
+ };
359
+ await saveUnlockSession(options.paths.walletUnlockSessionPath, nextSession, options.access);
360
+ return {
361
+ changed: true,
362
+ session: nextSession,
363
+ state: nextState,
364
+ };
365
+ }
@@ -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 { DEFAULT_PROACTIVE_RESERVE_SATS, normalizeWalletStateRecord, persistWalletCoinControlStateIfNeeded, } from "./coin-control.js";
11
12
  import { normalizeWalletDescriptorState, persistNormalizedWalletDescriptorStateIfNeeded, persistWalletStateUpdate, resolveNormalizedWalletDescriptorState, stripDescriptorChecksum, } from "./descriptor-normalization.js";
12
13
  import { acquireFileLock, clearOrphanedFileLock } from "./fs/lock.js";
13
14
  import { createInternalCoreWalletPassphrase, createMnemonicConfirmationChallenge, deriveWalletMaterialFromMnemonic, generateWalletMaterial, isEnglishMnemonicWord, validateEnglishMnemonic, } from "./material.js";
@@ -104,6 +105,8 @@ function createInitialWalletState(options) {
104
105
  walletRootId: options.walletRootId,
105
106
  network: "mainnet",
106
107
  anchorValueSats: 2_000,
108
+ proactiveReserveSats: DEFAULT_PROACTIVE_RESERVE_SATS,
109
+ proactiveReserveOutpoints: [],
107
110
  nextDedicatedIndex: 1,
108
111
  fundingIndex: 0,
109
112
  mnemonic: {
@@ -239,6 +242,21 @@ async function normalizeUnlockedWalletStateIfNeeded(options) {
239
242
  state = normalized.state;
240
243
  session = normalized.session ?? session;
241
244
  source = normalized.changed ? "primary" : options.source;
245
+ const coinControl = await persistWalletCoinControlStateIfNeeded({
246
+ state,
247
+ access: {
248
+ provider: options.provider,
249
+ secretReference: createWalletSecretReference(state.walletRootId),
250
+ },
251
+ session,
252
+ paths: options.paths,
253
+ nowUnixMs: options.nowUnixMs,
254
+ replacePrimary: source === "backup",
255
+ rpc: createRpcClient(node.rpc),
256
+ });
257
+ state = coinControl.state;
258
+ session = coinControl.session ?? session;
259
+ source = coinControl.changed ? "primary" : source;
242
260
  }
243
261
  finally {
244
262
  await node.stop?.().catch(() => undefined);
@@ -246,10 +264,10 @@ async function normalizeUnlockedWalletStateIfNeeded(options) {
246
264
  }
247
265
  return {
248
266
  session,
249
- state: {
267
+ state: normalizeWalletStateRecord({
250
268
  ...state,
251
269
  miningState: normalizeMiningStateRecord(state.miningState),
252
- },
270
+ }),
253
271
  source,
254
272
  };
255
273
  }
@@ -260,6 +278,8 @@ function createPortableWalletArchivePayload(state, exportedAtUnixMs) {
260
278
  walletRootId: state.walletRootId,
261
279
  network: state.network,
262
280
  anchorValueSats: state.anchorValueSats,
281
+ proactiveReserveSats: state.proactiveReserveSats,
282
+ proactiveReserveOutpoints: state.proactiveReserveOutpoints,
263
283
  nextDedicatedIndex: state.nextDedicatedIndex,
264
284
  fundingIndex: state.fundingIndex,
265
285
  mnemonic: {
@@ -306,6 +326,8 @@ function createWalletStateFromPortableArchive(options) {
306
326
  walletRootId: options.payload.walletRootId,
307
327
  network: options.payload.network,
308
328
  anchorValueSats: options.payload.anchorValueSats,
329
+ proactiveReserveSats: options.payload.proactiveReserveSats,
330
+ proactiveReserveOutpoints: options.payload.proactiveReserveOutpoints,
309
331
  nextDedicatedIndex: options.payload.nextDedicatedIndex,
310
332
  fundingIndex: options.payload.fundingIndex,
311
333
  walletBirthTime: options.payload.expected.walletBirthTime,
@@ -1842,6 +1864,21 @@ export async function repairWallet(options) {
1842
1864
  repairedState = normalizedDescriptorState.state;
1843
1865
  repairStateNeedsPersist = true;
1844
1866
  }
1867
+ const reconciledCoinControl = await persistWalletCoinControlStateIfNeeded({
1868
+ state: repairedState,
1869
+ access: {
1870
+ provider,
1871
+ secretReference,
1872
+ },
1873
+ paths,
1874
+ nowUnixMs,
1875
+ replacePrimary: recoveredFromBackup && !repairStateNeedsPersist,
1876
+ rpc: createRpcClient(bitcoindHandle.rpc),
1877
+ });
1878
+ repairedState = reconciledCoinControl.state;
1879
+ if (reconciledCoinControl.changed) {
1880
+ repairStateNeedsPersist = false;
1881
+ }
1845
1882
  let replica = await verifyManagedCoreWalletReplica(repairedState, options.dataDir, {
1846
1883
  nodeHandle: bitcoindHandle,
1847
1884
  attachService: options.attachService,