@cogcoin/client 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -7,6 +7,7 @@ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, } fro
7
7
  import { resolveCogcoinProcessingStartHeight } from "../../bitcoind/processing-start-height.js";
8
8
  import {} from "../../bitcoind/types.js";
9
9
  import { loadOrAutoUnlockWalletState, verifyManagedCoreWalletReplica, } from "../lifecycle.js";
10
+ import { normalizeWalletStateRecord, persistWalletCoinControlStateIfNeeded } from "../coin-control.js";
10
11
  import { persistNormalizedWalletDescriptorStateIfNeeded } from "../descriptor-normalization.js";
11
12
  import { inspectMiningControlPlane } from "../mining/index.js";
12
13
  import { normalizeMiningStateRecord } from "../mining/state.js";
@@ -76,9 +77,17 @@ async function normalizeLoadedWalletStateForRead(options) {
76
77
  replacePrimary: options.loaded.source === "backup",
77
78
  rpc: createRpcClient(node.rpc),
78
79
  });
79
- return {
80
- source: normalized.changed ? "primary" : options.loaded.source,
80
+ const coinControl = await persistWalletCoinControlStateIfNeeded({
81
81
  state: normalized.state,
82
+ access,
83
+ paths: options.paths,
84
+ nowUnixMs: options.now,
85
+ replacePrimary: (normalized.changed ? "primary" : options.loaded.source) === "backup",
86
+ rpc: createRpcClient(node.rpc),
87
+ });
88
+ return {
89
+ source: coinControl.changed ? "primary" : normalized.changed ? "primary" : options.loaded.source,
90
+ state: coinControl.state,
82
91
  };
83
92
  }
84
93
  finally {
@@ -186,10 +195,10 @@ async function inspectWalletLocalState(options = {}) {
186
195
  return {
187
196
  availability: "ready",
188
197
  walletRootId: unlocked.state.walletRootId,
189
- state: {
198
+ state: normalizeWalletStateRecord({
190
199
  ...unlocked.state,
191
200
  miningState: normalizeMiningStateRecord(unlocked.state.miningState),
192
- },
201
+ }),
193
202
  source: unlocked.source,
194
203
  unlockUntilUnixMs: unlocked.session.unlockUntilUnixMs,
195
204
  hasPrimaryStateFile,
@@ -226,10 +235,10 @@ async function inspectWalletLocalState(options = {}) {
226
235
  return {
227
236
  availability: "ready",
228
237
  walletRootId: loaded.state.walletRootId,
229
- state: {
238
+ state: normalizeWalletStateRecord({
230
239
  ...loaded.state,
231
240
  miningState: normalizeMiningStateRecord(loaded.state.miningState),
232
- },
241
+ }),
233
242
  source: loaded.source,
234
243
  unlockUntilUnixMs: null,
235
244
  hasPrimaryStateFile,
@@ -187,6 +187,8 @@ function createEntropyRetainedWalletState(previousState, nowUnixMs) {
187
187
  walletRootId,
188
188
  network: previousState.network,
189
189
  anchorValueSats: previousState.anchorValueSats,
190
+ proactiveReserveSats: previousState.proactiveReserveSats,
191
+ proactiveReserveOutpoints: previousState.proactiveReserveOutpoints,
190
192
  nextDedicatedIndex: previousState.nextDedicatedIndex,
191
193
  fundingIndex: previousState.fundingIndex,
192
194
  mnemonic: {
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { writeJsonFileAtomic } from "../fs/atomic.js";
3
+ import { normalizeWalletStateRecord } from "../coin-control.js";
3
4
  import { decryptJsonWithPassphrase, decryptJsonWithSecretProvider, encryptJsonWithPassphrase, encryptJsonWithSecretProvider, } from "./crypto.js";
4
5
  async function readEnvelope(path) {
5
6
  const raw = await readFile(path, "utf8");
@@ -73,18 +74,18 @@ export async function loadWalletState(paths, access) {
73
74
  try {
74
75
  return {
75
76
  source: "primary",
76
- state: typeof access === "string" || access instanceof Uint8Array
77
+ state: normalizeWalletStateRecord(typeof access === "string" || access instanceof Uint8Array
77
78
  ? await decryptJsonWithPassphrase(await readEnvelope(paths.primaryPath), access)
78
- : await decryptJsonWithSecretProvider(await readEnvelope(paths.primaryPath), access.provider),
79
+ : await decryptJsonWithSecretProvider(await readEnvelope(paths.primaryPath), access.provider)),
79
80
  };
80
81
  }
81
82
  catch (primaryError) {
82
83
  try {
83
84
  return {
84
85
  source: "backup",
85
- state: typeof access === "string" || access instanceof Uint8Array
86
+ state: normalizeWalletStateRecord(typeof access === "string" || access instanceof Uint8Array
86
87
  ? await decryptJsonWithPassphrase(await readEnvelope(paths.backupPath), access)
87
- : await decryptJsonWithSecretProvider(await readEnvelope(paths.backupPath), access.provider),
88
+ : await decryptJsonWithSecretProvider(await readEnvelope(paths.backupPath), access.provider)),
88
89
  };
89
90
  }
90
91
  catch {
@@ -5,11 +5,12 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
5
5
  import { createRpcClient } from "../../bitcoind/node.js";
6
6
  import { acquireFileLock } from "../fs/lock.js";
7
7
  import { deriveWalletIdentityMaterial, } from "../material.js";
8
+ import { computeDesignatedProactiveReserveOutpoints } from "../coin-control.js";
8
9
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
9
10
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
10
11
  import { serializeDomainAnchor, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
11
12
  import { openWalletReadContext } from "../read/index.js";
12
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, } from "./common.js";
13
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
13
14
  import { confirmYesNo } from "./confirm.js";
14
15
  const ACTIVE_FAMILY_STATUSES = new Set([
15
16
  "draft",
@@ -463,9 +464,7 @@ function buildTx1Plan(options) {
463
464
  return {
464
465
  sender: options.operation.sourceSender,
465
466
  changeAddress: options.state.funding.address,
466
- inputs: fundingUtxos.map((entry, index) => index === 0
467
- ? { txid: senderInput.txid, vout: senderInput.vout }
468
- : { txid: entry.txid, vout: entry.vout }),
467
+ fixedInputs: [{ txid: senderInput.txid, vout: senderInput.vout }],
469
468
  outputs,
470
469
  changePosition: 2,
471
470
  expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
@@ -474,6 +473,9 @@ function buildTx1Plan(options) {
474
473
  expectedReplacementAnchorScriptHex: null,
475
474
  expectedReplacementAnchorValueSats: null,
476
475
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
476
+ eligibleFundingOutpointKeys: new Set(fundingUtxos
477
+ .filter((entry) => !(entry.txid === senderInput.txid && entry.vout === senderInput.vout))
478
+ .map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
477
479
  requiredSenderOutpoint: null,
478
480
  requiredProvisionalOutpoint: null,
479
481
  errorPrefix: "wallet_anchor_tx1",
@@ -492,10 +494,7 @@ function buildTx1Plan(options) {
492
494
  return {
493
495
  sender: options.operation.sourceSender,
494
496
  changeAddress: options.state.funding.address,
495
- inputs: [
496
- { txid: sourceAnchor.txid, vout: sourceAnchor.vout },
497
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
498
- ],
497
+ fixedInputs: [{ txid: sourceAnchor.txid, vout: sourceAnchor.vout }],
499
498
  outputs,
500
499
  changePosition: 3,
501
500
  expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
@@ -504,6 +503,7 @@ function buildTx1Plan(options) {
504
503
  expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
505
504
  expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
506
505
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
506
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
507
507
  requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
508
508
  requiredProvisionalOutpoint: null,
509
509
  errorPrefix: "wallet_anchor_tx1",
@@ -535,10 +535,7 @@ function buildTx2Plan(options) {
535
535
  address: options.operation.targetIdentity.address,
536
536
  },
537
537
  changeAddress: options.state.funding.address,
538
- inputs: [
539
- { txid: provisional.txid, vout: provisional.vout },
540
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
541
- ],
538
+ fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
542
539
  outputs: [
543
540
  { data: Buffer.from(opReturnData).toString("hex") },
544
541
  { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -550,6 +547,7 @@ function buildTx2Plan(options) {
550
547
  expectedReplacementAnchorScriptHex: null,
551
548
  expectedReplacementAnchorValueSats: null,
552
549
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
550
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
553
551
  requiredSenderOutpoint: null,
554
552
  requiredProvisionalOutpoint: {
555
553
  txid: provisional.txid,
@@ -558,30 +556,13 @@ function buildTx2Plan(options) {
558
556
  errorPrefix: "wallet_anchor_tx2",
559
557
  };
560
558
  }
561
- function getDecodedInputScriptPubKeyHex(input) {
562
- return input.prevout?.scriptPubKey?.hex ?? null;
563
- }
564
- function getDecodedInputVout(input) {
565
- const vout = input.vout;
566
- return typeof vout === "number" ? vout : null;
567
- }
568
- function inputMatchesOutpoint(input, outpoint) {
569
- return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
570
- }
571
- function assertNoUnexpectedAnchorInputs(inputs, allowedScripts, unexpectedInputErrorCode) {
572
- for (const input of inputs) {
573
- const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
574
- if (scriptPubKeyHex === null || !allowedScripts.has(scriptPubKeyHex)) {
575
- throw new Error(unexpectedInputErrorCode);
576
- }
577
- }
578
- }
579
559
  function validateTx1Draft(decoded, funded, plan) {
580
560
  const inputs = decoded.tx.vin;
581
561
  const outputs = decoded.tx.vout;
582
562
  if (inputs.length === 0) {
583
563
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
584
564
  }
565
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
585
566
  const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
586
567
  if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
587
568
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
@@ -590,14 +571,14 @@ function validateTx1Draft(decoded, funded, plan) {
590
571
  if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
591
572
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
592
573
  }
593
- if (inputs.length < 2 || getDecodedInputScriptPubKeyHex(inputs[1]) !== plan.allowedFundingScriptPubKeyHex) {
594
- throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
595
- }
596
- assertNoUnexpectedAnchorInputs(inputs.slice(2), new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
597
- }
598
- else {
599
- assertNoUnexpectedAnchorInputs(inputs, new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
600
574
  }
575
+ assertFundingInputsAfterFixedPrefix({
576
+ inputs,
577
+ fixedInputs: plan.fixedInputs,
578
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
579
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
580
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
581
+ });
601
582
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
602
583
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
603
584
  }
@@ -635,15 +616,19 @@ function validateTx2Draft(decoded, funded, plan) {
635
616
  if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
636
617
  throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
637
618
  }
619
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
638
620
  const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
639
621
  if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
640
622
  || !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
641
623
  throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
642
624
  }
643
- if (inputs.length < 2 || getDecodedInputScriptPubKeyHex(inputs[1]) !== plan.allowedFundingScriptPubKeyHex) {
644
- throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
645
- }
646
- assertNoUnexpectedAnchorInputs(inputs.slice(2), new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
625
+ assertFundingInputsAfterFixedPrefix({
626
+ inputs,
627
+ fixedInputs: plan.fixedInputs,
628
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
629
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
630
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
631
+ });
647
632
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
648
633
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
649
634
  }
@@ -668,31 +653,29 @@ function validateTx2Draft(decoded, funded, plan) {
668
653
  }
669
654
  }
670
655
  async function buildTx1(options) {
671
- return buildWalletMutationTransaction({
656
+ const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
657
+ return buildWalletMutationTransactionWithReserveFallback({
672
658
  rpc: options.rpc,
673
659
  walletName: options.walletName,
660
+ state: options.state,
674
661
  plan: options.plan,
675
662
  validateFundedDraft: validateTx1Draft,
676
663
  finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
677
664
  mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
678
- builderOptions: {
679
- addInputs: false,
680
- },
665
+ reserveCandidates,
681
666
  });
682
667
  }
683
668
  async function buildTx2(options) {
684
- return buildWalletMutationTransaction({
669
+ const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
670
+ return buildWalletMutationTransactionWithReserveFallback({
685
671
  rpc: options.rpc,
686
672
  walletName: options.walletName,
673
+ state: options.state,
687
674
  plan: options.plan,
688
675
  validateFundedDraft: validateTx2Draft,
689
676
  finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
690
677
  mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
691
- builderOptions: {
692
- addInputs: false,
693
- includeUnsafe: true,
694
- minConf: 0,
695
- },
678
+ reserveCandidates,
696
679
  });
697
680
  }
698
681
  async function relockAnchorOutpoint(rpc, walletName, outpoint) {
@@ -998,6 +981,7 @@ async function submitTx2(options) {
998
981
  const builtTx2 = await buildTx2({
999
982
  rpc: options.rpc,
1000
983
  walletName: options.walletName,
984
+ state: nextState,
1001
985
  plan: tx2Plan,
1002
986
  });
1003
987
  const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
@@ -1232,6 +1216,7 @@ export async function anchorDomain(options) {
1232
1216
  const builtTx1 = await buildTx1({
1233
1217
  rpc,
1234
1218
  walletName,
1219
+ state: nextState,
1235
1220
  plan: tx1Plan,
1236
1221
  });
1237
1222
  const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
@@ -7,7 +7,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
7
7
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
8
8
  import { serializeCogClaim, serializeCogLock, serializeCogTransfer, } from "../cogop/index.js";
9
9
  import { openWalletReadContext } from "../read/index.js";
10
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
10
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, 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";
@@ -258,11 +258,8 @@ function buildPlanForCogOperation(options) {
258
258
  return {
259
259
  sender: options.sender,
260
260
  changeAddress: options.state.funding.address,
261
- inputs: [
261
+ fixedInputs: [
262
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
263
  ],
267
264
  outputs,
268
265
  changePosition: 1,
@@ -270,6 +267,9 @@ function buildPlanForCogOperation(options) {
270
267
  expectedAnchorScriptHex: null,
271
268
  expectedAnchorValueSats: null,
272
269
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
270
+ eligibleFundingOutpointKeys: new Set(fundingUtxos
271
+ .filter((entry) => !(entry.txid === senderUtxo.txid && entry.vout === senderUtxo.vout))
272
+ .map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
273
273
  errorPrefix: options.errorPrefix,
274
274
  };
275
275
  }
@@ -288,9 +288,8 @@ function buildPlanForCogOperation(options) {
288
288
  return {
289
289
  sender: options.sender,
290
290
  changeAddress: options.state.funding.address,
291
- inputs: [
291
+ fixedInputs: [
292
292
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
293
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
294
293
  ],
295
294
  outputs,
296
295
  changePosition: 2,
@@ -298,6 +297,7 @@ function buildPlanForCogOperation(options) {
298
297
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
299
298
  expectedAnchorValueSats: options.anchorValueSats,
300
299
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
300
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
301
301
  errorPrefix: options.errorPrefix,
302
302
  };
303
303
  }
@@ -307,14 +307,17 @@ function validateFundedDraft(decoded, funded, plan) {
307
307
  if (inputs.length === 0) {
308
308
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
309
309
  }
310
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
310
311
  if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
311
312
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
312
313
  }
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
- }
314
+ assertFundingInputsAfterFixedPrefix({
315
+ inputs,
316
+ fixedInputs: plan.fixedInputs,
317
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
318
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
319
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
320
+ });
318
321
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
319
322
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
320
323
  }
@@ -344,6 +347,7 @@ async function buildTransaction(options) {
344
347
  return buildWalletMutationTransaction({
345
348
  rpc: options.rpc,
346
349
  walletName: options.walletName,
350
+ state: options.state,
347
351
  plan: options.plan,
348
352
  validateFundedDraft,
349
353
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
@@ -699,6 +703,7 @@ export async function sendCog(options) {
699
703
  const built = await buildTransaction({
700
704
  rpc,
701
705
  walletName,
706
+ state: nextState,
702
707
  plan: buildPlanForCogOperation({
703
708
  state: nextState,
704
709
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -860,6 +865,7 @@ export async function lockCogToDomain(options) {
860
865
  const built = await buildTransaction({
861
866
  rpc,
862
867
  walletName,
868
+ state: nextState,
863
869
  plan: buildPlanForCogOperation({
864
870
  state: nextState,
865
871
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1004,6 +1010,7 @@ async function runClaimLikeMutation(options, reclaim) {
1004
1010
  const built = await buildTransaction({
1005
1011
  rpc,
1006
1012
  walletName,
1013
+ state: nextState,
1007
1014
  plan: buildPlanForCogOperation({
1008
1015
  state: nextState,
1009
1016
  allUtxos: await rpc.listUnspent(walletName, 1),