@cogcoin/client 0.5.12 → 0.5.14
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/bootstrap/getblock-archive.d.ts +23 -1
- package/dist/bitcoind/bootstrap/getblock-archive.js +127 -37
- package/dist/bitcoind/bootstrap.d.ts +1 -1
- package/dist/bitcoind/bootstrap.js +1 -1
- package/dist/bitcoind/client/managed-client.js +62 -40
- package/dist/bitcoind/client/sync-engine.js +7 -2
- package/dist/bitcoind/testing.d.ts +1 -1
- package/dist/bitcoind/testing.js +1 -1
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/commands/sync.js +99 -1
- package/dist/cli/commands/wallet-mutation.js +39 -2
- package/dist/cli/context.js +20 -3
- package/dist/cli/mutation-success.d.ts +2 -0
- package/dist/cli/mutation-success.js +2 -0
- package/dist/cli/mutation-text-write.d.ts +2 -0
- package/dist/cli/mutation-text-write.js +7 -0
- package/dist/cli/output.js +22 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/wallet-format.d.ts +1 -1
- package/dist/cli/wallet-format.js +2 -2
- package/dist/wallet/archive.js +10 -8
- package/dist/wallet/coin-control.d.ts +41 -0
- package/dist/wallet/coin-control.js +406 -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.d.ts +2 -0
- package/dist/wallet/tx/anchor.js +76 -56
- package/dist/wallet/tx/cog.js +19 -22
- package/dist/wallet/tx/common.d.ts +45 -10
- package/dist/wallet/tx/common.js +178 -6
- package/dist/wallet/tx/domain-admin.js +15 -9
- package/dist/wallet/tx/domain-market.js +19 -22
- package/dist/wallet/tx/field.js +19 -18
- package/dist/wallet/tx/register.js +19 -22
- package/dist/wallet/tx/reputation.js +15 -9
- package/dist/wallet/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,406 @@
|
|
|
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 = 1_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
|
+
const normalized = Math.max(0, Math.trunc(raw));
|
|
37
|
+
if (normalized === 0) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
return DEFAULT_PROACTIVE_RESERVE_SATS;
|
|
41
|
+
}
|
|
42
|
+
function sameOutpointList(left, right) {
|
|
43
|
+
if (left.length !== (right?.length ?? 0)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return left.every((outpoint, index) => outpoint.txid === right?.[index]?.txid && outpoint.vout === right?.[index]?.vout);
|
|
47
|
+
}
|
|
48
|
+
export function normalizeWalletStateRecord(state) {
|
|
49
|
+
const rawProactiveReserveSats = state.proactiveReserveSats;
|
|
50
|
+
const proactiveReserveSats = normalizeReserveSats(rawProactiveReserveSats);
|
|
51
|
+
const reserveValueChanged = proactiveReserveSats !== rawProactiveReserveSats;
|
|
52
|
+
const proactiveReserveOutpoints = normalizeOutpointRecordList(proactiveReserveSats <= 0 || reserveValueChanged
|
|
53
|
+
? []
|
|
54
|
+
: state.proactiveReserveOutpoints);
|
|
55
|
+
const pendingMutations = state.pendingMutations ?? [];
|
|
56
|
+
if (proactiveReserveSats === state.proactiveReserveSats
|
|
57
|
+
&& sameOutpointList(proactiveReserveOutpoints, state.proactiveReserveOutpoints)
|
|
58
|
+
&& pendingMutations === state.pendingMutations) {
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
proactiveReserveSats,
|
|
64
|
+
proactiveReserveOutpoints,
|
|
65
|
+
pendingMutations,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function normalizePortableWalletArchivePayload(payload) {
|
|
69
|
+
const rawProactiveReserveSats = payload.proactiveReserveSats;
|
|
70
|
+
const proactiveReserveSats = normalizeReserveSats(rawProactiveReserveSats);
|
|
71
|
+
const reserveValueChanged = proactiveReserveSats !== rawProactiveReserveSats;
|
|
72
|
+
const proactiveReserveOutpoints = normalizeOutpointRecordList(proactiveReserveSats <= 0 || reserveValueChanged
|
|
73
|
+
? []
|
|
74
|
+
: payload.proactiveReserveOutpoints);
|
|
75
|
+
if (proactiveReserveSats === payload.proactiveReserveSats
|
|
76
|
+
&& sameOutpointList(proactiveReserveOutpoints, payload.proactiveReserveOutpoints)) {
|
|
77
|
+
return payload;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...payload,
|
|
81
|
+
proactiveReserveSats,
|
|
82
|
+
proactiveReserveOutpoints,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function isSpendableUtxo(entry) {
|
|
86
|
+
return entry.spendable !== false && entry.safe !== false;
|
|
87
|
+
}
|
|
88
|
+
function isConfirmedFundingUtxo(state, entry) {
|
|
89
|
+
return entry.scriptPubKey === state.funding.scriptPubKeyHex
|
|
90
|
+
&& entry.confirmations >= 1
|
|
91
|
+
&& isSpendableUtxo(entry);
|
|
92
|
+
}
|
|
93
|
+
function sortFundingEntriesForReserve(entries) {
|
|
94
|
+
return entries.slice().sort((left, right) => {
|
|
95
|
+
const amount = btcNumberToSats(right.amount) - btcNumberToSats(left.amount);
|
|
96
|
+
if (amount !== 0n) {
|
|
97
|
+
return amount > 0n ? 1 : -1;
|
|
98
|
+
}
|
|
99
|
+
const txid = left.txid.localeCompare(right.txid);
|
|
100
|
+
if (txid !== 0) {
|
|
101
|
+
return txid;
|
|
102
|
+
}
|
|
103
|
+
return left.vout - right.vout;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function isActiveTrackedTransaction(record) {
|
|
107
|
+
if (record == null) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return record.status === "broadcasting"
|
|
111
|
+
|| record.status === "broadcast-unknown"
|
|
112
|
+
|| record.status === "live";
|
|
113
|
+
}
|
|
114
|
+
function isActiveTrackedStatus(status) {
|
|
115
|
+
return status === "broadcasting"
|
|
116
|
+
|| status === "broadcast-unknown"
|
|
117
|
+
|| status === "live";
|
|
118
|
+
}
|
|
119
|
+
function deriveLiveProvisionalOutpointKeys(state) {
|
|
120
|
+
const keys = new Set();
|
|
121
|
+
for (const family of state.proactiveFamilies) {
|
|
122
|
+
if ((family.type !== "anchor" && family.type !== "field") || family.tx1?.attemptedTxid == null) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (isActiveTrackedStatus(family.status)) {
|
|
126
|
+
keys.add(outpointKey({ txid: family.tx1.attemptedTxid, vout: 1 }));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return keys;
|
|
130
|
+
}
|
|
131
|
+
function deriveAuxiliaryDedicatedOutpoints(state, spendableUtxos) {
|
|
132
|
+
const canonicalAnchorKeys = new Set(state.domains
|
|
133
|
+
.map((domain) => domain.currentCanonicalAnchorOutpoint)
|
|
134
|
+
.filter((outpoint) => outpoint !== null)
|
|
135
|
+
.map((outpoint) => outpointKey(outpoint)));
|
|
136
|
+
const dedicatedScriptSet = new Set(state.identities
|
|
137
|
+
.filter((identity) => identity.status === "dedicated")
|
|
138
|
+
.map((identity) => identity.scriptPubKeyHex));
|
|
139
|
+
const liveProvisionalKeys = deriveLiveProvisionalOutpointKeys(state);
|
|
140
|
+
const auxiliary = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const entry of spendableUtxos) {
|
|
143
|
+
if (!isSpendableUtxo(entry) || !dedicatedScriptSet.has(entry.scriptPubKey)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const outpoint = { txid: entry.txid, vout: entry.vout };
|
|
147
|
+
const key = outpointKey(outpoint);
|
|
148
|
+
if (canonicalAnchorKeys.has(key) || liveProvisionalKeys.has(key) || seen.has(key)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
seen.add(key);
|
|
152
|
+
auxiliary.push(outpoint);
|
|
153
|
+
}
|
|
154
|
+
return auxiliary;
|
|
155
|
+
}
|
|
156
|
+
export function computeDesignatedProactiveReserveOutpoints(state, spendableUtxos) {
|
|
157
|
+
const normalizedState = normalizeWalletStateRecord(state);
|
|
158
|
+
if (normalizedState.proactiveReserveSats <= 0) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const conflictKey = normalizedState.miningState.sharedMiningConflictOutpoint === null
|
|
162
|
+
? null
|
|
163
|
+
: outpointKey(normalizedState.miningState.sharedMiningConflictOutpoint);
|
|
164
|
+
const eligible = sortFundingEntriesForReserve(spendableUtxos.filter((entry) => isConfirmedFundingUtxo(normalizedState, entry)
|
|
165
|
+
&& outpointKey({ txid: entry.txid, vout: entry.vout }) !== conflictKey));
|
|
166
|
+
const selected = [];
|
|
167
|
+
let total = 0n;
|
|
168
|
+
const target = BigInt(normalizedState.proactiveReserveSats);
|
|
169
|
+
for (const entry of eligible) {
|
|
170
|
+
selected.push({ txid: entry.txid, vout: entry.vout });
|
|
171
|
+
total += btcNumberToSats(entry.amount);
|
|
172
|
+
if (total >= target) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (total < target) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return selected;
|
|
180
|
+
}
|
|
181
|
+
function syncStateWithComputedReserve(state, spendableUtxos) {
|
|
182
|
+
const normalizedState = normalizeWalletStateRecord(state);
|
|
183
|
+
const proactiveReserveOutpoints = computeDesignatedProactiveReserveOutpoints(normalizedState, spendableUtxos);
|
|
184
|
+
const sameLength = proactiveReserveOutpoints.length === normalizedState.proactiveReserveOutpoints.length;
|
|
185
|
+
const sameKeys = sameLength && proactiveReserveOutpoints.every((outpoint, index) => outpointKey(outpoint) === outpointKey(normalizedState.proactiveReserveOutpoints[index]));
|
|
186
|
+
if (sameKeys && normalizedState === state) {
|
|
187
|
+
return {
|
|
188
|
+
state,
|
|
189
|
+
changed: false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (sameKeys) {
|
|
193
|
+
return {
|
|
194
|
+
state: normalizedState,
|
|
195
|
+
changed: true,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
state: {
|
|
200
|
+
...normalizedState,
|
|
201
|
+
proactiveReserveOutpoints,
|
|
202
|
+
},
|
|
203
|
+
changed: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function collectInactiveTemporaryBuilderLockCleanup(state) {
|
|
207
|
+
const normalizedState = normalizeWalletStateRecord(state);
|
|
208
|
+
const stale = new Map();
|
|
209
|
+
let familiesChanged = false;
|
|
210
|
+
let mutationsChanged = false;
|
|
211
|
+
const proactiveFamilies = normalizedState.proactiveFamilies.map((family) => {
|
|
212
|
+
let nextFamily = family;
|
|
213
|
+
for (const key of ["tx1", "tx2"]) {
|
|
214
|
+
const record = nextFamily[key];
|
|
215
|
+
if (record == null || isActiveTrackedTransaction(record) || record.temporaryBuilderLockedOutpoints.length === 0) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
for (const outpoint of record.temporaryBuilderLockedOutpoints) {
|
|
219
|
+
stale.set(outpointKey(outpoint), outpoint);
|
|
220
|
+
}
|
|
221
|
+
nextFamily = {
|
|
222
|
+
...nextFamily,
|
|
223
|
+
[key]: {
|
|
224
|
+
...record,
|
|
225
|
+
temporaryBuilderLockedOutpoints: [],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
familiesChanged = true;
|
|
229
|
+
}
|
|
230
|
+
return nextFamily;
|
|
231
|
+
});
|
|
232
|
+
const pendingMutations = (normalizedState.pendingMutations ?? []).map((mutation) => {
|
|
233
|
+
if (isActiveTrackedStatus(mutation.status) || mutation.temporaryBuilderLockedOutpoints.length === 0) {
|
|
234
|
+
return mutation;
|
|
235
|
+
}
|
|
236
|
+
for (const outpoint of mutation.temporaryBuilderLockedOutpoints) {
|
|
237
|
+
stale.set(outpointKey(outpoint), outpoint);
|
|
238
|
+
}
|
|
239
|
+
mutationsChanged = true;
|
|
240
|
+
return {
|
|
241
|
+
...mutation,
|
|
242
|
+
temporaryBuilderLockedOutpoints: [],
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
if (!familiesChanged && !mutationsChanged && normalizedState === state) {
|
|
246
|
+
return {
|
|
247
|
+
state,
|
|
248
|
+
staleOutpoints: [],
|
|
249
|
+
changed: false,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
state: familiesChanged || mutationsChanged
|
|
254
|
+
? {
|
|
255
|
+
...normalizedState,
|
|
256
|
+
proactiveFamilies,
|
|
257
|
+
pendingMutations,
|
|
258
|
+
}
|
|
259
|
+
: normalizedState,
|
|
260
|
+
staleOutpoints: [...stale.values()],
|
|
261
|
+
changed: familiesChanged || mutationsChanged || normalizedState !== state,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function collectPersistentPolicyLockedOutpoints(state, spendableUtxos) {
|
|
265
|
+
const outpoints = [];
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
const pushUnique = (outpoint) => {
|
|
268
|
+
if (outpoint === null) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const key = outpointKey(outpoint);
|
|
272
|
+
if (seen.has(key)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
seen.add(key);
|
|
276
|
+
outpoints.push(outpoint);
|
|
277
|
+
};
|
|
278
|
+
for (const domain of state.domains) {
|
|
279
|
+
pushUnique(domain.currentCanonicalAnchorOutpoint);
|
|
280
|
+
}
|
|
281
|
+
for (const outpoint of deriveAuxiliaryDedicatedOutpoints(state, spendableUtxos)) {
|
|
282
|
+
pushUnique(outpoint);
|
|
283
|
+
}
|
|
284
|
+
for (const outpoint of state.proactiveReserveOutpoints) {
|
|
285
|
+
pushUnique(outpoint);
|
|
286
|
+
}
|
|
287
|
+
if (!miningFamilyMayStillExist(state.miningState)) {
|
|
288
|
+
pushUnique(state.miningState.sharedMiningConflictOutpoint);
|
|
289
|
+
}
|
|
290
|
+
return outpoints;
|
|
291
|
+
}
|
|
292
|
+
export async function reconcilePersistentPolicyLocks(options) {
|
|
293
|
+
const rawReserveOutpoints = normalizeOutpointRecordList(options.state.proactiveReserveOutpoints);
|
|
294
|
+
let state = normalizeWalletStateRecord(options.state);
|
|
295
|
+
let changed = state !== options.state;
|
|
296
|
+
const fixedInputKeys = new Set((options.fixedInputs ?? []).map((outpoint) => outpointKey(outpoint)));
|
|
297
|
+
const temporarilyUnlockedKeys = new Set((options.temporarilyUnlockedOutpoints ?? []).map((outpoint) => outpointKey(outpoint)));
|
|
298
|
+
if (options.cleanupInactiveTemporaryBuilderLocks === true) {
|
|
299
|
+
const cleaned = collectInactiveTemporaryBuilderLockCleanup(state);
|
|
300
|
+
state = cleaned.state;
|
|
301
|
+
changed ||= cleaned.changed;
|
|
302
|
+
if (cleaned.staleOutpoints.length > 0) {
|
|
303
|
+
await options.rpc.lockUnspent(options.walletName, true, cleaned.staleOutpoints).catch(() => undefined);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const lockedBeforeReserveInspection = await options.rpc.listLockUnspent(options.walletName).catch(() => []);
|
|
307
|
+
const lockedBeforeReserveInspectionKeys = new Set(lockedBeforeReserveInspection.map((outpoint) => outpointKey(outpoint)));
|
|
308
|
+
const reserveInspectionUnlocks = rawReserveOutpoints.filter((outpoint) => {
|
|
309
|
+
const key = outpointKey(outpoint);
|
|
310
|
+
return lockedBeforeReserveInspectionKeys.has(key) && !fixedInputKeys.has(key);
|
|
311
|
+
});
|
|
312
|
+
if (reserveInspectionUnlocks.length > 0) {
|
|
313
|
+
await options.rpc.lockUnspent(options.walletName, true, reserveInspectionUnlocks).catch(() => undefined);
|
|
314
|
+
}
|
|
315
|
+
const spendableUtxos = reserveInspectionUnlocks.length > 0 || options.spendableUtxos === undefined
|
|
316
|
+
? await options.rpc.listUnspent(options.walletName, 0).catch(() => [])
|
|
317
|
+
: options.spendableUtxos;
|
|
318
|
+
const previouslyProtectedUniverse = collectPersistentPolicyLockedOutpoints(state, spendableUtxos);
|
|
319
|
+
const reserveSynced = syncStateWithComputedReserve(state, spendableUtxos);
|
|
320
|
+
state = reserveSynced.state;
|
|
321
|
+
changed ||= reserveSynced.changed;
|
|
322
|
+
const protectedUniverse = collectPersistentPolicyLockedOutpoints(state, spendableUtxos);
|
|
323
|
+
if (protectedUniverse.length === 0 && previouslyProtectedUniverse.length === 0) {
|
|
324
|
+
return {
|
|
325
|
+
state,
|
|
326
|
+
changed,
|
|
327
|
+
spendableUtxos,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const protectedUniverseKeys = new Set(protectedUniverse.map((outpoint) => outpointKey(outpoint)));
|
|
331
|
+
const previouslyProtectedUniverseKeys = new Set(previouslyProtectedUniverse.map((outpoint) => outpointKey(outpoint)));
|
|
332
|
+
const managedProtectedKeys = new Set([
|
|
333
|
+
...protectedUniverseKeys,
|
|
334
|
+
...previouslyProtectedUniverseKeys,
|
|
335
|
+
]);
|
|
336
|
+
const locked = await options.rpc.listLockUnspent(options.walletName).catch(() => []);
|
|
337
|
+
const spendableKeys = new Set(spendableUtxos.map((entry) => outpointKey(entry)));
|
|
338
|
+
const lockedKeys = new Set(locked.map((outpoint) => outpointKey(outpoint)));
|
|
339
|
+
const expectedLocked = protectedUniverse.filter((outpoint) => {
|
|
340
|
+
const key = outpointKey(outpoint);
|
|
341
|
+
return (spendableKeys.has(key) || lockedKeys.has(key))
|
|
342
|
+
&& !fixedInputKeys.has(key)
|
|
343
|
+
&& !temporarilyUnlockedKeys.has(key);
|
|
344
|
+
});
|
|
345
|
+
const expectedLockedKeys = new Set(expectedLocked.map((outpoint) => outpointKey(outpoint)));
|
|
346
|
+
const lockedManaged = locked.filter((outpoint) => managedProtectedKeys.has(outpointKey(outpoint)));
|
|
347
|
+
const staleLocked = lockedManaged.filter((outpoint) => !expectedLockedKeys.has(outpointKey(outpoint)));
|
|
348
|
+
const missingLocked = protectedUniverse.filter((outpoint) => {
|
|
349
|
+
const key = outpointKey(outpoint);
|
|
350
|
+
return spendableKeys.has(key)
|
|
351
|
+
&& !fixedInputKeys.has(key)
|
|
352
|
+
&& !temporarilyUnlockedKeys.has(key)
|
|
353
|
+
&& !lockedKeys.has(key);
|
|
354
|
+
});
|
|
355
|
+
if (staleLocked.length > 0) {
|
|
356
|
+
await options.rpc.lockUnspent(options.walletName, true, staleLocked).catch(() => undefined);
|
|
357
|
+
}
|
|
358
|
+
if (missingLocked.length > 0) {
|
|
359
|
+
await options.rpc.lockUnspent(options.walletName, false, missingLocked).catch(() => undefined);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
state,
|
|
363
|
+
changed,
|
|
364
|
+
spendableUtxos,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export async function persistWalletCoinControlStateIfNeeded(options) {
|
|
368
|
+
const reconciled = await reconcilePersistentPolicyLocks({
|
|
369
|
+
rpc: options.rpc,
|
|
370
|
+
walletName: options.state.managedCoreWallet.walletName,
|
|
371
|
+
state: options.state,
|
|
372
|
+
cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks ?? true,
|
|
373
|
+
});
|
|
374
|
+
if (!reconciled.changed) {
|
|
375
|
+
return {
|
|
376
|
+
changed: false,
|
|
377
|
+
session: options.session ?? null,
|
|
378
|
+
state: reconciled.state,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const nextState = await persistWalletStateUpdate({
|
|
382
|
+
state: reconciled.state,
|
|
383
|
+
access: options.access,
|
|
384
|
+
paths: options.paths,
|
|
385
|
+
nowUnixMs: options.nowUnixMs,
|
|
386
|
+
replacePrimary: options.replacePrimary,
|
|
387
|
+
});
|
|
388
|
+
if (options.session == null) {
|
|
389
|
+
return {
|
|
390
|
+
changed: true,
|
|
391
|
+
session: null,
|
|
392
|
+
state: nextState,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const nextSession = {
|
|
396
|
+
...options.session,
|
|
397
|
+
walletRootId: nextState.walletRootId,
|
|
398
|
+
sourceStateRevision: nextState.stateRevision,
|
|
399
|
+
};
|
|
400
|
+
await saveUnlockSession(options.paths.walletUnlockSessionPath, nextSession, options.access);
|
|
401
|
+
return {
|
|
402
|
+
changed: true,
|
|
403
|
+
session: nextSession,
|
|
404
|
+
state: nextState,
|
|
405
|
+
};
|
|
406
|
+
}
|
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,
|
|
@@ -8,7 +8,7 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
|
8
8
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
9
9
|
import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
10
10
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
11
|
-
import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
11
|
+
import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
12
12
|
import { acquireFileLock } from "../fs/lock.js";
|
|
13
13
|
import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
|
|
14
14
|
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
@@ -584,10 +584,9 @@ function createMiningPlan(options) {
|
|
|
584
584
|
]).toString("hex");
|
|
585
585
|
return {
|
|
586
586
|
sender: options.candidate.sender,
|
|
587
|
-
|
|
587
|
+
fixedInputs: [
|
|
588
588
|
options.candidate.anchorOutpoint,
|
|
589
589
|
options.conflictOutpoint,
|
|
590
|
-
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
591
590
|
],
|
|
592
591
|
outputs: [
|
|
593
592
|
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
@@ -599,6 +598,7 @@ function createMiningPlan(options) {
|
|
|
599
598
|
expectedAnchorScriptHex: options.candidate.sender.scriptPubKeyHex,
|
|
600
599
|
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
601
600
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
601
|
+
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => walletMutationOutpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
602
602
|
expectedConflictOutpoint: options.conflictOutpoint,
|
|
603
603
|
feeRateSatVb: options.feeRateSatVb,
|
|
604
604
|
};
|
|
@@ -609,17 +609,22 @@ function validateMiningDraft(decoded, funded, plan) {
|
|
|
609
609
|
if (inputs.length < 2) {
|
|
610
610
|
throw new Error("wallet_mining_missing_inputs");
|
|
611
611
|
}
|
|
612
|
+
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
|
|
612
613
|
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
613
614
|
throw new Error("wallet_mining_sender_input_mismatch");
|
|
614
615
|
}
|
|
615
|
-
if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
|
|
616
|
+
if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
|
|
617
|
+
|| inputs[1]?.txid !== plan.expectedConflictOutpoint.txid
|
|
618
|
+
|| inputs[1].vout !== plan.expectedConflictOutpoint.vout) {
|
|
616
619
|
throw new Error("wallet_mining_conflict_input_mismatch");
|
|
617
620
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
621
|
+
assertFundingInputsAfterFixedPrefix({
|
|
622
|
+
inputs,
|
|
623
|
+
fixedInputs: plan.fixedInputs,
|
|
624
|
+
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
625
|
+
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
626
|
+
errorCode: "wallet_mining_unexpected_funding_input",
|
|
627
|
+
});
|
|
623
628
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
624
629
|
throw new Error("wallet_mining_opreturn_mismatch");
|
|
625
630
|
}
|
|
@@ -637,17 +642,12 @@ async function buildMiningTransaction(options) {
|
|
|
637
642
|
return buildWalletMutationTransaction({
|
|
638
643
|
rpc: options.rpc,
|
|
639
644
|
walletName: options.walletName,
|
|
645
|
+
state: options.state,
|
|
640
646
|
plan: options.plan,
|
|
641
647
|
validateFundedDraft: validateMiningDraft,
|
|
642
648
|
finalizeErrorCode: "wallet_mining_finalize_failed",
|
|
643
649
|
mempoolRejectPrefix: "wallet_mining_mempool_rejected",
|
|
644
650
|
feeRate: options.plan.feeRateSatVb,
|
|
645
|
-
builderOptions: {
|
|
646
|
-
addInputs: true,
|
|
647
|
-
includeUnsafe: true,
|
|
648
|
-
minConf: 0,
|
|
649
|
-
lockUnspents: true,
|
|
650
|
-
},
|
|
651
651
|
});
|
|
652
652
|
}
|
|
653
653
|
function resolveEligibleAnchoredRoots(context) {
|
|
@@ -1244,30 +1244,6 @@ function miningCandidateIsCurrent(options) {
|
|
|
1244
1244
|
&& options.nodeBestHeight !== null
|
|
1245
1245
|
&& options.state.currentBlockTargetHeight === (options.nodeBestHeight + 1);
|
|
1246
1246
|
}
|
|
1247
|
-
async function rebuildPersistentAnchorLocks(options) {
|
|
1248
|
-
const walletName = options.state.managedCoreWallet.walletName;
|
|
1249
|
-
const [locked, spendable] = await Promise.all([
|
|
1250
|
-
options.rpc.listLockUnspent(walletName).catch(() => []),
|
|
1251
|
-
options.rpc.listUnspent(walletName, 0).catch(() => []),
|
|
1252
|
-
]);
|
|
1253
|
-
const spendableKeys = new Set(spendable.map((entry) => `${entry.txid}:${entry.vout}`));
|
|
1254
|
-
const expected = options.state.domains
|
|
1255
|
-
.map((domain) => domain.currentCanonicalAnchorOutpoint)
|
|
1256
|
-
.filter((outpoint) => outpoint !== null)
|
|
1257
|
-
.map((outpoint) => ({ txid: outpoint.txid, vout: outpoint.vout }))
|
|
1258
|
-
.filter((outpoint) => spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1259
|
-
const expectedKeys = new Set(expected.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
|
|
1260
|
-
const lockedKeys = new Set(locked.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
|
|
1261
|
-
const staleLocked = locked.filter((outpoint) => !expectedKeys.has(`${outpoint.txid}:${outpoint.vout}`)
|
|
1262
|
-
|| !spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1263
|
-
const missingLocked = expected.filter((outpoint) => !lockedKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1264
|
-
if (staleLocked.length > 0) {
|
|
1265
|
-
await options.rpc.lockUnspent(walletName, true, staleLocked).catch(() => undefined);
|
|
1266
|
-
}
|
|
1267
|
-
if (missingLocked.length > 0) {
|
|
1268
|
-
await options.rpc.lockUnspent(walletName, false, missingLocked).catch(() => undefined);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
1247
|
async function reconcileLiveMiningState(options) {
|
|
1272
1248
|
let state = {
|
|
1273
1249
|
...options.state,
|
|
@@ -1275,7 +1251,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1275
1251
|
};
|
|
1276
1252
|
const currentTxid = state.miningState.currentTxid;
|
|
1277
1253
|
if (currentTxid === null || !miningFamilyMayStillExist(state.miningState)) {
|
|
1278
|
-
await
|
|
1254
|
+
await reconcilePersistentPolicyLocks({
|
|
1255
|
+
rpc: options.rpc,
|
|
1256
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1257
|
+
state,
|
|
1258
|
+
fixedInputs: [],
|
|
1259
|
+
});
|
|
1279
1260
|
return state;
|
|
1280
1261
|
}
|
|
1281
1262
|
const walletName = state.managedCoreWallet.walletName;
|
|
@@ -1295,7 +1276,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1295
1276
|
currentPublishDecision: "tx-confirmed-while-down",
|
|
1296
1277
|
},
|
|
1297
1278
|
};
|
|
1298
|
-
await
|
|
1279
|
+
await reconcilePersistentPolicyLocks({
|
|
1280
|
+
rpc: options.rpc,
|
|
1281
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1282
|
+
state,
|
|
1283
|
+
fixedInputs: [],
|
|
1284
|
+
});
|
|
1299
1285
|
return state;
|
|
1300
1286
|
}
|
|
1301
1287
|
if (inMempool) {
|
|
@@ -1319,7 +1305,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1319
1305
|
: null,
|
|
1320
1306
|
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-family",
|
|
1321
1307
|
});
|
|
1322
|
-
await
|
|
1308
|
+
await reconcilePersistentPolicyLocks({
|
|
1309
|
+
rpc: options.rpc,
|
|
1310
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1311
|
+
state,
|
|
1312
|
+
fixedInputs: [],
|
|
1313
|
+
});
|
|
1323
1314
|
return state;
|
|
1324
1315
|
}
|
|
1325
1316
|
if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
|
|
@@ -1333,7 +1324,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1333
1324
|
? "repair-required-broadcast-conflict"
|
|
1334
1325
|
: "repair-required-wallet-conflict",
|
|
1335
1326
|
});
|
|
1336
|
-
await
|
|
1327
|
+
await reconcilePersistentPolicyLocks({
|
|
1328
|
+
rpc: options.rpc,
|
|
1329
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1330
|
+
state,
|
|
1331
|
+
fixedInputs: [],
|
|
1332
|
+
});
|
|
1337
1333
|
return state;
|
|
1338
1334
|
}
|
|
1339
1335
|
state = defaultMiningStatePatch(state, {
|
|
@@ -1342,7 +1338,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1342
1338
|
? "broadcast-unknown-not-seen"
|
|
1343
1339
|
: "live-family-not-seen",
|
|
1344
1340
|
});
|
|
1345
|
-
await
|
|
1341
|
+
await reconcilePersistentPolicyLocks({
|
|
1342
|
+
rpc: options.rpc,
|
|
1343
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1344
|
+
state,
|
|
1345
|
+
fixedInputs: [],
|
|
1346
|
+
});
|
|
1346
1347
|
return state;
|
|
1347
1348
|
}
|
|
1348
1349
|
async function publishCandidate(options) {
|
|
@@ -1405,6 +1406,7 @@ async function publishCandidate(options) {
|
|
|
1405
1406
|
const built = await buildMiningTransaction({
|
|
1406
1407
|
rpc,
|
|
1407
1408
|
walletName: state.managedCoreWallet.walletName,
|
|
1409
|
+
state,
|
|
1408
1410
|
plan,
|
|
1409
1411
|
});
|
|
1410
1412
|
const intentFingerprintHex = computeIntentFingerprint(state, options.candidate);
|