@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
@@ -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 {
@@ -18,6 +18,7 @@ interface WalletAnchorRpcClient extends WalletMutationRpcClient {
18
18
  export interface AnchorDomainOptions {
19
19
  domainName: string;
20
20
  foundingMessageText?: string | null;
21
+ promptForFoundingMessageWhenMissing?: boolean;
21
22
  dataDir: string;
22
23
  databasePath: string;
23
24
  provider?: WalletSecretProvider;
@@ -36,6 +37,7 @@ export interface AnchorDomainResult {
36
37
  dedicatedIndex: number;
37
38
  status: "live" | "confirmed";
38
39
  reusedExisting: boolean;
40
+ foundingMessageText?: string | null;
39
41
  }
40
42
  export interface ClearPendingAnchorOptions {
41
43
  domainName: string;
@@ -9,7 +9,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
9
9
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
10
10
  import { serializeDomainAnchor, serializeDomainTransfer, validateDomainName, } from "../cogop/index.js";
11
11
  import { openWalletReadContext } from "../read/index.js";
12
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, } from "./common.js";
12
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
13
13
  import { confirmYesNo } from "./confirm.js";
14
14
  const ACTIVE_FAMILY_STATUSES = new Set([
15
15
  "draft",
@@ -115,6 +115,11 @@ function findActiveAnchorFamilyByDomain(state, domainName) {
115
115
  && family.domainName === domainName
116
116
  && ACTIVE_FAMILY_STATUSES.has(family.status)) ?? null;
117
117
  }
118
+ function isClearableReservedAnchorFamily(family) {
119
+ return family?.type === "anchor"
120
+ && family.status === "draft"
121
+ && family.currentStep === "reserved";
122
+ }
118
123
  function findAnchorFamilyById(state, familyId) {
119
124
  return state.proactiveFamilies.find((family) => family.familyId === familyId) ?? null;
120
125
  }
@@ -215,6 +220,35 @@ function encodeFoundingMessage(foundingMessageText) {
215
220
  throw new Error(error instanceof Error ? `wallet_anchor_invalid_message_${error.message}` : "wallet_anchor_invalid_message");
216
221
  });
217
222
  }
223
+ function extractAnchorInvalidMessageReason(error) {
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ if (message === "wallet_anchor_invalid_message") {
226
+ return null;
227
+ }
228
+ if (!message.startsWith("wallet_anchor_invalid_message_")) {
229
+ return null;
230
+ }
231
+ const reason = message.slice("wallet_anchor_invalid_message_".length).trim();
232
+ return reason === "" ? null : reason;
233
+ }
234
+ async function resolveFoundingMessage(options) {
235
+ if (!options.promptForFoundingMessageWhenMissing || options.foundingMessageText != null) {
236
+ return encodeFoundingMessage(options.foundingMessageText ?? null);
237
+ }
238
+ for (;;) {
239
+ const answer = await options.prompter.prompt("Founding message (optional, press Enter to skip): ");
240
+ try {
241
+ return await encodeFoundingMessage(answer);
242
+ }
243
+ catch (error) {
244
+ const reason = extractAnchorInvalidMessageReason(error);
245
+ options.prompter.writeLine("Founding message cannot be encoded in canonical Coglex.");
246
+ if (reason !== null) {
247
+ options.prompter.writeLine(`Reason: ${reason}`);
248
+ }
249
+ }
250
+ }
251
+ }
218
252
  function resolveAnchorOutpointForSender(state, senderIndex) {
219
253
  const anchoredDomain = state.domains.find((domain) => domain.currentOwnerLocalIndex === senderIndex
220
254
  && domain.canonicalChainStatus === "anchored"
@@ -456,16 +490,10 @@ function buildTx1Plan(options) {
456
490
  { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
457
491
  ];
458
492
  if (options.operation.sourceAnchorOutpoint === null) {
459
- if (fundingUtxos.length === 0) {
460
- throw new Error("wallet_anchor_sender_utxo_unavailable");
461
- }
462
- const senderInput = fundingUtxos[0];
463
493
  return {
464
494
  sender: options.operation.sourceSender,
465
495
  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 }),
496
+ fixedInputs: [],
469
497
  outputs,
470
498
  changePosition: 2,
471
499
  expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
@@ -474,6 +502,7 @@ function buildTx1Plan(options) {
474
502
  expectedReplacementAnchorScriptHex: null,
475
503
  expectedReplacementAnchorValueSats: null,
476
504
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
505
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
477
506
  requiredSenderOutpoint: null,
478
507
  requiredProvisionalOutpoint: null,
479
508
  errorPrefix: "wallet_anchor_tx1",
@@ -492,10 +521,7 @@ function buildTx1Plan(options) {
492
521
  return {
493
522
  sender: options.operation.sourceSender,
494
523
  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
- ],
524
+ fixedInputs: [{ txid: sourceAnchor.txid, vout: sourceAnchor.vout }],
499
525
  outputs,
500
526
  changePosition: 3,
501
527
  expectedOpReturnScriptHex: encodeOpReturnScript(serializeDomainTransfer(options.operation.chainDomain.domainId, Buffer.from(options.operation.targetIdentity.scriptPubKeyHex, "hex")).opReturnData),
@@ -504,6 +530,7 @@ function buildTx1Plan(options) {
504
530
  expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
505
531
  expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
506
532
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
533
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
507
534
  requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
508
535
  requiredProvisionalOutpoint: null,
509
536
  errorPrefix: "wallet_anchor_tx1",
@@ -535,10 +562,7 @@ function buildTx2Plan(options) {
535
562
  address: options.operation.targetIdentity.address,
536
563
  },
537
564
  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
- ],
565
+ fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
542
566
  outputs: [
543
567
  { data: Buffer.from(opReturnData).toString("hex") },
544
568
  { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -550,6 +574,7 @@ function buildTx2Plan(options) {
550
574
  expectedReplacementAnchorScriptHex: null,
551
575
  expectedReplacementAnchorValueSats: null,
552
576
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
577
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
553
578
  requiredSenderOutpoint: null,
554
579
  requiredProvisionalOutpoint: {
555
580
  txid: provisional.txid,
@@ -558,30 +583,13 @@ function buildTx2Plan(options) {
558
583
  errorPrefix: "wallet_anchor_tx2",
559
584
  };
560
585
  }
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
586
  function validateTx1Draft(decoded, funded, plan) {
580
587
  const inputs = decoded.tx.vin;
581
588
  const outputs = decoded.tx.vout;
582
589
  if (inputs.length === 0) {
583
590
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
584
591
  }
592
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
585
593
  const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
586
594
  if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
587
595
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
@@ -590,14 +598,14 @@ function validateTx1Draft(decoded, funded, plan) {
590
598
  if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
591
599
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
592
600
  }
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
601
  }
602
+ assertFundingInputsAfterFixedPrefix({
603
+ inputs,
604
+ fixedInputs: plan.fixedInputs,
605
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
606
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
607
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
608
+ });
601
609
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
602
610
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
603
611
  }
@@ -635,15 +643,19 @@ function validateTx2Draft(decoded, funded, plan) {
635
643
  if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
636
644
  throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
637
645
  }
646
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
638
647
  const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
639
648
  if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
640
649
  || !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
641
650
  throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
642
651
  }
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`);
652
+ assertFundingInputsAfterFixedPrefix({
653
+ inputs,
654
+ fixedInputs: plan.fixedInputs,
655
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
656
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
657
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
658
+ });
647
659
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
648
660
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
649
661
  }
@@ -668,31 +680,27 @@ function validateTx2Draft(decoded, funded, plan) {
668
680
  }
669
681
  }
670
682
  async function buildTx1(options) {
671
- return buildWalletMutationTransaction({
683
+ return buildWalletMutationTransactionWithReserveFallback({
672
684
  rpc: options.rpc,
673
685
  walletName: options.walletName,
686
+ state: options.state,
674
687
  plan: options.plan,
675
688
  validateFundedDraft: validateTx1Draft,
676
689
  finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
677
690
  mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
678
- builderOptions: {
679
- addInputs: false,
680
- },
691
+ reserveCandidates: options.state.proactiveReserveOutpoints,
681
692
  });
682
693
  }
683
694
  async function buildTx2(options) {
684
- return buildWalletMutationTransaction({
695
+ return buildWalletMutationTransactionWithReserveFallback({
685
696
  rpc: options.rpc,
686
697
  walletName: options.walletName,
698
+ state: options.state,
687
699
  plan: options.plan,
688
700
  validateFundedDraft: validateTx2Draft,
689
701
  finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
690
702
  mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
691
- builderOptions: {
692
- addInputs: false,
693
- includeUnsafe: true,
694
- minConf: 0,
695
- },
703
+ reserveCandidates: options.state.proactiveReserveOutpoints,
696
704
  });
697
705
  }
698
706
  async function relockAnchorOutpoint(rpc, walletName, outpoint) {
@@ -998,6 +1006,7 @@ async function submitTx2(options) {
998
1006
  const builtTx2 = await buildTx2({
999
1007
  rpc: options.rpc,
1000
1008
  walletName: options.walletName,
1009
+ state: nextState,
1001
1010
  plan: tx2Plan,
1002
1011
  });
1003
1012
  const broadcastingTx2 = createBroadcastingTxRecord(builtTx2);
@@ -1123,6 +1132,7 @@ async function submitTx2(options) {
1123
1132
  dedicatedIndex: options.operation.targetIdentity.localIndex,
1124
1133
  status: finalStatus,
1125
1134
  reusedExisting: false,
1135
+ foundingMessageText: options.operation.foundingMessageText,
1126
1136
  };
1127
1137
  }
1128
1138
  function ensureSameTipHeight(context, bestHeight, errorCode) {
@@ -1147,7 +1157,11 @@ export async function anchorDomain(options) {
1147
1157
  paths,
1148
1158
  reason: "wallet-anchor",
1149
1159
  });
1150
- const message = await encodeFoundingMessage(options.foundingMessageText ?? null);
1160
+ const message = await resolveFoundingMessage({
1161
+ foundingMessageText: options.foundingMessageText,
1162
+ promptForFoundingMessageWhenMissing: options.promptForFoundingMessageWhenMissing,
1163
+ prompter: options.prompter,
1164
+ });
1151
1165
  const readContext = await (options.openReadContext ?? openWalletReadContext)({
1152
1166
  dataDir: options.dataDir,
1153
1167
  databasePath: options.databasePath,
@@ -1160,6 +1174,9 @@ export async function anchorDomain(options) {
1160
1174
  const initialFamily = createDraftAnchorFamily(operation, nowUnixMs);
1161
1175
  const existingFamily = findAnchorFamilyByIntent(operation.state, initialFamily.intentFingerprintHex);
1162
1176
  const conflictingFamily = findActiveAnchorFamilyByDomain(operation.state, normalizedDomainName);
1177
+ if (existingFamily === null && isClearableReservedAnchorFamily(conflictingFamily)) {
1178
+ throw new Error(`wallet_anchor_clear_pending_first_${conflictingFamily.domainName}`);
1179
+ }
1163
1180
  if (existingFamily === null && conflictingFamily !== null) {
1164
1181
  throw new Error("wallet_anchor_prior_family_unresolved");
1165
1182
  }
@@ -1201,6 +1218,7 @@ export async function anchorDomain(options) {
1201
1218
  dedicatedIndex: reconciled.family.reservedDedicatedIndex ?? existingTargetIdentity.localIndex,
1202
1219
  status: reconciled.resolution,
1203
1220
  reusedExisting: true,
1221
+ foundingMessageText: reconciled.family.foundingMessageText,
1204
1222
  };
1205
1223
  }
1206
1224
  if (reconciled.resolution === "repair-required") {
@@ -1232,6 +1250,7 @@ export async function anchorDomain(options) {
1232
1250
  const builtTx1 = await buildTx1({
1233
1251
  rpc,
1234
1252
  walletName,
1253
+ state: nextState,
1235
1254
  plan: tx1Plan,
1236
1255
  });
1237
1256
  const broadcastingTx1 = createBroadcastingTxRecord(builtTx1);
@@ -1351,6 +1370,7 @@ export async function anchorDomain(options) {
1351
1370
  return {
1352
1371
  ...result,
1353
1372
  reusedExisting: resumedExisting,
1373
+ foundingMessageText: result.foundingMessageText ?? operation.foundingMessageText,
1354
1374
  };
1355
1375
  }
1356
1376
  finally {
@@ -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, buildWalletMutationTransactionWithReserveFallback, 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";
@@ -248,28 +248,17 @@ function buildPlanForCogOperation(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 buildPlanForCogOperation(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 buildPlanForCogOperation(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) {
@@ -699,6 +693,7 @@ export async function sendCog(options) {
699
693
  const built = await buildTransaction({
700
694
  rpc,
701
695
  walletName,
696
+ state: nextState,
702
697
  plan: buildPlanForCogOperation({
703
698
  state: nextState,
704
699
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -860,6 +855,7 @@ export async function lockCogToDomain(options) {
860
855
  const built = await buildTransaction({
861
856
  rpc,
862
857
  walletName,
858
+ state: nextState,
863
859
  plan: buildPlanForCogOperation({
864
860
  state: nextState,
865
861
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1004,6 +1000,7 @@ async function runClaimLikeMutation(options, reclaim) {
1004
1000
  const built = await buildTransaction({
1005
1001
  rpc,
1006
1002
  walletName,
1003
+ state: nextState,
1007
1004
  plan: buildPlanForCogOperation({
1008
1005
  state: nextState,
1009
1006
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1,4 +1,4 @@
1
- import type { RpcDecodedPsbt, RpcFinalizePsbtResult, RpcListUnspentEntry, RpcLockedUnspent, RpcTestMempoolAcceptResult, RpcTransaction, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult } from "../../bitcoind/types.js";
1
+ import type { RpcDecodedPsbt, RpcFinalizePsbtResult, RpcListUnspentEntry, RpcLockedUnspent, RpcTestMempoolAcceptResult, RpcTransaction, RpcVin, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult } from "../../bitcoind/types.js";
2
2
  import { type WalletSecretProvider } from "../state/provider.js";
3
3
  import type { OutpointRecord, PendingMutationRecord, PendingMutationStatus, WalletStateV1 } from "../types.js";
4
4
  import type { WalletReadContext } from "../read/index.js";
@@ -33,6 +33,8 @@ export interface BuiltWalletMutationTransaction {
33
33
  wtxid: string | null;
34
34
  temporaryBuilderLockedOutpoints: OutpointRecord[];
35
35
  }
36
+ export interface FixedWalletInput extends OutpointRecord {
37
+ }
36
38
  export declare function saveWalletStatePreservingUnlock(options: {
37
39
  state: WalletStateV1;
38
40
  provider: WalletSecretProvider;
@@ -49,8 +51,28 @@ export declare function updateMutationRecord(mutation: PendingMutationRecord, st
49
51
  }): PendingMutationRecord;
50
52
  export declare function unlockTemporaryBuilderLocks(rpc: Pick<WalletMutationRpcClient, "lockUnspent">, walletName: string, outpoints: OutpointRecord[]): Promise<void>;
51
53
  export declare function diffTemporaryLockedOutpoints(before: RpcLockedUnspent[], after: RpcLockedUnspent[]): OutpointRecord[];
54
+ export declare function getDecodedInputScriptPubKeyHex(input: RpcVin): string | null;
55
+ export declare function getDecodedInputVout(input: RpcVin): number | null;
56
+ export declare function inputMatchesOutpoint(input: RpcVin, outpoint: OutpointRecord): boolean;
57
+ export declare function assertFixedInputPrefixMatches(inputs: RpcVin[], fixedInputs: FixedWalletInput[], errorCode: string): void;
58
+ export declare function assertFundingInputsAfterFixedPrefix(options: {
59
+ inputs: RpcVin[];
60
+ fixedInputs: FixedWalletInput[];
61
+ allowedFundingScriptPubKeyHex: string;
62
+ eligibleFundingOutpointKeys: Set<string>;
63
+ errorCode: string;
64
+ }): void;
65
+ export declare function reconcilePersistentPolicyLocks(options: {
66
+ rpc: Pick<WalletMutationRpcClient, "listLockUnspent" | "lockUnspent" | "listUnspent">;
67
+ walletName: string;
68
+ state: WalletStateV1;
69
+ fixedInputs: FixedWalletInput[];
70
+ temporarilyUnlockedOutpoints?: readonly OutpointRecord[];
71
+ cleanupInactiveTemporaryBuilderLocks?: boolean;
72
+ }): Promise<void>;
52
73
  export declare function isBroadcastUnknownError(error: unknown): boolean;
53
74
  export declare function isAlreadyAcceptedError(error: unknown): boolean;
75
+ export declare function isInsufficientFundsError(error: unknown): boolean;
54
76
  export declare function assertWalletMutationContextReady(context: WalletReadContext, errorPrefix: string): asserts context is WalletReadContext & {
55
77
  localState: {
56
78
  availability: "ready";
@@ -67,23 +89,36 @@ export declare function pauseMiningForWalletMutation(options: {
67
89
  export declare function buildWalletMutationTransaction<TPlan>(options: {
68
90
  rpc: WalletMutationRpcClient;
69
91
  walletName: string;
92
+ state: WalletStateV1;
70
93
  plan: TPlan & {
71
- inputs: Array<{
72
- txid: string;
73
- vout: number;
74
- }>;
94
+ fixedInputs: FixedWalletInput[];
75
95
  outputs: unknown[];
76
96
  changeAddress: string;
77
97
  changePosition: number;
98
+ allowedFundingScriptPubKeyHex: string;
99
+ eligibleFundingOutpointKeys: Set<string>;
78
100
  };
79
101
  validateFundedDraft(decoded: RpcDecodedPsbt, funded: RpcWalletCreateFundedPsbtResult, plan: TPlan): void;
80
102
  finalizeErrorCode: string;
81
103
  mempoolRejectPrefix: string;
82
104
  feeRate?: number;
83
- builderOptions?: {
84
- addInputs?: boolean;
85
- includeUnsafe?: boolean;
86
- minConf?: number;
87
- lockUnspents?: boolean;
105
+ temporarilyUnlockedPolicyOutpoints?: readonly OutpointRecord[];
106
+ }): Promise<BuiltWalletMutationTransaction>;
107
+ export declare function buildWalletMutationTransactionWithReserveFallback<TPlan>(options: {
108
+ rpc: WalletMutationRpcClient;
109
+ walletName: string;
110
+ state: WalletStateV1;
111
+ plan: TPlan & {
112
+ fixedInputs: FixedWalletInput[];
113
+ outputs: unknown[];
114
+ changeAddress: string;
115
+ changePosition: number;
116
+ allowedFundingScriptPubKeyHex: string;
117
+ eligibleFundingOutpointKeys: Set<string>;
88
118
  };
119
+ validateFundedDraft(decoded: RpcDecodedPsbt, funded: RpcWalletCreateFundedPsbtResult, plan: TPlan): void;
120
+ finalizeErrorCode: string;
121
+ mempoolRejectPrefix: string;
122
+ feeRate?: number;
123
+ reserveCandidates: readonly OutpointRecord[];
89
124
  }): Promise<BuiltWalletMutationTransaction>;