Skip to main content

satellite_bitcoin_transactions/
lib.rs

1//! Arch Bitcoin transaction helpers.
2//!
3//! This crate offers zero-heap utilities and a strongly-typed builder – [`TransactionBuilder`] –
4//! for composing, fee–tuning and finalising Bitcoin transactions that will be forwarded through
5//! the **Arch** runtime.  All helpers are `no_std` friendly (apart from the unavoidable `alloc`
6//! use inside the `bitcoin` dependency) and can therefore be called from on-chain programs that
7//! run inside the Solana BPF VM.
8//!
9//! ## Key Features
10//!
11//! - **Zero-heap allocation**: Uses fixed-size collections for constrained environments
12//! - **Type-safe builder pattern**: Compile-time bounds prevent common mistakes
13//! - **Fee calculation**: Automatic fee estimation and adjustment with mempool ancestry tracking
14//! - **Rune support**: Optional rune transaction support when compiled with `runes` feature
15//! - **UTXO consolidation**: Optional consolidation features for managing fragmented UTXOs
16//! - **BPF compatibility**: Suitable for on-chain programs running in Solana BPF VM
17//!
18//! ## Quick Start
19//!
20//! ```rust,no_run
21//! use satellite_bitcoin_transactions::TransactionBuilder;
22//! use satellite_bitcoin_transactions::fee_rate::FeeRate;
23//!
24//! // Create a builder that can handle up to 8 modified accounts and 4 inputs to sign
25//! let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
26//!
27//! // Add inputs, outputs, and state transitions...
28//! // (See TransactionBuilder documentation for detailed examples)
29//!
30//! // Adjust fees and finalize
31//! let fee_rate = FeeRate::try_from(10.0).unwrap(); // 10 sat/vB
32//! builder.adjust_transaction_to_pay_fees(&fee_rate, None)?;
33//! builder.finalize()?;
34//! # Ok::<(), Box<dyn std::error::Error>>(())
35//! ```
36//!
37//! ## Architecture
38//!
39//! The crate is built around the [`TransactionBuilder`] which maintains:
40//! - A Bitcoin transaction under construction
41//! - Metadata about which program accounts have been modified
42//! - Information about which transaction inputs need signatures
43//! - Running totals for fee calculation and validation
44//!
45//! All data structures use fixed-size collections to ensure zero-heap allocation,
46//! making them suitable for constrained environments like the Solana BPF VM.
47use std::fmt::Debug;
48#[cfg(test)]
49use std::str::FromStr;
50
51use arch_program::rune::RuneAmount;
52use arch_program::{
53    account::AccountInfo, helper::add_state_transition, input_to_sign::InputToSign,
54    program::set_transaction_to_sign, program_error::ProgramError, pubkey::Pubkey, utxo::UtxoMeta,
55};
56use bitcoin::{
57    absolute::LockTime, transaction::Version, OutPoint, ScriptBuf, Sequence, Transaction, TxIn,
58    TxOut, Witness,
59};
60pub use mempool::{AccountMempoolInfo, MempoolData, MempoolDataView, MempoolInfo, TxStatus};
61
62#[cfg(feature = "runes")]
63use ordinals::{Artifact, Runestone};
64
65use satellite_collections::generic::fixed_list_unchecked::FixedRefList;
66use satellite_collections::generic::{fixed_list::FixedList, fixed_set::FixedCapacitySet};
67
68use crate::btc_utxo_holder::BtcUtxoHolder;
69use crate::bytes::txid_to_bytes_big_endian;
70use crate::{
71    calc_fee::{
72        adjust_transaction_to_pay_fees, estimate_final_tx_vsize,
73        estimate_tx_size_with_additional_inputs_outputs,
74        estimate_tx_vsize_with_additional_inputs_outputs,
75    },
76    constants::DUST_LIMIT,
77    error::BitcoinTxError,
78    fee_rate::FeeRate,
79    mempool::generate_mempool_info,
80    utxo_info::UtxoInfo,
81};
82
83#[cfg(feature = "utxo-consolidation")]
84use crate::{consolidation::add_consolidation_utxos, input_calc::ARCH_INPUT_SIZE};
85
86mod arch;
87pub mod btc_utxo_holder;
88pub mod bytes;
89mod calc_fee;
90mod consolidation;
91pub mod constants;
92pub mod error;
93pub mod fee_rate;
94mod find_btc;
95pub mod input_calc;
96mod mempool;
97#[cfg(feature = "serde")]
98mod serde;
99pub mod util;
100pub mod utxo_info;
101#[cfg(feature = "serde")]
102pub mod utxo_info_json;
103
104#[derive(Clone, Debug, Default)]
105/// A zero-copy wrapper for tracking modified program accounts.
106///
107/// `ModifiedAccount` is a lightweight wrapper around [`AccountInfo`] that enables
108/// [`TransactionBuilder`] to track which program accounts have been modified during
109/// transaction construction, without requiring heap allocation.
110///
111/// ## Design
112///
113/// The wrapper stores a borrowed reference to the actual account data, avoiding
114/// copies or allocations. This makes it suitable for use in constrained environments
115/// like the Solana BPF VM where heap allocation is expensive or unavailable.
116///
117/// ## Lifetime Management
118///
119/// The `'a` lifetime parameter ensures that the wrapped account reference remains
120/// valid for the duration of the transaction building process. This is typically
121/// the lifetime of the instruction execution context.
122///
123/// ## Usage
124///
125/// You typically don't create `ModifiedAccount` instances directly. Instead, they
126/// are created automatically by [`TransactionBuilder`] methods like
127/// [`TransactionBuilder::add_state_transition`] and
128/// related helpers.
129///
130/// ## Memory Safety
131///
132/// The default value (created with `Default::default()`) contains `None` and will
133/// panic if accessed via `as_ref()`. This is by design since such instances should
134/// never be exposed outside of internal testing.
135struct ModifiedAccount<'info>(Option<AccountInfo<'info>>);
136
137impl<'info> ModifiedAccount<'info> {
138    #[inline]
139    /// Creates a new [`ModifiedAccount`] from a borrowed [`AccountInfo`].
140    ///
141    /// This is a zero-cost helper used by
142    /// `TransactionBuilder` internals.
143    pub fn new(account: AccountInfo<'info>) -> Self {
144        Self(Some(account))
145    }
146}
147
148impl<'info> AsRef<AccountInfo<'info>> for ModifiedAccount<'info> {
149    fn as_ref(&self) -> &AccountInfo<'info> {
150        self.0.as_ref().expect("ModifiedAccount is None")
151    }
152}
153
154/// Represents potential transaction inputs for size estimation.
155///
156/// This struct is used in "what-if" scenarios where you need to estimate the size
157/// of a transaction before actually adding inputs. It's particularly useful for
158/// fee calculation and UTXO consolidation planning.
159///
160/// ## Fields
161///
162/// - `count`: Number of inputs of this type to add
163/// - `item`: Template input to use for size calculation
164/// - `signer`: Whether these inputs will be signed by a program account
165///
166/// ## Examples
167///
168/// ```rust
169/// # use satellite_bitcoin_transactions::NewPotentialInputAmount;
170/// # use bitcoin::{TxIn, OutPoint, ScriptBuf, Sequence, Witness};
171/// // Estimate adding 3 similar inputs
172/// let potential_inputs = NewPotentialInputAmount {
173///     count: 3,
174///     item: TxIn {
175///         previous_output: OutPoint::null(),
176///         script_sig: ScriptBuf::new(),
177///         sequence: Sequence::MAX,
178///         witness: Witness::new(),
179///     },
180///     signer: true,
181/// };
182/// ```
183pub struct NewPotentialInputAmount {
184    pub count: usize,
185    pub item: TxIn,
186    pub signer: bool,
187}
188
189/// Represents potential transaction outputs for size estimation.
190///
191/// Used in conjunction with [`NewPotentialInputAmount`] to estimate transaction
192/// sizes before actually constructing the outputs. This is essential for accurate
193/// fee calculation and transaction planning.
194///
195/// ## Fields
196///
197/// - `count`: Number of outputs of this type to add
198/// - `item`: Template output to use for size calculation
199///
200/// ## Examples
201///
202/// ```rust
203/// # use satellite_bitcoin_transactions::NewPotentialOutputAmount;
204/// # use bitcoin::{TxOut, Amount, ScriptBuf};
205/// // Estimate adding 2 similar outputs
206/// let potential_outputs = NewPotentialOutputAmount {
207///     count: 2,
208///     item: TxOut {
209///         value: Amount::from_sat(50000),
210///         script_pubkey: ScriptBuf::new(),
211///     },
212/// };
213/// ```
214pub struct NewPotentialOutputAmount {
215    pub count: usize,
216    pub item: TxOut,
217}
218
219/// Container for potential inputs and outputs used in size estimation.
220///
221/// This struct aggregates potential inputs and outputs into a single parameter
222/// for methods like [`TransactionBuilder::estimate_tx_size_with_additional_inputs_outputs`].
223/// It enables comprehensive "what-if" analysis for transaction planning.
224///
225/// ## Usage Patterns
226///
227/// ```rust
228/// # use satellite_bitcoin_transactions::{NewPotentialInputsAndOutputs, NewPotentialInputAmount, NewPotentialOutputAmount};
229/// # use bitcoin::{TxIn, TxOut, OutPoint, ScriptBuf, Sequence, Witness, Amount};
230/// // Planning a transaction with multiple potential changes
231/// let potential_changes = NewPotentialInputsAndOutputs {
232///     inputs: Some(NewPotentialInputAmount {
233///         count: 2,
234///         item: TxIn {
235///             previous_output: OutPoint::null(),
236///             script_sig: ScriptBuf::new(),
237///             sequence: Sequence::MAX,
238///             witness: Witness::new(),
239///         },
240///         signer: true,
241///     }),
242///     outputs: vec![
243///         NewPotentialOutputAmount {
244///             count: 1,
245///             item: TxOut {
246///                 value: Amount::from_sat(50000),
247///                 script_pubkey: ScriptBuf::new(),
248///             },
249///         },
250///         NewPotentialOutputAmount {
251///             count: 1,
252///             item: TxOut {
253///                 value: Amount::from_sat(25000),
254///                 script_pubkey: ScriptBuf::new(),
255///             },
256///         },
257///     ],
258/// };
259/// ```
260///
261/// ## See Also
262///
263/// - [`TransactionBuilder::estimate_tx_size_with_additional_inputs_outputs`]
264/// - [`TransactionBuilder::estimate_tx_vsize_with_additional_inputs_outputs`]
265pub struct NewPotentialInputsAndOutputs {
266    pub inputs: Option<NewPotentialInputAmount>,
267    pub outputs: Vec<NewPotentialOutputAmount>,
268}
269
270#[derive(Debug)]
271/// A zero-heap Bitcoin transaction builder for the Arch runtime.
272///
273/// `TransactionBuilder` provides a type-safe, heap-free way to construct Bitcoin transactions
274/// that interact with the Arch runtime. It manages transaction inputs, outputs, state transitions,
275/// and fee calculations while maintaining compatibility with constrained environments like the
276/// Solana BPF VM.
277///
278/// ## Design Philosophy
279///
280/// The builder is designed around three core principles:
281/// - **Zero-heap allocation**: All collections use fixed-size arrays determined at compile time
282/// - **Type safety**: Generic parameters prevent runtime errors by enforcing limits at compile time
283/// - **Arch integration**: Built-in support for Arch-specific concepts like state transitions and account modifications
284///
285/// ## Generic Parameters
286///
287/// - `MAX_MODIFIED_ACCOUNTS`: Maximum number of program accounts that can be modified in a single transaction
288/// - `MAX_INPUTS_TO_SIGN`: Maximum number of transaction inputs that require signatures
289/// - `RuneSet`: Collection type for tracking rune inputs (only used with `runes` feature)
290///
291/// These bounds are enforced at compile time to ensure the builder remains heap-free.
292///
293/// ## Feature Flags
294///
295/// - `runes`: Enables rune transaction support with automatic rune input/output tracking
296/// - `utxo-consolidation`: Enables UTXO consolidation features for managing fragmented UTXOs
297/// - `serde`: Enables serialization support for transaction data
298///
299/// ## Basic Usage
300///
301/// ```rust
302/// use satellite_bitcoin_transactions::TransactionBuilder;
303/// use satellite_bitcoin_transactions::fee_rate::FeeRate;
304///
305/// // Create a builder with capacity for 8 modified accounts and 4 inputs to sign
306/// let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
307///
308/// // The builder starts with an empty version 2 transaction
309/// assert_eq!(builder.transaction.input.len(), 0);
310/// assert_eq!(builder.transaction.output.len(), 0);
311/// assert_eq!(builder.total_btc_input, 0);
312///
313/// // Add inputs and outputs using the builder methods...
314/// # Ok::<(), Box<dyn std::error::Error>>(())
315/// ```
316///
317/// ## Working with State Transitions
318///
319/// State transitions are a core concept in Arch. Use these methods to manage program account updates:
320///
321/// ```rust,no_run
322/// # use satellite_bitcoin_transactions::{TransactionBuilder, SignPolicy};
323/// # use arch_program::account::AccountInfo;
324/// # use arch_program::pubkey::Pubkey;
325/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
326/// # let account: AccountInfo<'static> = unsafe { std::mem::zeroed() };
327/// // Add a state transition for an existing account
328/// builder.add_state_transition(&account, SignPolicy::Managed)?;
329///
330/// // The builder automatically:
331/// // 1. Adds the account to modified_accounts list
332/// // 2. Creates an InputToSign entry
333/// // 3. Updates total_btc_input with DUST_LIMIT
334/// // 4. Adds the state transition to the transaction
335/// # Ok::<(), satellite_bitcoin_transactions::error::BitcoinTxError>(())
336/// ```
337///
338/// ## Adding User Inputs
339///
340/// Add user-controlled UTXOs to the transaction:
341///
342/// ```rust
343/// # use satellite_bitcoin_transactions::TransactionBuilder;
344/// # use satellite_bitcoin_transactions::utxo_info::{UtxoInfo, SingleRuneSet};
345/// # use satellite_bitcoin_transactions::TxStatus;
346/// # use arch_program::pubkey::Pubkey;
347/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
348/// # let utxo: UtxoInfo<SingleRuneSet> = unsafe { std::mem::zeroed() };
349/// # let status = TxStatus::Confirmed;
350/// # let signer = Pubkey::system_program();
351/// // Add a regular input that requires signing
352/// builder.add_tx_input(&utxo, &status, Some(&signer))?;
353///
354/// // For precise control over input order:
355/// builder.insert_tx_input(0, &utxo, &status, Some(&signer))?;
356/// # Ok::<(), satellite_bitcoin_transactions::error::BitcoinTxError>(())
357/// ```
358///
359/// ## Fee Management
360///
361/// The builder provides sophisticated fee management with mempool ancestry tracking:
362///
363/// ```rust,no_run
364/// # use satellite_bitcoin_transactions::TransactionBuilder;
365/// # use satellite_bitcoin_transactions::fee_rate::FeeRate;
366/// # use bitcoin::ScriptBuf;
367/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
368/// # let change_address = ScriptBuf::new();
369/// // Set target fee rate
370/// let fee_rate = FeeRate::try_from(25.0)?; // 25 sat/vB
371///
372/// // Automatically adjust transaction to meet fee requirements
373/// builder.adjust_transaction_to_pay_fees(&fee_rate, Some(change_address))?;
374///
375/// // Validate the effective fee rate (including ancestors)
376/// builder.is_fee_rate_valid(&fee_rate)?;
377///
378/// // Get fee breakdown
379/// let user_fee = builder.get_fee_paid_by_user(&fee_rate);
380/// let total_fee = builder.get_fee_paid()?;
381/// # Ok::<(), Box<dyn std::error::Error>>(())
382/// ```
383///
384/// ## UTXO Selection
385///
386/// Automatically select UTXOs to meet funding requirements:
387///
388/// ```rust,no_run
389/// # use satellite_bitcoin_transactions::TransactionBuilder;
390/// # use satellite_bitcoin_transactions::utxo_info::UtxoInfo;
391/// # use arch_program::pubkey::Pubkey;
392/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
393/// # let utxos: Vec<UtxoInfo<_>> = vec![];
394/// # let program_pubkey = Pubkey::system_program();
395/// // Find UTXOs to cover a specific amount
396/// let amount_needed = 100_000; // 100k sats
397/// let (selected_indices, total_found) = builder.find_btc_in_utxos(
398///     &utxos,
399///     &program_pubkey,
400///     amount_needed
401/// )?;
402///
403/// // The builder automatically selects the most efficient UTXOs
404/// // and adds them to the transaction
405/// # Ok::<(), satellite_bitcoin_transactions::error::BitcoinTxError>(())
406/// ```
407///
408/// ## Rune Support (with `runes` feature)
409///
410/// When compiled with the `runes` feature, the builder automatically tracks rune inputs and outputs:
411///
412/// ```rust
413/// # #[cfg(feature = "runes")]
414/// # {
415/// # use satellite_bitcoin_transactions::TransactionBuilder;
416/// # use satellite_bitcoin_transactions::utxo_info::UtxoInfo;
417/// # use arch_program::rune::RuneAmount;
418/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
419/// # let rune_utxo: UtxoInfo<_> = unsafe { std::mem::zeroed() };
420/// // Rune inputs are automatically tracked when adding UTXOs
421/// // The builder maintains total_rune_inputs and runestone data
422///
423/// // Access rune information
424/// let rune_count = builder.total_rune_inputs.len();
425/// let runestone = &builder.runestone;
426/// # }
427/// ```
428///
429/// ## UTXO Consolidation (with `utxo-consolidation` feature)
430///
431/// Automatically consolidate fragmented UTXOs to reduce transaction sizes:
432///
433/// ```rust,no_run
434/// # #[cfg(feature = "utxo-consolidation")]
435/// # {
436/// # use satellite_bitcoin_transactions::TransactionBuilder;
437/// # use satellite_bitcoin_transactions::fee_rate::FeeRate;
438/// # use satellite_bitcoin_transactions::NewPotentialInputsAndOutputs;
439/// # use satellite_bitcoin_transactions::utxo_info::UtxoInfo;
440/// # use arch_program::pubkey::Pubkey;
441/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
442/// # let pool_pubkey = Pubkey::system_program();
443/// # let fee_rate = FeeRate::try_from(10.0).unwrap();
444/// # let utxos: Vec<UtxoInfo> = vec![];
445/// # let potential_changes = NewPotentialInputsAndOutputs { inputs: None, outputs: vec![] };
446/// // Add consolidation UTXOs to reduce fragmentation
447/// builder.add_consolidation_utxos(
448///     &pool_pubkey,
449///     &fee_rate,
450///     &utxos,
451///     &potential_changes
452/// );
453///
454/// // Get fee breakdown
455/// let program_fee = builder.get_fee_paid_by_program(&fee_rate);
456/// let user_fee = builder.get_fee_paid_by_user(&fee_rate);
457/// # }
458/// ```
459///
460/// ## Size Estimation
461///
462/// Estimate transaction sizes for fee calculation:
463///
464/// ```rust
465/// # use satellite_bitcoin_transactions::TransactionBuilder;
466/// # use satellite_bitcoin_transactions::NewPotentialInputsAndOutputs;
467/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
468/// # let potential_changes = NewPotentialInputsAndOutputs { inputs: None, outputs: vec![] };
469/// // Estimate current transaction size
470/// let current_vsize = builder.estimate_final_tx_vsize();
471///
472/// // Estimate size with additional inputs/outputs
473/// let estimated_vsize = builder.estimate_tx_vsize_with_additional_inputs_outputs(
474///     &potential_changes
475/// );
476/// ```
477///
478/// ## Error Handling
479///
480/// The builder provides detailed error information:
481///
482/// ```rust
483/// # use satellite_bitcoin_transactions::TransactionBuilder;
484/// # use satellite_bitcoin_transactions::error::BitcoinTxError;
485/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
486/// // Capacity limits are enforced at runtime
487/// match builder.inputs_to_sign.len() {
488///     len if len >= 4 => {
489///         // Handle InputToSignListFull error
490///     }
491///     _ => {
492///         // Safe to add more inputs
493///     }
494/// }
495///
496/// // Fee validation errors
497/// match builder.get_fee_paid() {
498///     Ok(fee) => println!("Fee: {} sats", fee),
499///     Err(BitcoinTxError::InsufficientInputAmount) => {
500///         // Handle insufficient input funds
501///     }
502///     Err(e) => {
503///         // Handle other errors
504///     }
505/// }
506/// ```
507///
508/// ## Finalization
509///
510/// Complete the transaction and prepare it for signing:
511///
512/// ```rust
513/// # use satellite_bitcoin_transactions::TransactionBuilder;
514/// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
515/// // After adding all inputs, outputs, and adjusting fees
516/// builder.finalize()?;
517///
518/// // The transaction is now ready for the Arch runtime to collect signatures
519/// // and broadcast to the Bitcoin network
520/// # Ok::<(), arch_program::program_error::ProgramError>(())
521/// ```
522///
523/// ## Performance Considerations
524///
525/// - All operations are O(1) or O(n) where n is bounded by the generic parameters
526/// - No heap allocations occur during normal operation
527/// - Memory usage is deterministic and known at compile time
528/// - Suitable for use in constrained environments like the Solana BPF VM
529///
530/// ## Thread Safety
531///
532/// `TransactionBuilder` is not thread-safe and should not be shared between threads.
533/// Create separate builders for concurrent transaction construction.
534pub struct TransactionBuilder<
535    'info,
536    const MAX_MODIFIED_ACCOUNTS: usize,
537    const MAX_INPUTS_TO_SIGN: usize,
538    RuneSet: FixedCapacitySet<Item = RuneAmount> + Default,
539> {
540    /// This transaction will be broadcast through Arch to indicate a state
541    /// transition in the program
542    pub transaction: Transaction,
543    pub tx_statuses: MempoolInfo,
544
545    /// This tells Arch which accounts have been modified, and thus required
546    /// their data to be saved
547    modified_accounts: FixedList<ModifiedAccount<'info>, MAX_MODIFIED_ACCOUNTS>,
548
549    /// This tells Arch which inputs in [InstructionContext::transaction] still
550    /// need to be signed, along with which key needs to sign each of them
551    pub inputs_to_sign: FixedRefList<InputToSign, MAX_INPUTS_TO_SIGN>,
552
553    pub total_btc_input: u64,
554
555    /// Tracks whether any **non–state-transition** inputs or *any* outputs
556    /// have been added to the transaction via builder helpers.
557    ///
558    /// Once this becomes `true`, no further state transitions may be added
559    /// through [`TransactionBuilder::add_state_transition`] or
560    /// [`TransactionBuilder::insert_state_transition_input`].
561    has_seen_non_state_io_or_output: bool,
562
563    _phantom: std::marker::PhantomData<RuneSet>,
564
565    #[cfg(feature = "runes")]
566    pub total_rune_inputs: RuneSet,
567
568    #[cfg(feature = "runes")]
569    pub runestone: Runestone,
570
571    #[cfg(feature = "utxo-consolidation")]
572    pub total_btc_consolidation_input: u64,
573
574    #[cfg(feature = "utxo-consolidation")]
575    pub extra_tx_size_for_consolidation: usize,
576}
577
578/// Specifies how an added state-transition input should be signed.
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum SignPolicy {
581    /// The program (via Arch) manages signing responsibility for this input.
582    ///
583    /// This applies when the signer has either:
584    /// - Already signed the transaction (e.g., fee payer or any other account that has signed)
585    /// - Will be signed by the program itself (e.g., program PDAs)
586    ///
587    /// An `InputToSign` entry is recorded for Arch to handle the signature.
588    Managed,
589    /// A third-party will provide the signature for this input.
590    ///
591    /// This applies to accounts owned by other programs (e.g., via CPI) or signers
592    /// which have not signed this transaction. No `InputToSign` entry is recorded.
593    External,
594}
595
596impl<
597        'info,
598        const MAX_MODIFIED_ACCOUNTS: usize,
599        const MAX_INPUTS_TO_SIGN: usize,
600        RuneSet: FixedCapacitySet<Item = RuneAmount> + Default + Debug,
601    > TransactionBuilder<'info, MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RuneSet>
602{
603    /// Creates a new empty transaction builder.
604    ///
605    /// Initializes a blank builder containing an empty **version 2** Bitcoin transaction with `lock_time = 0`.
606    /// All internal counters and collections start empty, ready for you to populate through the various
607    /// `add_*` and `insert_*` methods.
608    ///
609    /// ## Initial State
610    ///
611    /// - `transaction`: Empty version 2 transaction
612    /// - `total_btc_input`: 0 satoshis
613    /// - `modified_accounts`: Empty fixed-size list
614    /// - `inputs_to_sign`: Empty fixed-size list
615    /// - `tx_statuses`: Default mempool info
616    ///
617    /// ## Typical Workflow
618    ///
619    /// 1. Create builder with `new()`
620    /// 2. Add inputs with `add_tx_input()` or `add_state_transition()`
621    /// 3. Add outputs directly to `builder.transaction.output`
622    /// 4. Adjust fees with `adjust_transaction_to_pay_fees()`
623    /// 5. Finalize with `finalize()`
624    ///
625    /// ## Examples
626    ///
627    /// ```rust
628    /// use satellite_bitcoin_transactions::TransactionBuilder;
629    /// use bitcoin::Transaction;
630    ///
631    /// // Create a builder that can handle up to 8 modified accounts and 4 inputs to sign
632    /// let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
633    ///
634    /// // Verify initial state
635    /// assert_eq!(builder.transaction.input.len(), 0);
636    /// assert_eq!(builder.transaction.output.len(), 0);
637    /// assert_eq!(builder.total_btc_input, 0);
638    /// assert_eq!(builder.inputs_to_sign.len(), 0);
639    /// ```
640    ///
641    /// ## Generic Parameters
642    ///
643    /// Choose your bounds based on your use case:
644    /// - **Small transactions**: `TransactionBuilder<4, 2>` for simple operations
645    /// - **Medium transactions**: `TransactionBuilder<8, 4>` for typical use cases
646    /// - **Large transactions**: `TransactionBuilder<16, 8>` for complex operations
647    ///
648    /// Remember that larger bounds use more stack space but provide more flexibility.
649    #[cfg(not(feature = "runes"))]
650    pub fn new() -> Self {
651        let transaction = Transaction {
652            version: Version::TWO,
653            lock_time: LockTime::ZERO,
654            input: vec![],
655            output: vec![],
656        };
657
658        Self {
659            transaction,
660            tx_statuses: MempoolInfo::default(),
661            modified_accounts: FixedList::new(),
662            inputs_to_sign: FixedRefList::new(),
663            total_btc_input: 0,
664            has_seen_non_state_io_or_output: false,
665
666            #[cfg(feature = "utxo-consolidation")]
667            total_btc_consolidation_input: 0,
668            #[cfg(feature = "utxo-consolidation")]
669            extra_tx_size_for_consolidation: 0,
670            _phantom: std::marker::PhantomData::<RuneSet>,
671        }
672    }
673
674    #[cfg(not(feature = "runes"))]
675    pub fn new_with_transaction<const MAX_UTXOS: usize, const MAX_ACCOUNTS: usize>(
676        transaction: Transaction,
677        mempool_data: &MempoolData<MAX_UTXOS, MAX_ACCOUNTS>,
678        user_utxos: &[UtxoInfo],
679    ) -> Result<Self, BitcoinTxError> {
680        assert_eq!(transaction.input.len(), user_utxos.len(), "TransactionBuilder::replace_transaction: Transaction input length must match user UTXOs length");
681
682        for input in &transaction.input {
683            let previous_output = &input.previous_output;
684            let utxo_meta = UtxoMeta::from_outpoint(previous_output.txid, previous_output.vout);
685            let utxo = user_utxos.iter().find(|utxo| utxo.meta == utxo_meta);
686            if utxo.is_none() {
687                return Err(BitcoinTxError::UtxoNotFoundInUserUtxos);
688            }
689        }
690
691        let tx_statuses = generate_mempool_info(user_utxos, mempool_data);
692        let total_btc_input = user_utxos.iter().map(|u| u.value).sum::<u64>();
693
694        Ok(Self {
695            transaction,
696            tx_statuses,
697            modified_accounts: FixedList::new(),
698            inputs_to_sign: FixedRefList::new(),
699            total_btc_input,
700            has_seen_non_state_io_or_output: false,
701
702            #[cfg(feature = "utxo-consolidation")]
703            total_btc_consolidation_input: 0,
704            #[cfg(feature = "utxo-consolidation")]
705            extra_tx_size_for_consolidation: 0,
706            _phantom: std::marker::PhantomData::<RuneSet>,
707        })
708    }
709
710    /// Creates a new empty transaction builder with rune support.
711    ///
712    /// Initializes a blank builder containing an empty **version 2** Bitcoin transaction with `lock_time = 0`.
713    /// All internal counters and collections start empty, including rune-specific tracking.
714    ///
715    /// ## Initial State
716    ///
717    /// - `transaction`: Empty version 2 transaction
718    /// - `total_btc_input`: 0 satoshis
719    /// - `total_rune_inputs`: Empty rune set
720    /// - `runestone`: Default runestone
721    /// - `modified_accounts`: Empty fixed-size list
722    /// - `inputs_to_sign`: Empty fixed-size list
723    /// - `tx_statuses`: Default mempool info
724    ///
725    /// ## Rune Features
726    ///
727    /// With the `runes` feature enabled, the builder automatically:
728    /// - Tracks rune inputs when adding UTXOs
729    /// - Maintains runestone data for rune operations
730    /// - Handles rune arithmetic and validation
731    ///
732    /// ## Examples
733    ///
734    /// ```rust
735    /// # #[cfg(feature = "runes")]
736    /// # {
737    /// use satellite_bitcoin_transactions::TransactionBuilder;
738    /// use arch_program::rune::RuneAmount;
739    ///
740    /// // Create a builder that can handle up to 8 modified accounts and 4 inputs to sign
741    /// let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
742    ///
743    /// // Verify initial state
744    /// assert_eq!(builder.transaction.input.len(), 0);
745    /// assert_eq!(builder.transaction.output.len(), 0);
746    /// assert_eq!(builder.total_btc_input, 0);
747    /// assert_eq!(builder.total_rune_inputs.len(), 0);
748    /// assert_eq!(builder.inputs_to_sign.len(), 0);
749    /// # }
750    /// ```
751    ///
752    /// ## Generic Parameters
753    ///
754    /// Choose your bounds based on your use case:
755    /// - **Small transactions**: `TransactionBuilder<4, 2, SmallRuneSet>` for simple operations
756    /// - **Medium transactions**: `TransactionBuilder<8, 4, MediumRuneSet>` for typical use cases
757    /// - **Large transactions**: `TransactionBuilder<16, 8, LargeRuneSet>` for complex operations
758    ///
759    /// The `RuneSet` parameter determines how many different rune types can be tracked simultaneously.
760    #[cfg(feature = "runes")]
761    pub fn new() -> Self {
762        let transaction = Transaction {
763            version: Version::TWO,
764            lock_time: LockTime::ZERO,
765            input: vec![],
766            output: vec![],
767        };
768
769        Self {
770            transaction,
771            tx_statuses: MempoolInfo::default(),
772            modified_accounts: FixedList::new(),
773            inputs_to_sign: FixedRefList::new(),
774            total_btc_input: 0,
775            has_seen_non_state_io_or_output: false,
776
777            total_rune_inputs: RuneSet::default(),
778            runestone: Runestone::default(),
779
780            #[cfg(feature = "utxo-consolidation")]
781            total_btc_consolidation_input: 0,
782            #[cfg(feature = "utxo-consolidation")]
783            extra_tx_size_for_consolidation: 0,
784            _phantom: std::marker::PhantomData::<RuneSet>,
785        }
786    }
787
788    #[cfg(feature = "runes")]
789    pub fn new_with_transaction(
790        transaction: Transaction,
791        mempool_data: &impl MempoolDataView,
792        user_utxos: &[UtxoInfo<RuneSet>],
793    ) -> Result<Self, BitcoinTxError> {
794        if transaction.input.len() != user_utxos.len() {
795            return Err(BitcoinTxError::TransactionInputLengthMustMatchUserUtxosLength);
796        }
797
798        let mut total_rune_inputs = RuneSet::default();
799        for input in &transaction.input {
800            let previous_output = &input.previous_output;
801            let utxo_meta = UtxoMeta::from_outpoint(previous_output.txid, previous_output.vout);
802            let utxo = user_utxos.iter().find(|utxo| utxo.meta == utxo_meta);
803            if let Some(utxo) = utxo {
804                for rune in utxo.runes.as_slice() {
805                    add_rune_input(&mut total_rune_inputs, *rune)?;
806                }
807            } else {
808                return Err(BitcoinTxError::UtxoNotFoundInUserUtxos);
809            }
810        }
811
812        let tx_statuses = generate_mempool_info(user_utxos, mempool_data);
813        let total_btc_input = user_utxos.iter().map(|u| u.value).sum::<u64>();
814
815        let runestone = match Runestone::decipher(&transaction) {
816            Some(artifact) => match artifact {
817                Artifact::Runestone(runestone) => Ok(runestone),
818                _ => Err(BitcoinTxError::RunestoneDecipherError),
819            },
820            None => Ok(Runestone::default()),
821        }?;
822
823        Ok(Self {
824            transaction,
825            tx_statuses,
826            modified_accounts: FixedList::new(),
827            inputs_to_sign: FixedRefList::new(),
828            total_btc_input,
829            has_seen_non_state_io_or_output: false,
830
831            total_rune_inputs,
832            runestone,
833
834            #[cfg(feature = "utxo-consolidation")]
835            total_btc_consolidation_input: 0,
836            #[cfg(feature = "utxo-consolidation")]
837            extra_tx_size_for_consolidation: 0,
838            _phantom: std::marker::PhantomData::<RuneSet>,
839        })
840    }
841
842    /// Adds a state transition for an existing program account.
843    ///
844    /// This method handles the complete process of adding a state transition to the transaction,
845    /// which is required when updating any program-derived account (PDA) or state account on Arch.
846    ///
847    /// ## What it does
848    ///
849    /// The method performs these operations atomically:
850    /// 1. **Adds signing requirement**: Creates an [`InputToSign`] entry so Arch knows which key must sign the input
851    /// 2. **Adds meta-instruction**: Appends the state transition meta-instruction to the transaction
852    /// 3. **Tracks modification**: Adds the account to the `modified_accounts` list for Arch's state saving
853    /// 4. **Updates input total**: Increments `total_btc_input` by [`constants::DUST_LIMIT`] (546 sats)
854    ///
855    /// ## When to use
856    ///
857    /// Use this method when you need to:
858    /// - Update an existing program account
859    /// - Modify state stored in a PDA
860    /// - Perform any operation that changes account data
861    ///
862    /// ## Account Requirements
863    ///
864    /// The account must:
865    /// - Have a valid UTXO backing it on-chain
866    /// - Be owned by a program that you have authority to modify
867    /// - Have exactly [`constants::DUST_LIMIT`] satoshis in its UTXO
868    ///
869    /// ## Examples
870    ///
871    /// ```rust,no_run
872    /// # use satellite_bitcoin_transactions::{TransactionBuilder, SignPolicy};
873    /// # use arch_program::account::AccountInfo;
874    /// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
875    /// # let account: AccountInfo<'static> = unsafe { std::mem::zeroed() };
876    /// // Add a state transition for an existing liquidity pool account
877    /// builder.add_state_transition(&account, SignPolicy::Managed)?;
878    ///
879    /// // The builder now knows:
880    /// // - This account will be modified
881    /// // - The account's key must sign the transaction
882    /// // - 546 sats are consumed from the account's UTXO
883    /// # Ok::<(), satellite_bitcoin_transactions::error::BitcoinTxError>(())
884    /// ```
885    ///
886    /// ## Error Handling
887    ///
888    /// Returns [`BitcoinTxError::InputToSignListFull`] if the builder has reached its
889    /// `MAX_INPUTS_TO_SIGN` limit, or [`BitcoinTxError::ModifiedAccountListFull`] if
890    /// the `MAX_MODIFIED_ACCOUNTS` limit is exceeded.
891    ///
892    /// ## See Also
893    ///
894    /// - [`Self::insert_state_transition_input`] for position-specific insertions
895    pub fn add_state_transition(
896        &mut self,
897        account: &AccountInfo<'info>,
898        policy: SignPolicy,
899    ) -> Result<u32, BitcoinTxError> {
900        if self.has_seen_non_state_io_or_output {
901            return Err(BitcoinTxError::InvalidStateTransitionOrdering);
902        }
903
904        let new_input_index = self.transaction.input.len() as u32;
905        if let SignPolicy::Managed = policy {
906            self.inputs_to_sign
907                .push(InputToSign {
908                    index: new_input_index,
909                    signer: account.key.clone(),
910                })
911                .map_err(|_| BitcoinTxError::InputToSignListFull)?;
912        }
913
914        self.modified_accounts
915            .push(ModifiedAccount::new(account.clone()))
916            .map_err(|_| BitcoinTxError::ModifiedAccountListFull)?;
917
918        let utxo_value = add_state_transition(&mut self.transaction, account)
919            .map_err(|_| BitcoinTxError::FailedStateTransition)?;
920        self.total_btc_input += utxo_value;
921
922        Ok(new_input_index)
923    }
924
925    /// Inserts an **existing state‐transition input** at the given `tx_index` keeping all
926    /// internal bookkeeping consistent.
927    ///
928    /// Use this when the input *order matters* and you need a state-transition (program
929    /// account) input to appear in a specific position.  The function updates
930    /// [`TransactionBuilder::inputs_to_sign`] indices, tracks the modified account and bumps
931    /// [`TransactionBuilder::total_btc_input`].
932    pub fn insert_state_transition_input(
933        &mut self,
934        tx_index: usize,
935        account: &AccountInfo<'info>,
936        policy: SignPolicy,
937    ) -> Result<(), BitcoinTxError> {
938        if self.has_seen_non_state_io_or_output {
939            return Err(BitcoinTxError::InvalidStateTransitionOrdering);
940        }
941
942        let txid = account.utxo.to_txid();
943        let utxo_outpoint = OutPoint {
944            txid,
945            vout: account.utxo.vout(),
946        };
947
948        self.transaction.input.insert(
949            tx_index,
950            TxIn {
951                previous_output: utxo_outpoint,
952                script_sig: ScriptBuf::new(),
953                sequence: Sequence::MAX,
954                witness: Witness::new(),
955            },
956        );
957
958        let tx_index_u32 = tx_index as u32;
959        for input in self.inputs_to_sign.iter_mut() {
960            if input.index >= tx_index_u32 {
961                input.index += 1;
962            }
963        }
964
965        if let SignPolicy::Managed = policy {
966            self.inputs_to_sign
967                .push(InputToSign {
968                    index: tx_index_u32,
969                    signer: account.key.clone(),
970                })
971                .map_err(|_| BitcoinTxError::InputToSignListFull)?;
972        }
973
974        self.modified_accounts
975            .push(ModifiedAccount::new(account.clone()))
976            .map_err(|_| BitcoinTxError::ModifiedAccountListFull)?;
977
978        // UTXO accounts always have dust limit amount.
979        self.total_btc_input += DUST_LIMIT;
980
981        Ok(())
982    }
983
984    /// Adds a regular input owned by `signer`.
985    ///
986    /// Besides pushing the `TxIn` into the underlying `transaction`, this helper:
987    /// * Records mempool ancestry via [`TransactionBuilder::add_tx_status`].
988    /// * Adds an [`InputToSign`].
989    /// * Updates `total_btc_input` (and `total_rune_input` when compiled with the `runes` feature).
990    pub fn add_tx_input<RS>(
991        &mut self,
992        utxo: &UtxoInfo<RS>,
993        status: &TxStatus,
994        signer: Option<&Pubkey>,
995    ) -> Result<(), BitcoinTxError>
996    where
997        RS: FixedCapacitySet<Item = RuneAmount>,
998    {
999        self.has_seen_non_state_io_or_output = true;
1000
1001        if let Some(signer) = signer {
1002            self.inputs_to_sign
1003                .push(InputToSign {
1004                    index: self.transaction.input.len() as u32,
1005                    signer: *signer,
1006                })
1007                .map_err(|_| BitcoinTxError::InputToSignListFull)?;
1008        }
1009
1010        let outpoint = utxo.meta.to_outpoint();
1011
1012        self.add_tx_status(utxo, &status);
1013
1014        self.transaction.input.push(TxIn {
1015            previous_output: outpoint,
1016            script_sig: ScriptBuf::new(),
1017            sequence: Sequence::MAX,
1018            witness: Witness::new(),
1019        });
1020
1021        self.total_btc_input += utxo.value;
1022
1023        #[cfg(feature = "runes")]
1024        {
1025            for rune in utxo.runes.as_slice() {
1026                self.add_rune_input(*rune)?;
1027            }
1028        }
1029
1030        Ok(())
1031    }
1032
1033    /// Appends a **user-supplied** [`TxIn`] (already built elsewhere) while still tracking the
1034    /// UTXO ancestry for fee-rate purposes.
1035    pub fn add_user_tx_input<RS>(
1036        &mut self,
1037        utxo: &UtxoInfo<RS>,
1038        status: &TxStatus,
1039        tx_in: TxIn,
1040    ) -> Result<(), BitcoinTxError>
1041    where
1042        RS: FixedCapacitySet<Item = RuneAmount>,
1043    {
1044        self.has_seen_non_state_io_or_output = true;
1045
1046        self.add_tx_status(utxo, status);
1047
1048        self.transaction.input.push(tx_in);
1049
1050        self.total_btc_input += utxo.value;
1051
1052        #[cfg(feature = "runes")]
1053        {
1054            for rune in utxo.runes.as_slice() {
1055                self.add_rune_input(*rune)?;
1056            }
1057        }
1058
1059        Ok(())
1060    }
1061
1062    /// Inserts a **regular** (non-state–account) [`TxIn`] at the given position `tx_index`.
1063    ///
1064    /// Besides pushing the new input into [`TransactionBuilder::transaction`], this helper keeps
1065    /// all *internal bookkeeping* consistent:
1066    ///
1067    /// 1. Records the mempool ancestry for fee-rate calculations via [`Self::add_tx_status`].
1068    /// 2. Shifts the `index` of every existing [`arch_program::input_to_sign::InputToSign`] that
1069    ///    appears **at or after** `tx_index` so their indices continue to match the underlying
1070    ///    transaction after the insertion.
1071    /// 3. Pushes a fresh [`InputToSign`] for `signer` so Arch knows which key must later provide
1072    ///    a witness for the inserted input.
1073    /// 4. Bumps [`Self::total_btc_input`] (and `total_rune_input` when compiled with the `runes`
1074    ///    feature) by the value of `utxo`.
1075    ///
1076    /// Use this when the *order* of inputs matters – for example when signing with PSBTs that
1077    /// expect user inputs to appear before program-generated ones.
1078    ///
1079    /// # Parameters
1080    /// * `tx_index` – zero-based index where the input should be inserted.
1081    /// * `utxo` – metadata of the UTXO being spent.
1082    /// * `status` – mempool status of `utxo`; contributes to ancestor fee/size tracking.
1083    /// * `signer` – optional public key that will sign the input.
1084    pub fn insert_tx_input<RS>(
1085        &mut self,
1086        tx_index: usize,
1087        utxo: &UtxoInfo<RS>,
1088        status: &TxStatus,
1089        signer: Option<&Pubkey>,
1090    ) -> Result<(), BitcoinTxError>
1091    where
1092        RS: FixedCapacitySet<Item = RuneAmount>,
1093    {
1094        self.has_seen_non_state_io_or_output = true;
1095
1096        let outpoint = utxo.meta.to_outpoint();
1097
1098        self.add_tx_status(utxo, status);
1099
1100        self.transaction.input.insert(
1101            tx_index,
1102            TxIn {
1103                previous_output: outpoint,
1104                script_sig: ScriptBuf::new(),
1105                sequence: Sequence::MAX,
1106                witness: Witness::new(),
1107            },
1108        );
1109
1110        // More efficient update of indices
1111        let tx_index_u32 = tx_index as u32;
1112        for input in self.inputs_to_sign.iter_mut() {
1113            if input.index >= tx_index_u32 {
1114                input.index += 1;
1115            }
1116        }
1117
1118        if let Some(signer) = signer {
1119            self.inputs_to_sign
1120                .push(InputToSign {
1121                    index: tx_index_u32,
1122                    signer: *signer,
1123                })
1124                .map_err(|_| BitcoinTxError::InputToSignListFull)?;
1125        }
1126
1127        self.total_btc_input += utxo.value;
1128
1129        #[cfg(feature = "runes")]
1130        {
1131            for rune in utxo.runes.as_slice() {
1132                self.add_rune_input(*rune)?;
1133            }
1134        }
1135
1136        Ok(())
1137    }
1138
1139    /// Inserts a **pre-constructed** [`TxIn`] – built elsewhere – at the specified `tx_index`.
1140    ///
1141    /// The function behaves similarly to [`Self::insert_tx_input`] but **does not** create a new
1142    /// [`InputToSign`], as the caller may already have handled signature tracking. It still:
1143    ///
1144    /// * Accounts for the input's mempool ancestry using [`Self::add_tx_status`].
1145    /// * Shifts the indices of all existing [`InputToSign`] that come after `tx_index` so they
1146    ///   remain correct.
1147    /// * Updates BTC / rune running totals.
1148    ///
1149    /// This is handy when you have a non-standard script or any other reason to fully craft the
1150    /// `TxIn` outside of the builder but still need to place it at a precise position inside the
1151    /// transaction.
1152    ///
1153    /// # Parameters
1154    /// * `tx_index` – position where `tx_in` should be inserted.
1155    /// * `utxo` – the UTXO consumed by `tx_in`.
1156    /// * `status` – mempool status of `utxo`.
1157    /// * `tx_in` – ready-made transaction input (will be cloned).
1158    pub fn insert_user_tx_input<RS>(
1159        &mut self,
1160        tx_index: usize,
1161        utxo: &UtxoInfo<RS>,
1162        status: &TxStatus,
1163        tx_in: &TxIn,
1164    ) -> Result<(), BitcoinTxError>
1165    where
1166        RS: FixedCapacitySet<Item = RuneAmount>,
1167    {
1168        self.has_seen_non_state_io_or_output = true;
1169
1170        self.add_tx_status(utxo, status);
1171
1172        self.transaction.input.insert(tx_index, tx_in.clone());
1173
1174        // More efficient update of indices
1175        let tx_index_u32 = tx_index as u32;
1176        for input in self.inputs_to_sign.iter_mut() {
1177            if input.index >= tx_index_u32 {
1178                input.index += 1;
1179            }
1180        }
1181
1182        self.total_btc_input += utxo.value;
1183
1184        #[cfg(feature = "runes")]
1185        {
1186            for rune in utxo.runes.as_slice() {
1187                self.add_rune_input(*rune)?;
1188            }
1189        }
1190
1191        Ok(())
1192    }
1193
1194    /// Greedily selects UTXOs until at least `amount` satoshis are gathered.
1195    ///
1196    /// Selection strategy:
1197    /// * With the `utxo-consolidation` feature **enabled**: prefer UTXOs **without** the
1198    ///   `needs_consolidation` flag, then sort by descending value.
1199    /// * Without the feature: simply sort by descending value.
1200    ///
1201    /// Returns the **indices** of the chosen items inside the original slice plus the total value
1202    /// selected.
1203    ///
1204    /// find_btc_in_utxos has been moved to the `find_btc` module for clarity.
1205
1206    #[cfg(feature = "utxo-consolidation")]
1207    fn set_consolidation_tracking(&mut self, consolidation_amount: u64, extra_tx_size: usize) {
1208        self.total_btc_consolidation_input += consolidation_amount;
1209        self.extra_tx_size_for_consolidation += extra_tx_size;
1210    }
1211
1212    #[cfg(not(feature = "utxo-consolidation"))]
1213    fn set_consolidation_tracking(&mut self, _consolidation_amount: u64, _extra_tx_size: usize) {}
1214
1215    // btc selection helpers moved into `find_btc` module
1216
1217    /// Greedily selects UTXOs until at least `amount` satoshis are gathered.
1218    ///
1219    /// Selection strategy:
1220    /// * With the `utxo-consolidation` feature **enabled**: prefer UTXOs **without** the
1221    ///   `needs_consolidation` flag, then sort by descending value.
1222    /// * Without the feature: simply sort by descending value.
1223    ///
1224    /// Returns the **indices** of the chosen items inside the original slice plus the total value
1225    /// selected.
1226    ///
1227    /// # Errors
1228    /// * [`BitcoinTxError::NotEnoughBtcInPool`] – not enough value in `utxos` to satisfy `amount`.
1229    /// find_btc_in_utxos_from_holder has been moved to the `find_btc` module for clarity.
1230
1231    /// Automatically adjusts the transaction to meet the target fee rate.
1232    ///
1233    /// This method optimizes the transaction's fee structure by analyzing the current input/output
1234    /// balance and adjusting outputs to achieve the desired fee rate. It handles both overpayment
1235    /// (creating change) and underpayment (reducing outputs) scenarios.
1236    ///
1237    /// ## How it works
1238    ///
1239    /// The method evaluates the current transaction and:
1240    /// 1. **Calculates required fee**: Based on transaction size and target fee rate
1241    /// 2. **Handles excess funds**: Creates or increases change output if inputs exceed requirements
1242    /// 3. **Handles insufficient funds**: Reduces change output or returns error if impossible
1243    /// 4. **Considers ancestors**: Accounts for mempool ancestry when calculating effective fee rate
1244    ///
1245    /// ## Change Output Behavior
1246    ///
1247    /// - **`address_to_send_remaining_btc = Some(address)`**: Creates new change output or increases existing one
1248    /// - **`address_to_send_remaining_btc = None`**: Only adjusts existing outputs, never creates new ones
1249    ///
1250    /// ## Examples
1251    ///
1252    /// ```rust,no_run
1253    /// # use satellite_bitcoin_transactions::TransactionBuilder;
1254    /// # use satellite_bitcoin_transactions::fee_rate::FeeRate;
1255    /// # use bitcoin::ScriptBuf;
1256    /// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
1257    /// // Set target fee rate (25 sat/vB)
1258    /// let fee_rate = FeeRate::try_from(25.0)?;
1259    ///
1260    /// // Create change output for excess funds
1261    /// let change_address = ScriptBuf::new(); // Your change address
1262    /// builder.adjust_transaction_to_pay_fees(&fee_rate, Some(change_address))?;
1263    ///
1264    /// // Or adjust without creating change (only reduce existing outputs)
1265    /// builder.adjust_transaction_to_pay_fees(&fee_rate, None)?;
1266    /// # Ok::<(), Box<dyn std::error::Error>>(())
1267    /// ```
1268    ///
1269    /// ## Fee Calculation Details
1270    ///
1271    /// The method considers:
1272    /// - **Transaction size**: Estimated final size including witness data
1273    /// - **Input signatures**: Size overhead for each required signature
1274    /// - **Mempool ancestry**: Fees and sizes of unconfirmed parent transactions
1275    /// - **Consolidation**: Extra size from UTXO consolidation (if enabled)
1276    ///
1277    /// ## Error Handling
1278    ///
1279    /// Returns an error if:
1280    /// - Insufficient funds to cover minimum fee requirements
1281    /// - Cannot reduce outputs enough to meet fee rate
1282    /// - Transaction would exceed size limits
1283    /// - Fee rate calculation fails
1284    ///
1285    /// ## Best Practices
1286    ///
1287    /// ```rust,no_run
1288    /// # use satellite_bitcoin_transactions::TransactionBuilder;
1289    /// # use satellite_bitcoin_transactions::fee_rate::FeeRate;
1290    /// # use bitcoin::ScriptBuf;
1291    /// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
1292    /// # let change_address = ScriptBuf::new();
1293    /// // Always validate fee rate after adjustment
1294    /// let fee_rate = FeeRate::try_from(15.0)?;
1295    /// builder.adjust_transaction_to_pay_fees(&fee_rate, Some(change_address))?;
1296    ///
1297    /// // Verify the final fee rate meets requirements
1298    /// builder.is_fee_rate_valid(&fee_rate)?;
1299    ///
1300    /// // Check final fee amount
1301    /// let final_fee = builder.get_fee_paid()?;
1302    /// println!("Final fee: {} sats", final_fee);
1303    /// # Ok::<(), Box<dyn std::error::Error>>(())
1304    /// ```
1305    ///
1306    /// ## See Also
1307    ///
1308    /// - [`Self::is_fee_rate_valid`] for validating the resulting fee rate
1309    /// - [`Self::get_fee_paid`] for checking the final fee amount
1310    /// - [`Self::get_fee_paid_by_user`] for user-specific fee calculation
1311    pub fn adjust_transaction_to_pay_fees(
1312        &mut self,
1313        fee_rate: &FeeRate,
1314        address_to_send_remaining_btc: Option<ScriptBuf>,
1315    ) -> Result<(), BitcoinTxError> {
1316        // This helper may create or adjust outputs; once called we are
1317        // definitively past the state-transition-only phase.
1318        self.has_seen_non_state_io_or_output = true;
1319
1320        adjust_transaction_to_pay_fees(
1321            &mut self.transaction,
1322            &self.tx_statuses,
1323            self.total_btc_input,
1324            address_to_send_remaining_btc,
1325            fee_rate,
1326        )
1327    }
1328
1329    /// Attempts to **sweep** pool-owned UTXOs marked for consolidation into the current
1330    /// transaction.
1331    ///
1332    /// This helper is only available when the `utxo-consolidation` feature is enabled. It acts as
1333    /// a thin wrapper around [`crate::consolidation::add_consolidation_utxos`], forwarding the
1334    /// relevant context from the builder and then updating the builder's running totals so that
1335    /// fee-calculation logic is aware of the extra inputs.
1336    ///
1337    /// The consolidated inputs are signed by `pool_pubkey`. Only UTXOs whose
1338    /// `needs_consolidation` value is **greater than or equal to** `fee_rate` are considered. The
1339    /// function stops adding inputs as soon as the draft transaction would exceed
1340    /// [`arch_program::MAX_BTC_TX_SIZE`].
1341    ///
1342    /// After execution the following builder fields are updated:
1343    /// * [`Self::total_btc_input`]
1344    /// * [`Self::total_btc_consolidation_input`]
1345    /// * [`Self::extra_tx_size_for_consolidation`]
1346    ///
1347    /// # Parameters
1348    /// * `pool_pubkey` – public key of the liquidity-pool program (signer of consolidation inputs).
1349    /// * `fee_rate` – current mempool fee-rate used to decide which UTXOs are worth consolidating.
1350    /// * `pool_shard_btc_utxos` – slice with the candidate pool UTXOs.
1351    /// * `new_potential_inputs_and_outputs` – hypothetical inputs/outputs the caller *may* add
1352    ///    later; needed to keep size estimations accurate.
1353    #[cfg(feature = "utxo-consolidation")]
1354    pub fn add_consolidation_utxos<BtcHolder: BtcUtxoHolder>(
1355        &mut self,
1356        pool_pubkey: &Pubkey,
1357        fee_rate: &FeeRate,
1358        pool_shard_btc_utxos: &[BtcHolder],
1359        new_potential_inputs_and_outputs: &NewPotentialInputsAndOutputs,
1360    ) {
1361        // Consolidation always introduces additional non-state inputs.
1362        self.has_seen_non_state_io_or_output = true;
1363
1364        let (total_consolidation_input_amount, extra_tx_size) = add_consolidation_utxos(
1365            &mut self.transaction,
1366            &mut self.tx_statuses,
1367            &mut self.inputs_to_sign,
1368            pool_pubkey,
1369            pool_shard_btc_utxos,
1370            fee_rate,
1371            new_potential_inputs_and_outputs,
1372            ARCH_INPUT_SIZE,
1373        );
1374
1375        self.total_btc_input += total_consolidation_input_amount;
1376        self.set_consolidation_tracking(total_consolidation_input_amount, extra_tx_size);
1377    }
1378
1379    #[cfg(feature = "utxo-consolidation")]
1380    pub fn get_fee_paid_by_program(&self, fee_rate: &FeeRate) -> u64 {
1381        fee_rate.fee(self.extra_tx_size_for_consolidation).to_sat()
1382    }
1383
1384    pub fn get_fee_paid_by_user(&mut self, fee_rate: &FeeRate) -> Result<u64, BitcoinTxError> {
1385        let tx_size = self.estimate_final_tx_vsize()?;
1386
1387        let tx_size_to_be_paid_by_user = {
1388            #[cfg(feature = "utxo-consolidation")]
1389            {
1390                tx_size - self.extra_tx_size_for_consolidation
1391            }
1392            #[cfg(not(feature = "utxo-consolidation"))]
1393            {
1394                tx_size
1395            }
1396        };
1397
1398        Ok(fee_rate.fee(tx_size_to_be_paid_by_user).to_sat())
1399    }
1400
1401    pub fn estimate_final_tx_vsize(&mut self) -> Result<usize, BitcoinTxError> {
1402        estimate_final_tx_vsize(&mut self.transaction)
1403    }
1404
1405    /// Returns the *weight* (in bytes) the transaction would have **if** the draft
1406    /// `new_potential_inputs_and_outputs` were added.
1407    ///
1408    /// Helpful during fee-bumping logic when you need to know "how much bigger will the TX get
1409    /// if I add N more inputs/outputs?".
1410    pub fn estimate_tx_size_with_additional_inputs_outputs(
1411        &mut self,
1412        new_potential_inputs_and_outputs: &NewPotentialInputsAndOutputs,
1413    ) -> Result<usize, BitcoinTxError> {
1414        Ok(estimate_tx_size_with_additional_inputs_outputs(
1415            &mut self.transaction,
1416            &self.inputs_to_sign,
1417            new_potential_inputs_and_outputs,
1418        )?)
1419    }
1420
1421    /// Same as [`Self::estimate_tx_size_with_additional_inputs_outputs`] but returns **vsize**
1422    /// instead of raw size.
1423    pub fn estimate_tx_vsize_with_additional_inputs_outputs(
1424        &mut self,
1425        new_potential_inputs_and_outputs: &NewPotentialInputsAndOutputs,
1426    ) -> Result<usize, BitcoinTxError> {
1427        Ok(estimate_tx_vsize_with_additional_inputs_outputs(
1428            &mut self.transaction,
1429            &mut self.inputs_to_sign,
1430            new_potential_inputs_and_outputs,
1431        )?)
1432    }
1433
1434    /// Returns the **aggregate mempool size (bytes) and fees (sats)** of all ancestor
1435    /// transactions referenced by *pending* inputs.
1436    pub fn get_ancestors_totals(&self) -> Result<(usize, u64), BitcoinTxError> {
1437        Ok((
1438            self.tx_statuses.total_size as usize,
1439            self.tx_statuses.total_fee,
1440        ))
1441    }
1442
1443    /// Calculates the fee currently paid by the partially-built transaction (`inputs − outputs`).
1444    ///
1445    /// Fails with [`BitcoinTxError::InsufficientInputAmount`] if outputs exceed inputs.
1446    pub fn get_fee_paid(&self) -> Result<u64, BitcoinTxError> {
1447        let output_amount = self
1448            .transaction
1449            .output
1450            .iter()
1451            .map(|output| output.value.to_sat())
1452            .sum::<u64>();
1453
1454        let fee_paid = self
1455            .total_btc_input
1456            .checked_sub(output_amount)
1457            .ok_or(BitcoinTxError::InsufficientInputAmount)?;
1458
1459        Ok(fee_paid)
1460    }
1461
1462    /// Checks that the *effective* fee-rate (including ancestors) is at least `fee_rate`.
1463    ///
1464    /// Returns an error when the calculated rate is below the target.
1465    pub fn is_fee_rate_valid(&mut self, fee_rate: &FeeRate) -> Result<(), BitcoinTxError> {
1466        // Transaction by itself should have a valid fee
1467        let fee_paid = self.get_fee_paid()?;
1468        let tx_size = self.estimate_final_tx_vsize()?;
1469
1470        let real_fee_rate = FeeRate::try_from(fee_paid as f64 / tx_size as f64)
1471            .map_err(|_| BitcoinTxError::InvalidFeeRateTooLow)?;
1472
1473        if real_fee_rate.n() < fee_rate.n() {
1474            return Err(BitcoinTxError::InvalidFeeRateTooLow);
1475        }
1476
1477        // But also with ancestors.
1478        let (total_size_of_pending_utxos, total_fee_of_pending_utxos) =
1479            self.get_ancestors_totals()?;
1480
1481        let fee_paid_with_ancestors = fee_paid
1482            .checked_add(total_fee_of_pending_utxos)
1483            .ok_or(BitcoinTxError::InsufficientInputAmount)?;
1484
1485        let tx_size_with_ancestors = tx_size + total_size_of_pending_utxos;
1486
1487        let real_fee_rate_with_ancestors =
1488            FeeRate::try_from(fee_paid_with_ancestors as f64 / tx_size_with_ancestors as f64)
1489                .map_err(|_| BitcoinTxError::InvalidFeeRateTooLow)?;
1490
1491        if real_fee_rate_with_ancestors.n() < fee_rate.n() {
1492            return Err(BitcoinTxError::InvalidFeeRateTooLow);
1493        }
1494
1495        Ok(())
1496    }
1497
1498    /// Returns a slice of transaction inputs that are not state transitions.
1499    ///
1500    /// State transitions are always at the beginning of the transaction and correspond
1501    /// to entries in `modified_accounts`. This method returns all inputs after the
1502    /// state transition inputs.
1503    ///
1504    /// ## Returns
1505    ///
1506    /// A slice of [`TxIn`] containing only non-state-transition inputs. Returns an
1507    /// empty slice if all inputs are state transitions or if there are no inputs.
1508    ///
1509    /// ## Examples
1510    ///
1511    /// ```rust,no_run
1512    /// # use satellite_bitcoin_transactions::TransactionBuilder;
1513    /// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
1514    /// // After adding state transitions and regular inputs...
1515    /// let non_state_transition_inputs = builder.get_non_state_transition_inputs();
1516    ///
1517    /// // Process only non-state-transition inputs
1518    /// for input in non_state_transition_inputs {
1519    ///     // Handle regular UTXO inputs
1520    /// }
1521    /// ```
1522    pub fn get_non_state_transition_inputs(&self) -> &[TxIn] {
1523        let state_transition_count = self.modified_accounts.len();
1524        &self.transaction.input[state_transition_count..]
1525    }
1526
1527    /// Finalizes the transaction and prepares it for signing by the Arch runtime.
1528    ///
1529    /// This method completes the transaction building process by transferring the constructed
1530    /// transaction and all associated metadata to the Arch runtime. Once called, the transaction
1531    /// is ready for the signature collection phase.
1532    ///
1533    /// ## What it does
1534    ///
1535    /// The method performs these final steps:
1536    /// 1. **Transfers ownership**: Passes the transaction to the Arch runtime
1537    /// 2. **Provides metadata**: Includes modified accounts and signing requirements
1538    /// 3. **Enables signing**: Makes the transaction available for signature collection
1539    /// 4. **Prepares broadcast**: Sets up the transaction for network submission
1540    ///
1541    /// ## Important Notes
1542    ///
1543    /// - **No further changes**: After calling `finalize()`, the builder should not be modified
1544    /// - **Not broadcasting**: This method does NOT broadcast the transaction to the network
1545    /// - **Signing phase**: The transaction enters the signing phase, handled by Arch runtime
1546    /// - **State consistency**: All modified accounts and inputs must be properly configured
1547    ///
1548    /// ## Prerequisites
1549    ///
1550    /// Before calling `finalize()`, ensure:
1551    /// - All required inputs have been added
1552    /// - All outputs have been configured
1553    /// - Fees have been adjusted with [`Self::adjust_transaction_to_pay_fees`]
1554    /// - Fee rate has been validated with [`Self::is_fee_rate_valid`]
1555    ///
1556    /// ## Transaction Lifecycle
1557    ///
1558    /// ```text
1559    /// 1. TransactionBuilder::new()          ← Create builder
1560    /// 2. Add inputs/outputs                 ← Populate transaction
1561    /// 3. adjust_transaction_to_pay_fees()   ← Set correct fees
1562    /// 4. finalize()                         ← Prepare for signing
1563    /// 5. [Arch runtime signs]              ← Automatic signing
1564    /// 6. [Arch runtime broadcasts]         ← Network submission
1565    /// ```
1566    ///
1567    /// ## Examples
1568    ///
1569    /// ```rust,no_run
1570    /// # use satellite_bitcoin_transactions::TransactionBuilder;
1571    /// # use satellite_bitcoin_transactions::fee_rate::FeeRate;
1572    /// # use bitcoin::ScriptBuf;
1573    /// # let mut builder: TransactionBuilder<8, 4, satellite_bitcoin_transactions::utxo_info::SingleRuneSet> = TransactionBuilder::new();
1574    /// // After building your transaction...
1575    ///
1576    /// // 1. Adjust fees
1577    /// let fee_rate = FeeRate::try_from(20.0)?;
1578    /// let change_address = ScriptBuf::new();
1579    /// builder.adjust_transaction_to_pay_fees(&fee_rate, Some(change_address))?;
1580    ///
1581    /// // 2. Validate fee rate
1582    /// builder.is_fee_rate_valid(&fee_rate)?;
1583    ///
1584    /// // 3. Finalize and hand over to Arch
1585    /// builder.finalize()?;
1586    ///
1587    /// // Transaction is now ready for signing and broadcast
1588    /// # Ok::<(), Box<dyn std::error::Error>>(())
1589    /// ```
1590    ///
1591    /// ## Error Handling
1592    ///
1593    /// Returns [`ProgramError`] if:
1594    /// - The transaction data is invalid
1595    /// - Required metadata is missing
1596    /// - The Arch runtime cannot accept the transaction
1597    /// - Internal state is inconsistent
1598    ///
1599    /// ## See Also
1600    ///
1601    /// - [`Self::adjust_transaction_to_pay_fees`] for fee adjustment
1602    /// - [`Self::is_fee_rate_valid`] for fee validation
1603    /// - [`arch_program::program::set_transaction_to_sign`] for the underlying mechanism
1604    pub fn finalize(&mut self) -> Result<(), ProgramError> {
1605        set_transaction_to_sign(
1606            self.modified_accounts.as_slice(),
1607            &self.transaction,
1608            self.inputs_to_sign.as_slice(),
1609        )?;
1610
1611        Ok(())
1612    }
1613
1614    fn add_tx_status<RS>(&mut self, utxo: &UtxoInfo<RS>, status: &TxStatus)
1615    where
1616        RS: FixedCapacitySet<Item = RuneAmount>,
1617    {
1618        // Check if we have not added this txid yet.
1619        for input in &self.transaction.input {
1620            let input_txid = txid_to_bytes_big_endian(&input.previous_output.txid);
1621            if input_txid == utxo.meta.txid_big_endian() {
1622                return;
1623            }
1624        }
1625
1626        match status {
1627            TxStatus::Pending(info) => {
1628                self.tx_statuses.total_fee += info.total_fee;
1629                self.tx_statuses.total_size += info.total_size;
1630            }
1631            TxStatus::Confirmed => {}
1632        }
1633    }
1634
1635    #[cfg(feature = "runes")]
1636    fn add_rune_input(&mut self, rune: RuneAmount) -> Result<(), BitcoinTxError> {
1637        add_rune_input(&mut self.total_rune_inputs, rune)?;
1638
1639        Ok(())
1640    }
1641}
1642
1643pub fn add_rune_input<RuneSet: FixedCapacitySet<Item = RuneAmount> + Default>(
1644    total_rune_inputs: &mut RuneSet,
1645    rune: RuneAmount,
1646) -> Result<(), BitcoinTxError> {
1647    total_rune_inputs.insert_or_modify::<BitcoinTxError, _>(rune, |rune_input| {
1648        rune_input.amount = rune_input
1649            .amount
1650            .checked_add(rune.amount)
1651            .ok_or(BitcoinTxError::RuneAdditionOverflow)?;
1652        Ok(())
1653    })?;
1654
1655    Ok(())
1656}
1657
1658#[cfg(test)]
1659mod tests {
1660    use crate::utxo_info::UtxoInfoTrait;
1661
1662    #[cfg(feature = "utxo-consolidation")]
1663    use crate::utxo_info::FixedOptionF64;
1664
1665    use super::*;
1666    use crate::utxo_info::SingleRuneSet;
1667    use arch_program::rune::{RuneAmount, RuneId};
1668    use arch_program::utxo::UtxoMeta;
1669    use bitcoin::{Amount, TxOut};
1670
1671    #[allow(unused_macros)]
1672    macro_rules! new_tb {
1673        ($max_mod:expr, $max_inputs:expr) => {{
1674            // Always specify the `SingleRuneSet` type parameter so the invocation
1675            // matches the `TransactionBuilder` definition regardless of whether the
1676            // `runes` feature is enabled.
1677            TransactionBuilder::<$max_mod, $max_inputs, SingleRuneSet>::new()
1678        }};
1679    }
1680
1681    // Helper function to create a mock UtxoInfo
1682    fn create_mock_utxo(value: u64, txid: [u8; 32], vout: u32) -> UtxoInfo<SingleRuneSet> {
1683        UtxoInfo::new(UtxoMeta::from(txid, vout), value)
1684    }
1685
1686    // Helper function to create a mock UtxoInfo with runes
1687    fn create_mock_utxo_with_runes(
1688        value: u64,
1689        txid: [u8; 32],
1690        vout: u32,
1691        rune_amount: u128,
1692    ) -> UtxoInfo<SingleRuneSet> {
1693        let runes = {
1694            #[cfg(feature = "runes")]
1695            {
1696                let mut runes = SingleRuneSet::default();
1697                runes
1698                    .insert(RuneAmount {
1699                        id: RuneId::new(1, 1),
1700                        amount: rune_amount,
1701                    })
1702                    .unwrap();
1703
1704                runes
1705            }
1706            #[cfg(not(feature = "runes"))]
1707            {
1708                SingleRuneSet::default()
1709            }
1710        };
1711
1712        let mut utxo = UtxoInfo::new(UtxoMeta::from(txid, vout), value);
1713
1714        #[cfg(feature = "runes")]
1715        {
1716            utxo.runes = runes;
1717        }
1718
1719        utxo
1720    }
1721
1722    mod new {
1723        use super::*;
1724
1725        #[test]
1726        fn creates_empty_transaction_builder() {
1727            let builder = new_tb!(0, 0);
1728
1729            assert_eq!(builder.transaction.version, Version::TWO);
1730            assert_eq!(builder.transaction.lock_time, LockTime::ZERO);
1731            assert_eq!(builder.transaction.input.len(), 0);
1732            assert_eq!(builder.transaction.output.len(), 0);
1733            assert_eq!(builder.total_btc_input, 0);
1734            #[cfg(feature = "runes")]
1735            assert_eq!(builder.total_rune_inputs.len(), 0);
1736            #[cfg(feature = "utxo-consolidation")]
1737            assert_eq!(builder.total_btc_consolidation_input, 0);
1738            #[cfg(feature = "utxo-consolidation")]
1739            assert_eq!(builder.extra_tx_size_for_consolidation, 0);
1740            assert_eq!(builder.modified_accounts.len(), 0);
1741            assert_eq!(builder.inputs_to_sign.len(), 0);
1742        }
1743    }
1744
1745    mod new_with_transaction {
1746        use super::*;
1747
1748        #[test]
1749        fn new_with_transaction_successfully() {
1750            // Create a transaction without inputs to avoid UTXO lookup issues
1751            let tx_output = TxOut {
1752                value: Amount::from_sat(50000),
1753                script_pubkey: ScriptBuf::new(),
1754            };
1755
1756            let transaction = Transaction {
1757                version: Version::ONE,
1758                lock_time: LockTime::ZERO,
1759                input: vec![TxIn {
1760                    previous_output: OutPoint::from_str(
1761                        "1111111111111111111111111111111111111111111111111111111111111111:0",
1762                    )
1763                    .unwrap(),
1764                    script_sig: ScriptBuf::new(),
1765                    sequence: Sequence::MAX,
1766                    witness: Witness::new(),
1767                }], // Empty inputs to avoid lookup
1768                output: vec![tx_output],
1769            };
1770
1771            let utxo_metas = transaction
1772                .input
1773                .iter()
1774                .map(|input| {
1775                    UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout)
1776                })
1777                .collect::<Vec<_>>();
1778
1779            // Prepare mock mempool data reflecting a pending UTXO with specific fee/size
1780            let user_utxos = vec![create_mock_utxo_with_runes(
1781                25000,
1782                utxo_metas[0].txid_big_endian(),
1783                utxo_metas[0].vout(),
1784                1000,
1785            )];
1786
1787            let mempool_data = {
1788                let mut utxo_mempool_info = [None; 10];
1789                utxo_mempool_info[0] = Some((
1790                    utxo_metas[0].txid_big_endian(),
1791                    MempoolInfo {
1792                        total_fee: 1000,
1793                        total_size: 250,
1794                    },
1795                ));
1796
1797                mempool::MempoolData::<10, 10>::new(
1798                    utxo_mempool_info,
1799                    std::array::from_fn(|_| mempool::AccountMempoolInfo::default()),
1800                )
1801            };
1802
1803            // Build the transaction builder directly from an existing transaction.
1804            let builder = TransactionBuilder::<10, 10, SingleRuneSet>::new_with_transaction(
1805                transaction.clone(),
1806                &mempool_data,
1807                &user_utxos,
1808            )
1809            .expect("Failed to create builder from transaction");
1810
1811            // The builder should now reflect the data derived from `transaction`.
1812            assert_eq!(builder.transaction.version, Version::ONE);
1813            assert_eq!(builder.transaction.input.len(), 1);
1814            assert_eq!(builder.transaction.output.len(), 1);
1815            assert_eq!(builder.total_btc_input, 25000);
1816            #[cfg(feature = "runes")]
1817            assert_eq!(builder.total_rune_inputs.len(), 1);
1818            #[cfg(feature = "runes")]
1819            assert_eq!(
1820                builder.total_rune_inputs.find(&RuneAmount {
1821                    id: RuneId::new(1, 1),
1822                    amount: 1000,
1823                }),
1824                Some(&RuneAmount {
1825                    id: RuneId::new(1, 1),
1826                    amount: 1000,
1827                })
1828            );
1829            assert_eq!(builder.tx_statuses.total_fee, 1000);
1830            assert_eq!(builder.tx_statuses.total_size, 250);
1831        }
1832
1833        #[cfg(feature = "runes")]
1834        #[test]
1835        fn calculates_rune_input_correctly() {
1836            let transaction =
1837                Transaction {
1838                    version: Version::TWO,
1839                    lock_time: LockTime::ZERO,
1840                    input: vec![TxIn {
1841                    previous_output: OutPoint::from_str(
1842                        "1111111111111111111111111111111111111111111111111111111111111111:0",
1843                    )
1844                    .unwrap(),
1845                    script_sig: ScriptBuf::new(),
1846                    sequence: Sequence::MAX,
1847                    witness: Witness::new(),
1848                },
1849                TxIn {
1850                    previous_output: OutPoint::from_str(
1851                        "2222222222222222222222222222222222222222222222222222222222222222:1",
1852                    )
1853                    .unwrap(),
1854                    script_sig: ScriptBuf::new(),
1855                    sequence: Sequence::MAX,
1856                    witness: Witness::new(),
1857                },
1858                TxIn {
1859                    previous_output: OutPoint::from_str(
1860                        "3333333333333333333333333333333333333333333333333333333333333333:2",
1861                    )
1862                    .unwrap(),
1863                    script_sig: ScriptBuf::new(),
1864                    sequence: Sequence::MAX,
1865                    witness: Witness::new(),
1866                }],
1867                    output: vec![],
1868                };
1869
1870            let utxo_metas = transaction
1871                .input
1872                .iter()
1873                .map(|input| {
1874                    UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout)
1875                })
1876                .collect::<Vec<_>>();
1877
1878            // Test with multiple rune UTXOs but no pending mempool data required
1879            let user_utxos = vec![
1880                create_mock_utxo_with_runes(
1881                    10000,
1882                    utxo_metas[0].txid_big_endian(),
1883                    utxo_metas[0].vout(),
1884                    500,
1885                ),
1886                create_mock_utxo_with_runes(
1887                    20000,
1888                    utxo_metas[1].txid_big_endian(),
1889                    utxo_metas[1].vout(),
1890                    750,
1891                ),
1892                create_mock_utxo(30000, utxo_metas[2].txid_big_endian(), utxo_metas[2].vout()), // No runes
1893            ];
1894
1895            let mempool_data = mempool::MempoolData::<10, 10>::default();
1896
1897            let builder = TransactionBuilder::<10, 10, SingleRuneSet>::new_with_transaction(
1898                transaction,
1899                &mempool_data,
1900                &user_utxos,
1901            )
1902            .expect("Failed to build transaction");
1903
1904            assert_eq!(builder.total_rune_inputs.len(), 1);
1905            assert_eq!(
1906                builder.total_rune_inputs.find(&RuneAmount {
1907                    id: RuneId::new(1, 1),
1908                    amount: 1000,
1909                }),
1910                Some(&RuneAmount {
1911                    id: RuneId::new(1, 1),
1912                    amount: 1000,
1913                })
1914            );
1915        }
1916    }
1917
1918    mod get_fee_paid {
1919        use bitcoin::Amount;
1920
1921        use super::*;
1922
1923        #[test]
1924        fn calculates_fee_paid_correctly() {
1925            let mut builder = new_tb!(10, 10);
1926
1927            // Set total BTC input directly for this test
1928            builder.total_btc_input = 100000;
1929
1930            // Add output
1931            builder.transaction.output.push(TxOut {
1932                value: Amount::from_sat(95000),
1933                script_pubkey: ScriptBuf::new(),
1934            });
1935
1936            let fee_paid = builder.get_fee_paid().unwrap();
1937            assert_eq!(fee_paid, 5000); // 100000 - 95000
1938        }
1939
1940        #[test]
1941        fn returns_error_when_insufficient_input() {
1942            let mut builder = new_tb!(10, 10);
1943
1944            // Add output but no input
1945            builder.transaction.output.push(TxOut {
1946                value: Amount::from_sat(50000),
1947                script_pubkey: ScriptBuf::new(),
1948            });
1949
1950            let result = builder.get_fee_paid();
1951            assert!(result.is_err());
1952            assert_eq!(result.unwrap_err(), BitcoinTxError::InsufficientInputAmount);
1953        }
1954    }
1955
1956    mod get_ancestors_totals {
1957        use super::*;
1958
1959        #[test]
1960        fn returns_correct_ancestors_totals() {
1961            let mut builder = new_tb!(10, 10);
1962            builder.tx_statuses = MempoolInfo {
1963                total_fee: 1500,
1964                total_size: 300,
1965            };
1966
1967            let (total_size, total_fee) = builder.get_ancestors_totals().unwrap();
1968            assert_eq!(total_size, 300);
1969            assert_eq!(total_fee, 1500);
1970        }
1971    }
1972
1973    mod is_fee_rate_valid {
1974        use super::*;
1975
1976        #[test]
1977        fn validates_fee_rate_correctly() {
1978            let mut builder = new_tb!(10, 10);
1979
1980            // Set inputs and outputs manually for controlled test
1981            builder.total_btc_input = 100000;
1982
1983            // Add output with fee of 10000 sats
1984            builder.transaction.output.push(TxOut {
1985                value: Amount::from_sat(90000),
1986                script_pubkey: ScriptBuf::new(),
1987            });
1988
1989            // Assume transaction size is about 200 bytes, so fee rate is 50 sat/vB
1990            let fee_rate = FeeRate::try_from(30.0).unwrap(); // 30 sat/vB
1991            let result = builder.is_fee_rate_valid(&fee_rate);
1992
1993            // This should pass as our effective fee rate (50) is higher than required (30)
1994            assert!(result.is_ok());
1995        }
1996
1997        #[test]
1998        fn rejects_insufficient_fee_rate() {
1999            let mut builder = new_tb!(10, 10);
2000
2001            // Set inputs and outputs manually
2002            builder.total_btc_input = 100000;
2003
2004            // Add output with very low fee
2005            builder.transaction.output.push(TxOut {
2006                value: Amount::from_sat(99900),
2007                script_pubkey: ScriptBuf::new(),
2008            });
2009
2010            // Require high fee rate
2011            let fee_rate = FeeRate::try_from(100.0).unwrap(); // 100 sat/vB
2012            let result = builder.is_fee_rate_valid(&fee_rate);
2013
2014            assert!(result.is_err());
2015            assert_eq!(result.unwrap_err(), BitcoinTxError::InvalidFeeRateTooLow);
2016        }
2017    }
2018
2019    mod tx_status_handling {
2020        use super::*;
2021
2022        #[test]
2023        fn handles_confirmed_tx_status() {
2024            let mut builder = new_tb!(10, 10);
2025            let utxo = create_mock_utxo(50000, [1u8; 32], 0);
2026            let status = TxStatus::Confirmed;
2027
2028            // Manually test the add_tx_status logic
2029            builder.add_tx_status(&utxo, &status);
2030
2031            assert_eq!(builder.tx_statuses.total_fee, 0);
2032            assert_eq!(builder.tx_statuses.total_size, 0);
2033        }
2034
2035        #[test]
2036        fn handles_pending_tx_status() {
2037            let mut builder = new_tb!(10, 10);
2038            let utxo = create_mock_utxo(50000, [1u8; 32], 0);
2039            let pending_info = MempoolInfo {
2040                total_fee: 2000,
2041                total_size: 250,
2042            };
2043            let status = TxStatus::Pending(pending_info);
2044
2045            // Manually test the add_tx_status logic
2046            builder.add_tx_status(&utxo, &status);
2047
2048            assert_eq!(builder.tx_statuses.total_fee, 2000);
2049            assert_eq!(builder.tx_statuses.total_size, 250);
2050        }
2051    }
2052
2053    mod modified_account {
2054        use super::*;
2055
2056        #[test]
2057        fn modified_account_new_works() {
2058            // This test would require a mock AccountInfo which is complex to create
2059            // Skipping for now since we tested the core functionality elsewhere
2060        }
2061
2062        #[test]
2063        fn modified_account_default_is_none() {
2064            let modified = ModifiedAccount::default();
2065            assert!(modified.0.is_none());
2066        }
2067
2068        #[test]
2069        #[should_panic(expected = "ModifiedAccount is None")]
2070        fn modified_account_as_ref_panics_when_none() {
2071            let modified = ModifiedAccount::default();
2072            let _ = modified.as_ref();
2073        }
2074    }
2075
2076    mod estimate_final_tx_vsize {
2077        use super::*;
2078        use arch_program::input_to_sign::InputToSign;
2079        use arch_program::pubkey::Pubkey;
2080
2081        #[test]
2082        fn estimates_empty_transaction_size() {
2083            let mut builder = new_tb!(10, 10);
2084
2085            let vsize = builder.estimate_final_tx_vsize().unwrap();
2086
2087            // Empty transaction should have minimal size
2088            assert!(vsize > 0);
2089            assert!(vsize < 100); // Should be quite small
2090        }
2091
2092        #[test]
2093        fn estimates_transaction_size_with_inputs_to_sign() {
2094            let mut builder = new_tb!(10, 10);
2095
2096            // Add some mock inputs to sign
2097            let pubkey = Pubkey::system_program();
2098            builder
2099                .inputs_to_sign
2100                .push(InputToSign {
2101                    index: 0,
2102                    signer: pubkey,
2103                })
2104                .unwrap();
2105
2106            builder
2107                .inputs_to_sign
2108                .push(InputToSign {
2109                    index: 1,
2110                    signer: pubkey,
2111                })
2112                .unwrap();
2113
2114            // Add some transaction inputs
2115            builder.transaction.input.push(TxIn {
2116                previous_output: OutPoint::null(),
2117                script_sig: ScriptBuf::new(),
2118                sequence: Sequence::MAX,
2119                witness: Witness::new(),
2120            });
2121            builder.transaction.input.push(TxIn {
2122                previous_output: OutPoint::null(),
2123                script_sig: ScriptBuf::new(),
2124                sequence: Sequence::MAX,
2125                witness: Witness::new(),
2126            });
2127
2128            let vsize = builder.estimate_final_tx_vsize().unwrap();
2129
2130            // Should be larger than empty transaction due to witness overhead
2131            assert!(vsize > 100);
2132        }
2133    }
2134
2135    #[cfg(feature = "utxo-consolidation")]
2136    mod get_fee_paid_by_program {
2137        use super::*;
2138
2139        #[test]
2140        fn calculates_consolidation_fee_correctly() {
2141            let mut builder = new_tb!(10, 10);
2142
2143            // Set consolidation values
2144            builder.extra_tx_size_for_consolidation = 500; // 500 bytes
2145
2146            let fee_rate = FeeRate::try_from(10.0).unwrap(); // 10 sat/vB
2147            let fee = builder.get_fee_paid_by_program(&fee_rate);
2148
2149            assert_eq!(fee, 5000); // 500 bytes * 10 sat/vB = 5000 sats
2150        }
2151
2152        #[test]
2153        fn returns_zero_when_no_consolidation() {
2154            let builder = new_tb!(10, 10);
2155
2156            let fee_rate = FeeRate::try_from(50.0).unwrap();
2157            let fee = builder.get_fee_paid_by_program(&fee_rate);
2158
2159            assert_eq!(fee, 0);
2160        }
2161    }
2162
2163    mod input_index_management {
2164        use super::*;
2165        use arch_program::input_to_sign::InputToSign;
2166        use arch_program::pubkey::Pubkey;
2167
2168        #[test]
2169        fn updates_indices_correctly_when_inserting() {
2170            let mut builder = new_tb!(10, 10);
2171            let pubkey = Pubkey::system_program();
2172
2173            // Add initial inputs to sign
2174            builder
2175                .inputs_to_sign
2176                .push(InputToSign {
2177                    index: 0,
2178                    signer: pubkey,
2179                })
2180                .unwrap();
2181            builder
2182                .inputs_to_sign
2183                .push(InputToSign {
2184                    index: 1,
2185                    signer: pubkey,
2186                })
2187                .unwrap();
2188            builder
2189                .inputs_to_sign
2190                .push(InputToSign {
2191                    index: 2,
2192                    signer: pubkey,
2193                })
2194                .unwrap();
2195
2196            // Manually call the index update logic (simulate insertion at index 1)
2197            let insert_index = 1u32;
2198            for input in builder.inputs_to_sign.iter_mut() {
2199                if input.index >= insert_index {
2200                    input.index += 1;
2201                }
2202            }
2203
2204            // Check that indices were updated correctly
2205            let slice = builder.inputs_to_sign.as_slice();
2206            assert_eq!(slice[0].index, 0); // Should remain 0
2207            assert_eq!(slice[1].index, 2); // Should be incremented from 1 to 2
2208            assert_eq!(slice[2].index, 3); // Should be incremented from 2 to 3
2209        }
2210
2211        #[test]
2212        fn handles_multiple_insertions() {
2213            let mut builder = new_tb!(10, 10);
2214            let pubkey = Pubkey::system_program();
2215
2216            // Add inputs to sign
2217            for i in 0..5 {
2218                builder
2219                    .inputs_to_sign
2220                    .push(InputToSign {
2221                        index: i,
2222                        signer: pubkey,
2223                    })
2224                    .unwrap();
2225            }
2226
2227            // Simulate multiple insertions
2228            // Insert at index 2 - indices 2,3,4 become 3,4,5
2229            for input in builder.inputs_to_sign.iter_mut() {
2230                if input.index >= 2 {
2231                    input.index += 1;
2232                }
2233            }
2234
2235            // Insert at index 1 - indices 1,3,4,5 become 2,4,5,6
2236            for input in builder.inputs_to_sign.iter_mut() {
2237                if input.index >= 1 {
2238                    input.index += 1;
2239                }
2240            }
2241
2242            // Check final indices - let's trace through what actually happens:
2243            // Original: 0,1,2,3,4
2244            // After first insertion at 2: 0,1,3,4,5
2245            // After second insertion at 1: 0,2,4,5,6
2246            let slice = builder.inputs_to_sign.as_slice();
2247            assert_eq!(slice[0].index, 0); // Should remain 0
2248            assert_eq!(slice[1].index, 2); // 1 -> 2
2249            assert_eq!(slice[2].index, 4); // 2 -> 3 -> 4
2250            assert_eq!(slice[3].index, 5); // 3 -> 4 -> 5
2251            assert_eq!(slice[4].index, 6); // 4 -> 5 -> 6
2252        }
2253    }
2254
2255    mod modified_accounts_tracking {
2256        use super::*;
2257
2258        #[test]
2259        fn tracks_modified_accounts_correctly() {
2260            let builder = new_tb!(10, 10);
2261
2262            // Test that we start with empty modified accounts
2263            assert_eq!(builder.modified_accounts.len(), 0);
2264
2265            // Test that the list is initially empty
2266            assert!(builder.modified_accounts.is_empty());
2267        }
2268
2269        #[test]
2270        fn respects_max_modified_accounts_limit() {
2271            let builder = new_tb!(10, 10);
2272
2273            // Test that we can't exceed MAX_MODIFIED_ACCOUNTS
2274            assert_eq!(builder.modified_accounts.len(), 0);
2275            // Note: FixedList doesn't have a capacity() method, but we can test max length through other means
2276        }
2277    }
2278
2279    mod boundary_conditions {
2280        use super::*;
2281
2282        #[test]
2283        fn handles_max_inputs_to_sign() {
2284            let builder = new_tb!(10, 10);
2285
2286            // Test that the list starts empty and can hold items
2287            assert_eq!(builder.inputs_to_sign.len(), 0);
2288        }
2289
2290        #[test]
2291        fn handles_large_btc_amounts() {
2292            let mut builder = new_tb!(10, 10);
2293
2294            // Test with large BTC amounts (but not MAX to avoid overflow in calculations)
2295            builder.total_btc_input = 21_000_000 * 100_000_000; // 21M BTC in satoshis
2296
2297            assert_eq!(builder.total_btc_input, 21_000_000 * 100_000_000);
2298        }
2299    }
2300
2301    mod fee_rate_validation_edge_cases {
2302        use super::*;
2303
2304        #[test]
2305        fn handles_zero_fee_rate() {
2306            let mut builder = new_tb!(10, 10);
2307
2308            builder.total_btc_input = 100000;
2309            builder.transaction.output.push(TxOut {
2310                value: Amount::from_sat(95000), // 5000 sat fee for a more reasonable rate
2311                script_pubkey: ScriptBuf::new(),
2312            });
2313
2314            // Low fee rate
2315            let fee_rate = FeeRate::try_from(1.0).unwrap(); // 1 sat/vB
2316            let result = builder.is_fee_rate_valid(&fee_rate);
2317
2318            // Should pass since we have sufficient fee
2319            assert!(result.is_ok());
2320        }
2321
2322        #[test]
2323        fn handles_ancestors_with_high_fees() {
2324            let mut builder = new_tb!(10, 10);
2325
2326            builder.total_btc_input = 100000;
2327            builder.transaction.output.push(TxOut {
2328                value: Amount::from_sat(95000),
2329                script_pubkey: ScriptBuf::new(),
2330            });
2331
2332            // Set high ancestor fees
2333            builder.tx_statuses = MempoolInfo {
2334                total_fee: 50000, // High ancestor fees
2335                total_size: 1000,
2336            };
2337
2338            let fee_rate = FeeRate::try_from(10.0).unwrap();
2339            let result = builder.is_fee_rate_valid(&fee_rate);
2340
2341            // Should pass due to high ancestor fees contributing to overall rate
2342            assert!(result.is_ok());
2343        }
2344
2345        #[test]
2346        fn handles_very_large_transactions() {
2347            let mut builder = new_tb!(10, 10);
2348
2349            // Create a large transaction with many inputs
2350            for i in 0..50 {
2351                builder.transaction.input.push(TxIn {
2352                    previous_output: OutPoint {
2353                        txid: bitcoin::Txid::from_str(&format!("{:064x}", i)).unwrap(),
2354                        vout: 0,
2355                    },
2356                    script_sig: ScriptBuf::new(),
2357                    sequence: Sequence::MAX,
2358                    witness: Witness::new(),
2359                });
2360            }
2361
2362            builder.total_btc_input = 5000000; // 5M sats
2363            builder.transaction.output.push(TxOut {
2364                value: Amount::from_sat(4950000), // 50k sats fee
2365                script_pubkey: ScriptBuf::new(),
2366            });
2367
2368            let fee_rate = FeeRate::try_from(10.0).unwrap();
2369            let result = builder.is_fee_rate_valid(&fee_rate);
2370
2371            // Should handle large transactions gracefully
2372            assert!(result.is_ok() || result.is_err()); // Just ensure it doesn't panic
2373        }
2374    }
2375
2376    #[cfg(feature = "utxo-consolidation")]
2377    mod consolidation_tests {
2378        use super::*;
2379
2380        #[test]
2381        fn tracks_consolidation_input_amounts() {
2382            let mut builder = new_tb!(10, 10);
2383
2384            // Manually set consolidation amounts (normally set by add_consolidation_utxos)
2385            builder.total_btc_consolidation_input = 250000;
2386
2387            assert_eq!(builder.total_btc_consolidation_input, 250000);
2388        }
2389
2390        #[test]
2391        fn tracks_extra_consolidation_size() {
2392            let mut builder = new_tb!(10, 10);
2393
2394            // Manually set extra tx size (normally set by add_consolidation_utxos)
2395            builder.extra_tx_size_for_consolidation = 1500;
2396
2397            assert_eq!(builder.extra_tx_size_for_consolidation, 1500);
2398        }
2399
2400        #[test]
2401        fn consolidation_fee_calculation_integration() {
2402            let mut builder = new_tb!(10, 10);
2403
2404            builder.extra_tx_size_for_consolidation = 800;
2405
2406            let fee_rate = FeeRate::try_from(25.0).unwrap(); // 25 sat/vB
2407            let fee = builder.get_fee_paid_by_program(&fee_rate);
2408
2409            assert_eq!(fee, 20000); // 800 * 25 = 20000 sats
2410        }
2411    }
2412
2413    mod transaction_structure {
2414        use super::*;
2415
2416        #[test]
2417        fn maintains_transaction_structure_integrity() {
2418            let mut builder = new_tb!(10, 10);
2419
2420            // Add inputs and outputs
2421            builder.transaction.input.push(TxIn {
2422                previous_output: OutPoint::null(),
2423                script_sig: ScriptBuf::new(),
2424                sequence: Sequence::MAX,
2425                witness: Witness::new(),
2426            });
2427
2428            builder.transaction.output.push(TxOut {
2429                value: Amount::from_sat(50000),
2430                script_pubkey: ScriptBuf::new(),
2431            });
2432
2433            // Verify structure
2434            assert_eq!(builder.transaction.input.len(), 1);
2435            assert_eq!(builder.transaction.output.len(), 1);
2436            assert_eq!(builder.transaction.version, Version::TWO);
2437            assert_eq!(builder.transaction.lock_time, LockTime::ZERO);
2438        }
2439
2440        #[test]
2441        fn handles_empty_transaction_gracefully() {
2442            let mut builder = new_tb!(10, 10);
2443
2444            // Empty transaction should be valid
2445            assert_eq!(builder.transaction.input.len(), 0);
2446            assert_eq!(builder.transaction.output.len(), 0);
2447
2448            // Should be able to estimate size even when empty
2449            let vsize = builder.estimate_final_tx_vsize().unwrap();
2450            assert!(vsize > 0);
2451        }
2452    }
2453
2454    mod error_handling {
2455        use super::*;
2456
2457        #[test]
2458        fn handles_fee_calculation_edge_cases() {
2459            let mut builder = new_tb!(10, 10);
2460
2461            // Test with zero input
2462            builder.total_btc_input = 0;
2463            builder.transaction.output.push(TxOut {
2464                value: Amount::from_sat(1000),
2465                script_pubkey: ScriptBuf::new(),
2466            });
2467
2468            let result = builder.get_fee_paid();
2469            assert!(result.is_err());
2470            assert_eq!(result.unwrap_err(), BitcoinTxError::InsufficientInputAmount);
2471        }
2472
2473        #[test]
2474        fn handles_ancestor_totals_correctly() {
2475            let mut builder = new_tb!(10, 10);
2476
2477            // Test with default (empty) mempool info
2478            let (size, fee) = builder.get_ancestors_totals().unwrap();
2479            assert_eq!(size, 0);
2480            assert_eq!(fee, 0);
2481
2482            // Test with some ancestor data
2483            builder.tx_statuses.total_fee = 5000;
2484            builder.tx_statuses.total_size = 500;
2485
2486            let (size, fee) = builder.get_ancestors_totals().unwrap();
2487            assert_eq!(size, 500);
2488            assert_eq!(fee, 5000);
2489        }
2490    }
2491
2492    mod find_btc {
2493        use super::*;
2494
2495        const PUBKEY: Pubkey = Pubkey([0; 32]);
2496
2497        #[test]
2498        fn finds_btc_with_one_utxo() {
2499            let utxos = vec![UtxoInfo::new(UtxoMeta::from([0; 32], 0), 10_000)];
2500
2501            let amount = 10_000;
2502
2503            let mut transaction_builder = new_tb!(10, 10);
2504
2505            let utxo_refs: Vec<&UtxoInfo<SingleRuneSet>> = utxos.iter().collect();
2506            let (found_utxo_indices, found_amount) = transaction_builder
2507                .find_btc_in_utxos(&utxo_refs, &PUBKEY, amount)
2508                .unwrap();
2509
2510            assert_eq!(found_utxo_indices.len(), 1, "Found a single UTXO");
2511            assert_eq!(found_amount, 10_000);
2512        }
2513
2514        #[test]
2515        fn finds_btc_with_multiple_utxos() {
2516            let utxos = vec![
2517                UtxoInfo::new(UtxoMeta::from([0; 32], 0), 5_000),
2518                UtxoInfo::new(UtxoMeta::from([0; 32], 1), 8_000),
2519                UtxoInfo::new(UtxoMeta::from([0; 32], 2), 12_000),
2520            ];
2521
2522            let amount = 10_000;
2523
2524            let mut transaction_builder = new_tb!(10, 10);
2525
2526            let utxo_refs: Vec<&UtxoInfo<SingleRuneSet>> = utxos.iter().collect();
2527            let (found_utxo_indices, found_amount) = transaction_builder
2528                .find_btc_in_utxos(&utxo_refs, &PUBKEY, amount)
2529                .unwrap();
2530
2531            assert_eq!(found_utxo_indices.len(), 1, "Found a single UTXO");
2532            assert_eq!(utxos[found_utxo_indices[0]].meta.vout(), 2);
2533            assert_eq!(found_amount, 12_000);
2534        }
2535
2536        #[test]
2537        #[cfg(feature = "utxo-consolidation")]
2538        fn finds_btc_with_consolidation_utxos() {
2539            let mut utxos = vec![
2540                UtxoInfo::new(UtxoMeta::from([0; 32], 0), 5_000),
2541                UtxoInfo::new(UtxoMeta::from([0; 32], 1), 8_000),
2542                UtxoInfo::new(UtxoMeta::from([0; 32], 2), 12_000),
2543            ];
2544
2545            *utxos[1].needs_consolidation_mut() = FixedOptionF64::some(1.0);
2546            *utxos[2].needs_consolidation_mut() = FixedOptionF64::some(1.0);
2547
2548            let amount = 10_000;
2549
2550            let mut transaction_builder = new_tb!(10, 10);
2551
2552            let utxo_refs: Vec<&UtxoInfo<SingleRuneSet>> = utxos.iter().collect();
2553            let (found_utxo_indices, found_amount) = transaction_builder
2554                .find_btc_in_utxos(&utxo_refs, &PUBKEY, amount)
2555                .unwrap();
2556
2557            assert_eq!(found_utxo_indices.len(), 2, "Found two UTXOs");
2558            assert_eq!(
2559                utxos[found_utxo_indices[0]].meta.vout(),
2560                0,
2561                "First UTXO matches"
2562            );
2563            assert_eq!(
2564                utxos[found_utxo_indices[1]].meta.vout(),
2565                2,
2566                "Second UTXO matches"
2567            );
2568            assert_eq!(found_amount, 17_000);
2569        }
2570    }
2571
2572    mod find_btc_from_holder {
2573        use super::*;
2574
2575        const PUBKEY: Pubkey = Pubkey([0; 32]);
2576
2577        struct TestHolder {
2578            utxos: Vec<UtxoInfo<SingleRuneSet>>,
2579        }
2580
2581        impl BtcUtxoHolder for TestHolder {
2582            fn btc_utxos(&self) -> &[UtxoInfo] {
2583                self.utxos.as_slice()
2584            }
2585        }
2586
2587        fn utxo(value: u64, vout: u32) -> UtxoInfo<SingleRuneSet> {
2588            UtxoInfo::new(UtxoMeta::from([0; 32], vout), value)
2589        }
2590
2591        #[cfg(feature = "utxo-consolidation")]
2592        fn utxo_cons(value: u64, vout: u32) -> UtxoInfo<SingleRuneSet> {
2593            let mut u = utxo(value, vout);
2594            *u.needs_consolidation_mut() = FixedOptionF64::some(1.0);
2595            u
2596        }
2597
2598        #[test]
2599        fn selects_basic_across_holders() {
2600            // Two holders with one UTXO each; ensure we pick the largest to satisfy the amount.
2601            let holders = vec![
2602                TestHolder {
2603                    utxos: vec![utxo(5_000, 0)],
2604                },
2605                TestHolder {
2606                    utxos: vec![utxo(12_000, 1)],
2607                },
2608            ];
2609
2610            let mut builder = new_tb!(10, 10);
2611            let amount = 10_000;
2612
2613            let found_amount = builder
2614                .find_btc_in_utxos_from_holder(&holders, &PUBKEY, amount, false)
2615                .unwrap();
2616
2617            assert_eq!(found_amount, 17_000);
2618
2619            #[cfg(feature = "utxo-consolidation")]
2620            {
2621                assert_eq!(builder.total_btc_consolidation_input, 0);
2622                assert_eq!(builder.extra_tx_size_for_consolidation, 0);
2623            }
2624        }
2625
2626        #[test]
2627        #[cfg(feature = "utxo-consolidation")]
2628        fn tracks_consolidation_single_input() {
2629            // Prefer non-consolidation UTXOs first, then include one consolidation UTXO.
2630            let holders = vec![
2631                TestHolder {
2632                    utxos: vec![utxo(5_000, 0), utxo_cons(12_000, 1)],
2633                },
2634                TestHolder {
2635                    utxos: vec![utxo(9_000, 2)],
2636                },
2637            ];
2638
2639            let mut builder = new_tb!(10, 10);
2640            let amount = 17_000;
2641
2642            let found_amount = builder
2643                .find_btc_in_utxos_from_holder(&holders, &PUBKEY, amount, false)
2644                .unwrap();
2645
2646            // Selection: 9k (non-cons) + 5k (non-cons) + 12k (cons) = 26k
2647            assert_eq!(found_amount, 26_000);
2648            assert_eq!(builder.total_btc_consolidation_input, 12_000);
2649            assert_eq!(
2650                builder.extra_tx_size_for_consolidation,
2651                crate::input_calc::ARCH_INPUT_SIZE
2652            );
2653        }
2654
2655        #[test]
2656        #[cfg(feature = "utxo-consolidation")]
2657        fn tracks_consolidation_multiple_inputs() {
2658            // Require two consolidation UTXOs to reach the target amount.
2659            let holders = vec![
2660                TestHolder {
2661                    utxos: vec![utxo(4_000, 0)],
2662                },
2663                TestHolder {
2664                    utxos: vec![utxo_cons(7_000, 1)],
2665                },
2666                TestHolder {
2667                    utxos: vec![utxo_cons(6_000, 2)],
2668                },
2669            ];
2670
2671            let mut builder = new_tb!(10, 10);
2672            let amount = 15_000;
2673
2674            let found_amount = builder
2675                .find_btc_in_utxos_from_holder(&holders, &PUBKEY, amount, false)
2676                .unwrap();
2677
2678            // Selection: 4k (non-cons) + 7k (cons) + 6k (cons) = 17k
2679            assert_eq!(found_amount, 17_000);
2680            assert_eq!(builder.total_btc_consolidation_input, 13_000);
2681            assert_eq!(
2682                builder.extra_tx_size_for_consolidation,
2683                crate::input_calc::ARCH_INPUT_SIZE * 2
2684            );
2685        }
2686    }
2687}