@cogcoin/client 0.5.14 → 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@0.5.14` is the store-backed Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@0.5.15` is the store-backed Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
4
4
 
5
5
  Use Node 22 or newer.
6
6
 
@@ -275,6 +275,7 @@ export interface RpcPrevout {
275
275
  }
276
276
  export interface RpcVin {
277
277
  txid?: string;
278
+ vout?: number;
278
279
  coinbase?: string;
279
280
  prevout?: RpcPrevout;
280
281
  }
@@ -310,6 +311,11 @@ export interface RpcWalletTransaction {
310
311
  blockheight?: number;
311
312
  time?: number;
312
313
  timereceived?: number;
314
+ details?: Array<{
315
+ address?: string;
316
+ vout?: number;
317
+ }>;
318
+ decoded?: RpcTransaction;
313
319
  }
314
320
  export interface RpcBlock {
315
321
  hash: string;
@@ -409,6 +415,10 @@ export interface RpcListUnspentEntry {
409
415
  }
410
416
  export interface RpcDecodedPsbt {
411
417
  tx: RpcTransaction;
418
+ inputs?: Array<{
419
+ witness_utxo?: RpcVout;
420
+ non_witness_utxo?: RpcTransaction;
421
+ }>;
412
422
  }
413
423
  export interface RpcFinalizePsbtResult {
414
424
  psbt?: string;
@@ -1,4 +1,4 @@
1
- import type { RpcListUnspentEntry, RpcLockedUnspent } from "../bitcoind/types.js";
1
+ import type { RpcListUnspentEntry, RpcLockedUnspent, RpcWalletTransaction } from "../bitcoind/types.js";
2
2
  import { persistWalletStateUpdate } from "./descriptor-normalization.js";
3
3
  import type { WalletRuntimePaths } from "./runtime.js";
4
4
  import type { OutpointRecord, PortableWalletArchivePayloadV1, UnlockSessionStateV1, WalletStateV1 } from "./types.js";
@@ -7,6 +7,7 @@ export interface WalletCoinControlRpc {
7
7
  listUnspent(walletName: string, minConf?: number): Promise<RpcListUnspentEntry[]>;
8
8
  listLockUnspent(walletName: string): Promise<RpcLockedUnspent[]>;
9
9
  lockUnspent(walletName: string, unlock: boolean, outputs: RpcLockedUnspent[]): Promise<boolean>;
10
+ getTransaction?(walletName: string, txid: string): Promise<RpcWalletTransaction>;
10
11
  }
11
12
  export declare function outpointKey(outpoint: OutpointRecord): string;
12
13
  export declare function normalizeWalletStateRecord(state: WalletStateV1): WalletStateV1;
@@ -289,6 +289,64 @@ function collectPersistentPolicyLockedOutpoints(state, spendableUtxos) {
289
289
  }
290
290
  return outpoints;
291
291
  }
292
+ function collectManagedScriptPubKeyHexes(state) {
293
+ const scripts = new Set();
294
+ const add = (scriptPubKeyHex) => {
295
+ if (typeof scriptPubKeyHex === "string" && scriptPubKeyHex.length > 0) {
296
+ scripts.add(scriptPubKeyHex);
297
+ }
298
+ };
299
+ add(state.funding.scriptPubKeyHex);
300
+ for (const identity of state.identities) {
301
+ add(identity.scriptPubKeyHex);
302
+ }
303
+ for (const domain of state.domains) {
304
+ add(domain.currentOwnerScriptPubKeyHex);
305
+ }
306
+ for (const family of state.proactiveFamilies) {
307
+ add(family.sourceSenderScriptPubKeyHex);
308
+ add(family.reservedScriptPubKeyHex);
309
+ }
310
+ add(state.miningState.currentSenderScriptPubKeyHex);
311
+ return scripts;
312
+ }
313
+ function findWalletTransactionOutputScriptPubKeyHex(transaction, vout) {
314
+ const decodedScriptPubKeyHex = transaction?.decoded?.vout.find((output) => output.n === vout)?.scriptPubKey?.hex;
315
+ return typeof decodedScriptPubKeyHex === "string" && decodedScriptPubKeyHex.length > 0
316
+ ? decodedScriptPubKeyHex
317
+ : null;
318
+ }
319
+ async function collectManagedInspectionUnlocks(options) {
320
+ if (options.rpc.getTransaction === undefined) {
321
+ return [];
322
+ }
323
+ const managedScripts = collectManagedScriptPubKeyHexes(options.state);
324
+ if (managedScripts.size === 0 || options.lockedOutpoints.length === 0) {
325
+ return [];
326
+ }
327
+ const transactionCache = new Map();
328
+ const inspectionUnlocks = [];
329
+ const loadTransaction = (txid) => {
330
+ let cached = transactionCache.get(txid);
331
+ if (cached === undefined) {
332
+ cached = options.rpc.getTransaction?.(options.walletName, txid).catch(() => null) ?? Promise.resolve(null);
333
+ transactionCache.set(txid, cached);
334
+ }
335
+ return cached;
336
+ };
337
+ for (const outpoint of options.lockedOutpoints) {
338
+ const key = outpointKey(outpoint);
339
+ if (options.fixedInputKeys.has(key) || options.temporarilyUnlockedKeys.has(key)) {
340
+ continue;
341
+ }
342
+ const transaction = await loadTransaction(outpoint.txid);
343
+ const scriptPubKeyHex = findWalletTransactionOutputScriptPubKeyHex(transaction, outpoint.vout);
344
+ if (scriptPubKeyHex !== null && managedScripts.has(scriptPubKeyHex)) {
345
+ inspectionUnlocks.push({ txid: outpoint.txid, vout: outpoint.vout });
346
+ }
347
+ }
348
+ return inspectionUnlocks;
349
+ }
292
350
  export async function reconcilePersistentPolicyLocks(options) {
293
351
  const rawReserveOutpoints = normalizeOutpointRecordList(options.state.proactiveReserveOutpoints);
294
352
  let state = normalizeWalletStateRecord(options.state);
@@ -309,10 +367,23 @@ export async function reconcilePersistentPolicyLocks(options) {
309
367
  const key = outpointKey(outpoint);
310
368
  return lockedBeforeReserveInspectionKeys.has(key) && !fixedInputKeys.has(key);
311
369
  });
312
- if (reserveInspectionUnlocks.length > 0) {
313
- await options.rpc.lockUnspent(options.walletName, true, reserveInspectionUnlocks).catch(() => undefined);
370
+ const managedInspectionUnlocks = await collectManagedInspectionUnlocks({
371
+ rpc: options.rpc,
372
+ walletName: options.walletName,
373
+ state,
374
+ lockedOutpoints: lockedBeforeReserveInspection,
375
+ fixedInputKeys,
376
+ temporarilyUnlockedKeys,
377
+ });
378
+ const inspectionUnlockMap = new Map();
379
+ for (const outpoint of [...reserveInspectionUnlocks, ...managedInspectionUnlocks]) {
380
+ inspectionUnlockMap.set(outpointKey(outpoint), outpoint);
381
+ }
382
+ const inspectionUnlocks = [...inspectionUnlockMap.values()];
383
+ if (inspectionUnlocks.length > 0) {
384
+ await options.rpc.lockUnspent(options.walletName, true, inspectionUnlocks).catch(() => undefined);
314
385
  }
315
- const spendableUtxos = reserveInspectionUnlocks.length > 0 || options.spendableUtxos === undefined
386
+ const spendableUtxos = inspectionUnlocks.length > 0 || options.spendableUtxos === undefined
316
387
  ? await options.rpc.listUnspent(options.walletName, 0).catch(() => [])
317
388
  : options.spendableUtxos;
318
389
  const previouslyProtectedUniverse = collectPersistentPolicyLockedOutpoints(state, spendableUtxos);
@@ -1,12 +1,12 @@
1
1
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
2
2
  import { createRpcClient } from "../../bitcoind/node.js";
3
3
  import type { ProgressOutputMode } from "../../bitcoind/types.js";
4
- import { type MutationSender, type WalletMutationRpcClient } from "../tx/common.js";
4
+ import { type FixedWalletInput, type MutationSender, type WalletMutationRpcClient } from "../tx/common.js";
5
5
  import { type WalletPrompter } from "../lifecycle.js";
6
6
  import { openWalletReadContext } from "../read/index.js";
7
7
  import { type WalletRuntimePaths } from "../runtime.js";
8
8
  import { type WalletSecretProvider } from "../state/provider.js";
9
- import type { MiningStateRecord } from "../types.js";
9
+ import type { MiningStateRecord, OutpointRecord, WalletStateV1 } from "../types.js";
10
10
  import type { MiningRuntimeStatusV1 } from "./types.js";
11
11
  type MiningRpcClient = WalletMutationRpcClient & {
12
12
  getBlockchainInfo(): Promise<{
@@ -64,6 +64,21 @@ type MiningRpcClient = WalletMutationRpcClient & {
64
64
  sendRawTransaction(hex: string): Promise<string>;
65
65
  saveMempool?(): Promise<null>;
66
66
  };
67
+ interface MiningCandidate {
68
+ domainId: number;
69
+ domainName: string;
70
+ localIndex: number;
71
+ sender: MutationSender;
72
+ anchorOutpoint: OutpointRecord;
73
+ sentence: string;
74
+ encodedSentenceBytes: Uint8Array;
75
+ bip39WordIndices: number[];
76
+ bip39Words: readonly string[];
77
+ canonicalBlend: bigint;
78
+ referencedBlockHashDisplay: string;
79
+ referencedBlockHashInternal: Uint8Array;
80
+ targetBlockHeight: number;
81
+ }
67
82
  interface RunnerDependencies {
68
83
  openReadContext?: typeof openWalletReadContext;
69
84
  attachService?: typeof attachOrStartManagedBitcoindService;
@@ -104,6 +119,61 @@ export interface MiningStartResult {
104
119
  started: boolean;
105
120
  snapshot: MiningRuntimeStatusV1 | null;
106
121
  }
122
+ declare function createMiningPlan(options: {
123
+ state: WalletStateV1;
124
+ candidate: MiningCandidate;
125
+ conflictOutpoint: OutpointRecord;
126
+ allUtxos: Awaited<ReturnType<MiningRpcClient["listUnspent"]>>;
127
+ feeRateSatVb: number;
128
+ }): {
129
+ sender: MutationSender;
130
+ fixedInputs: FixedWalletInput[];
131
+ outputs: unknown[];
132
+ changeAddress: string;
133
+ changePosition: number;
134
+ expectedOpReturnScriptHex: string;
135
+ expectedAnchorScriptHex: string;
136
+ expectedAnchorValueSats: bigint;
137
+ allowedFundingScriptPubKeyHex: string;
138
+ eligibleFundingOutpointKeys: Set<string>;
139
+ expectedConflictOutpoint: OutpointRecord;
140
+ feeRateSatVb: number;
141
+ };
142
+ export declare function createMiningPlanForTesting(options: {
143
+ state: WalletStateV1;
144
+ candidate: {
145
+ domainId: number;
146
+ domainName: string;
147
+ localIndex: number;
148
+ sender: MutationSender;
149
+ anchorOutpoint: OutpointRecord;
150
+ sentence: string;
151
+ encodedSentenceBytes: Uint8Array;
152
+ bip39WordIndices: number[];
153
+ bip39Words: readonly string[];
154
+ canonicalBlend: bigint;
155
+ referencedBlockHashDisplay: string;
156
+ referencedBlockHashInternal: Uint8Array;
157
+ targetBlockHeight: number;
158
+ };
159
+ conflictOutpoint: OutpointRecord;
160
+ allUtxos: Awaited<ReturnType<MiningRpcClient["listUnspent"]>>;
161
+ feeRateSatVb: number;
162
+ }): {
163
+ sender: MutationSender;
164
+ fixedInputs: FixedWalletInput[];
165
+ outputs: unknown[];
166
+ changeAddress: string;
167
+ changePosition: number;
168
+ expectedOpReturnScriptHex: string;
169
+ expectedAnchorScriptHex: string;
170
+ expectedAnchorValueSats: bigint;
171
+ allowedFundingScriptPubKeyHex: string;
172
+ eligibleFundingOutpointKeys: Set<string>;
173
+ expectedConflictOutpoint: OutpointRecord;
174
+ feeRateSatVb: number;
175
+ };
176
+ export declare function validateMiningDraftForTesting(decoded: Awaited<ReturnType<MiningRpcClient["decodePsbt"]>>, funded: Awaited<ReturnType<MiningRpcClient["walletCreateFundedPsbt"]>>, plan: ReturnType<typeof createMiningPlan>): void;
107
177
  export declare function runForegroundMining(options: RunForegroundMiningOptions): Promise<void>;
108
178
  export declare function startBackgroundMining(options: StartBackgroundMiningOptions): Promise<MiningStartResult>;
109
179
  export declare function stopBackgroundMining(options: StopBackgroundMiningOptions): Promise<MiningRuntimeStatusV1 | null>;
@@ -8,7 +8,7 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
8
8
  import { createRpcClient } from "../../bitcoind/node.js";
9
9
  import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
10
10
  import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
11
- import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, saveWalletStatePreservingUnlock, } from "../tx/common.js";
11
+ import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, buildWalletMutationTransaction, getDecodedInputScriptPubKeyHex, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, saveWalletStatePreservingUnlock, } from "../tx/common.js";
12
12
  import { acquireFileLock } from "../fs/lock.js";
13
13
  import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
14
14
  import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
@@ -610,16 +610,16 @@ function validateMiningDraft(decoded, funded, plan) {
610
610
  throw new Error("wallet_mining_missing_inputs");
611
611
  }
612
612
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
613
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
613
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
614
614
  throw new Error("wallet_mining_sender_input_mismatch");
615
615
  }
616
- if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
616
+ if (getDecodedInputScriptPubKeyHex(decoded, 1) !== plan.allowedFundingScriptPubKeyHex
617
617
  || inputs[1]?.txid !== plan.expectedConflictOutpoint.txid
618
- || inputs[1].vout !== plan.expectedConflictOutpoint.vout) {
618
+ || inputs[1]?.vout !== plan.expectedConflictOutpoint.vout) {
619
619
  throw new Error("wallet_mining_conflict_input_mismatch");
620
620
  }
621
621
  assertFundingInputsAfterFixedPrefix({
622
- inputs,
622
+ decoded,
623
623
  fixedInputs: plan.fixedInputs,
624
624
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
625
625
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -650,6 +650,12 @@ async function buildMiningTransaction(options) {
650
650
  feeRate: options.plan.feeRateSatVb,
651
651
  });
652
652
  }
653
+ export function createMiningPlanForTesting(options) {
654
+ return createMiningPlan(options);
655
+ }
656
+ export function validateMiningDraftForTesting(decoded, funded, plan) {
657
+ validateMiningDraft(decoded, funded, plan);
658
+ }
653
659
  function resolveEligibleAnchoredRoots(context) {
654
660
  const state = context.localState.state;
655
661
  const model = context.model;
@@ -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 { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, getDecodedInputScriptPubKeyHex, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, inputMatchesOutpoint, } from "./common.js";
12
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, findSpendableFundingInputsFromTransaction, 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",
@@ -551,6 +551,12 @@ function buildTx2Plan(options) {
551
551
  }
552
552
  const fundingUtxos = sortUtxos(options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
553
553
  && isSpendableConfirmedUtxo(entry)));
554
+ const tx1FundingChangeInputs = findSpendableFundingInputsFromTransaction({
555
+ allUtxos: options.allUtxos,
556
+ txid: tx1Txid,
557
+ fundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
558
+ minConf: 0,
559
+ });
554
560
  const foundingPayload = options.operation.foundingMessagePayloadHex === null
555
561
  ? undefined
556
562
  : Buffer.from(options.operation.foundingMessagePayloadHex, "hex");
@@ -562,7 +568,10 @@ function buildTx2Plan(options) {
562
568
  address: options.operation.targetIdentity.address,
563
569
  },
564
570
  changeAddress: options.state.funding.address,
565
- fixedInputs: [{ txid: provisional.txid, vout: provisional.vout }],
571
+ fixedInputs: [
572
+ { txid: provisional.txid, vout: provisional.vout },
573
+ ...tx1FundingChangeInputs,
574
+ ],
566
575
  outputs: [
567
576
  { data: Buffer.from(opReturnData).toString("hex") },
568
577
  { [options.operation.targetIdentity.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -590,7 +599,7 @@ function validateTx1Draft(decoded, funded, plan) {
590
599
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
591
600
  }
592
601
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
593
- const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
602
+ const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(decoded, 0);
594
603
  if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
595
604
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
596
605
  }
@@ -600,7 +609,7 @@ function validateTx1Draft(decoded, funded, plan) {
600
609
  }
601
610
  }
602
611
  assertFundingInputsAfterFixedPrefix({
603
- inputs,
612
+ decoded,
604
613
  fixedInputs: plan.fixedInputs,
605
614
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
606
615
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -644,13 +653,13 @@ function validateTx2Draft(decoded, funded, plan) {
644
653
  throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
645
654
  }
646
655
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_provisional_input_mismatch`);
647
- const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
656
+ const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(decoded, 0);
648
657
  if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
649
658
  || !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
650
659
  throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
651
660
  }
652
661
  assertFundingInputsAfterFixedPrefix({
653
- inputs,
662
+ decoded,
654
663
  fixedInputs: plan.fixedInputs,
655
664
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
656
665
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -700,6 +709,7 @@ async function buildTx2(options) {
700
709
  validateFundedDraft: validateTx2Draft,
701
710
  finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
702
711
  mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
712
+ availableFundingMinConf: 0,
703
713
  reserveCandidates: options.state.proactiveReserveOutpoints,
704
714
  });
705
715
  }
@@ -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 { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, formatCogAmount, isAlreadyAcceptedError, isBroadcastUnknownError, outpointKey, pauseMiningForWalletMutation, saveWalletStatePreservingUnlock, unlockTemporaryBuilderLocks, updateMutationRecord, } from "./common.js";
10
+ import { assertFixedInputPrefixMatches, assertFundingInputsAfterFixedPrefix, assertWalletMutationContextReady, buildWalletMutationTransactionWithReserveFallback, formatCogAmount, 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";
@@ -297,11 +297,11 @@ function validateFundedDraft(decoded, funded, plan) {
297
297
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
298
298
  }
299
299
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
300
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
300
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
301
301
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
302
302
  }
303
303
  assertFundingInputsAfterFixedPrefix({
304
- inputs,
304
+ decoded,
305
305
  fixedInputs: plan.fixedInputs,
306
306
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
307
307
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -1,4 +1,4 @@
1
- import type { RpcDecodedPsbt, RpcFinalizePsbtResult, RpcListUnspentEntry, RpcLockedUnspent, RpcTestMempoolAcceptResult, RpcTransaction, RpcVin, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult } from "../../bitcoind/types.js";
1
+ import type { RpcDecodedPsbt, RpcFinalizePsbtResult, RpcListUnspentEntry, RpcLockedUnspent, RpcTestMempoolAcceptResult, RpcTransaction, RpcVin, RpcWalletCreateFundedPsbtResult, RpcWalletTransaction, 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";
@@ -14,12 +14,15 @@ export interface WalletMutationRpcClient {
14
14
  listUnspent(walletName: string, minConf?: number): Promise<RpcListUnspentEntry[]>;
15
15
  listLockUnspent(walletName: string): Promise<RpcLockedUnspent[]>;
16
16
  lockUnspent(walletName: string, unlock: boolean, outputs: RpcLockedUnspent[]): Promise<boolean>;
17
+ getTransaction?(walletName: string, txid: string): Promise<RpcWalletTransaction>;
17
18
  walletCreateFundedPsbt(walletName: string, inputs: Array<{
18
19
  txid: string;
19
20
  vout: number;
20
21
  }>, outputs: unknown[], locktime: number, options: Record<string, unknown>, bip32Derivs?: boolean): Promise<RpcWalletCreateFundedPsbtResult>;
21
22
  decodePsbt(psbt: string): Promise<RpcDecodedPsbt>;
23
+ walletPassphrase(walletName: string, passphrase: string, timeoutSeconds: number): Promise<null>;
22
24
  walletProcessPsbt(walletName: string, psbt: string, sign?: boolean, sighashType?: string): Promise<RpcWalletProcessPsbtResult>;
25
+ walletLock(walletName: string): Promise<null>;
23
26
  finalizePsbt(psbt: string, extract?: boolean): Promise<RpcFinalizePsbtResult>;
24
27
  decodeRawTransaction(hex: string): Promise<RpcTransaction>;
25
28
  testMempoolAccept(rawTransactions: string[]): Promise<RpcTestMempoolAcceptResult[]>;
@@ -44,6 +47,12 @@ export declare function saveWalletStatePreservingUnlock(options: {
44
47
  }): Promise<void>;
45
48
  export declare function formatCogAmount(value: bigint): string;
46
49
  export declare function outpointKey(outpoint: OutpointRecord): string;
50
+ export declare function findSpendableFundingInputsFromTransaction(options: {
51
+ allUtxos: RpcListUnspentEntry[];
52
+ txid: string;
53
+ fundingScriptPubKeyHex: string;
54
+ minConf?: number;
55
+ }): FixedWalletInput[];
47
56
  export declare function updateMutationRecord(mutation: PendingMutationRecord, status: PendingMutationStatus, nowUnixMs: number, options?: {
48
57
  attemptedTxid?: string | null;
49
58
  attemptedWtxid?: string | null;
@@ -51,12 +60,12 @@ export declare function updateMutationRecord(mutation: PendingMutationRecord, st
51
60
  }): PendingMutationRecord;
52
61
  export declare function unlockTemporaryBuilderLocks(rpc: Pick<WalletMutationRpcClient, "lockUnspent">, walletName: string, outpoints: OutpointRecord[]): Promise<void>;
53
62
  export declare function diffTemporaryLockedOutpoints(before: RpcLockedUnspent[], after: RpcLockedUnspent[]): OutpointRecord[];
54
- export declare function getDecodedInputScriptPubKeyHex(input: RpcVin): string | null;
55
63
  export declare function getDecodedInputVout(input: RpcVin): number | null;
64
+ export declare function getDecodedInputScriptPubKeyHex(decoded: RpcDecodedPsbt, inputIndex: number): string | null;
56
65
  export declare function inputMatchesOutpoint(input: RpcVin, outpoint: OutpointRecord): boolean;
57
66
  export declare function assertFixedInputPrefixMatches(inputs: RpcVin[], fixedInputs: FixedWalletInput[], errorCode: string): void;
58
67
  export declare function assertFundingInputsAfterFixedPrefix(options: {
59
- inputs: RpcVin[];
68
+ decoded: RpcDecodedPsbt;
60
69
  fixedInputs: FixedWalletInput[];
61
70
  allowedFundingScriptPubKeyHex: string;
62
71
  eligibleFundingOutpointKeys: Set<string>;
@@ -102,6 +111,7 @@ export declare function buildWalletMutationTransaction<TPlan>(options: {
102
111
  finalizeErrorCode: string;
103
112
  mempoolRejectPrefix: string;
104
113
  feeRate?: number;
114
+ availableFundingMinConf?: number;
105
115
  temporarilyUnlockedPolicyOutpoints?: readonly OutpointRecord[];
106
116
  }): Promise<BuiltWalletMutationTransaction>;
107
117
  export declare function buildWalletMutationTransactionWithReserveFallback<TPlan>(options: {
@@ -120,5 +130,6 @@ export declare function buildWalletMutationTransactionWithReserveFallback<TPlan>
120
130
  finalizeErrorCode: string;
121
131
  mempoolRejectPrefix: string;
122
132
  feeRate?: number;
133
+ availableFundingMinConf?: number;
123
134
  reserveCandidates: readonly OutpointRecord[];
124
135
  }): Promise<BuiltWalletMutationTransaction>;
@@ -5,6 +5,7 @@ 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;
8
9
  function btcNumberToSats(value) {
9
10
  return BigInt(Math.round(value * 100_000_000));
10
11
  }
@@ -48,12 +49,24 @@ export function formatCogAmount(value) {
48
49
  export function outpointKey(outpoint) {
49
50
  return `${outpoint.txid}:${outpoint.vout}`;
50
51
  }
51
- function isSpendableConfirmedFundingUtxo(entry, fundingScriptPubKeyHex) {
52
+ function isSpendableFundingUtxo(entry, fundingScriptPubKeyHex, minConf) {
52
53
  return entry.scriptPubKey === fundingScriptPubKeyHex
53
- && entry.confirmations >= 1
54
+ && entry.confirmations >= minConf
54
55
  && entry.spendable !== false
55
56
  && entry.safe !== false;
56
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
+ }
57
70
  export function updateMutationRecord(mutation, status, nowUnixMs, options = {}) {
58
71
  return {
59
72
  ...mutation,
@@ -79,12 +92,33 @@ export function diffTemporaryLockedOutpoints(before, after) {
79
92
  vout: entry.vout,
80
93
  }));
81
94
  }
82
- export function getDecodedInputScriptPubKeyHex(input) {
83
- return input.prevout?.scriptPubKey?.hex ?? null;
84
- }
85
95
  export function getDecodedInputVout(input) {
86
- const vout = input.vout;
87
- 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;
88
122
  }
89
123
  export function inputMatchesOutpoint(input, outpoint) {
90
124
  return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
@@ -100,9 +134,9 @@ export function assertFixedInputPrefixMatches(inputs, fixedInputs, errorCode) {
100
134
  }
101
135
  }
102
136
  export function assertFundingInputsAfterFixedPrefix(options) {
103
- for (let index = options.fixedInputs.length; index < options.inputs.length; index += 1) {
104
- const input = options.inputs[index];
105
- const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
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);
106
140
  const vout = getDecodedInputVout(input);
107
141
  if (scriptPubKeyHex !== options.allowedFundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
108
142
  throw new Error(options.errorCode);
@@ -155,8 +189,8 @@ function computeRemainingFundingValueSats(options) {
155
189
  for (const value of options.availableFundingValueByKey.values()) {
156
190
  remaining += value;
157
191
  }
158
- for (const input of options.transaction.vin) {
159
- const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
192
+ for (const [index, input] of options.decoded.tx.vin.entries()) {
193
+ const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(options.decoded, index);
160
194
  const vout = getDecodedInputVout(input);
161
195
  if (scriptPubKeyHex !== options.fundingScriptPubKeyHex || vout === null || typeof input.txid !== "string") {
162
196
  continue;
@@ -166,7 +200,7 @@ function computeRemainingFundingValueSats(options) {
166
200
  vout,
167
201
  })) ?? 0n;
168
202
  }
169
- for (const output of options.transaction.vout) {
203
+ for (const output of options.decoded.tx.vout) {
170
204
  if (output.scriptPubKey?.hex !== options.fundingScriptPubKeyHex) {
171
205
  continue;
172
206
  }
@@ -211,8 +245,9 @@ export async function buildWalletMutationTransaction(options) {
211
245
  fixedInputs: options.plan.fixedInputs,
212
246
  temporarilyUnlockedOutpoints: options.temporarilyUnlockedPolicyOutpoints,
213
247
  });
214
- const availableFundingUtxos = (await options.rpc.listUnspent(options.walletName, 1))
215
- .filter((entry) => isSpendableConfirmedFundingUtxo(entry, options.plan.allowedFundingScriptPubKeyHex));
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));
216
251
  const availableFundingValueByKey = new Map(availableFundingUtxos.map((entry) => [
217
252
  outpointKey({ txid: entry.txid, vout: entry.vout }),
218
253
  btcNumberToSats(entry.amount),
@@ -244,7 +279,7 @@ export async function buildWalletMutationTransaction(options) {
244
279
  options.validateFundedDraft(decoded, funded, validationPlan);
245
280
  if (options.state.proactiveReserveSats > 0) {
246
281
  const remainingFundingValueSats = computeRemainingFundingValueSats({
247
- transaction: decoded.tx,
282
+ decoded,
248
283
  fundingScriptPubKeyHex: options.plan.allowedFundingScriptPubKeyHex,
249
284
  availableFundingValueByKey,
250
285
  });
@@ -252,16 +287,25 @@ export async function buildWalletMutationTransaction(options) {
252
287
  throw new Error("wallet_mutation_insufficient_funding_after_reserve");
253
288
  }
254
289
  }
255
- const signed = await options.rpc.walletProcessPsbt(options.walletName, funded.psbt, true, "DEFAULT");
256
- const finalized = await options.rpc.finalizePsbt(signed.psbt, true);
257
- if (!finalized.complete || finalized.hex == null) {
258
- throw new Error(options.finalizeErrorCode);
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
+ }
259
306
  }
260
- const decodedRaw = await options.rpc.decodeRawTransaction(finalized.hex);
261
- const mempoolResult = await options.rpc.testMempoolAccept([finalized.hex]);
262
- const accepted = mempoolResult[0];
263
- if (accepted == null || !accepted.allowed) {
264
- throw new Error(`${options.mempoolRejectPrefix}_${accepted?.["reject-reason"] ?? "unknown"}`);
307
+ finally {
308
+ await options.rpc.walletLock(options.walletName).catch(() => undefined);
265
309
  }
266
310
  if ((options.temporarilyUnlockedPolicyOutpoints?.length ?? 0) > 0) {
267
311
  await reconcilePersistentPolicyLocks({
@@ -295,31 +339,44 @@ export async function buildWalletMutationTransaction(options) {
295
339
  }
296
340
  }
297
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];
298
354
  let unlockedReserveOutpoints = [];
299
355
  let lastError = null;
300
- for (let attempt = 0; attempt <= options.reserveCandidates.length; attempt += 1) {
356
+ for (let attempt = 0; attempt <= reserveCandidates.length; attempt += 1) {
301
357
  if (attempt > 0) {
302
358
  unlockedReserveOutpoints = [
303
359
  ...unlockedReserveOutpoints,
304
- options.reserveCandidates[attempt - 1],
360
+ reserveCandidates[attempt - 1],
305
361
  ];
306
362
  }
307
363
  try {
308
364
  return await buildWalletMutationTransaction({
309
365
  rpc: options.rpc,
310
366
  walletName: options.walletName,
311
- state: options.state,
367
+ state: effectiveState,
312
368
  plan: options.plan,
313
369
  validateFundedDraft: options.validateFundedDraft,
314
370
  finalizeErrorCode: options.finalizeErrorCode,
315
371
  mempoolRejectPrefix: options.mempoolRejectPrefix,
316
372
  feeRate: options.feeRate,
373
+ availableFundingMinConf: options.availableFundingMinConf,
317
374
  temporarilyUnlockedPolicyOutpoints: unlockedReserveOutpoints,
318
375
  });
319
376
  }
320
377
  catch (error) {
321
378
  lastError = error;
322
- if ((!isInsufficientFundsError(error) && !isReserveFloorFundingError(error)) || attempt === options.reserveCandidates.length) {
379
+ if ((!isInsufficientFundsError(error) && !isReserveFloorFundingError(error)) || attempt === reserveCandidates.length) {
323
380
  throw error;
324
381
  }
325
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, buildWalletMutationTransactionWithReserveFallback, 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,
@@ -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, buildWalletMutationTransactionWithReserveFallback, 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";
@@ -297,11 +297,11 @@ function validateFundedDraft(decoded, funded, plan) {
297
297
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
298
298
  }
299
299
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
300
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
300
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
301
301
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
302
302
  }
303
303
  assertFundingInputsAfterFixedPrefix({
304
- inputs,
304
+ decoded,
305
305
  fixedInputs: plan.fixedInputs,
306
306
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
307
307
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -10,7 +10,7 @@ import { createDefaultWalletSecretProvider, } from "../state/provider.js";
10
10
  import { FIELD_FORMAT_BYTES, serializeDataUpdate, serializeFieldReg, } from "../cogop/index.js";
11
11
  import { validateFieldName } from "../cogop/validate-name.js";
12
12
  import { findDomainField, openWalletReadContext, } from "../read/index.js";
13
- import { 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";
14
14
  import { confirmTypedAcknowledgement as confirmSharedTypedAcknowledgement, confirmYesNo as confirmSharedYesNo, } from "./confirm.js";
15
15
  import { getCanonicalIdentitySelector } from "./identity-selector.js";
16
16
  import { findPendingMutationByIntent, upsertPendingMutation } from "./journal.js";
@@ -274,10 +274,19 @@ function buildFieldFamilyTx2Plan(options) {
274
274
  && entry.confirmations >= 1
275
275
  && entry.spendable !== false
276
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
+ });
277
283
  return {
278
284
  sender: options.sender,
279
285
  changeAddress: options.state.funding.address,
280
- fixedInputs: [{ txid: options.tx1Txid, vout: 1 }],
286
+ fixedInputs: [
287
+ { txid: options.tx1Txid, vout: 1 },
288
+ ...tx1FundingChangeInputs,
289
+ ],
281
290
  outputs: [
282
291
  { data: Buffer.from(options.opReturnData).toString("hex") },
283
292
  { [options.sender.address]: satsToBtcNumber(BigInt(options.state.anchorValueSats)) },
@@ -298,11 +307,11 @@ function validateFieldDraft(decoded, funded, plan) {
298
307
  throw new Error(`${plan.errorPrefix}_missing_sender_input`);
299
308
  }
300
309
  assertFixedInputPrefixMatches(inputs, plan.fixedInputs, `${plan.errorPrefix}_sender_input_mismatch`);
301
- if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
310
+ if (getDecodedInputScriptPubKeyHex(decoded, 0) !== plan.sender.scriptPubKeyHex) {
302
311
  throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
303
312
  }
304
313
  assertFundingInputsAfterFixedPrefix({
305
- inputs,
314
+ decoded,
306
315
  fixedInputs: plan.fixedInputs,
307
316
  allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
308
317
  eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
@@ -339,6 +348,7 @@ async function buildFieldTransaction(options) {
339
348
  validateFundedDraft: validateFieldDraft,
340
349
  finalizeErrorCode: `${options.plan.errorPrefix}_finalize_failed`,
341
350
  mempoolRejectPrefix: `${options.plan.errorPrefix}_mempool_rejected`,
351
+ availableFundingMinConf: options.availableFundingMinConf,
342
352
  reserveCandidates: options.state.proactiveReserveOutpoints,
343
353
  });
344
354
  }
@@ -1581,9 +1591,10 @@ async function submitFieldCreateFamily(options) {
1581
1591
  rpc,
1582
1592
  walletName,
1583
1593
  state: workingState,
1594
+ availableFundingMinConf: 0,
1584
1595
  plan: buildFieldFamilyTx2Plan({
1585
1596
  state: workingState,
1586
- allUtxos: await rpc.listUnspent(walletName, 1),
1597
+ allUtxos: await rpc.listUnspent(walletName, 0),
1587
1598
  sender: operation.sender,
1588
1599
  tx1Txid,
1589
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, buildWalletMutationTransactionWithReserveFallback, 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,
@@ -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, buildWalletMutationTransactionWithReserveFallback, 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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "0.5.14",
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",