@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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/bootstrap/getblock-archive.d.ts +23 -1
  3. package/dist/bitcoind/bootstrap/getblock-archive.js +127 -37
  4. package/dist/bitcoind/bootstrap.d.ts +1 -1
  5. package/dist/bitcoind/bootstrap.js +1 -1
  6. package/dist/bitcoind/client/managed-client.js +62 -40
  7. package/dist/bitcoind/client/sync-engine.js +7 -2
  8. package/dist/bitcoind/testing.d.ts +1 -1
  9. package/dist/bitcoind/testing.js +1 -1
  10. package/dist/cli/commands/status.js +1 -1
  11. package/dist/cli/commands/sync.js +99 -1
  12. package/dist/cli/commands/wallet-mutation.js +39 -2
  13. package/dist/cli/context.js +20 -3
  14. package/dist/cli/mutation-success.d.ts +2 -0
  15. package/dist/cli/mutation-success.js +2 -0
  16. package/dist/cli/mutation-text-write.d.ts +2 -0
  17. package/dist/cli/mutation-text-write.js +7 -0
  18. package/dist/cli/output.js +22 -1
  19. package/dist/cli/types.d.ts +2 -0
  20. package/dist/cli/wallet-format.d.ts +1 -1
  21. package/dist/cli/wallet-format.js +2 -2
  22. package/dist/wallet/archive.js +10 -8
  23. package/dist/wallet/coin-control.d.ts +41 -0
  24. package/dist/wallet/coin-control.js +406 -0
  25. package/dist/wallet/lifecycle.js +39 -2
  26. package/dist/wallet/mining/runner.js +46 -44
  27. package/dist/wallet/read/context.js +15 -6
  28. package/dist/wallet/reset.js +2 -0
  29. package/dist/wallet/state/storage.js +5 -4
  30. package/dist/wallet/tx/anchor.d.ts +2 -0
  31. package/dist/wallet/tx/anchor.js +76 -56
  32. package/dist/wallet/tx/cog.js +19 -22
  33. package/dist/wallet/tx/common.d.ts +45 -10
  34. package/dist/wallet/tx/common.js +178 -6
  35. package/dist/wallet/tx/domain-admin.js +15 -9
  36. package/dist/wallet/tx/domain-market.js +19 -22
  37. package/dist/wallet/tx/field.js +19 -18
  38. package/dist/wallet/tx/register.js +19 -22
  39. package/dist/wallet/tx/reputation.js +15 -9
  40. package/dist/wallet/types.d.ts +4 -0
  41. package/package.json +1 -1
@@ -2,8 +2,17 @@ import { randomBytes } from "node:crypto";
2
2
  import { saveUnlockSession } from "../state/session.js";
3
3
  import { saveWalletState } from "../state/storage.js";
4
4
  import { createWalletSecretReference, } from "../state/provider.js";
5
+ import { reconcilePersistentPolicyLocks as reconcileWalletCoinControlLocks } from "../coin-control.js";
5
6
  import { requestMiningGenerationPreemption } from "../mining/coordination.js";
6
7
  export const DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB = 10;
8
+ function btcNumberToSats(value) {
9
+ return BigInt(Math.round(value * 100_000_000));
10
+ }
11
+ function valueToSats(value) {
12
+ return typeof value === "string"
13
+ ? BigInt(Math.round(Number(value) * 100_000_000))
14
+ : btcNumberToSats(value);
15
+ }
7
16
  function createUnlockSessionState(state, unlockUntilUnixMs, nowUnixMs) {
8
17
  return {
9
18
  schemaVersion: 1,
@@ -39,6 +48,12 @@ export function formatCogAmount(value) {
39
48
  export function outpointKey(outpoint) {
40
49
  return `${outpoint.txid}:${outpoint.vout}`;
41
50
  }
51
+ function isSpendableConfirmedFundingUtxo(entry, fundingScriptPubKeyHex) {
52
+ return entry.scriptPubKey === fundingScriptPubKeyHex
53
+ && entry.confirmations >= 1
54
+ && entry.spendable !== false
55
+ && entry.safe !== false;
56
+ }
42
57
  export function updateMutationRecord(mutation, status, nowUnixMs, options = {}) {
43
58
  return {
44
59
  ...mutation,
@@ -64,6 +79,53 @@ export function diffTemporaryLockedOutpoints(before, after) {
64
79
  vout: entry.vout,
65
80
  }));
66
81
  }
82
+ export function getDecodedInputScriptPubKeyHex(input) {
83
+ return input.prevout?.scriptPubKey?.hex ?? null;
84
+ }
85
+ export function getDecodedInputVout(input) {
86
+ const vout = input.vout;
87
+ return typeof vout === "number" ? vout : null;
88
+ }
89
+ export function inputMatchesOutpoint(input, outpoint) {
90
+ return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
91
+ }
92
+ export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
93
+ if (inputs.length < fixedInputs.length) {
94
+ throw new Error(errorCode);
95
+ }
96
+ for (const [index, fixedInput] of fixedInputs.entries()) {
97
+ if (!inputMatchesOutpoint(inputs[index], fixedInput)) {
98
+ throw new Error(errorCode);
99
+ }
100
+ }
101
+ }
102
+ export function assertFundingInputsAfterFixedPrefix(options) {
103
+ for (let index = options.fixedInputs.length; index < options.inputs.length; index += 1) {
104
+ const input = options.inputs[index];
105
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
106
+ const vout = getDecodedInputVout(input);
107
+ if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
108
+ throw new Error(options.errorCode);
109
+ }
110
+ const key = outpointKey({
111
+ txid: input.txid,
112
+ vout,
113
+ });
114
+ if (!options.eligibleFundingOutpointKeys.has(key)) {
115
+ throw new Error(options.errorCode);
116
+ }
117
+ }
118
+ }
119
+ export async function reconcilePersistentPolicyLocks(options) {
120
+ await reconcileWalletCoinControlLocks({
121
+ rpc: options.rpc,
122
+ walletName: options.walletName,
123
+ state: options.state,
124
+ fixedInputs: options.fixedInputs,
125
+ temporarilyUnlockedOutpoints: options.temporarilyUnlockedOutpoints,
126
+ cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks,
127
+ });
128
+ }
67
129
  export function isBroadcastUnknownError(error) {
68
130
  const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
69
131
  return message.includes("timeout")
@@ -80,6 +142,38 @@ export function isAlreadyAcceptedError(error) {
80
142
  || message.includes("already in blockchain")
81
143
  || message.includes("txn-already-known");
82
144
  }
145
+ export function isInsufficientFundsError(error) {
146
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
147
+ return message.includes("insufficient funds");
148
+ }
149
+ function isReserveFloorFundingError(error) {
150
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
151
+ return message.includes("insufficient_funding_after_reserve");
152
+ }
153
+ function computeRemainingFundingValueSats(options) {
154
+ let remaining = 0n;
155
+ for (const value of options.availableFundingValueByKey.values()) {
156
+ remaining += value;
157
+ }
158
+ for (const input of options.transaction.vin) {
159
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
160
+ const vout = getDecodedInputVout(input);
161
+ if (scriptPubKeyHex !== options.fundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
162
+ continue;
163
+ }
164
+ remaining -= options.availableFundingValueByKey.get(outpointKey({
165
+ txid: input.txid,
166
+ vout,
167
+ })) ?? 0n;
168
+ }
169
+ for (const output of options.transaction.vout) {
170
+ if (output.scriptPubKey?.hex !== options.fundingScriptPubKeyHex) {
171
+ continue;
172
+ }
173
+ remaining += valueToSats(output.value);
174
+ }
175
+ return remaining;
176
+ }
83
177
  export function assertWalletMutationContextReady(context, errorPrefix) {
84
178
  if (context.localState.availability === "uninitialized") {
85
179
  throw new Error("wallet_uninitialized");
@@ -110,16 +204,36 @@ export async function pauseMiningForWalletMutation(options) {
110
204
  });
111
205
  }
112
206
  export async function buildWalletMutationTransaction(options) {
207
+ await reconcilePersistentPolicyLocks({
208
+ rpc: options.rpc,
209
+ walletName: options.walletName,
210
+ state: options.state,
211
+ fixedInputs: options.plan.fixedInputs,
212
+ temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
213
+ });
214
+ const availableFundingUtxos = (await options.rpc.listUnspent(options.walletName, 1))
215
+ .filter((entry) => isSpendableConfirmedFundingUtxo(entry, options.plan.allowedFundingScriptPubKeyHex));
216
+ const availableFundingValueByKey = new Map(availableFundingUtxos.map((entry) => [
217
+ outpointKey({ txid: entry.txid, vout: entry.vout }),
218
+ btcNumberToSats(entry.amount),
219
+ ]));
220
+ const validationPlan = {
221
+ ...options.plan,
222
+ eligibleFundingOutpointKeys: new Set([
223
+ ...options.plan.eligibleFundingOutpointKeys,
224
+ ...availableFundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout })),
225
+ ]),
226
+ };
113
227
  const lockedBefore = await options.rpc.listLockUnspent(options.walletName);
114
228
  let temporaryBuilderLockedOutpoints = [];
115
229
  try {
116
- const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.inputs, options.plan.outputs, 0, {
117
- add_inputs: options.builderOptions?.addInputs ?? true,
118
- include_unsafe: options.builderOptions?.includeUnsafe ?? false,
119
- minconf: options.builderOptions?.minConf ?? 1,
230
+ const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.fixedInputs, options.plan.outputs, 0, {
231
+ add_inputs: true,
232
+ include_unsafe: false,
233
+ minconf: 1,
120
234
  changeAddress: options.plan.changeAddress,
121
235
  changePosition: options.plan.changePosition,
122
- lockUnspents: options.builderOptions?.lockUnspents ?? true,
236
+ lockUnspents: true,
123
237
  fee_rate: options.feeRate ?? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB,
124
238
  replaceable: true,
125
239
  subtractFeeFromOutputs: [],
@@ -127,7 +241,17 @@ export async function buildWalletMutationTransaction(options) {
127
241
  const lockedAfter = await options.rpc.listLockUnspent(options.walletName);
128
242
  temporaryBuilderLockedOutpoints = diffTemporaryLockedOutpoints(lockedBefore, lockedAfter);
129
243
  const decoded = await options.rpc.decodePsbt(funded.psbt);
130
- options.validateFundedDraft(decoded, funded, options.plan);
244
+ options.validateFundedDraft(decoded, funded, validationPlan);
245
+ if (options.state.proactiveReserveSats > 0) {
246
+ const remainingFundingValueSats = computeRemainingFundingValueSats({
247
+ transaction: decoded.tx,
248
+ fundingScriptPubKeyHex: options.plan.allowedFundingScriptPubKeyHex,
249
+ availableFundingValueByKey,
250
+ });
251
+ if (remainingFundingValueSats < BigInt(options.state.proactiveReserveSats)) {
252
+ throw new Error("wallet_mutation_insufficient_funding_after_reserve");
253
+ }
254
+ }
131
255
  const signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
132
256
  const finalized = await options.rpc.finalizePsbt(signed.psbt, true);
133
257
  if (!finalized.complete || finalized.hex == null) {
@@ -139,6 +263,14 @@ export async function buildWalletMutationTransaction(options) {
139
263
  if (accepted == null || !accepted.allowed) {
140
264
  throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
141
265
  }
266
+ if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
267
+ await reconcilePersistentPolicyLocks({
268
+ rpc: options.rpc,
269
+ walletName: options.walletName,
270
+ state: options.state,
271
+ fixedInputs: options.plan.fixedInputs,
272
+ });
273
+ }
142
274
  return {
143
275
  funded,
144
276
  decoded,
@@ -151,6 +283,46 @@ export async function buildWalletMutationTransaction(options) {
151
283
  }
152
284
  catch (error) {
153
285
  await unlockTemporaryBuilderLocks(options.rpc, options.walletName, temporaryBuilderLockedOutpoints);
286
+ if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
287
+ await reconcilePersistentPolicyLocks({
288
+ rpc: options.rpc,
289
+ walletName: options.walletName,
290
+ state: options.state,
291
+ fixedInputs: options.plan.fixedInputs,
292
+ });
293
+ }
154
294
  throw error;
155
295
  }
156
296
  }
297
+ export async function buildWalletMutationTransactionWithReserveFallback(options) {
298
+ let unlockedReserveOutpoints = [];
299
+ let lastError = null;
300
+ for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
301
+ if (attempt > 0) {
302
+ unlockedReserveOutpoints = [
303
+ ...unlockedReserveOutpoints,
304
+ options.reserveCandidates[attempt - 1],
305
+ ];
306
+ }
307
+ try {
308
+ return await buildWalletMutationTransaction({
309
+ rpc: options.rpc,
310
+ walletName: options.walletName,
311
+ state: options.state,
312
+ plan: options.plan,
313
+ validateFundedDraft: options.validateFundedDraft,
314
+ finalizeErrorCode: options.finalizeErrorCode,
315
+ mempoolRejectPrefix: options.mempoolRejectPrefix,
316
+ feeRate: options.feeRate,
317
+ temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
318
+ });
319
+ }
320
+ catch (error) {
321
+ lastError = error;
322
+ if ((!isInsufficientFundsError(error) && !isReserveFloorFundingError(error)) || attempt === options.reserveCandidates.length) {
323
+ throw error;
324
+ }
325
+ }
326
+ }
327
+ throw lastError;
328
+ }
@@ -9,7 +9,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
9
9
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
10
10
  import { serializeSetCanonical, serializeSetDelegate, serializeSetEndpoint, serializeSetMiner, validateDomainName, } from "../cogop/index.js";
11
11
  import { openWalletReadContext } from "../read/index.js";
12
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
12
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
13
13
  import { confirmYesNo } from "./confirm.js";
14
14
  import { getCanonicalIdentitySelector } from "./identity-selector.js";
15
15
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -133,9 +133,8 @@ function buildPlanForDomainAdminOperation(options) {
133
133
  return {
134
134
  sender: options.sender,
135
135
  changeAddress: options.state.funding.address,
136
- inputs: [
136
+ fixedInputs: [
137
137
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
138
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
139
138
  ],
140
139
  outputs: [
141
140
  { data: Buffer.from(options.opReturnData).toString("hex") },
@@ -146,6 +145,7 @@ function buildPlanForDomainAdminOperation(options) {
146
145
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
147
146
  expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
148
147
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
148
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
149
149
  errorPrefix: options.errorPrefix,
150
150
  };
151
151
  }
@@ -155,14 +155,17 @@ function validateFundedDraft(decoded, funded, plan) {
155
155
  if (inputs.length === 0) {
156
156
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
157
157
  }
158
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
158
159
  if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
159
160
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
160
161
  }
161
- for (let index = 1; index < inputs.length; index += 1) {
162
- if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
163
- throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
164
- }
165
- }
162
+ assertFundingInputsAfterFixedPrefix({
163
+ inputs,
164
+ fixedInputs: plan.fixedInputs,
165
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
166
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
167
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
168
+ });
166
169
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
167
170
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
168
171
  }
@@ -186,13 +189,15 @@ function validateFundedDraft(decoded, funded, plan) {
186
189
  }
187
190
  }
188
191
  async function buildTransaction(options) {
189
- return buildWalletMutationTransaction({
192
+ return buildWalletMutationTransactionWithReserveFallback({
190
193
  rpc: options.rpc,
191
194
  walletName: options.walletName,
195
+ state: options.state,
192
196
  plan: options.plan,
193
197
  validateFundedDraft,
194
198
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
195
199
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
200
+ reserveCandidates: options.state.proactiveReserveOutpoints,
196
201
  });
197
202
  }
198
203
  function createDraftMutation(options) {
@@ -632,6 +637,7 @@ async function submitDomainAdminMutation(options) {
632
637
  const built = await buildTransaction({
633
638
  rpc,
634
639
  walletName,
640
+ state: nextState,
635
641
  plan: buildPlanForDomainAdminOperation({
636
642
  state: nextState,
637
643
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -7,7 +7,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
7
7
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
8
8
  import { serializeDomainBuy, serializeDomainSell, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
9
9
  import { openWalletReadContext } from "../read/index.js";
10
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
10
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
11
  import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
12
12
  import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
13
13
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -248,28 +248,17 @@ function buildPlanForDomainOperation(options) {
248
248
  && entry.safe !== false);
249
249
  const outputs = [{ data: Buffer.from(options.opReturnData).toString("hex") }];
250
250
  if (options.anchorOutpoint === null) {
251
- const senderUtxo = options.allUtxos.find((entry) => entry.scriptPubKey === options.sender.scriptPubKeyHex
252
- && entry.confirmations >= 1
253
- && entry.spendable !== false
254
- && entry.safe !== false);
255
- if (senderUtxo === undefined) {
256
- throw new Error(`${options.errorPrefix}_sender_utxo_unavailable`);
257
- }
258
251
  return {
259
252
  sender: options.sender,
260
253
  changeAddress: options.state.funding.address,
261
- inputs: [
262
- { txid: senderUtxo.txid, vout: senderUtxo.vout },
263
- ...fundingUtxos
264
- .filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
265
- .map((entry) => ({ txid: entry.txid, vout: entry.vout })),
266
- ],
254
+ fixedInputs: [],
267
255
  outputs,
268
256
  changePosition: 1,
269
257
  expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
270
258
  expectedAnchorScriptHex: null,
271
259
  expectedAnchorValueSats: null,
272
260
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
261
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
273
262
  errorPrefix: options.errorPrefix,
274
263
  };
275
264
  }
@@ -288,9 +277,8 @@ function buildPlanForDomainOperation(options) {
288
277
  return {
289
278
  sender: options.sender,
290
279
  changeAddress: options.state.funding.address,
291
- inputs: [
280
+ fixedInputs: [
292
281
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
293
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
294
282
  ],
295
283
  outputs,
296
284
  changePosition: 2,
@@ -298,6 +286,7 @@ function buildPlanForDomainOperation(options) {
298
286
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
299
287
  expectedAnchorValueSats: options.anchorValueSats,
300
288
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
289
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
301
290
  errorPrefix: options.errorPrefix,
302
291
  };
303
292
  }
@@ -307,14 +296,17 @@ function validateFundedDraft(decoded, funded, plan) {
307
296
  if (inputs.length === 0) {
308
297
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
309
298
  }
299
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
310
300
  if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
311
301
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
312
302
  }
313
- for (let index = 1; index < inputs.length; index += 1) {
314
- if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
315
- throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
316
- }
317
- }
303
+ assertFundingInputsAfterFixedPrefix({
304
+ inputs,
305
+ fixedInputs: plan.fixedInputs,
306
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
307
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
308
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
309
+ });
318
310
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
319
311
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
320
312
  }
@@ -341,13 +333,15 @@ function validateFundedDraft(decoded, funded, plan) {
341
333
  }
342
334
  }
343
335
  async function buildTransaction(options) {
344
- return buildWalletMutationTransaction({
336
+ return buildWalletMutationTransactionWithReserveFallback({
345
337
  rpc: options.rpc,
346
338
  walletName: options.walletName,
339
+ state: options.state,
347
340
  plan: options.plan,
348
341
  validateFundedDraft,
349
342
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
350
343
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
344
+ reserveCandidates: options.state.proactiveReserveOutpoints,
351
345
  });
352
346
  }
353
347
  function createDraftMutation(options) {
@@ -768,6 +762,7 @@ export async function transferDomain(options) {
768
762
  const built = await buildTransaction({
769
763
  rpc,
770
764
  walletName,
765
+ state: nextState,
771
766
  plan: buildPlanForDomainOperation({
772
767
  state: nextState,
773
768
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -998,6 +993,7 @@ async function runSellMutation(options) {
998
993
  const built = await buildTransaction({
999
994
  rpc,
1000
995
  walletName,
996
+ state: nextState,
1001
997
  plan: buildPlanForDomainOperation({
1002
998
  state: nextState,
1003
999
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1233,6 +1229,7 @@ export async function buyDomain(options) {
1233
1229
  const built = await buildTransaction({
1234
1230
  rpc,
1235
1231
  walletName,
1232
+ state: nextState,
1236
1233
  plan: buildPlanForDomainOperation({
1237
1234
  state: nextState,
1238
1235
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -10,7 +10,7 @@ import { createDefaultWalletSecretProvider, } from "../state/provider.js";
10
10
  import { FIELD_FORMAT_BYTES, serializeDataUpdate, serializeFieldReg, } from "../cogop/index.js";
11
11
  import { validateFieldName } from "../cogop/validate-name.js";
12
12
  import { findDomainField, openWalletReadContext, } from "../read/index.js";
13
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
13
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
14
14
  import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
15
15
  import { getCanonicalIdentitySelector } from "./identity-selector.js";
16
16
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -253,9 +253,8 @@ function buildAnchoredFieldPlan(options) {
253
253
  return {
254
254
  sender: options.sender,
255
255
  changeAddress: options.state.funding.address,
256
- inputs: [
256
+ fixedInputs: [
257
257
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
258
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
259
258
  ],
260
259
  outputs: [
261
260
  { data: Buffer.from(options.opReturnData).toString("hex") },
@@ -266,6 +265,7 @@ function buildAnchoredFieldPlan(options) {
266
265
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
267
266
  expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
268
267
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
268
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
269
269
  errorPrefix: options.errorPrefix,
270
270
  };
271
271
  }
@@ -277,10 +277,7 @@ function buildFieldFamilyTx2Plan(options) {
277
277
  return {
278
278
  sender: options.sender,
279
279
  changeAddress: options.state.funding.address,
280
- inputs: [
281
- { txid: options.tx1Txid, vout: 1 },
282
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
283
- ],
280
+ fixedInputs: [{ txid: options.tx1Txid, vout: 1 }],
284
281
  outputs: [
285
282
  { data: Buffer.from(options.opReturnData).toString("hex") },
286
283
  { [options.sender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -290,6 +287,7 @@ function buildFieldFamilyTx2Plan(options) {
290
287
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
291
288
  expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
292
289
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
290
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
293
291
  errorPrefix: "wallet_field_create_tx2",
294
292
  };
295
293
  }
@@ -299,14 +297,17 @@ function validateFieldDraft(decoded, funded, plan) {
299
297
  if (inputs.length === 0) {
300
298
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
301
299
  }
300
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
302
301
  if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
303
302
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
304
303
  }
305
- for (let index = 1; index < inputs.length; index += 1) {
306
- if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
307
- throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
308
- }
309
- }
304
+ assertFundingInputsAfterFixedPrefix({
305
+ inputs,
306
+ fixedInputs: plan.fixedInputs,
307
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
308
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
309
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
310
+ });
310
311
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
311
312
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
312
313
  }
@@ -330,14 +331,15 @@ function validateFieldDraft(decoded, funded, plan) {
330
331
  }
331
332
  }
332
333
  async function buildFieldTransaction(options) {
333
- return buildWalletMutationTransaction({
334
+ return buildWalletMutationTransactionWithReserveFallback({
334
335
  rpc: options.rpc,
335
336
  walletName: options.walletName,
337
+ state: options.state,
336
338
  plan: options.plan,
337
339
  validateFundedDraft: validateFieldDraft,
338
340
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
339
341
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
340
- builderOptions: options.builderOptions,
342
+ reserveCandidates: options.state.proactiveReserveOutpoints,
341
343
  });
342
344
  }
343
345
  async function saveUpdatedState(options) {
@@ -1348,6 +1350,7 @@ async function submitStandaloneFieldMutation(options) {
1348
1350
  const built = await buildFieldTransaction({
1349
1351
  rpc,
1350
1352
  walletName,
1353
+ state: nextState,
1351
1354
  plan: buildAnchoredFieldPlan({
1352
1355
  state: nextState,
1353
1356
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1544,6 +1547,7 @@ async function submitFieldCreateFamily(options) {
1544
1547
  const tx1 = await buildFieldTransaction({
1545
1548
  rpc,
1546
1549
  walletName,
1550
+ state: nextState,
1547
1551
  plan: buildAnchoredFieldPlan({
1548
1552
  state: nextState,
1549
1553
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1576,6 +1580,7 @@ async function submitFieldCreateFamily(options) {
1576
1580
  const tx2 = await buildFieldTransaction({
1577
1581
  rpc,
1578
1582
  walletName,
1583
+ state: workingState,
1579
1584
  plan: buildFieldFamilyTx2Plan({
1580
1585
  state: workingState,
1581
1586
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1583,10 +1588,6 @@ async function submitFieldCreateFamily(options) {
1583
1588
  tx1Txid,
1584
1589
  opReturnData: serializeDataUpdate(operation.chainDomain.domainId, resumedFamily.expectedFieldId ?? operation.chainDomain.nextFieldId, options.value.format, options.value.value).opReturnData,
1585
1590
  }),
1586
- builderOptions: {
1587
- includeUnsafe: true,
1588
- minConf: 0,
1589
- },
1590
1591
  });
1591
1592
  const final = await sendFamilyTx2({
1592
1593
  rpc,