@cogcoin/client 0.5.11 → 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.
Files changed (46) hide show
  1. package/README.md +3 -1
  2. package/dist/app-paths.d.ts +1 -0
  3. package/dist/app-paths.js +3 -0
  4. package/dist/bitcoind/bootstrap/controller.d.ts +3 -0
  5. package/dist/bitcoind/bootstrap/controller.js +7 -5
  6. package/dist/bitcoind/client/factory.d.ts +8 -0
  7. package/dist/bitcoind/client/factory.js +43 -6
  8. package/dist/bitcoind/client/managed-client.js +19 -10
  9. package/dist/bitcoind/client/sync-engine.js +35 -4
  10. package/dist/bitcoind/progress/formatting.js +1 -1
  11. package/dist/bitcoind/testing.d.ts +1 -0
  12. package/dist/bitcoind/testing.js +1 -0
  13. package/dist/cli/commands/follow.js +47 -14
  14. package/dist/cli/commands/sync.js +48 -15
  15. package/dist/cli/context.js +5 -1
  16. package/dist/cli/output.js +11 -0
  17. package/dist/cli/runner.js +2 -0
  18. package/dist/cli/signals.d.ts +1 -1
  19. package/dist/cli/signals.js +17 -4
  20. package/dist/cli/types.d.ts +4 -0
  21. package/dist/cli/update-notifier.d.ts +2 -0
  22. package/dist/cli/update-notifier.js +276 -0
  23. package/dist/client/default-client.d.ts +1 -1
  24. package/dist/client/default-client.js +7 -1
  25. package/dist/client/factory.js +6 -1
  26. package/dist/sqlite/store.js +3 -0
  27. package/dist/types.d.ts +2 -0
  28. package/dist/wallet/archive.js +10 -8
  29. package/dist/wallet/coin-control.d.ts +41 -0
  30. package/dist/wallet/coin-control.js +365 -0
  31. package/dist/wallet/lifecycle.js +39 -2
  32. package/dist/wallet/mining/runner.js +46 -44
  33. package/dist/wallet/read/context.js +15 -6
  34. package/dist/wallet/reset.js +2 -0
  35. package/dist/wallet/state/storage.js +5 -4
  36. package/dist/wallet/tx/anchor.js +36 -51
  37. package/dist/wallet/tx/cog.js +19 -12
  38. package/dist/wallet/tx/common.d.ts +41 -10
  39. package/dist/wallet/tx/common.js +112 -5
  40. package/dist/wallet/tx/domain-admin.js +13 -8
  41. package/dist/wallet/tx/domain-market.js +19 -12
  42. package/dist/wallet/tx/field.js +21 -18
  43. package/dist/wallet/tx/register.js +17 -12
  44. package/dist/wallet/tx/reputation.js +13 -8
  45. package/dist/wallet/types.d.ts +4 -0
  46. package/package.json +1 -1
@@ -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,
@@ -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
- inputs: [
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
- for (let index = 2; index < inputs.length; index += 1) {
619
- if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
620
- throw new Error("wallet_mining_unexpected_funding_input");
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 rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
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 rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
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 rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
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 rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
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 rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
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);