1use crate::anchor_output;
2use crate::contract::SpendSelection;
3use crate::script::extract_checksig_pubkeys;
4use crate::server;
5use crate::Error;
6use crate::ErrorContext;
7use crate::VTXO_CONDITION_KEY;
8use crate::VTXO_INPUT_INDEX;
9use bitcoin::absolute::LockTime;
10use bitcoin::consensus::Decodable;
11use bitcoin::hashes::Hash;
12use bitcoin::hex::DisplayHex;
13use bitcoin::key::Secp256k1;
14use bitcoin::psbt;
15use bitcoin::secp256k1;
16use bitcoin::secp256k1::schnorr;
17use bitcoin::sighash::Prevouts;
18use bitcoin::sighash::SighashCache;
19use bitcoin::taproot;
20use bitcoin::transaction;
21use bitcoin::Address;
22use bitcoin::Amount;
23use bitcoin::OutPoint;
24use bitcoin::Psbt;
25use bitcoin::ScriptBuf;
26use bitcoin::Sequence;
27use bitcoin::TapLeafHash;
28use bitcoin::TapSighashType;
29use bitcoin::Transaction;
30use bitcoin::TxIn;
31use bitcoin::TxOut;
32use bitcoin::Txid;
33use bitcoin::VarInt;
34use bitcoin::Weight;
35use bitcoin::Witness;
36use bitcoin::XOnlyPublicKey;
37use std::collections::HashMap;
38use std::collections::HashSet;
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct OnChainInput {
44 sequence: Sequence,
46 script_pubkey: ScriptBuf,
48 spend_info: (ScriptBuf, taproot::ControlBlock),
50 amount: Amount,
52 outpoint: OutPoint,
54}
55
56impl OnChainInput {
57 pub fn new(
58 sequence: Sequence,
59 script_pubkey: ScriptBuf,
60 spend_info: (ScriptBuf, taproot::ControlBlock),
61 amount: Amount,
62 outpoint: OutPoint,
63 ) -> Self {
64 Self {
65 sequence,
66 script_pubkey,
67 spend_info,
68 amount,
69 outpoint,
70 }
71 }
72
73 pub fn new_with_spend_selection(
74 default_sequence: Sequence,
75 script_pubkey: ScriptBuf,
76 spend_selection: SpendSelection,
77 amount: Amount,
78 outpoint: OutPoint,
79 ) -> Self {
80 Self::new(
81 spend_selection.sequence.unwrap_or(default_sequence),
82 script_pubkey,
83 spend_selection.spend_info(),
84 amount,
85 outpoint,
86 )
87 }
88
89 pub fn previous_output(&self) -> TxOut {
90 TxOut {
91 value: self.amount,
92 script_pubkey: self.script_pubkey.clone(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98pub struct VtxoInput {
99 outpoint: OutPoint,
100 sequence: Sequence,
101 witness_utxo: TxOut,
102 spend_info: (ScriptBuf, taproot::ControlBlock),
104}
105
106impl VtxoInput {
107 pub fn new(
108 outpoint: OutPoint,
109 sequence: Sequence,
110 witness_utxo: TxOut,
111 spend_info: (ScriptBuf, taproot::ControlBlock),
112 ) -> Self {
113 Self {
114 outpoint,
115 sequence,
116 witness_utxo,
117 spend_info,
118 }
119 }
120
121 pub fn new_with_spend_selection(
122 outpoint: OutPoint,
123 default_sequence: Sequence,
124 witness_utxo: TxOut,
125 spend_selection: SpendSelection,
126 ) -> Self {
127 Self::new(
128 outpoint,
129 spend_selection.sequence.unwrap_or(default_sequence),
130 witness_utxo,
131 spend_selection.spend_info(),
132 )
133 }
134
135 pub fn previous_output(&self) -> TxOut {
136 self.witness_utxo.clone()
137 }
138}
139
140pub fn create_unilateral_exit_transaction<S>(
150 to_address: Address,
151 to_amount: Amount,
152 change_address: Address,
153 onchain_inputs: &[OnChainInput],
154 vtxo_inputs: &[VtxoInput],
155 sign_fn: S,
156) -> Result<Transaction, Error>
157where
158 S: Fn(
159 &mut psbt::Input,
160 secp256k1::Message,
161 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
162{
163 if onchain_inputs.is_empty() && vtxo_inputs.is_empty() {
164 return Err(Error::transaction(
165 "cannot create transaction without inputs",
166 ));
167 }
168
169 let secp = Secp256k1::new();
170
171 let mut output = vec![TxOut {
172 value: to_amount,
173 script_pubkey: to_address.script_pubkey(),
174 }];
175
176 let total_amount: Amount = onchain_inputs
177 .iter()
178 .map(|o| o.amount)
179 .chain(vtxo_inputs.iter().map(|v| v.witness_utxo.value))
180 .sum();
181
182 let change_amount = total_amount.checked_sub(to_amount).ok_or_else(|| {
183 Error::transaction(format!(
184 "cannot cover to_amount ({to_amount}) with total input amount ({total_amount})"
185 ))
186 })?;
187
188 if change_amount > Amount::ZERO {
189 output.push(TxOut {
190 value: change_amount,
191 script_pubkey: change_address.script_pubkey(),
192 });
193 }
194
195 let input = {
196 let onchain_inputs = onchain_inputs.iter().map(|o| TxIn {
197 previous_output: o.outpoint,
198 sequence: o.sequence,
199 ..Default::default()
200 });
201
202 let vtxo_inputs = vtxo_inputs.iter().map(|v| TxIn {
203 previous_output: v.outpoint,
204 sequence: v.sequence,
205 ..Default::default()
206 });
207
208 onchain_inputs.chain(vtxo_inputs).collect::<Vec<_>>()
209 };
210
211 let mut psbt = Psbt::from_unsigned_tx(Transaction {
212 version: transaction::Version::TWO,
213 lock_time: LockTime::ZERO,
214 input,
215 output,
216 })
217 .map_err(Error::transaction)?;
218
219 for (i, input) in psbt.inputs.iter_mut().enumerate() {
221 let outpoint = psbt.unsigned_tx.input[i].previous_output;
222
223 for onchain_input in onchain_inputs {
224 if onchain_input.outpoint == outpoint {
225 input.witness_utxo = Some(TxOut {
226 value: onchain_input.amount,
227 script_pubkey: onchain_input.script_pubkey.clone(),
228 });
229
230 let (script, cb) = onchain_input.spend_info.clone();
231 let leaf_version = cb.leaf_version;
232 input.tap_scripts.insert(cb, (script, leaf_version));
233 }
234 }
235
236 for vtxo_input in vtxo_inputs.iter() {
237 if vtxo_input.outpoint == outpoint {
238 input.witness_utxo = Some(TxOut {
239 value: vtxo_input.witness_utxo.value,
240 script_pubkey: vtxo_input.witness_utxo.script_pubkey.clone(),
241 });
242
243 let (script, cb) = vtxo_input.spend_info.clone();
244 let leaf_version = cb.leaf_version;
245 input.tap_scripts.insert(cb, (script, leaf_version));
246 }
247 }
248 }
249
250 let prevouts = psbt
252 .inputs
253 .iter()
254 .filter_map(|i| i.witness_utxo.clone())
255 .collect::<Vec<_>>();
256
257 for (i, input) in psbt.inputs.iter_mut().enumerate() {
259 let (exit_control_block, (exit_script, leaf_version)) = input
260 .tap_scripts
261 .pop_first()
262 .ok_or_else(|| Error::ad_hoc(format!("no exit script found for input {i}")))?;
263
264 input.witness_script = Some(exit_script.clone());
265
266 let leaf_hash = TapLeafHash::from_script(&exit_script, leaf_version);
267
268 let tap_sighash = SighashCache::new(&psbt.unsigned_tx)
269 .taproot_script_spend_signature_hash(
270 i,
271 &Prevouts::All(&prevouts),
272 leaf_hash,
273 TapSighashType::Default,
274 )
275 .map_err(Error::crypto)?;
276
277 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
278
279 let sigs = sign_fn(input, msg)?;
280
281 let mut witness = Vec::new();
282 for (sig, pk) in sigs.iter() {
283 secp.verify_schnorr(sig, &msg, pk)
284 .map_err(Error::crypto)
285 .with_context(|| format!("failed to verify own signature for input {i}"))?;
286
287 witness.push(&sig[..]);
288 }
289
290 witness.push(exit_script.as_bytes());
291
292 let control_block = exit_control_block.serialize();
293 witness.push(control_block.as_slice());
294
295 let witness = Witness::from_slice(&witness);
296
297 input.final_script_witness = Some(witness);
298 }
299
300 let tx = psbt.clone().extract_tx().map_err(Error::transaction)?;
301
302 tracing::debug!(
303 ?onchain_inputs,
304 ?vtxo_inputs,
305 raw_tx = %bitcoin::consensus::serialize(&tx).as_hex(),
306 "Built transaction sending inputs to on-chain address"
307 );
308
309 Ok(tx)
310}
311
312pub fn build_unilateral_exit_tree_txids(
319 vtxo_chains: &server::VtxoChains,
320 ark_txid: Txid,
322) -> Result<Vec<Vec<Txid>>, Error> {
323 let chain_map = vtxo_chains
324 .inner
325 .iter()
326 .map(|vtxo_chain| (vtxo_chain.txid, vtxo_chain))
327 .collect::<HashMap<_, _>>();
328
329 fn visit_virtual_ancestors(
330 current_txid: Txid,
331 chain_map: &HashMap<Txid, &server::VtxoChain>,
332 visiting: &mut HashSet<Txid>,
333 visited: &mut HashSet<Txid>,
334 sorted: &mut Vec<Txid>,
335 ) -> Result<bool, Error> {
336 if visited.contains(¤t_txid) {
337 return Ok(true);
338 }
339
340 if !visiting.insert(current_txid) {
341 return Err(Error::ad_hoc("chain traversal led to cycle"));
342 }
343
344 let chain = chain_map.get(¤t_txid).ok_or_else(|| {
345 Error::ad_hoc(format!("could not find VtxoChain for TXID: {current_txid}"))
346 })?;
347
348 if chain.spends.is_empty() {
349 return Err(Error::ad_hoc(format!(
350 "dead end reached at TXID {current_txid} with no commitment transaction"
351 )));
352 }
353
354 let mut reached_commitment = false;
355 for &parent_txid in &chain.spends {
356 let parent_chain = chain_map.get(&parent_txid).ok_or_else(|| {
357 Error::ad_hoc(format!(
358 "could not find VtxoChain for parent TXID: {parent_txid}",
359 ))
360 })?;
361
362 match parent_chain.tx_type {
363 server::ChainedTxType::Commitment => {
364 reached_commitment = true;
365 }
366 server::ChainedTxType::Ark
367 | server::ChainedTxType::Checkpoint
368 | server::ChainedTxType::Tree => {
369 reached_commitment |=
370 visit_virtual_ancestors(parent_txid, chain_map, visiting, visited, sorted)?;
371 }
372 server::ChainedTxType::Unspecified => {
373 tracing::warn!(
374 txid = %parent_txid,
375 "Found unspecified TX type when walking up virtual TX tree. \
376 Treating it like a virtual TX"
377 );
378
379 reached_commitment |=
380 visit_virtual_ancestors(parent_txid, chain_map, visiting, visited, sorted)?;
381 }
382 }
383 }
384
385 visiting.remove(¤t_txid);
386 visited.insert(current_txid);
387 sorted.push(current_txid);
388
389 Ok(reached_commitment)
390 }
391
392 let mut visiting = HashSet::new();
393 let mut visited = HashSet::new();
394 let mut sorted = Vec::new();
395
396 if !visit_virtual_ancestors(
397 ark_txid,
398 &chain_map,
399 &mut visiting,
400 &mut visited,
401 &mut sorted,
402 )? {
403 return Err(Error::ad_hoc(format!(
404 "no path found from Ark TX {ark_txid} to commitment transaction",
405 )));
406 }
407
408 Ok(vec![sorted])
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 fn txid(n: u8) -> Txid {
416 Txid::from_byte_array([n; 32])
417 }
418
419 fn chain(
420 txid: Txid,
421 tx_type: server::ChainedTxType,
422 spends: impl Into<Vec<Txid>>,
423 ) -> server::VtxoChain {
424 server::VtxoChain {
425 txid,
426 tx_type,
427 spends: spends.into(),
428 expires_at: 0,
429 }
430 }
431
432 fn exit_branch(chains: Vec<server::VtxoChain>, ark_txid: Txid) -> Vec<Txid> {
433 build_unilateral_exit_tree_txids(&server::VtxoChains { inner: chains }, ark_txid)
434 .expect("valid unilateral exit branch")
435 .pop()
436 .expect("one topological branch")
437 }
438
439 #[test]
440 fn condition_witness_elements_decode_encoded_witness() {
441 let elements = vec![
442 b"preimage".to_vec(),
443 Vec::new(),
444 vec![0; 253],
445 vec![1, 2, 3, 4],
446 ];
447 let mut input = psbt::Input::default();
448
449 input.unknown.insert(
450 psbt::raw::Key {
451 type_value: 222,
452 key: VTXO_CONDITION_KEY.to_vec(),
453 },
454 crate::intent::encode_witness(&elements),
455 );
456
457 assert_eq!(condition_witness_elements(&input).unwrap(), elements);
458 }
459
460 #[test]
461 fn unilateral_exit_txids_for_linear_chain_are_parent_first() {
462 let commitment = txid(1);
463 let tree = txid(2);
464 let ark = txid(3);
465
466 let branch = exit_branch(
467 vec![
468 chain(commitment, server::ChainedTxType::Commitment, []),
469 chain(tree, server::ChainedTxType::Tree, [commitment]),
470 chain(ark, server::ChainedTxType::Ark, [tree]),
471 ],
472 ark,
473 );
474
475 assert_eq!(branch, vec![tree, ark]);
476 }
477
478 #[test]
479 fn unilateral_exit_txids_deduplicate_merged_ancestor_dag() {
480 let commitment = txid(1);
481 let left = txid(2);
482 let right = txid(3);
483 let merge = txid(4);
484 let ark = txid(5);
485
486 let branch = exit_branch(
487 vec![
488 chain(commitment, server::ChainedTxType::Commitment, []),
489 chain(left, server::ChainedTxType::Tree, [commitment]),
490 chain(right, server::ChainedTxType::Tree, [commitment]),
491 chain(merge, server::ChainedTxType::Checkpoint, [left, right]),
492 chain(ark, server::ChainedTxType::Ark, [merge]),
493 ],
494 ark,
495 );
496
497 assert_eq!(branch, vec![left, right, merge, ark]);
498 }
499
500 #[test]
501 fn unilateral_exit_txids_avoid_exponential_path_enumeration() {
502 let commitment = txid(1);
503 let a1 = txid(2);
504 let b1 = txid(3);
505 let m1 = txid(4);
506 let a2 = txid(5);
507 let b2 = txid(6);
508 let m2 = txid(7);
509 let ark = txid(8);
510
511 let branch = exit_branch(
512 vec![
513 chain(commitment, server::ChainedTxType::Commitment, []),
514 chain(a1, server::ChainedTxType::Tree, [commitment]),
515 chain(b1, server::ChainedTxType::Tree, [commitment]),
516 chain(m1, server::ChainedTxType::Checkpoint, [a1, b1]),
517 chain(a2, server::ChainedTxType::Tree, [m1]),
518 chain(b2, server::ChainedTxType::Tree, [m1]),
519 chain(m2, server::ChainedTxType::Checkpoint, [a2, b2]),
520 chain(ark, server::ChainedTxType::Ark, [m2]),
521 ],
522 ark,
523 );
524
525 assert_eq!(branch, vec![a1, b1, m1, a2, b2, m2, ark]);
526 }
527
528 #[test]
529 fn unilateral_exit_txids_reject_cycles() {
530 let a = txid(1);
531 let b = txid(2);
532
533 let err = build_unilateral_exit_tree_txids(
534 &server::VtxoChains {
535 inner: vec![
536 chain(a, server::ChainedTxType::Ark, [b]),
537 chain(b, server::ChainedTxType::Checkpoint, [a]),
538 ],
539 },
540 a,
541 )
542 .expect_err("cycle should be rejected");
543
544 assert!(err.to_string().contains("cycle"));
545 }
546}
547
548pub struct UnilateralExitTree {
556 commitment_txids: Vec<Txid>,
560 inner: Vec<Vec<Psbt>>,
565}
566
567impl UnilateralExitTree {
568 pub fn new(commitment_txids: Vec<Txid>, virtual_tx_tree: Vec<Vec<Psbt>>) -> Self {
569 Self {
570 commitment_txids,
571 inner: virtual_tx_tree,
572 }
573 }
574
575 pub fn inner(&self) -> &Vec<Vec<Psbt>> {
576 &self.inner
577 }
578
579 pub fn commitment_txids(&self) -> &[Txid] {
580 &self.commitment_txids
581 }
582}
583
584pub fn finalize_virtual_tx_input(
592 mut psbt: Psbt,
593 input_index: usize,
594 witness_utxo: TxOut,
595) -> Result<Transaction, Error> {
596 let input = psbt
597 .inputs
598 .get_mut(input_index)
599 .ok_or_else(|| Error::transaction(format!("missing PSBT input {input_index}")))?;
600
601 input.witness_utxo = Some(witness_utxo);
602
603 let txid = psbt.unsigned_tx.compute_txid();
604
605 if let Some(tap_key_sig) = input.tap_key_sig {
606 tracing::debug!(%txid, "Finalizing batch-tree internal node key spend");
607
608 input.final_script_witness = Some(Witness::p2tr_key_spend(&tap_key_sig));
609 } else {
610 tracing::debug!(%txid, "Finalizing VTXO script spend");
611
612 input.final_script_witness = Some(finalize_taproot_script_spend_witness(input)?);
613 }
614
615 psbt.extract_tx().map_err(Error::transaction)
616}
617
618pub fn finalize_taproot_script_spend_witness(input: &psbt::Input) -> Result<Witness, Error> {
629 for (control_block, (script, leaf_version)) in input.tap_scripts.iter() {
630 let leaf_hash = TapLeafHash::from_script(script, *leaf_version);
631 let pubkeys = extract_checksig_pubkeys(script);
632
633 if pubkeys.is_empty() {
634 continue;
635 }
636
637 let signatures = pubkeys
638 .iter()
639 .map(|pk| {
640 input
641 .tap_script_sigs
642 .get(&(*pk, leaf_hash))
643 .map(|sig| sig.to_vec())
644 })
645 .collect::<Option<Vec<_>>>();
646
647 let Some(signatures) = signatures else {
648 continue;
649 };
650
651 let mut witness = Witness::new();
652
653 for signature in signatures.into_iter().rev() {
654 witness.push(signature);
655 }
656
657 for element in condition_witness_elements(input)? {
658 witness.push(element);
659 }
660
661 witness.push(script.as_bytes());
662 witness.push(control_block.serialize());
663
664 return Ok(witness);
665 }
666
667 Err(Error::transaction(
668 "no satisfiable taproot script-spend leaf found in PSBT input",
669 ))
670}
671
672fn condition_witness_elements(input: &psbt::Input) -> Result<Vec<Vec<u8>>, Error> {
673 let condition_key = psbt::raw::Key {
674 type_value: 222,
675 key: VTXO_CONDITION_KEY.to_vec(),
676 };
677
678 let Some(condition_data) = input.unknown.get(&condition_key) else {
679 return Ok(Vec::new());
680 };
681
682 let mut cursor = std::io::Cursor::new(condition_data);
683 let element_count = VarInt::consensus_decode(&mut cursor)
684 .map_err(|e| Error::transaction(format!("failed to decode condition count: {e}")))?
685 .0;
686
687 let count_end = usize::try_from(cursor.position())
688 .map_err(|_| Error::transaction("condition cursor position overflow"))?;
689 let remaining_after_count = condition_data.len().saturating_sub(count_end);
690 let element_count = usize::try_from(element_count)
691 .map_err(|_| Error::transaction("condition witness element count overflow"))?;
692
693 if element_count > remaining_after_count {
696 return Err(Error::transaction(format!(
697 "condition witness element count {element_count} exceeds remaining buffer size {remaining_after_count}"
698 )));
699 }
700
701 let mut elements = Vec::with_capacity(element_count);
702 for _ in 0..element_count {
703 let element_len = VarInt::consensus_decode(&mut cursor)
704 .map_err(|e| Error::transaction(format!("failed to decode condition length: {e}")))?
705 .0;
706 let element_len = usize::try_from(element_len)
707 .map_err(|_| Error::transaction("condition witness element length overflow"))?;
708 let start = usize::try_from(cursor.position())
709 .map_err(|_| Error::transaction("condition cursor position overflow"))?;
710 let end = start
711 .checked_add(element_len)
712 .ok_or_else(|| Error::transaction("condition witness element end overflow"))?;
713
714 if condition_data.len() < end {
715 return Err(Error::transaction(format!(
716 "condition witness element too short: expected {element_len} bytes, got {}",
717 condition_data.len().saturating_sub(start)
718 )));
719 }
720
721 elements.push(condition_data[start..end].to_vec());
722 cursor.set_position(end as u64);
723 }
724
725 Ok(elements)
726}
727
728pub fn finalize_unilateral_exit_tree(
730 unilateral_exit_tree: &UnilateralExitTree,
731 commitment_txs: &[Transaction],
732) -> Result<Vec<Vec<Transaction>>, Error> {
733 let mut finalized_virtual_tx_branches = Vec::new();
734 for unilateral_exit_branch in unilateral_exit_tree.inner.iter() {
735 let mut finalized_unilateral_exit_branch = Vec::new();
736 for virtual_tx in unilateral_exit_branch.iter() {
737 let psbt = virtual_tx.clone();
738
739 let virtual_tx_previous_output =
740 psbt.unsigned_tx.input[VTXO_INPUT_INDEX].previous_output;
741
742 let witness_utxo = {
743 unilateral_exit_branch
744 .iter()
745 .map(|p| &p.unsigned_tx)
746 .chain(commitment_txs.iter())
747 .find_map(|other_psbt| {
748 (other_psbt.compute_txid() == virtual_tx_previous_output.txid).then_some(
749 other_psbt.output[virtual_tx_previous_output.vout as usize].clone(),
750 )
751 })
752 }
753 .ok_or_else(|| {
754 Error::ad_hoc(format!(
755 "no witness UTXO found for virtual TX outpoint {virtual_tx_previous_output}"
756 ))
757 })?;
758
759 let tx = finalize_virtual_tx_input(psbt, VTXO_INPUT_INDEX, witness_utxo)?;
760
761 finalized_unilateral_exit_branch.push(tx);
762 }
763 finalized_virtual_tx_branches.push(finalized_unilateral_exit_branch);
764 }
765
766 Ok(finalized_virtual_tx_branches)
767}
768
769#[deprecated(note = "use finalize_unilateral_exit_tree")]
770pub fn sign_unilateral_exit_tree(
771 unilateral_exit_tree: &UnilateralExitTree,
772 commitment_txs: &[Transaction],
773) -> Result<Vec<Vec<Transaction>>, Error> {
774 finalize_unilateral_exit_tree(unilateral_exit_tree, commitment_txs)
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
778pub struct SelectedUtxo {
779 pub outpoint: OutPoint,
780 pub amount: Amount,
781 pub address: Address,
782}
783
784#[derive(Debug, Clone)]
785pub struct UtxoCoinSelection {
786 pub selected_utxos: Vec<SelectedUtxo>,
787 pub total_selected: Amount,
788 pub change_amount: Amount,
789}
790
791pub fn build_anchor_tx<F>(
794 bumpable_tx: &Transaction,
795 change_address: Address,
796 fee_rate: f64,
797 select_coins_fn: F,
798) -> Result<Psbt, Error>
799where
800 F: FnOnce(Amount) -> Result<UtxoCoinSelection, Error>,
801{
802 let anchor = find_anchor_outpoint(bumpable_tx)?;
803
804 const P2TR_KEYSPEND_INPUT_WEIGHT: u64 = 57 * 4 + 64; const NESTED_P2WSH_INPUT_WEIGHT: u64 = 91 * 4 + 3 * 4; const P2TR_OUTPUT_WEIGHT: u64 = 43 * 4; let estimated_weight = Weight::from_wu(
811 NESTED_P2WSH_INPUT_WEIGHT + P2TR_KEYSPEND_INPUT_WEIGHT + P2TR_OUTPUT_WEIGHT,
812 );
813
814 let child_vsize = estimated_weight.to_vbytes_ceil();
815 let package_size = child_vsize + bumpable_tx.weight().to_vbytes_ceil();
816
817 let fee = Amount::from_sat((package_size as f64 * fee_rate).ceil() as u64);
818
819 let UtxoCoinSelection {
821 selected_utxos,
822 total_selected,
823 change_amount,
824 } = select_coins_fn(fee)?;
825
826 if total_selected < fee {
827 return Err(Error::coin_select(format!(
828 "insufficient coins selected to cover {fee} fee"
829 )));
830 }
831
832 let mut inputs = vec![anchor];
834 let mut sequences = vec![Sequence::MAX];
835
836 for utxo in selected_utxos.iter() {
837 inputs.push(utxo.outpoint);
838 sequences.push(Sequence::MAX);
839 }
840
841 let outputs = vec![TxOut {
842 value: change_amount,
843 script_pubkey: change_address.script_pubkey(),
844 }];
845
846 let mut psbt = Psbt::from_unsigned_tx(Transaction {
848 version: transaction::Version::non_standard(3),
849 lock_time: LockTime::ZERO,
850 input: inputs
851 .iter()
852 .zip(sequences.iter())
853 .map(|(outpoint, sequence)| TxIn {
854 previous_output: *outpoint,
855 script_sig: ScriptBuf::new(),
856 sequence: *sequence,
857 witness: Witness::new(),
858 })
859 .collect(),
860 output: outputs,
861 })
862 .map_err(|e| Error::transaction(format!("Failed to create PSBT: {e}")))?;
863
864 psbt.inputs[0].witness_utxo = Some(anchor_output());
867 psbt.inputs[0].final_script_witness = Some(Witness::new());
868
869 for i in 1..psbt.inputs.len() {
871 if let Some(utxo) = selected_utxos.get(i - 1) {
872 psbt.inputs[i].witness_utxo = Some(TxOut {
873 value: utxo.amount,
874 script_pubkey: utxo.address.script_pubkey(),
875 });
876 }
877 }
878
879 Ok(psbt)
880}
881
882fn find_anchor_outpoint(tx: &Transaction) -> Result<OutPoint, Error> {
883 let anchor_output_template = anchor_output();
884
885 for (index, output) in tx.output.iter().enumerate() {
886 if output == &anchor_output_template {
887 return Ok(OutPoint {
888 txid: tx.compute_txid(),
889 vout: index as u32,
890 });
891 }
892 }
893
894 Err(Error::transaction("anchor output not found in transaction"))
895}