@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.
@@ -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,11 +89,9 @@ 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;
@@ -80,10 +100,21 @@ export declare function buildWalletMutationTransaction<TPlan>(options: {
80
100
  finalizeErrorCode: string;
81
101
  mempoolRejectPrefix: string;
82
102
  feeRate?: number;
83
- builderOptions?: {
84
- addInputs?: boolean;
85
- includeUnsafe?: boolean;
86
- minConf?: number;
87
- lockUnspents?: boolean;
103
+ temporarilyUnlockedPolicyOutpoints?: readonly OutpointRecord[];
104
+ }): Promise<BuiltWalletMutationTransaction>;
105
+ export declare function buildWalletMutationTransactionWithReserveFallback<TPlan>(options: {
106
+ rpc: WalletMutationRpcClient;
107
+ walletName: string;
108
+ state: WalletStateV1;
109
+ plan: TPlan & {
110
+ fixedInputs: FixedWalletInput[];
111
+ outputs: unknown[];
112
+ changeAddress: string;
113
+ changePosition: number;
88
114
  };
115
+ validateFundedDraft(decoded: RpcDecodedPsbt, funded: RpcWalletCreateFundedPsbtResult, plan: TPlan): void;
116
+ finalizeErrorCode: string;
117
+ mempoolRejectPrefix: string;
118
+ feeRate?: number;
119
+ reserveCandidates: readonly OutpointRecord[];
89
120
  }): Promise<BuiltWalletMutationTransaction>;
@@ -2,6 +2,7 @@ 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;
7
8
  function createUnlockSessionState(state, unlockUntilUnixMs, nowUnixMs) {
@@ -64,6 +65,53 @@ export function diffTemporaryLockedOutpoints(before, after) {
64
65
  vout: entry.vout,
65
66
  }));
66
67
  }
68
+ export function getDecodedInputScriptPubKeyHex(input) {
69
+ return input.prevout?.scriptPubKey?.hex ?? null;
70
+ }
71
+ export function getDecodedInputVout(input) {
72
+ const vout = input.vout;
73
+ return typeof vout === "number" ? vout : null;
74
+ }
75
+ export function inputMatchesOutpoint(input, outpoint) {
76
+ return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
77
+ }
78
+ export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
79
+ if (inputs.length < fixedInputs.length) {
80
+ throw new Error(errorCode);
81
+ }
82
+ for (const [index, fixedInput] of fixedInputs.entries()) {
83
+ if (!inputMatchesOutpoint(inputs[index], fixedInput)) {
84
+ throw new Error(errorCode);
85
+ }
86
+ }
87
+ }
88
+ export function assertFundingInputsAfterFixedPrefix(options) {
89
+ for (let index = options.fixedInputs.length; index < options.inputs.length; index += 1) {
90
+ const input = options.inputs[index];
91
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
92
+ const vout = getDecodedInputVout(input);
93
+ if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
94
+ throw new Error(options.errorCode);
95
+ }
96
+ const key = outpointKey({
97
+ txid: input.txid,
98
+ vout,
99
+ });
100
+ if (!options.eligibleFundingOutpointKeys.has(key)) {
101
+ throw new Error(options.errorCode);
102
+ }
103
+ }
104
+ }
105
+ export async function reconcilePersistentPolicyLocks(options) {
106
+ await reconcileWalletCoinControlLocks({
107
+ rpc: options.rpc,
108
+ walletName: options.walletName,
109
+ state: options.state,
110
+ fixedInputs: options.fixedInputs,
111
+ temporarilyUnlockedOutpoints: options.temporarilyUnlockedOutpoints,
112
+ cleanupInactiveTemporaryBuilderLocks: options.cleanupInactiveTemporaryBuilderLocks,
113
+ });
114
+ }
67
115
  export function isBroadcastUnknownError(error) {
68
116
  const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
69
117
  return message.includes("timeout")
@@ -80,6 +128,10 @@ export function isAlreadyAcceptedError(error) {
80
128
  || message.includes("already in blockchain")
81
129
  || message.includes("txn-already-known");
82
130
  }
131
+ export function isInsufficientFundsError(error) {
132
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
133
+ return message.includes("insufficient funds");
134
+ }
83
135
  export function assertWalletMutationContextReady(context, errorPrefix) {
84
136
  if (context.localState.availability === "uninitialized") {
85
137
  throw new Error("wallet_uninitialized");
@@ -110,16 +162,23 @@ export async function pauseMiningForWalletMutation(options) {
110
162
  });
111
163
  }
112
164
  export async function buildWalletMutationTransaction(options) {
165
+ await reconcilePersistentPolicyLocks({
166
+ rpc: options.rpc,
167
+ walletName: options.walletName,
168
+ state: options.state,
169
+ fixedInputs: options.plan.fixedInputs,
170
+ temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
171
+ });
113
172
  const lockedBefore = await options.rpc.listLockUnspent(options.walletName);
114
173
  let temporaryBuilderLockedOutpoints = [];
115
174
  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,
175
+ const funded = await options.rpc.walletCreateFundedPsbt(options.walletName, options.plan.fixedInputs, options.plan.outputs, 0, {
176
+ add_inputs: true,
177
+ include_unsafe: false,
178
+ minconf: 1,
120
179
  changeAddress: options.plan.changeAddress,
121
180
  changePosition: options.plan.changePosition,
122
- lockUnspents: options.builderOptions?.lockUnspents ?? true,
181
+ lockUnspents: true,
123
182
  fee_rate: options.feeRate ?? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB,
124
183
  replaceable: true,
125
184
  subtractFeeFromOutputs: [],
@@ -139,6 +198,14 @@ export async function buildWalletMutationTransaction(options) {
139
198
  if (accepted == null || !accepted.allowed) {
140
199
  throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
141
200
  }
201
+ if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
202
+ await reconcilePersistentPolicyLocks({
203
+ rpc: options.rpc,
204
+ walletName: options.walletName,
205
+ state: options.state,
206
+ fixedInputs: options.plan.fixedInputs,
207
+ });
208
+ }
142
209
  return {
143
210
  funded,
144
211
  decoded,
@@ -151,6 +218,46 @@ export async function buildWalletMutationTransaction(options) {
151
218
  }
152
219
  catch (error) {
153
220
  await unlockTemporaryBuilderLocks(options.rpc, options.walletName, temporaryBuilderLockedOutpoints);
221
+ if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
222
+ await reconcilePersistentPolicyLocks({
223
+ rpc: options.rpc,
224
+ walletName: options.walletName,
225
+ state: options.state,
226
+ fixedInputs: options.plan.fixedInputs,
227
+ });
228
+ }
154
229
  throw error;
155
230
  }
156
231
  }
232
+ export async function buildWalletMutationTransactionWithReserveFallback(options) {
233
+ let unlockedReserveOutpoints = [];
234
+ let lastError = null;
235
+ for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
236
+ if (attempt > 0) {
237
+ unlockedReserveOutpoints = [
238
+ ...unlockedReserveOutpoints,
239
+ options.reserveCandidates[attempt - 1],
240
+ ];
241
+ }
242
+ try {
243
+ return await buildWalletMutationTransaction({
244
+ rpc: options.rpc,
245
+ walletName: options.walletName,
246
+ state: options.state,
247
+ plan: options.plan,
248
+ validateFundedDraft: options.validateFundedDraft,
249
+ finalizeErrorCode: options.finalizeErrorCode,
250
+ mempoolRejectPrefix: options.mempoolRejectPrefix,
251
+ feeRate: options.feeRate,
252
+ temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
253
+ });
254
+ }
255
+ catch (error) {
256
+ lastError = error;
257
+ if (!isInsufficientFundsError(error) || attempt === options.reserveCandidates.length) {
258
+ throw error;
259
+ }
260
+ }
261
+ }
262
+ throw lastError;
263
+ }
@@ -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, buildWalletMutationTransaction, 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
  }
@@ -189,6 +192,7 @@ async function buildTransaction(options) {
189
192
  return buildWalletMutationTransaction({
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`,
@@ -632,6 +636,7 @@ async function submitDomainAdminMutation(options) {
632
636
  const built = await buildTransaction({
633
637
  rpc,
634
638
  walletName,
639
+ state: nextState,
635
640
  plan: buildPlanForDomainAdminOperation({
636
641
  state: nextState,
637
642
  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, buildWalletMutationTransaction, 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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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 buildPlanForDomainOperation(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`,
@@ -768,6 +772,7 @@ export async function transferDomain(options) {
768
772
  const built = await buildTransaction({
769
773
  rpc,
770
774
  walletName,
775
+ state: nextState,
771
776
  plan: buildPlanForDomainOperation({
772
777
  state: nextState,
773
778
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -998,6 +1003,7 @@ async function runSellMutation(options) {
998
1003
  const built = await buildTransaction({
999
1004
  rpc,
1000
1005
  walletName,
1006
+ state: nextState,
1001
1007
  plan: buildPlanForDomainOperation({
1002
1008
  state: nextState,
1003
1009
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1233,6 +1239,7 @@ export async function buyDomain(options) {
1233
1239
  const built = await buildTransaction({
1234
1240
  rpc,
1235
1241
  walletName,
1242
+ state: nextState,
1236
1243
  plan: buildPlanForDomainOperation({
1237
1244
  state: nextState,
1238
1245
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -5,12 +5,13 @@ import { getBalance, lookupDomain, } from "@cogcoin/indexer/queries";
5
5
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
6
6
  import { createRpcClient } from "../../bitcoind/node.js";
7
7
  import { acquireFileLock } from "../fs/lock.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 { FIELD_FORMAT_BYTES, serializeDataUpdate, serializeFieldReg, } from "../cogop/index.js";
11
12
  import { validateFieldName } from "../cogop/validate-name.js";
12
13
  import { findDomainField, openWalletReadContext, } from "../read/index.js";
13
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
14
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
14
15
  import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
15
16
  import { getCanonicalIdentitySelector } from "./identity-selector.js";
16
17
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -253,9 +254,8 @@ function buildAnchoredFieldPlan(options) {
253
254
  return {
254
255
  sender: options.sender,
255
256
  changeAddress: options.state.funding.address,
256
- inputs: [
257
+ fixedInputs: [
257
258
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
258
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
259
259
  ],
260
260
  outputs: [
261
261
  { data: Buffer.from(options.opReturnData).toString("hex") },
@@ -266,6 +266,7 @@ function buildAnchoredFieldPlan(options) {
266
266
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
267
267
  expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
268
268
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
269
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
269
270
  errorPrefix: options.errorPrefix,
270
271
  };
271
272
  }
@@ -277,10 +278,7 @@ function buildFieldFamilyTx2Plan(options) {
277
278
  return {
278
279
  sender: options.sender,
279
280
  changeAddress: options.state.funding.address,
280
- inputs: [
281
- { txid: options.tx1Txid, vout: 1 },
282
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
283
- ],
281
+ fixedInputs: [{ txid: options.tx1Txid, vout: 1 }],
284
282
  outputs: [
285
283
  { data: Buffer.from(options.opReturnData).toString("hex") },
286
284
  { [options.sender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -290,6 +288,7 @@ function buildFieldFamilyTx2Plan(options) {
290
288
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
291
289
  expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
292
290
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
291
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
293
292
  errorPrefix: "wallet_field_create_tx2",
294
293
  };
295
294
  }
@@ -299,14 +298,17 @@ function validateFieldDraft(decoded, funded, plan) {
299
298
  if (inputs.length === 0) {
300
299
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
301
300
  }
301
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
302
302
  if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
303
303
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
304
304
  }
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
- }
305
+ assertFundingInputsAfterFixedPrefix({
306
+ inputs,
307
+ fixedInputs: plan.fixedInputs,
308
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
309
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
310
+ errorCode: `${plan.errorPrefix}_unexpected_funding_input`,
311
+ });
310
312
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
311
313
  throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
312
314
  }
@@ -330,14 +332,16 @@ function validateFieldDraft(decoded, funded, plan) {
330
332
  }
331
333
  }
332
334
  async function buildFieldTransaction(options) {
333
- return buildWalletMutationTransaction({
335
+ const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
336
+ return buildWalletMutationTransactionWithReserveFallback({
334
337
  rpc: options.rpc,
335
338
  walletName: options.walletName,
339
+ state: options.state,
336
340
  plan: options.plan,
337
341
  validateFundedDraft: validateFieldDraft,
338
342
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
339
343
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
340
- builderOptions: options.builderOptions,
344
+ reserveCandidates,
341
345
  });
342
346
  }
343
347
  async function saveUpdatedState(options) {
@@ -1348,6 +1352,7 @@ async function submitStandaloneFieldMutation(options) {
1348
1352
  const built = await buildFieldTransaction({
1349
1353
  rpc,
1350
1354
  walletName,
1355
+ state: nextState,
1351
1356
  plan: buildAnchoredFieldPlan({
1352
1357
  state: nextState,
1353
1358
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1544,6 +1549,7 @@ async function submitFieldCreateFamily(options) {
1544
1549
  const tx1 = await buildFieldTransaction({
1545
1550
  rpc,
1546
1551
  walletName,
1552
+ state: nextState,
1547
1553
  plan: buildAnchoredFieldPlan({
1548
1554
  state: nextState,
1549
1555
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1576,6 +1582,7 @@ async function submitFieldCreateFamily(options) {
1576
1582
  const tx2 = await buildFieldTransaction({
1577
1583
  rpc,
1578
1584
  walletName,
1585
+ state: workingState,
1579
1586
  plan: buildFieldFamilyTx2Plan({
1580
1587
  state: workingState,
1581
1588
  allUtxos: await rpc.listUnspent(walletName, 1),
@@ -1583,10 +1590,6 @@ async function submitFieldCreateFamily(options) {
1583
1590
  tx1Txid,
1584
1591
  opReturnData: serializeDataUpdate(operation.chainDomain.domainId, resumedFamily.expectedFieldId ?? operation.chainDomain.nextFieldId, options.value.format, options.value.value).opReturnData,
1585
1592
  }),
1586
- builderOptions: {
1587
- includeUnsafe: true,
1588
- minConf: 0,
1589
- },
1590
1593
  });
1591
1594
  const final = await sendFamilyTx2({
1592
1595
  rpc,
@@ -8,7 +8,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
8
8
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
9
9
  import { computeRootRegistrationPriceSats, serializeDomainReg } from "../cogop/index.js";
10
10
  import { openWalletReadContext } from "../read/index.js";
11
- import { assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
12
12
  import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
13
13
  import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
14
14
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -401,14 +401,17 @@ function validateFundedDraft(decoded, funded, plan) {
401
401
  if (inputs.length === 0) {
402
402
  throw new Error("wallet_register_missing_sender_input");
403
403
  }
404
+ assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_register_sender_input_mismatch");
404
405
  if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
405
406
  throw new Error("wallet_register_sender_input_mismatch");
406
407
  }
407
- for (let index = 1; index < inputs.length; index += 1) {
408
- if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
409
- throw new Error("wallet_register_unexpected_funding_input");
410
- }
411
- }
408
+ assertFundingInputsAfterFixedPrefix({
409
+ inputs,
410
+ fixedInputs: plan.fixedInputs,
411
+ allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
412
+ eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
413
+ errorCode: "wallet_register_unexpected_funding_input",
414
+ });
412
415
  if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
413
416
  throw new Error("wallet_register_opreturn_mismatch");
414
417
  }
@@ -467,9 +470,8 @@ function buildRegisterPlan(options) {
467
470
  registerKind: "root",
468
471
  sender: options.sender,
469
472
  changeAddress: options.state.funding.address,
470
- inputs: [
473
+ fixedInputs: [
471
474
  { txid: senderInput.txid, vout: senderInput.vout },
472
- ...additionalFunding,
473
475
  ],
474
476
  outputs: rootOutputs.outputs,
475
477
  changePosition: rootOutputs.changePosition,
@@ -481,6 +483,7 @@ function buildRegisterPlan(options) {
481
483
  expectedAnchorScriptHex: null,
482
484
  expectedAnchorValueSats: null,
483
485
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
486
+ eligibleFundingOutpointKeys: new Set(additionalFunding.map((entry) => outpointKey(entry))),
484
487
  };
485
488
  }
486
489
  const anchorUtxo = options.allUtxos.find((entry) => entry.txid === options.anchorOutpoint?.txid
@@ -494,9 +497,8 @@ function buildRegisterPlan(options) {
494
497
  registerKind: "root",
495
498
  sender: options.sender,
496
499
  changeAddress: options.state.funding.address,
497
- inputs: [
500
+ fixedInputs: [
498
501
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
499
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
500
502
  ],
501
503
  outputs: rootOutputs.outputs,
502
504
  changePosition: rootOutputs.changePosition,
@@ -508,6 +510,7 @@ function buildRegisterPlan(options) {
508
510
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
509
511
  expectedAnchorValueSats: options.anchorValueSats,
510
512
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
513
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey(entry))),
511
514
  };
512
515
  }
513
516
  const anchor = options.anchorOutpoint;
@@ -530,9 +533,8 @@ function buildRegisterPlan(options) {
530
533
  registerKind: "subdomain",
531
534
  sender: options.sender,
532
535
  changeAddress: options.state.funding.address,
533
- inputs: [
536
+ fixedInputs: [
534
537
  { txid: anchorUtxo.txid, vout: anchorUtxo.vout },
535
- ...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
536
538
  ],
537
539
  outputs: subdomainOutputs.outputs,
538
540
  changePosition: subdomainOutputs.changePosition,
@@ -544,12 +546,14 @@ function buildRegisterPlan(options) {
544
546
  expectedAnchorScriptHex: options.sender.scriptPubKeyHex,
545
547
  expectedAnchorValueSats: options.anchorValueSats,
546
548
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
549
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey(entry))),
547
550
  };
548
551
  }
549
552
  async function buildRegisterTransaction(options) {
550
553
  return buildWalletMutationTransaction({
551
554
  rpc: options.rpc,
552
555
  walletName: options.walletName,
556
+ state: options.state,
553
557
  plan: options.plan,
554
558
  validateFundedDraft,
555
559
  finalizeErrorCode: "wallet_register_finalize_failed",
@@ -810,6 +814,7 @@ export async function registerDomain(options) {
810
814
  const built = await buildRegisterTransaction({
811
815
  rpc,
812
816
  walletName,
817
+ state: nextState,
813
818
  plan,
814
819
  });
815
820
  const currentMutation = nextState.pendingMutations?.find((mutation) => mutation.intentFingerprintHex === intentFingerprintHex)