@cogcoin/client 0.5.13 → 0.5.15

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 (36) 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/testing.d.ts +1 -1
  8. package/dist/bitcoind/testing.js +1 -1
  9. package/dist/bitcoind/types.d.ts +10 -0
  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/coin-control.d.ts +3 -2
  23. package/dist/wallet/coin-control.js +127 -15
  24. package/dist/wallet/mining/runner.d.ts +72 -2
  25. package/dist/wallet/mining/runner.js +11 -5
  26. package/dist/wallet/tx/anchor.d.ts +2 -0
  27. package/dist/wallet/tx/anchor.js +65 -20
  28. package/dist/wallet/tx/cog.js +7 -17
  29. package/dist/wallet/tx/common.d.ts +18 -3
  30. package/dist/wallet/tx/common.js +144 -22
  31. package/dist/wallet/tx/domain-admin.js +5 -4
  32. package/dist/wallet/tx/domain-market.js +7 -17
  33. package/dist/wallet/tx/field.js +17 -8
  34. package/dist/wallet/tx/register.js +7 -15
  35. package/dist/wallet/tx/reputation.js +5 -4
  36. package/package.json +1 -1
@@ -5,6 +5,15 @@ import { createWalletSecretReference, } from "../state/provider.js";
5
5
  import { reconcilePersistentPolicyLocks as reconcileWalletCoinControlLocks } from "../coin-control.js";
6
6
  import { requestMiningGenerationPreemption } from "../mining/coordination.js";
7
7
  export const DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB = 10;
8
+ const MANAGED_CORE_WALLET_SIGNING_UNLOCK_TIMEOUT_SECONDS = 10;
9
+ function btcNumberToSats(value) {
10
+ return BigInt(Math.round(value * 100_000_000));
11
+ }
12
+ function valueToSats(value) {
13
+ return typeof value === "string"
14
+ ? BigInt(Math.round(Number(value) * 100_000_000))
15
+ : btcNumberToSats(value);
16
+ }
8
17
  function createUnlockSessionState(state, unlockUntilUnixMs, nowUnixMs) {
9
18
  return {
10
19
  schemaVersion: 1,
@@ -40,6 +49,24 @@ export function formatCogAmount(value) {
40
49
  export function outpointKey(outpoint) {
41
50
  return `${outpoint.txid}:${outpoint.vout}`;
42
51
  }
52
+ function isSpendableFundingUtxo(entry, fundingScriptPubKeyHex, minConf) {
53
+ return entry.scriptPubKey === fundingScriptPubKeyHex
54
+ && entry.confirmations >= minConf
55
+ && entry.spendable !== false
56
+ && entry.safe !== false;
57
+ }
58
+ export function findSpendableFundingInputsFromTransaction(options) {
59
+ const minConf = options.minConf ?? 0;
60
+ return options.allUtxos
61
+ .filter((entry) => entry.txid === options.txid
62
+ && isSpendableFundingUtxo(entry, options.fundingScriptPubKeyHex, minConf))
63
+ .sort((left, right) => left.vout - right.vout
64
+ || left.txid.localeCompare(right.txid))
65
+ .map((entry) => ({
66
+ txid: entry.txid,
67
+ vout: entry.vout,
68
+ }));
69
+ }
43
70
  export function updateMutationRecord(mutation, status, nowUnixMs, options = {}) {
44
71
  return {
45
72
  ...mutation,
@@ -65,12 +92,33 @@ export function diffTemporaryLockedOutpoints(before, after) {
65
92
  vout: entry.vout,
66
93
  }));
67
94
  }
68
- export function getDecodedInputScriptPubKeyHex(input) {
69
- return input.prevout?.scriptPubKey?.hex ?? null;
70
- }
71
95
  export function getDecodedInputVout(input) {
72
- const vout = input.vout;
73
- return typeof vout === "number" ? vout : null;
96
+ return typeof input.vout === "number" ? input.vout : null;
97
+ }
98
+ export function getDecodedInputScriptPubKeyHex(decoded, inputIndex) {
99
+ const input = decoded.tx.vin[inputIndex];
100
+ if (input === undefined) {
101
+ return null;
102
+ }
103
+ const prevoutScriptPubKeyHex = input.prevout?.scriptPubKey?.hex;
104
+ if (typeof prevoutScriptPubKeyHex === "string" && prevoutScriptPubKeyHex.length > 0) {
105
+ return prevoutScriptPubKeyHex;
106
+ }
107
+ const psbtInput = decoded.inputs?.[inputIndex];
108
+ const witnessScriptPubKeyHex = psbtInput?.witness_utxo?.scriptPubKey?.hex;
109
+ if (typeof witnessScriptPubKeyHex === "string" && witnessScriptPubKeyHex.length > 0) {
110
+ return witnessScriptPubKeyHex;
111
+ }
112
+ const vout = getDecodedInputVout(input);
113
+ if (vout === null) {
114
+ return null;
115
+ }
116
+ const nonWitnessScriptPubKeyHex = psbtInput?.non_witness_utxo?.vout
117
+ .find((output) => output.n === vout)
118
+ ?.scriptPubKey?.hex;
119
+ return typeof nonWitnessScriptPubKeyHex === "string" && nonWitnessScriptPubKeyHex.length > 0
120
+ ? nonWitnessScriptPubKeyHex
121
+ : null;
74
122
  }
75
123
  export function inputMatchesOutpoint(input, outpoint) {
76
124
  return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
@@ -86,9 +134,9 @@ export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
86
134
  }
87
135
  }
88
136
  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);
137
+ for (let index = options.fixedInputs.length; index < options.decoded.tx.vin.length; index += 1) {
138
+ const input = options.decoded.tx.vin[index];
139
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(options.decoded, index);
92
140
  const vout = getDecodedInputVout(input);
93
141
  if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
94
142
  throw new Error(options.errorCode);
@@ -132,6 +180,34 @@ export function isInsufficientFundsError(error) {
132
180
  const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
133
181
  return message.includes("insufficient funds");
134
182
  }
183
+ function isReserveFloorFundingError(error) {
184
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
185
+ return message.includes("insufficient_funding_after_reserve");
186
+ }
187
+ function computeRemainingFundingValueSats(options) {
188
+ let remaining = 0n;
189
+ for (const value of options.availableFundingValueByKey.values()) {
190
+ remaining += value;
191
+ }
192
+ for (const [index, input] of options.decoded.tx.vin.entries()) {
193
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(options.decoded, index);
194
+ const vout = getDecodedInputVout(input);
195
+ if (scriptPubKeyHex !== options.fundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
196
+ continue;
197
+ }
198
+ remaining -= options.availableFundingValueByKey.get(outpointKey({
199
+ txid: input.txid,
200
+ vout,
201
+ })) ?? 0n;
202
+ }
203
+ for (const output of options.decoded.tx.vout) {
204
+ if (output.scriptPubKey?.hex !== options.fundingScriptPubKeyHex) {
205
+ continue;
206
+ }
207
+ remaining += valueToSats(output.value);
208
+ }
209
+ return remaining;
210
+ }
135
211
  export function assertWalletMutationContextReady(context, errorPrefix) {
136
212
  if (context.localState.availability === "uninitialized") {
137
213
  throw new Error("wallet_uninitialized");
@@ -169,6 +245,20 @@ export async function buildWalletMutationTransaction(options) {
169
245
  fixedInputs: options.plan.fixedInputs,
170
246
  temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
171
247
  });
248
+ const availableFundingMinConf = options.availableFundingMinConf ?? 1;
249
+ const availableFundingUtxos = (await options.rpc.listUnspent(options.walletName, availableFundingMinConf))
250
+ .filter((entry) => isSpendableFundingUtxo(entry, options.plan.allowedFundingScriptPubKeyHex, availableFundingMinConf));
251
+ const availableFundingValueByKey = new Map(availableFundingUtxos.map((entry) => [
252
+ outpointKey({ txid: entry.txid, vout: entry.vout }),
253
+ btcNumberToSats(entry.amount),
254
+ ]));
255
+ const validationPlan = {
256
+ ...options.plan,
257
+ eligibleFundingOutpointKeys: new Set([
258
+ ...options.plan.eligibleFundingOutpointKeys,
259
+ ...availableFundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout })),
260
+ ]),
261
+ };
172
262
  const lockedBefore = await options.rpc.listLockUnspent(options.walletName);
173
263
  let temporaryBuilderLockedOutpoints = [];
174
264
  try {
@@ -186,17 +276,36 @@ export async function buildWalletMutationTransaction(options) {
186
276
  const lockedAfter = await options.rpc.listLockUnspent(options.walletName);
187
277
  temporaryBuilderLockedOutpoints = diffTemporaryLockedOutpoints(lockedBefore, lockedAfter);
188
278
  const decoded = await options.rpc.decodePsbt(funded.psbt);
189
- options.validateFundedDraft(decoded, funded, options.plan);
190
- const signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
191
- const finalized = await options.rpc.finalizePsbt(signed.psbt, true);
192
- if (!finalized.complete || finalized.hex == null) {
193
- throw new Error(options.finalizeErrorCode);
279
+ options.validateFundedDraft(decoded, funded, validationPlan);
280
+ if (options.state.proactiveReserveSats > 0) {
281
+ const remainingFundingValueSats = computeRemainingFundingValueSats({
282
+ decoded,
283
+ fundingScriptPubKeyHex: options.plan.allowedFundingScriptPubKeyHex,
284
+ availableFundingValueByKey,
285
+ });
286
+ if (remainingFundingValueSats < BigInt(options.state.proactiveReserveSats)) {
287
+ throw new Error("wallet_mutation_insufficient_funding_after_reserve");
288
+ }
289
+ }
290
+ await options.rpc.walletPassphrase(options.walletName, options.state.managedCoreWallet.internalPassphrase, MANAGED_CORE_WALLET_SIGNING_UNLOCK_TIMEOUT_SECONDS);
291
+ let signed;
292
+ let finalized;
293
+ let decodedRaw;
294
+ try {
295
+ signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
296
+ finalized = await options.rpc.finalizePsbt(signed.psbt, true);
297
+ if (!finalized.complete || finalized.hex == null) {
298
+ throw new Error(options.finalizeErrorCode);
299
+ }
300
+ decodedRaw = await options.rpc.decodeRawTransaction(finalized.hex);
301
+ const mempoolResult = await options.rpc.testMempoolAccept([finalized.hex]);
302
+ const accepted = mempoolResult[0];
303
+ if (accepted == null || !accepted.allowed) {
304
+ throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
305
+ }
194
306
  }
195
- const decodedRaw = await options.rpc.decodeRawTransaction(finalized.hex);
196
- const mempoolResult = await options.rpc.testMempoolAccept([finalized.hex]);
197
- const accepted = mempoolResult[0];
198
- if (accepted == null || !accepted.allowed) {
199
- throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
307
+ finally {
308
+ await options.rpc.walletLock(options.walletName).catch(() => undefined);
200
309
  }
201
310
  if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
202
311
  await reconcilePersistentPolicyLocks({
@@ -230,31 +339,44 @@ export async function buildWalletMutationTransaction(options) {
230
339
  }
231
340
  }
232
341
  export async function buildWalletMutationTransactionWithReserveFallback(options) {
342
+ const preflightReconciled = options.reserveCandidates.length > 0
343
+ ? null
344
+ : await reconcileWalletCoinControlLocks({
345
+ rpc: options.rpc,
346
+ walletName: options.walletName,
347
+ state: options.state,
348
+ fixedInputs: options.plan.fixedInputs,
349
+ });
350
+ const effectiveState = preflightReconciled?.state ?? options.state;
351
+ const reserveCandidates = options.reserveCandidates.length > 0
352
+ ? [...options.reserveCandidates]
353
+ : [...effectiveState.proactiveReserveOutpoints];
233
354
  let unlockedReserveOutpoints = [];
234
355
  let lastError = null;
235
- for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
356
+ for (let attempt = 0; attempt <= reserveCandidates.length; attempt += 1) {
236
357
  if (attempt > 0) {
237
358
  unlockedReserveOutpoints = [
238
359
  ...unlockedReserveOutpoints,
239
- options.reserveCandidates[attempt - 1],
360
+ reserveCandidates[attempt - 1],
240
361
  ];
241
362
  }
242
363
  try {
243
364
  return await buildWalletMutationTransaction({
244
365
  rpc: options.rpc,
245
366
  walletName: options.walletName,
246
- state: options.state,
367
+ state: effectiveState,
247
368
  plan: options.plan,
248
369
  validateFundedDraft: options.validateFundedDraft,
249
370
  finalizeErrorCode: options.finalizeErrorCode,
250
371
  mempoolRejectPrefix: options.mempoolRejectPrefix,
251
372
  feeRate: options.feeRate,
373
+ availableFundingMinConf: options.availableFundingMinConf,
252
374
  temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
253
375
  });
254
376
  }
255
377
  catch (error) {
256
378
  lastError = error;
257
- if (!isInsufficientFundsError(error) || attempt === options.reserveCandidates.length) {
379
+ if ((!isInsufficientFundsError(error) && !isReserveFloorFundingError(error)) || attempt === reserveCandidates.length) {
258
380
  throw error;
259
381
  }
260
382
  }
@@ -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 { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
12
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, 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";
@@ -156,11 +156,11 @@ function validateFundedDraft(decoded, funded, plan) {
156
156
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
157
157
  }
158
158
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
159
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
159
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
160
160
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
161
161
  }
162
162
  assertFundingInputsAfterFixedPrefix({
163
- inputs,
163
+ decoded,
164
164
  fixedInputs: plan.fixedInputs,
165
165
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
166
166
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -189,7 +189,7 @@ function validateFundedDraft(decoded, funded, plan) {
189
189
  }
190
190
  }
191
191
  async function buildTransaction(options) {
192
- return buildWalletMutationTransaction({
192
+ return buildWalletMutationTransactionWithReserveFallback({
193
193
  rpc: options.rpc,
194
194
  walletName: options.walletName,
195
195
  state: options.state,
@@ -197,6 +197,7 @@ async function buildTransaction(options) {
197
197
  validateFundedDraft,
198
198
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
199
199
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
200
+ reserveCandidates: options.state.proactiveReserveOutpoints,
200
201
  });
201
202
  }
202
203
  function createDraftMutation(options) {
@@ -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 { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
10
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
11
  import { confirmTypedAcknowledgement, confirmYesNo } from "./confirm.js";
12
12
  import { getCanonicalIdentitySelector, resolveIdentityBySelector, } from "./identity-selector.js";
13
13
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -248,28 +248,17 @@ function buildPlanForDomainOperation(options) {
248
248
  && entry.safe !== false);
249
249
  const outputs = [{ data: Buffer.from(options.opReturnData).toString("hex") }];
250
250
  if (options.anchorOutpoint === null) {
251
- const senderUtxo = options.allUtxos.find((entry) => entry.scriptPubKey === options.sender.scriptPubKeyHex
252
- && entry.confirmations >= 1
253
- && entry.spendable !== false
254
- && entry.safe !== false);
255
- if (senderUtxo === undefined) {
256
- throw new Error(`${options.errorPrefix}_sender_utxo_unavailable`);
257
- }
258
251
  return {
259
252
  sender: options.sender,
260
253
  changeAddress: options.state.funding.address,
261
- fixedInputs: [
262
- { txid: senderUtxo.txid, vout: senderUtxo.vout },
263
- ],
254
+ fixedInputs: [],
264
255
  outputs,
265
256
  changePosition: 1,
266
257
  expectedOpReturnScriptHex: encodeOpReturnScript(options.opReturnData),
267
258
  expectedAnchorScriptHex: null,
268
259
  expectedAnchorValueSats: null,
269
260
  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 }))),
261
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey({ txid: entry.txid, vout: entry.vout }))),
273
262
  errorPrefix: options.errorPrefix,
274
263
  };
275
264
  }
@@ -308,11 +297,11 @@ function validateFundedDraft(decoded, funded, plan) {
308
297
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
309
298
  }
310
299
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
311
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
300
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
312
301
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
313
302
  }
314
303
  assertFundingInputsAfterFixedPrefix({
315
- inputs,
304
+ decoded,
316
305
  fixedInputs: plan.fixedInputs,
317
306
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
318
307
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -344,7 +333,7 @@ function validateFundedDraft(decoded, funded, plan) {
344
333
  }
345
334
  }
346
335
  async function buildTransaction(options) {
347
- return buildWalletMutationTransaction({
336
+ return buildWalletMutationTransactionWithReserveFallback({
348
337
  rpc: options.rpc,
349
338
  walletName: options.walletName,
350
339
  state: options.state,
@@ -352,6 +341,7 @@ async function buildTransaction(options) {
352
341
  validateFundedDraft,
353
342
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
354
343
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
344
+ reserveCandidates: options.state.proactiveReserveOutpoints,
355
345
  });
356
346
  }
357
347
  function createDraftMutation(options) {
@@ -5,13 +5,12 @@ 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";
9
8
  import { resolveWalletRuntimePathsForTesting, } from "../runtime.js";
10
9
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
11
10
  import { FIELD_FORMAT_BYTES, serializeDataUpdate, serializeFieldReg, } from "../cogop/index.js";
12
11
  import { validateFieldName } from "../cogop/validate-name.js";
13
12
  import { findDomainField, openWalletReadContext, } from "../read/index.js";
14
- import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
13
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, findSpendableFundingInputsFromTransaction, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
15
14
  import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
16
15
  import { getCanonicalIdentitySelector } from "./identity-selector.js";
17
16
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -275,10 +274,19 @@ function buildFieldFamilyTx2Plan(options) {
275
274
  && entry.confirmations >= 1
276
275
  && entry.spendable !== false
277
276
  && entry.safe !== false);
277
+ const tx1FundingChangeInputs = findSpendableFundingInputsFromTransaction({
278
+ allUtxos: options.allUtxos,
279
+ txid: options.tx1Txid,
280
+ fundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
281
+ minConf: 0,
282
+ });
278
283
  return {
279
284
  sender: options.sender,
280
285
  changeAddress: options.state.funding.address,
281
- fixedInputs: [{ txid: options.tx1Txid, vout: 1 }],
286
+ fixedInputs: [
287
+ { txid: options.tx1Txid, vout: 1 },
288
+ ...tx1FundingChangeInputs,
289
+ ],
282
290
  outputs: [
283
291
  { data: Buffer.from(options.opReturnData).toString("hex") },
284
292
  { [options.sender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -299,11 +307,11 @@ function validateFieldDraft(decoded, funded, plan) {
299
307
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
300
308
  }
301
309
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
302
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
310
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
303
311
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
304
312
  }
305
313
  assertFundingInputsAfterFixedPrefix({
306
- inputs,
314
+ decoded,
307
315
  fixedInputs: plan.fixedInputs,
308
316
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
309
317
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -332,7 +340,6 @@ function validateFieldDraft(decoded, funded, plan) {
332
340
  }
333
341
  }
334
342
  async function buildFieldTransaction(options) {
335
- const reserveCandidates = computeDesignatedProactiveReserveOutpoints(options.state, await options.rpc.listUnspent(options.walletName, 1));
336
343
  return buildWalletMutationTransactionWithReserveFallback({
337
344
  rpc: options.rpc,
338
345
  walletName: options.walletName,
@@ -341,7 +348,8 @@ async function buildFieldTransaction(options) {
341
348
  validateFundedDraft: validateFieldDraft,
342
349
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
343
350
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
344
- reserveCandidates,
351
+ availableFundingMinConf: options.availableFundingMinConf,
352
+ reserveCandidates: options.state.proactiveReserveOutpoints,
345
353
  });
346
354
  }
347
355
  async function saveUpdatedState(options) {
@@ -1583,9 +1591,10 @@ async function submitFieldCreateFamily(options) {
1583
1591
  rpc,
1584
1592
  walletName,
1585
1593
  state: workingState,
1594
+ availableFundingMinConf: 0,
1586
1595
  plan: buildFieldFamilyTx2Plan({
1587
1596
  state: workingState,
1588
- allUtxos: await rpc.listUnspent(walletName, 1),
1597
+ allUtxos: await rpc.listUnspent(walletName, 0),
1589
1598
  sender: operation.sender,
1590
1599
  tx1Txid,
1591
1600
  opReturnData: serializeDataUpdate(operation.chainDomain.domainId, resumedFamily.expectedFieldId ?? operation.chainDomain.nextFieldId, options.value.format, options.value.value).opReturnData,
@@ -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 { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, formatCogAmount, getDecodedInputScriptPubKeyHex, 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";
@@ -402,11 +402,11 @@ function validateFundedDraft(decoded, funded, plan) {
402
402
  throw new Error("wallet_register_missing_sender_input");
403
403
  }
404
404
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_register_sender_input_mismatch");
405
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
405
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
406
406
  throw new Error("wallet_register_sender_input_mismatch");
407
407
  }
408
408
  assertFundingInputsAfterFixedPrefix({
409
- inputs,
409
+ decoded,
410
410
  fixedInputs: plan.fixedInputs,
411
411
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
412
412
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -459,20 +459,11 @@ function buildRegisterPlan(options) {
459
459
  anchorValueSats: options.anchorOutpoint === null ? null : options.anchorValueSats,
460
460
  });
461
461
  if (options.anchorOutpoint === null) {
462
- if (fundingUtxos.length === 0) {
463
- throw new Error("wallet_register_sender_utxo_unavailable");
464
- }
465
- const senderInput = fundingUtxos[0];
466
- const additionalFunding = fundingUtxos
467
- .slice(1)
468
- .map((entry) => ({ txid: entry.txid, vout: entry.vout }));
469
462
  return {
470
463
  registerKind: "root",
471
464
  sender: options.sender,
472
465
  changeAddress: options.state.funding.address,
473
- fixedInputs: [
474
- { txid: senderInput.txid, vout: senderInput.vout },
475
- ],
466
+ fixedInputs: [],
476
467
  outputs: rootOutputs.outputs,
477
468
  changePosition: rootOutputs.changePosition,
478
469
  expectedOpReturnScriptHex: rootOutputs.expectedOpReturnScriptHex,
@@ -483,7 +474,7 @@ function buildRegisterPlan(options) {
483
474
  expectedAnchorScriptHex: null,
484
475
  expectedAnchorValueSats: null,
485
476
  allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
486
- eligibleFundingOutpointKeys: new Set(additionalFunding.map((entry) => outpointKey(entry))),
477
+ eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => outpointKey(entry))),
487
478
  };
488
479
  }
489
480
  const anchorUtxo = options.allUtxos.find((entry) => entry.txid === options.anchorOutpoint?.txid
@@ -550,7 +541,7 @@ function buildRegisterPlan(options) {
550
541
  };
551
542
  }
552
543
  async function buildRegisterTransaction(options) {
553
- return buildWalletMutationTransaction({
544
+ return buildWalletMutationTransactionWithReserveFallback({
554
545
  rpc: options.rpc,
555
546
  walletName: options.walletName,
556
547
  state: options.state,
@@ -558,6 +549,7 @@ async function buildRegisterTransaction(options) {
558
549
  validateFundedDraft,
559
550
  finalizeErrorCode: "wallet_register_finalize_failed",
560
551
  mempoolRejectPrefix: "wallet_register_mempool_rejected",
552
+ reserveCandidates: options.state.proactiveReserveOutpoints,
561
553
  });
562
554
  }
563
555
  async function reconcilePendingRegisterMutation(options) {
@@ -8,7 +8,7 @@ import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
8
8
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
9
9
  import { serializeRepCommit, serializeRepRevoke, validateDomainName, } from "../cogop/index.js";
10
10
  import { openWalletReadContext } from "../read/index.js";
11
- import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransaction, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
11
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, formatCogAmount, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
12
12
  import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
13
13
  import { getCanonicalIdentitySelector } from "./identity-selector.js";
14
14
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -185,11 +185,11 @@ function validateFundedDraft(decoded, funded, plan) {
185
185
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
186
186
  }
187
187
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
188
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
188
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
189
189
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
190
190
  }
191
191
  assertFundingInputsAfterFixedPrefix({
192
- inputs,
192
+ decoded,
193
193
  fixedInputs: plan.fixedInputs,
194
194
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
195
195
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -218,7 +218,7 @@ function validateFundedDraft(decoded, funded, plan) {
218
218
  }
219
219
  }
220
220
  async function buildTransaction(options) {
221
- return buildWalletMutationTransaction({
221
+ return buildWalletMutationTransactionWithReserveFallback({
222
222
  rpc: options.rpc,
223
223
  walletName: options.walletName,
224
224
  state: options.state,
@@ -226,6 +226,7 @@ async function buildTransaction(options) {
226
226
  validateFundedDraft,
227
227
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
228
228
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
229
+ reserveCandidates: options.state.proactiveReserveOutpoints,
229
230
  });
230
231
  }
231
232
  function createDraftMutation(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",