@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 +1 -1
- package/dist/bitcoind/client/sync-engine.js +7 -2
- package/dist/wallet/archive.js +10 -8
- package/dist/wallet/coin-control.d.ts +41 -0
- package/dist/wallet/coin-control.js +365 -0
- package/dist/wallet/lifecycle.js +39 -2
- package/dist/wallet/mining/runner.js +46 -44
- package/dist/wallet/read/context.js +15 -6
- package/dist/wallet/reset.js +2 -0
- package/dist/wallet/state/storage.js +5 -4
- package/dist/wallet/tx/anchor.js +36 -51
- package/dist/wallet/tx/cog.js +19 -12
- package/dist/wallet/tx/common.d.ts +41 -10
- package/dist/wallet/tx/common.js +112 -5
- package/dist/wallet/tx/domain-admin.js +13 -8
- package/dist/wallet/tx/domain-market.js +19 -12
- package/dist/wallet/tx/field.js +21 -18
- package/dist/wallet/tx/register.js +17 -12
- package/dist/wallet/tx/reputation.js +13 -8
- package/dist/wallet/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@0.5.
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
package/dist/wallet/archive.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
||
|
|
9
|
-
||
|
|
10
|
-
||
|
|
11
|
-
||
|
|
12
|
-
||
|
|
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
|
|
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
|
+
}
|
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 { 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,
|