satellite_shard/
split.rs

1//! Helpers to **redistribute remaining liquidity** (BTC or Rune) across a
2//! **selected** set of shards after user-funded inputs and fees have already
3//! been applied.
4//!
5//! This module is **purely functional**: it contains algorithms that *calculate*
6//! balanced allocations or *extend* the in-flight
7//! [`satellite_bitcoin::TransactionBuilder`].  *It never mutates the
8//! on-chain shard accounts directly.*  State changes are ultimately carried
9//! out by the higher-level wrappers on [`ShardSet`] which borrow the underlying
10//! [`AccountLoader`]s only for the minimum time required.
11//!
12//! High-level BTC redistribution flow
13//! ==================================
14//! 1. [`compute_unsettled_btc_in_shards`] – sums the satoshis still owned by the
15//!    selected shards (minus already-removed liquidity & fees).
16//! 2. [`plan_btc_distribution_among_shards`] – derives an as-even-as-possible
17//!    per-shard allocation while respecting the Bitcoin dust limit and, when
18//!    the `utxo-consolidation` feature is active, marks consolidation inputs.
19//! 3. [`redistribute_remaining_btc_to_shards`] – appends one change output per
20//!    shard to the transaction.
21//!
22//! Rune helpers follow the same pattern behind the `runes` feature flag
23//! (`compute_unsettled_rune_in_shards`, `plan_rune_distribution_among_shards`,
24//! `redistribute_remaining_rune_to_shards`).
25//!
26//! Type parameters & generics
27//! -------------------------
28//! Most public functions are generic over the following compile-time bounds:
29//! * `MAX_MODIFIED_ACCOUNTS` – maximum user-provided inputs accepted by the
30//!   transaction builder.
31//! * `MAX_INPUTS_TO_SIGN` – upper limit on shards per program instance.
32//! * `MAX_SELECTED` – upper limit on simultaneously *selected* shards.
33//! * `RS`, `U`, `S` – concrete types implementing the required Saturn traits.
34//!
35//! Error semantics
36//! ---------------
37//! * **Arithmetic overflow / underflow** ⇒ [`satellite_bitcoin::MathError`]
38//! * **Rune-specific validation errors** ⇒ [`crate::StateShardError`]
39//!
40//! All algorithms are `no_std`-compatible and rely on the fixed-size
41//! collections from `satellite_bitcoin`, keeping worst-case memory usage
42//! bounded at compile time.
43use std::cell::Ref;
44
45use arch_program::{
46    msg,
47    rune::{RuneAmount, RuneId},
48    utxo::UtxoMeta,
49};
50use bitcoin::{Amount, ScriptBuf, TxOut};
51use satellite_bitcoin::generic::fixed_set::FixedCapacitySet;
52use satellite_bitcoin::{
53    constants::DUST_LIMIT, fee_rate::FeeRate, utxo_info::UtxoInfoTrait, TransactionBuilder,
54};
55use satellite_bitcoin::{safe_add, safe_div, safe_mul, safe_sub, MathError};
56
57use super::error::StateShardError;
58use super::StateShard;
59
60#[cfg(feature = "runes")]
61use ordinals::Edict;
62
63use satellite_lang::prelude::Owner;
64use satellite_lang::ZeroCopy;
65
66/// Errors specific to distribution and dust handling logic in this module.
67#[derive(Debug, PartialEq)]
68pub enum DistributionError {
69    /// Total amount to distribute is non-zero but below the dust limit.
70    TotalBelowDustLimit,
71    /// Wrapper around generic math errors encountered during redistribution.
72    Math(MathError),
73}
74
75impl From<MathError> for DistributionError {
76    fn from(value: MathError) -> Self {
77        DistributionError::Math(value)
78    }
79}
80
81/// Redistributes the *remaining* satoshi value belonging to the provided shards
82/// back into brand-new outputs (one per **retained allocation** after
83/// dust-limit filtering) to achieve optimal liquidity balance across all
84/// participating shards.
85///
86/// This function performs the complete BTC redistribution workflow:
87/// 1. **Calculate remaining value**: Determines how many satoshis are still owned
88///    by the shards **after** the caller has removed some liquidity
89///    (`removed_from_shards`) and the program has paid transaction fees.
90/// 2. **Plan optimal distribution**: Uses [`plan_btc_distribution_among_shards`]
91///    to derive an as-even-as-possible per-shard allocation that respects the
92///    Bitcoin dust limit.
93/// 3. **Create transaction outputs**: Appends one [`TxOut`] to the underlying
94///    transaction for every computed allocation, using `program_script_pubkey`
95///    to lock those outputs back to the program.
96///
97/// # Output Ordering and Filtering
98///
99/// The returned vector contains **one element for each output actually created**
100/// after dust filtering and is **sorted in descending order by amount (largest
101/// first)** for deterministic behavior. When allocations below the dust limit
102/// are removed, the length of the vector—and therefore the number of newly
103/// created change outputs—can be **smaller than the number of selected shards**.
104///
105/// Since the order no longer corresponds to the indices returned by
106/// [`ShardSet::selected_indices`], callers that need to map values back to
107/// specific shards must perform that mapping explicitly.
108///
109/// # Type Parameters
110/// * `MAX_MODIFIED_ACCOUNTS` – Maximum number of user-supplied UTXOs supported by
111///   the [`TransactionBuilder`].
112/// * `MAX_INPUTS_TO_SIGN` – Compile-time bound on the number of shards in a
113///   single program instance.
114/// * `RS` – Rune set type implementing [`FixedCapacitySet<Item = RuneAmount>`].
115/// * `U` – UTXO info type implementing [`UtxoInfoTrait<RS>`].
116/// * `S` – Shard type implementing [`StateShard<U, RS>`], [`ZeroCopy`], and [`Owner`].
117///
118/// # Parameters
119/// * `tx_builder` – Mutable reference to the transaction currently being assembled.
120/// * `selected_shards` – Slice of mutable shard references in the *Selected* state;
121///   only these shards participate in the redistribution.
122/// * `removed_from_shards` – Total satoshis that the caller already withdrew
123///   from the selected shards during the current instruction.
124/// * `program_script_pubkey` – Script that will lock the change outputs
125///   produced by this function (usually the program's own script).
126/// * `fee_rate` – Fee rate used to calculate how many satoshis were paid by the
127///   program for transaction fees.
128///
129/// # Returns
130/// A vector of `u128` values representing the satoshi amounts for each created
131/// output, sorted in descending order. The sum of all values equals the total
132/// redistributed amount.
133///
134/// # Errors
135/// * [`MathError`] – If any safe-math operation overflows or underflows during
136///   amount calculations or distribution planning.
137///
138/// # Example Scenarios
139/// * **Equal distribution**: If 3 shards each have 1000 sats and 3000 sats need
140///   redistribution, each gets 1000 sats.
141/// * **Dust filtering**: If redistribution would create outputs below the dust
142///   limit, those amounts are consolidated into larger outputs.
143/// * **Uneven remainders**: Extra satoshis from modulo operations are distributed
144///   evenly with preference given to earlier outputs.
145#[allow(clippy::too_many_arguments)]
146pub fn redistribute_remaining_btc_to_shards<
147    'info,
148    const MAX_MODIFIED_ACCOUNTS: usize,
149    const MAX_INPUTS_TO_SIGN: usize,
150    RS,
151    U,
152    S,
153>(
154    tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
155    selected_shards: &[Ref<'info, S>],
156    removed_from_shards: u64,
157    program_script_pubkey: &ScriptBuf,
158    fee_rate: &FeeRate,
159) -> Result<Vec<u128>, DistributionError>
160where
161    RS: FixedCapacitySet<Item = RuneAmount> + Default,
162    U: UtxoInfoTrait<RS>,
163    S: StateShard<U, RS> + ZeroCopy + Owner,
164{
165    let remaining_amount = compute_unsettled_btc_in_shards(
166        tx_builder,
167        selected_shards,
168        removed_from_shards,
169        fee_rate,
170    )?;
171
172    let mut distribution =
173        plan_btc_distribution_among_shards(tx_builder, selected_shards, remaining_amount as u128)?;
174
175    // Largest first for deterministic ordering.
176    distribution.sort_by(|a, b| b.cmp(a));
177
178    for amount in distribution.iter() {
179        let txout = TxOut {
180            value: Amount::from_sat(*amount as u64),
181            script_pubkey: program_script_pubkey.clone(),
182        };
183
184        tx_builder.transaction.output.push(txout);
185    }
186
187    Ok(distribution)
188}
189
190/// Sums the BTC value of shard-owned UTXOs that this transaction spends and
191/// subtracts the fees paid by the program and any amounts already removed from
192/// the shards to produce the unsettled amount.
193///
194/// Notes:
195/// * Only shard-owned UTXOs that appear as inputs in the current transaction are
196///   included in the sum.
197/// * Program-paid fees are subtracted when applicable (feature-gated via
198///   `utxo-consolidation`).
199/// * Amounts already withdrawn from the selected shards during this instruction
200///   (`removed_from_shards`) are also subtracted.
201pub fn compute_unsettled_btc_in_shards<
202    'info,
203    const MAX_MODIFIED_ACCOUNTS: usize,
204    const MAX_INPUTS_TO_SIGN: usize,
205    RS,
206    U,
207    S,
208>(
209    tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
210    selected_shards: &[Ref<'info, S>],
211    removed_from_shards: u64,
212    fee_rate: &FeeRate,
213) -> Result<u64, MathError>
214where
215    RS: FixedCapacitySet<Item = RuneAmount> + Default,
216    U: UtxoInfoTrait<RS>,
217    S: StateShard<U, RS> + ZeroCopy + Owner,
218{
219    // ---------------------------------------------------------------------
220    // 2. Sum the value of every shard-managed UTXO that appears in the set.
221    // ---------------------------------------------------------------------
222    let mut total_btc_amount: u64 = 0;
223
224    // Only consider non–state-transition inputs when tracking unsettled BTC.
225    // State-transition inputs correspond to program accounts, not shard-owned
226    // liquidity, and always appear at the beginning of the transaction.
227    let non_state_inputs = tx_builder.get_non_state_transition_inputs();
228
229    for shard in selected_shards.iter() {
230        let mut sum: u64 = 0;
231        for utxo in shard.btc_utxos().iter() {
232            for input in non_state_inputs.iter() {
233                let spent_meta =
234                    UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout);
235                if spent_meta == *utxo.meta() {
236                    sum = sum.saturating_add(utxo.value());
237                }
238            }
239        }
240
241        total_btc_amount = safe_add(total_btc_amount, sum)?;
242    }
243
244    let fee_paid_by_program = {
245        #[cfg(feature = "utxo-consolidation")]
246        {
247            tx_builder.get_fee_paid_by_program(fee_rate)
248        }
249        #[cfg(not(feature = "utxo-consolidation"))]
250        {
251            0
252        }
253    };
254
255    let remaining_amount = safe_sub(
256        safe_sub(total_btc_amount, removed_from_shards)?,
257        fee_paid_by_program,
258    )?;
259
260    Ok(remaining_amount)
261}
262
263/// Plans an optimal BTC distribution across selected shards while respecting
264/// the Bitcoin dust limit and ensuring no value is lost.
265///
266/// This internal helper function splits the given `amount` of satoshis across
267/// the provided shards as evenly as possible, then applies dust limit filtering
268/// to ensure all outputs meet Bitcoin's minimum value requirements.
269///
270/// The function delegates the core balancing logic to [`balance_amount_across_shards`]
271/// and then applies [`redistribute_sub_dust_values`] to handle allocations below
272/// the dust threshold.
273///
274/// # Type Parameters
275/// * `MAX_MODIFIED_ACCOUNTS` – Maximum number of user-supplied UTXOs in the transaction.
276/// * `MAX_INPUTS_TO_SIGN` – Maximum number of shards per program instance.
277/// * `RS` – Rune set type implementing [`FixedCapacitySet<Item = RuneAmount>`].
278/// * `U` – UTXO info type implementing [`UtxoInfoTrait<RS>`].
279/// * `S` – Shard type implementing [`StateShard<U, RS>`], [`ZeroCopy`], and [`Owner`].
280///
281/// # Parameters
282/// * `tx_builder` – Reference to the transaction builder for context.
283/// * `selected_shards` – Slice of selected shards to distribute value among.
284/// * `amount` – Total satoshis to distribute across the shards.
285///
286/// # Returns
287/// A vector of `u128` values representing the final allocation per output after
288/// dust filtering. The vector may be shorter than the number of input shards if
289/// some allocations were below the dust limit and consolidated.
290///
291/// # Errors
292/// * [`MathError`] – If any arithmetic operation overflows or underflows during
293///   the distribution calculation or dust redistribution process.
294///
295/// # Dust Handling
296/// Allocations below [`DUST_LIMIT`] are automatically:
297/// 1. Removed from the output vector
298/// 2. Aggregated into a single sum
299/// 3. Redistributed evenly among the remaining valid allocations
300/// 4. If no allocations remain above dust but the total exceeds dust, a single
301///    output containing the full amount is created
302fn plan_btc_distribution_among_shards<
303    'info,
304    const MAX_MODIFIED_ACCOUNTS: usize,
305    const MAX_INPUTS_TO_SIGN: usize,
306    RS,
307    U,
308    S,
309>(
310    tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
311    selected_shards: &[Ref<'info, S>],
312    amount: u128,
313) -> Result<Vec<u128>, DistributionError>
314where
315    RS: FixedCapacitySet<Item = RuneAmount> + Default,
316    U: UtxoInfoTrait<RS>,
317    S: StateShard<U, RS> + ZeroCopy + Owner,
318{
319    if amount == 0 {
320        return Ok(Vec::new());
321    }
322
323    let mut result = balance_amount_across_shards(
324        tx_builder,
325        selected_shards,
326        &RuneAmount {
327            id: RuneId::BTC,
328            amount,
329        },
330    )?;
331
332    redistribute_sub_dust_values(&mut result, DUST_LIMIT as u128)?;
333    Ok(result)
334}
335
336/// Computes an optimal balanced allocation of the specified amount across the
337/// provided shards without mutating any state.
338///
339/// This pure calculation function implements a sophisticated balancing algorithm
340/// that aims to equalize holdings across all selected shards after the
341/// redistribution is complete. The algorithm considers existing balances and
342/// attempts to achieve equal per-shard totals when possible.
343///
344/// # Algorithm Overview
345/// 1. **Current balance calculation**: Determines existing liquidity (BTC or Rune)
346///    for each shard, excluding any UTXOs already being spent in the transaction.
347/// 2. **Target balance derivation**: Calculates the ideal per-shard value that
348///    would result in perfect equality after redistribution.
349/// 3. **Need-based allocation**: If sufficient funds are available, each shard
350///    receives exactly what it needs to reach the target balance, with any
351///    leftover distributed evenly.
352/// 4. **Proportional fallback**: If insufficient funds exist for perfect balance,
353///    the available amount is distributed proportionally based on each shard's
354///    individual need.
355///
356/// # Type Parameters
357/// * `MAX_MODIFIED_ACCOUNTS` – Maximum number of user-supplied UTXOs in the transaction.
358/// * `MAX_INPUTS_TO_SIGN` – Maximum number of shards per program instance.
359/// * `RS` – Rune set type implementing [`FixedCapacitySet<Item = RuneAmount>`].
360/// * `U` – UTXO info type implementing [`UtxoInfoTrait<RS>`].
361/// * `S` – Shard type implementing [`StateShard<U, RS>`], [`ZeroCopy`], and [`Owner`].
362///
363/// # Parameters
364/// * `tx_builder` – Reference to the transaction builder to check which UTXOs
365///   are already being spent.
366/// * `selected_shards` – Slice of selected shards to balance the amount across.
367/// * `rune_amount` – The amount to distribute, specified as a [`RuneAmount`] where
368///   `RuneId::BTC` indicates Bitcoin satoshis and other IDs indicate Rune tokens.
369///
370/// # Returns
371/// A vector where the i-th element represents the allocation for the i-th
372/// selected shard. The vector length always equals `selected_shards.len()` and
373/// the sum of all entries equals `rune_amount.amount` (modulo rounding in
374/// proportional scenarios).
375///
376/// # Errors
377/// * [`MathError::Overflow`] – If intermediate calculations exceed numeric limits.
378/// * [`MathError::Underflow`] – If subtraction operations go below zero.
379/// * [`MathError::DivisionOverflow`] – If division by zero occurs (e.g., empty shard list).
380///
381/// # Invariants
382/// * **Length preservation**: Output vector length always equals input shard count.
383/// * **Value conservation**: Sum of allocations equals input amount (within rounding).
384/// * **Non-negativity**: All allocation values are non-negative.
385///
386/// # Example
387/// Given shards with balances [100, 200, 300] and 150 to distribute:
388/// - Target per-shard: (100+200+300+150)/3 = 250
389/// - Needs: [150, 50, 0] (to reach 250 each)
390/// - Result: [150, 0, 0] (insufficient funds for full balancing)
391fn balance_amount_across_shards<
392    'info,
393    const MAX_MODIFIED_ACCOUNTS: usize,
394    const MAX_INPUTS_TO_SIGN: usize,
395    RS,
396    U,
397    S,
398>(
399    tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
400    selected_shards: &[Ref<'info, S>],
401    rune_amount: &RuneAmount,
402) -> Result<Vec<u128>, MathError>
403where
404    RS: FixedCapacitySet<Item = RuneAmount> + Default,
405    U: UtxoInfoTrait<RS>,
406    S: StateShard<U, RS> + ZeroCopy + Owner,
407{
408    let num_shards = selected_shards.len();
409
410    // Allocate result vectors on the stack.
411    let mut assigned_amounts: Vec<u128> = Vec::with_capacity(num_shards);
412    let mut total_current_amount: u128 = 0;
413
414    // --------------------------------------------------
415    // Pre-compute a helper over non–state-transition inputs.
416    // --------------------------------------------------
417    let non_state_inputs = tx_builder.get_non_state_transition_inputs();
418    let is_meta_spent = |meta: &UtxoMeta| {
419        non_state_inputs.iter().any(|input| {
420            let spent_meta =
421                UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout);
422            spent_meta == *meta
423        })
424    };
425
426    // 1. Determine the current amount per shard and overall.
427    for shard in selected_shards.iter() {
428        let current_res = match rune_amount.id {
429            RuneId::BTC => shard
430                .btc_utxos()
431                .iter()
432                .filter_map(|u| {
433                    if is_meta_spent(u.meta()) {
434                        None
435                    } else {
436                        Some(u.value() as u128)
437                    }
438                })
439                .sum(),
440            _ => {
441                #[cfg(feature = "runes")]
442                {
443                    shard
444                        .rune_utxo()
445                        .filter(|u| !is_meta_spent(u.meta()))
446                        .and_then(|u| u.runes().find(&rune_amount.id).map(|r| r.amount))
447                        .unwrap_or(0)
448                }
449                #[cfg(not(feature = "runes"))]
450                {
451                    0
452                }
453            }
454        };
455
456        assigned_amounts.push(current_res);
457        total_current_amount = safe_add(total_current_amount, current_res)?;
458    }
459
460    msg!("total_current_amount: {}", total_current_amount);
461
462    // Determine target per-shard balance.
463    let total_after = safe_add(total_current_amount, rune_amount.amount)?;
464    let desired_per_shard = safe_div(total_after, num_shards as u128)?;
465
466    msg!("desired_per_shard: {}", desired_per_shard);
467
468    // Calculate additional amount needed per shard to reach desired balance.
469    let mut total_needed = 0u128;
470    for current in assigned_amounts.iter_mut() {
471        let needed = if desired_per_shard > *current {
472            safe_sub(desired_per_shard, *current)?
473        } else {
474            0
475        };
476        total_needed = safe_add(total_needed, needed)?;
477        *current = needed;
478    }
479
480    if total_needed <= rune_amount.amount {
481        // Distribute leftover evenly across shards.
482        let leftover = safe_sub(rune_amount.amount, total_needed)?;
483        let per_shard_extra = safe_div(leftover, num_shards as u128)?;
484        let mut extra_left = leftover % num_shards as u128;
485
486        for amt in assigned_amounts.iter_mut() {
487            *amt = safe_add(*amt, per_shard_extra)?;
488            if extra_left > 0 {
489                *amt = safe_add(*amt, 1)?;
490                extra_left -= 1;
491            }
492        }
493    } else {
494        // Not enough to reach equal balance – scale proportionally.
495        let mut cumulative = 0u128;
496        let mut cumulative_needed = 0u128;
497
498        for i in 0..num_shards {
499            let needed = assigned_amounts[i];
500            cumulative_needed = safe_add(cumulative_needed, needed)?;
501            let proportional = safe_mul(rune_amount.amount, cumulative_needed)? / total_needed;
502            assigned_amounts[i] = safe_sub(proportional, cumulative)?;
503            cumulative = proportional;
504        }
505    }
506
507    msg!("assigned_amounts: {:?}", assigned_amounts);
508    Ok(assigned_amounts)
509}
510
511/// Redistributes amounts below the dust limit to prevent value loss and ensure
512/// all outputs meet Bitcoin's minimum value requirements.
513///
514/// This function implements a dust consolidation algorithm that:
515/// 1. **Aggregates sub-dust amounts**: Sums all allocations below `dust_limit`
516/// 2. **Removes dust entries**: Filters out sub-dust allocations from the vector
517/// 3. **Redistributes dust value**: Distributes the aggregated dust evenly among
518///    remaining valid allocations
519/// 4. **Handles edge cases**: Creates a single output if only dust exists but
520///    the total exceeds the limit
521///
522/// The redistribution ensures that no satoshis are lost while maintaining
523/// compliance with Bitcoin's dust limit rules. If the total of all sub-dust
524/// amounts is itself above the dust limit, it forms a single consolidated output.
525///
526/// # Parameters
527/// * `amounts` – Mutable vector of allocation amounts to process. Modified in-place.
528/// * `dust_limit` – Minimum value threshold below which outputs are considered dust.
529///
530/// # Returns
531/// * `Ok(())` – If redistribution completed successfully
532/// * `Err(MathError)` – If arithmetic operations overflow during redistribution
533///
534/// # Algorithm Details
535/// For remaining amounts after dust removal:
536/// - Each gets `floor(dust_sum / remaining_count)` additional value
537/// - Remainder from integer division is distributed one unit at a time to
538///   the first `remainder` outputs
539///
540/// # Examples
541/// ```text
542/// Input: [1000, 200, 300, 2000], dust_limit: 546
543/// Sub-dust: 200 + 300 = 500
544/// Remaining: [1000, 2000]
545/// Final: [1250, 2250] (500 distributed evenly)
546/// ```
547///
548/// ```text
549/// Input: [200, 300, 100], dust_limit: 546  
550/// Sub-dust: 600 (>= dust_limit)
551/// Final: [600] (single consolidated output)
552/// ```
553fn redistribute_sub_dust_values(
554    amounts: &mut Vec<u128>,
555    dust_limit: u128,
556) -> Result<(), DistributionError> {
557    // 0. If the total amount to distribute is non-zero but below dust, return an error
558    //    to avoid silently losing value by producing no valid outputs.
559    let mut total_sum: u128 = 0;
560    for &amt in amounts.iter() {
561        total_sum = safe_add(total_sum, amt)?;
562    }
563    if total_sum > 0 && total_sum < dust_limit {
564        return Err(DistributionError::TotalBelowDustLimit);
565    }
566
567    // 1. Aggregate all allocations below dust.
568    let sum_of_small_amounts: u128 = amounts.iter().filter(|&&amount| amount < dust_limit).sum();
569
570    // 2. Remove sub-dust entries entirely.
571    amounts.retain(|&amount| amount >= dust_limit);
572
573    // 3. If nothing left after removal, decide whether to keep or discard.
574    if amounts.is_empty() {
575        if sum_of_small_amounts >= dust_limit {
576            amounts.push(sum_of_small_amounts);
577        } else {
578            amounts.clear();
579        }
580        return Ok(());
581    }
582
583    // 4. Redistribute the collected dust across remaining outputs.
584    let num_amounts = amounts.len() as u128;
585    let to_add = safe_div(sum_of_small_amounts, num_amounts)?;
586    let mut remainder = sum_of_small_amounts % num_amounts;
587
588    for amount in amounts.iter_mut() {
589        *amount = safe_add(*amount, to_add)?;
590        if remainder > 0 {
591            *amount = safe_add(*amount, 1)?;
592            remainder -= 1;
593        }
594    }
595
596    Ok(())
597}
598
599/// Calculates the total Rune token amount still owned by selected shards after
600/// accounting for tokens that have already been removed.
601///
602/// This function performs a comprehensive audit of Rune holdings across all
603/// selected shards, similar to [`compute_unsettled_btc_in_shards`] but for
604/// Rune tokens. It aggregates all Rune amounts from each shard's `rune_utxo`
605/// and subtracts any tokens specified in `removed_from_shards`.
606///
607/// # Type Parameters
608/// * `RS` – Rune set type implementing [`FixedCapacitySet<Item = RuneAmount>`].
609/// * `U` – UTXO info type implementing [`UtxoInfoTrait<RS>`].
610/// * `S` – Shard type implementing [`StateShard<U, RS>`], [`ZeroCopy`], and [`Owner`].
611///
612/// # Parameters
613/// * `selected_shards` – Slice of selected shards to audit for Rune holdings.
614/// * `removed_from_shards` – Rune amounts already withdrawn from the shards
615///   during the current instruction.
616///
617/// # Returns
618/// A [`FixedCapacitySet`] containing the net Rune amounts that still need to be
619/// redistributed back to the shards. Each [`RuneAmount`] in the set represents
620/// a different Rune ID and its corresponding quantity.
621///
622/// # Errors
623/// * [`StateShardError::RuneAmountAdditionOverflow`] – If aggregating Rune amounts
624///   of the same ID exceeds the maximum value.
625/// * [`StateShardError::RemovingMoreRunesThanPresentInShards`] – If
626///   `removed_from_shards` contains more tokens of a specific ID than the shards
627///   actually hold, indicating an inconsistent state.
628///
629/// # Implementation Details
630/// * Iterates through each shard exactly once to gather all Rune holdings
631/// * Uses `insert_or_modify` to aggregate amounts for Runes with identical IDs
632/// * Performs safe subtraction to account for already-removed tokens
633/// * Maintains type safety through the Rune set's fixed capacity constraints
634#[cfg(feature = "runes")]
635pub fn compute_unsettled_rune_in_shards<'info, RS, U, S>(
636    selected_shards: &[Ref<'info, S>],
637    removed_from_shards: RS,
638) -> Result<RS, StateShardError>
639where
640    RS: FixedCapacitySet<Item = RuneAmount> + Default,
641    U: UtxoInfoTrait<RS>,
642    S: StateShard<U, RS> + ZeroCopy + Owner,
643{
644    let mut total_rune_amount = RS::default();
645
646    for shard in selected_shards.iter() {
647        // Traverse rune amounts directly without allocating an intermediate Vec.
648        if let Some(utxo) = shard.rune_utxo() {
649            for rune in utxo.runes().iter() {
650                let _ = total_rune_amount.insert_or_modify::<StateShardError, _>(
651                    RuneAmount {
652                        id: rune.id,
653                        amount: rune.amount,
654                    },
655                    |r| {
656                        r.amount = safe_add(r.amount, rune.amount)
657                            .map_err(|_| StateShardError::RuneAmountAdditionOverflow)?;
658                        Ok(())
659                    },
660                );
661            }
662        };
663    }
664
665    // Subtract whatever was already removed.
666    for rune in removed_from_shards.iter() {
667        if let Some(output_rune) = total_rune_amount.find_mut(&rune.id) {
668            output_rune.amount = safe_sub(output_rune.amount, rune.amount)
669                .map_err(|_| StateShardError::RemovingMoreRunesThanPresentInShards)?;
670        }
671    }
672
673    Ok(total_rune_amount)
674}
675
676/// Plans an optimal Rune token distribution across selected shards to achieve
677/// balanced holdings without applying dust limits.
678///
679/// This function serves as the Rune equivalent of [`plan_btc_distribution_among_shards`],
680/// using the same core balancing algorithm via [`balance_amount_across_shards`]
681/// but without dust limit filtering since Runes don't have minimum value constraints
682/// like Bitcoin UTXOs.
683///
684/// The function processes each Rune ID separately, computing an optimal allocation
685/// for each token type across all selected shards and combining the results into
686/// per-shard Rune sets.
687///
688/// # Type Parameters
689/// * `MAX_MODIFIED_ACCOUNTS` – Maximum number of user-supplied UTXOs in the transaction.
690/// * `MAX_INPUTS_TO_SIGN` – Maximum number of shards per program instance.
691/// * `RS` – Rune set type implementing [`FixedCapacitySet<Item = RuneAmount>`].
692/// * `U` – UTXO info type implementing [`UtxoInfoTrait<RS>`].
693/// * `S` – Shard type implementing [`StateShard<U, RS>`], [`ZeroCopy`], and [`Owner`].
694///
695/// # Parameters
696/// * `tx_builder` – Mutable reference to the transaction builder for context.
697/// * `selected_shards` – Slice of selected shards to distribute tokens among.
698/// * `amounts` – Rune set containing the token amounts to distribute across shards.
699///
700/// # Returns
701/// A vector of Rune sets where each element corresponds to a selected shard and
702/// contains the optimal allocation of tokens for that shard. The vector length
703/// always equals `selected_shards.len()`.
704///
705/// # Errors
706/// * [`StateShardError::MathErrorInBalanceAmountAcrossShards`] – If the underlying
707///   balance calculation encounters arithmetic overflow or underflow.
708/// * [`StateShardError::RuneAmountAdditionOverflow`] – If aggregating multiple
709///   Rune allocations for the same ID exceeds numeric limits.
710///
711/// # Algorithm
712/// 1. **Per-Rune processing**: Each Rune ID in `amounts` is processed separately
713/// 2. **Balance calculation**: [`balance_amount_across_shards`] computes optimal
714///    allocation for each Rune type
715/// 3. **Result aggregation**: Allocations are combined into per-shard Rune sets
716/// 4. **Zero filtering**: Shards with zero allocation for a Rune type are skipped
717///
718/// # Example
719/// Given 2 shards and amounts `{RUNE_A: 1000, RUNE_B: 2000}`:
720/// - RUNE_A gets distributed as `[500, 500]`
721/// - RUNE_B gets distributed as `[1000, 1000]`  
722/// - Result: `[{RUNE_A: 500, RUNE_B: 1000}, {RUNE_A: 500, RUNE_B: 1000}]`
723#[cfg(feature = "runes")]
724#[allow(clippy::too_many_arguments)]
725pub fn plan_rune_distribution_among_shards<
726    'info,
727    const MAX_MODIFIED_ACCOUNTS: usize,
728    const MAX_INPUTS_TO_SIGN: usize,
729    RS,
730    U,
731    S,
732>(
733    tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
734    selected_shards: &[Ref<'info, S>],
735    amounts: &RS,
736) -> Result<Vec<RS>, StateShardError>
737where
738    RS: FixedCapacitySet<Item = RuneAmount> + Default,
739    U: UtxoInfoTrait<RS>,
740    S: StateShard<U, RS> + ZeroCopy + Owner,
741{
742    let num_shards = selected_shards.len();
743    let mut result: Vec<RS> = (0..num_shards).map(|_| RS::default()).collect();
744
745    for rune_amount in amounts.iter() {
746        let allocs = balance_amount_across_shards(tx_builder, selected_shards, rune_amount)
747            .map_err(|_| StateShardError::MathErrorInBalanceAmountAcrossShards)?;
748
749        for (i, amount) in allocs.iter().enumerate() {
750            result[i].insert_or_modify::<StateShardError, _>(
751                RuneAmount {
752                    id: rune_amount.id,
753                    amount: *amount,
754                },
755                |r| {
756                    r.amount = safe_add(r.amount, *amount)
757                        .map_err(|_| StateShardError::RuneAmountAdditionOverflow)?;
758                    Ok(())
759                },
760            )?;
761        }
762    }
763
764    Ok(result)
765}
766
767/// Redistributes remaining Rune tokens across shards and generates corresponding
768/// transaction outputs and runestone edicts for on-chain execution.
769///
770/// This function provides the complete Rune redistribution workflow, analogous to
771/// [`redistribute_remaining_btc_to_shards`] but for Rune tokens. In addition to
772/// planning the optimal distribution, it:
773/// * **Creates transaction outputs**: Adds one output per participating shard,
774///   each locked to `program_script_pubkey` with the minimum dust value.
775/// * **Updates runestone pointer**: Sets the pointer to the first newly created
776///   output so Runes are properly credited.
777/// * **Generates edicts**: Creates [`Edict`] entries for all outputs except the
778///   first (which receives Runes via the pointer mechanism).
779///
780/// # Type Parameters
781/// * `MAX_MODIFIED_ACCOUNTS` – Maximum number of user-supplied UTXOs in the transaction.
782/// * `MAX_INPUTS_TO_SIGN` – Maximum number of shards per program instance.
783/// * `RS` – Rune set type implementing [`FixedCapacitySet<Item = RuneAmount>`].
784/// * `U` – UTXO info type implementing [`UtxoInfoTrait<RS>`].
785/// * `S` – Shard type implementing [`StateShard<U, RS>`], [`ZeroCopy`], and [`Owner`].
786///
787/// # Parameters
788/// * `tx_builder` – Mutable reference to the transaction builder. The embedded
789///   runestone will be modified to include the necessary pointer and edicts.
790/// * `selected_shards` – Slice of selected shards to redistribute tokens among.
791/// * `removed_from_shards` – Rune amounts already withdrawn from the shards,
792///   used to calculate remaining amounts via [`compute_unsettled_rune_in_shards`].
793/// * `program_script_pubkey` – Script that will lock all newly created outputs
794///   back to the program.
795///
796/// # Returns
797/// A vector of Rune sets representing the final distribution, sorted in descending
798/// order by total Rune amount per shard. Each element corresponds to one created
799/// output and contains all Rune allocations for that output.
800///
801/// # Errors
802/// * [`StateShardError`] variants from underlying computation functions:
803///   * `RuneAmountAdditionOverflow` – If Rune amount calculations overflow
804///   * `RemovingMoreRunesThanPresentInShards` – If `removed_from_shards` exceeds holdings
805///   * `MathErrorInBalanceAmountAcrossShards` – If distribution planning fails
806///
807/// # Runestone Protocol Details
808/// * **Pointer mechanism**: The first output receives Runes automatically via the
809///   runestone pointer, requiring no explicit edict.
810/// * **Edict generation**: Subsequent outputs require explicit [`Edict`] entries
811///   specifying the Rune ID, amount, and destination output index.
812/// * **Output ordering**: Results are sorted by total Rune value for deterministic
813///   behavior, similar to the BTC redistribution function.
814///
815/// # Example
816/// For 2 shards with `{RUNE_A: 1000}` to redistribute:
817/// 1. Creates 2 outputs at indices N and N+1
818/// 2. Sets `runestone.pointer = Some(N)`
819/// 3. Adds edict: `{id: RUNE_A, amount: 500, output: N+1}`
820/// 4. First output (N) gets 500 RUNE_A via pointer
821/// 5. Second output (N+1) gets 500 RUNE_A via edict
822#[cfg(feature = "runes")]
823#[allow(clippy::too_many_arguments)]
824pub fn redistribute_remaining_rune_to_shards<
825    'info,
826    const MAX_MODIFIED_ACCOUNTS: usize,
827    const MAX_INPUTS_TO_SIGN: usize,
828    RS,
829    U,
830    S,
831>(
832    tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
833    selected_shards: &[Ref<'info, S>],
834    removed_from_shards: RS,
835    program_script_pubkey: ScriptBuf,
836) -> Result<Vec<RS>, StateShardError>
837where
838    RS: FixedCapacitySet<Item = RuneAmount> + Default,
839    U: UtxoInfoTrait<RS>,
840    S: StateShard<U, RS> + ZeroCopy + Owner,
841{
842    let remaining_amount = compute_unsettled_rune_in_shards(selected_shards, removed_from_shards)?;
843
844    let mut distribution =
845        plan_rune_distribution_among_shards(tx_builder, selected_shards, &remaining_amount)?;
846
847    // Sort descending by total rune amount for deterministic ordering.
848    distribution.sort_by(|a, b| {
849        let total_a: u128 = a.iter().map(|r| r.amount).sum();
850        let total_b: u128 = b.iter().map(|r| r.amount).sum();
851        total_b.cmp(&total_a)
852    });
853
854    let current_output_index = tx_builder.transaction.output.len();
855    tx_builder.runestone.pointer = Some(current_output_index as u32);
856
857    let mut index = current_output_index;
858    for amount_set in distribution.iter() {
859        tx_builder.transaction.output.push(TxOut {
860            value: Amount::from_sat(DUST_LIMIT),
861            script_pubkey: program_script_pubkey.clone(),
862        });
863
864        if index > current_output_index {
865            for rune_amount in amount_set.iter() {
866                tx_builder.runestone.edicts.push(Edict {
867                    id: ordinals::RuneId {
868                        block: rune_amount.id.block,
869                        tx: rune_amount.id.tx,
870                    },
871                    amount: rune_amount.amount,
872                    output: index as u32,
873                });
874            }
875        }
876
877        index += 1;
878    }
879
880    Ok(distribution)
881}
882
883#[cfg(test)]
884mod tests_loader {
885    use super::super::tests::common::{
886        create_btc_utxo, create_shard, leak_loaders_from_vec, MockShardZc,
887    };
888    use super::*;
889    // use crate::shard_set::ShardSet;
890    use satellite_bitcoin::utxo_info::SingleRuneSet;
891    use satellite_lang::prelude::AccountLoader;
892    use std::cell::Ref;
893
894    // Re-export for macro reuse
895    use satellite_bitcoin::TransactionBuilder as TB;
896
897    #[allow(unused_macros)]
898    macro_rules! new_tb {
899        ($max_modified_accounts:expr, $max_inputs_to_sign:expr) => {
900            TB::<$max_modified_accounts, $max_inputs_to_sign, SingleRuneSet>::new()
901        };
902    }
903
904    /// Helper function to create Ref instances directly from loaders for selected indices
905    pub fn create_shard_refs_from_loaders<'info>(
906        loaders: &'info [AccountLoader<'info, MockShardZc>],
907        indices: &[usize],
908    ) -> Result<Vec<Ref<'info, MockShardZc>>, arch_program::program_error::ProgramError> {
909        let mut refs = Vec::new();
910        for &idx in indices {
911            refs.push(loaders[idx].load()?);
912        }
913        Ok(refs)
914    }
915
916    mod plan_btc_distribution_among_shards {
917        use super::super::super::split;
918
919        use super::*;
920        use satellite_bitcoin::{constants::DUST_LIMIT, utxo_info::SingleRuneSet};
921        use split::plan_btc_distribution_among_shards;
922
923        #[test]
924        fn proportional_distribution_insufficient_remaining() {
925            const MAX_MODIFIED_ACCOUNTS: usize = 0;
926            const MAX_INPUTS_TO_SIGN: usize = 3;
927            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
928
929            // Shards with 100,200,300 sats respectively
930            let shards: Vec<MockShardZc> =
931                vec![create_shard(100), create_shard(200), create_shard(300)];
932            let loaders = leak_loaders_from_vec(shards);
933            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
934
935            // Remaining amount smaller than dust → expect empty dist
936            let dist = plan_btc_distribution_among_shards::<
937                MAX_MODIFIED_ACCOUNTS,
938                MAX_INPUTS_TO_SIGN,
939                SingleRuneSet,
940                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
941                MockShardZc,
942            >(&tx_builder, &shard_refs, 150u128);
943            assert!(matches!(dist, Err(DistributionError::TotalBelowDustLimit)));
944        }
945
946        #[test]
947        fn zero_remaining_amount() {
948            const MAX_MODIFIED_ACCOUNTS: usize = 0;
949            const MAX_INPUTS_TO_SIGN: usize = 2;
950            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
951
952            let shards = vec![create_shard(1_000), create_shard(2_000)];
953            let loaders = leak_loaders_from_vec(shards);
954            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
955
956            let dist = plan_btc_distribution_among_shards::<
957                MAX_MODIFIED_ACCOUNTS,
958                MAX_INPUTS_TO_SIGN,
959                SingleRuneSet,
960                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
961                MockShardZc,
962            >(&tx_builder, &shard_refs, 0u128)
963            .unwrap();
964            assert!(dist.is_empty());
965        }
966
967        #[test]
968        fn single_shard() {
969            const MAX_MODIFIED_ACCOUNTS: usize = 0;
970            const MAX_INPUTS_TO_SIGN: usize = 1;
971            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
972
973            let shards = vec![create_shard(500)];
974            let loaders = leak_loaders_from_vec(shards);
975            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
976
977            let dist = plan_btc_distribution_among_shards::<
978                MAX_MODIFIED_ACCOUNTS,
979                MAX_INPUTS_TO_SIGN,
980                SingleRuneSet,
981                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
982                MockShardZc,
983            >(&tx_builder, &shard_refs, 1_000u128)
984            .unwrap();
985
986            assert_eq!(dist, vec![1_000]);
987        }
988
989        #[test]
990        fn empty_shards_all_zero_balances() {
991            const MAX_MODIFIED_ACCOUNTS: usize = 0;
992            const MAX_INPUTS_TO_SIGN: usize = 3;
993            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
994
995            let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
996            let loaders = leak_loaders_from_vec(shards);
997            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
998
999            let dist = plan_btc_distribution_among_shards::<
1000                MAX_MODIFIED_ACCOUNTS,
1001                MAX_INPUTS_TO_SIGN,
1002                SingleRuneSet,
1003                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1004                MockShardZc,
1005            >(&tx_builder, &shard_refs, 1_500u128)
1006            .unwrap();
1007
1008            assert_eq!(dist, vec![1_500]);
1009        }
1010
1011        #[test]
1012        fn remainder_distribution_sub_dust_merge() {
1013            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1014            const MAX_INPUTS_TO_SIGN: usize = 3;
1015            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1016
1017            let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1018            let loaders = leak_loaders_from_vec(shards);
1019            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1020
1021            let amount = 1_001u128;
1022            let dist = plan_btc_distribution_among_shards::<
1023                MAX_MODIFIED_ACCOUNTS,
1024                MAX_INPUTS_TO_SIGN,
1025                SingleRuneSet,
1026                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1027                MockShardZc,
1028            >(&tx_builder, &shard_refs, amount)
1029            .unwrap();
1030            assert_eq!(dist.iter().sum::<u128>(), amount);
1031            assert_eq!(dist, vec![amount]);
1032        }
1033
1034        #[test]
1035        fn used_utxos_excluded() {
1036            use bitcoin::{transaction::Version, OutPoint, ScriptBuf, Sequence, TxIn, Witness};
1037
1038            const MAX_MODIFIED_ACCOUNTS: usize = 1;
1039            const MAX_INPUTS_TO_SIGN: usize = 2;
1040
1041            let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1042
1043            // Shards with 1_000 sats each
1044            let shard1 = create_shard(1_000);
1045            let shard2 = create_shard(1_000);
1046
1047            // Capture meta before loader creation via trait method
1048            let used_meta = shard1.btc_utxos()[0].meta;
1049
1050            let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1051            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1052
1053            // Mark first shard's utxo as spent
1054            tx_builder.transaction.version = Version::TWO;
1055            tx_builder.transaction.input.push(TxIn {
1056                previous_output: OutPoint::new(used_meta.to_txid(), used_meta.vout()),
1057                script_sig: ScriptBuf::new(),
1058                sequence: Sequence::MAX,
1059                witness: Witness::new(),
1060            });
1061
1062            let dist = plan_btc_distribution_among_shards::<
1063                MAX_MODIFIED_ACCOUNTS,
1064                MAX_INPUTS_TO_SIGN,
1065                SingleRuneSet,
1066                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1067                MockShardZc,
1068            >(&tx_builder, &shard_refs, 1_000u128)
1069            .unwrap();
1070
1071            assert_eq!(dist, vec![1_000]);
1072        }
1073
1074        #[test]
1075        fn partial_shard_selection() {
1076            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1077            const MAX_INPUTS_TO_SIGN: usize = 4;
1078            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1079
1080            let shards = vec![
1081                create_shard(1_000),
1082                create_shard(2_000),
1083                create_shard(3_000),
1084                create_shard(4_000),
1085            ];
1086            let loaders = leak_loaders_from_vec(shards);
1087            let shard_refs = create_shard_refs_from_loaders(&loaders, &[1, 2]).unwrap();
1088
1089            let dist = plan_btc_distribution_among_shards::<
1090                MAX_MODIFIED_ACCOUNTS,
1091                MAX_INPUTS_TO_SIGN,
1092                SingleRuneSet,
1093                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1094                MockShardZc,
1095            >(&tx_builder, &shard_refs, 2_000u128)
1096            .unwrap();
1097
1098            assert_eq!(dist.iter().sum::<u128>(), 2_000);
1099            assert_eq!(dist, vec![2_000]);
1100        }
1101
1102        #[test]
1103        fn large_numbers() {
1104            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1105            const MAX_INPUTS_TO_SIGN: usize = 2;
1106            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1107
1108            let shards = vec![create_shard(u64::MAX), create_shard(u64::MAX)];
1109            let loaders = leak_loaders_from_vec(shards);
1110            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1111
1112            let dist = plan_btc_distribution_among_shards::<
1113                MAX_MODIFIED_ACCOUNTS,
1114                MAX_INPUTS_TO_SIGN,
1115                SingleRuneSet,
1116                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1117                MockShardZc,
1118            >(&tx_builder, &shard_refs, 1_000u128)
1119            .unwrap();
1120
1121            assert_eq!(dist, vec![1_000]);
1122        }
1123
1124        #[test]
1125        fn split_remaining_amount_even_and_odd() {
1126            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1127            const MAX_INPUTS_TO_SIGN: usize = 2;
1128            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1129
1130            let shards = vec![create_shard(0), create_shard(0)];
1131            let loaders = leak_loaders_from_vec(shards);
1132            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1133
1134            // Odd amount
1135            let dist_odd = plan_btc_distribution_among_shards::<
1136                MAX_MODIFIED_ACCOUNTS,
1137                MAX_INPUTS_TO_SIGN,
1138                SingleRuneSet,
1139                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1140                MockShardZc,
1141            >(&tx_builder, &shard_refs, 2_041u128)
1142            .unwrap();
1143            assert_eq!(dist_odd, vec![1_021, 1_020]);
1144            assert_eq!(dist_odd.iter().sum::<u128>(), 2_041);
1145
1146            // Even amount
1147            let dist_even = plan_btc_distribution_among_shards::<
1148                MAX_MODIFIED_ACCOUNTS,
1149                MAX_INPUTS_TO_SIGN,
1150                SingleRuneSet,
1151                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1152                MockShardZc,
1153            >(&tx_builder, &shard_refs, 2_000u128)
1154            .unwrap();
1155            assert_eq!(dist_even, vec![1_000, 1_000]);
1156        }
1157
1158        #[test]
1159        fn split_remaining_amount_with_existing_balances() {
1160            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1161            const MAX_INPUTS_TO_SIGN: usize = 2;
1162            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1163
1164            let shards = vec![create_shard(1_000), create_shard(0)];
1165            let loaders = leak_loaders_from_vec(shards);
1166            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1167
1168            let dist = plan_btc_distribution_among_shards::<
1169                MAX_MODIFIED_ACCOUNTS,
1170                MAX_INPUTS_TO_SIGN,
1171                SingleRuneSet,
1172                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1173                MockShardZc,
1174            >(&tx_builder, &shard_refs, 2_041u128)
1175            .unwrap();
1176
1177            assert_eq!(dist.iter().sum::<u128>(), 2_041);
1178            assert_eq!(dist, vec![2_041]);
1179        }
1180
1181        #[test]
1182        fn single_shard_sub_dust_amount() {
1183            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1184            const MAX_INPUTS_TO_SIGN: usize = 1;
1185            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1186
1187            let shards = vec![create_shard(0)];
1188            let loaders = leak_loaders_from_vec(shards);
1189            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
1190
1191            let dist = plan_btc_distribution_among_shards::<
1192                MAX_MODIFIED_ACCOUNTS,
1193                MAX_INPUTS_TO_SIGN,
1194                SingleRuneSet,
1195                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1196                MockShardZc,
1197            >(&tx_builder, &shard_refs, (DUST_LIMIT as u128) - 1u128);
1198            assert!(matches!(dist, Err(DistributionError::TotalBelowDustLimit)));
1199        }
1200
1201        #[test]
1202        fn single_shard_exact_dust_limit() {
1203            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1204            const MAX_INPUTS_TO_SIGN: usize = 1;
1205            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1206
1207            let shards = vec![create_shard(0)];
1208            let loaders = leak_loaders_from_vec(shards);
1209            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
1210
1211            let dist = plan_btc_distribution_among_shards::<
1212                MAX_MODIFIED_ACCOUNTS,
1213                MAX_INPUTS_TO_SIGN,
1214                SingleRuneSet,
1215                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1216                MockShardZc,
1217            >(&tx_builder, &shard_refs, DUST_LIMIT as u128)
1218            .unwrap();
1219
1220            assert_eq!(dist, vec![DUST_LIMIT as u128]);
1221        }
1222
1223        #[test]
1224        fn two_shards_each_exact_dust_limit() {
1225            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1226            const MAX_INPUTS_TO_SIGN: usize = 2;
1227            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1228
1229            let shards = vec![create_shard(0), create_shard(0)];
1230            let loaders = leak_loaders_from_vec(shards);
1231            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1232
1233            let amount = (DUST_LIMIT as u128) * 2u128;
1234            let dist = plan_btc_distribution_among_shards::<
1235                MAX_MODIFIED_ACCOUNTS,
1236                MAX_INPUTS_TO_SIGN,
1237                SingleRuneSet,
1238                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1239                MockShardZc,
1240            >(&tx_builder, &shard_refs, amount)
1241            .unwrap();
1242
1243            assert_eq!(dist, vec![DUST_LIMIT as u128, DUST_LIMIT as u128]);
1244        }
1245
1246        #[test]
1247        fn mixed_dust_and_non_dust_allocations() {
1248            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1249            const MAX_INPUTS_TO_SIGN: usize = 3;
1250            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1251
1252            let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1253            let loaders = leak_loaders_from_vec(shards);
1254            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1255
1256            let amount = 1_600u128; // provisional 533/533/534 (< dust)
1257            let dist = plan_btc_distribution_among_shards::<
1258                MAX_MODIFIED_ACCOUNTS,
1259                MAX_INPUTS_TO_SIGN,
1260                SingleRuneSet,
1261                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1262                MockShardZc,
1263            >(&tx_builder, &shard_refs, amount)
1264            .unwrap();
1265
1266            assert_eq!(dist, vec![amount]);
1267        }
1268    }
1269
1270    // ---------------------------------------------------------------
1271    // compute_unsettled_btc_in_shards --------------------------------
1272    // ---------------------------------------------------------------
1273    mod compute_unsettled_btc_in_shards {
1274        use super::super::compute_unsettled_btc_in_shards;
1275        use super::*;
1276        use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, Witness};
1277        use satellite_bitcoin::fee_rate::FeeRate;
1278
1279        #[test]
1280        fn basic_unsettled_calculation() {
1281            const MAX_MODIFIED_ACCOUNTS: usize = 2;
1282            const MAX_INPUTS_TO_SIGN: usize = 2;
1283
1284            let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1285
1286            // Two shards with 1_000 and 500 sats respectively
1287            let shard1 = create_shard(1_000);
1288            let shard2 = create_shard(500);
1289
1290            // Capture meta before moving shards into loaders
1291            let spent_meta = shard1.btc_utxos()[0].meta;
1292
1293            let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1294            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1295
1296            // Spend shard 0's UTXO in the transaction
1297            tx_builder.transaction.input.push(TxIn {
1298                previous_output: OutPoint::new(spent_meta.to_txid(), spent_meta.vout()),
1299                script_sig: ScriptBuf::new(),
1300                sequence: Sequence::MAX,
1301                witness: Witness::new(),
1302            });
1303
1304            let unsettled = compute_unsettled_btc_in_shards::<
1305                MAX_MODIFIED_ACCOUNTS,
1306                MAX_INPUTS_TO_SIGN,
1307                SingleRuneSet,
1308                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1309                MockShardZc,
1310            >(&tx_builder, &shard_refs, 1_000, &FeeRate(1.0))
1311            .unwrap();
1312
1313            // Only shard 0's 1000 sats are unsettled (shard 1 untouched)
1314            assert_eq!(unsettled, 500);
1315        }
1316    }
1317
1318    // ---------------------------------------------------------------
1319    // Edge-case helpers & stress tests --------------------------------
1320    // ---------------------------------------------------------------
1321    mod edge_cases {
1322        use super::super::super::tests::common::add_btc_utxos_bulk;
1323        use super::super::super::tests::common::random_utxo_meta;
1324        use super::super::{
1325            balance_amount_across_shards as balance_loader, compute_unsettled_btc_in_shards,
1326            plan_btc_distribution_among_shards, redistribute_sub_dust_values,
1327        };
1328        use super::*;
1329        use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, Witness};
1330        use satellite_bitcoin::MathError;
1331        use satellite_bitcoin::{constants::DUST_LIMIT, fee_rate::FeeRate};
1332        use satellite_lang::prelude::AccountLoader;
1333
1334        // ---- redistribute_sub_dust_values tests ----
1335        #[test]
1336        fn redistribute_sub_dust_all_above_dust() {
1337            let mut amounts = vec![1000u128, 2000u128, 3000u128];
1338            let original = amounts.clone();
1339            redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
1340            assert_eq!(amounts, original);
1341        }
1342
1343        #[test]
1344        fn redistribute_sub_dust_all_below_but_sum_above() {
1345            let mut amounts = vec![200u128, 200u128, 200u128];
1346            redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
1347            assert_eq!(amounts, vec![600u128]);
1348        }
1349
1350        #[test]
1351        fn redistribute_sub_dust_mixed_with_remainder() {
1352            let mut amounts = vec![1000u128, 200u128, 300u128, 2000u128]; // 200+300 below dust
1353            redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
1354            assert_eq!(amounts.len(), 2);
1355            assert_eq!(amounts.iter().sum::<u128>(), 3500u128);
1356            assert!(amounts.contains(&1250u128));
1357            assert!(amounts.contains(&2250u128));
1358        }
1359
1360        #[test]
1361        fn redistribute_sub_dust_total_below_dust_returns_error() {
1362            let mut amounts = vec![200u128, 300u128]; // total 500 < dust
1363            let res = redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128);
1364            assert!(matches!(
1365                res,
1366                Err(super::super::DistributionError::TotalBelowDustLimit)
1367            ));
1368        }
1369
1370        // ---- zero-shard behaviour ----
1371        #[test]
1372        fn plan_btc_distribution_zero_shards() {
1373            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1374            const MAX_INPUTS_TO_SIGN: usize = 0;
1375            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1376
1377            // Empty loaders slice
1378            let loaders: &[AccountLoader<'static, MockShardZc>] = &[];
1379            let shard_refs = create_shard_refs_from_loaders(&loaders, &[]).unwrap();
1380
1381            let result = plan_btc_distribution_among_shards::<
1382                MAX_MODIFIED_ACCOUNTS,
1383                MAX_INPUTS_TO_SIGN,
1384                SingleRuneSet,
1385                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1386                MockShardZc,
1387            >(&tx_builder, &shard_refs, 1_000u128);
1388
1389            assert!(matches!(
1390                result,
1391                Err(DistributionError::Math(MathError::DivisionOverflow))
1392            ));
1393        }
1394
1395        // ---- max-capacity stress ----
1396        #[test]
1397        fn max_capacity_stress() {
1398            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1399            const MAX_INPUTS_TO_SIGN: usize = 10;
1400            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1401
1402            // Build 10 shards, each with 5 × 1_000-sat UTXOs
1403            let shards: Vec<MockShardZc> = (0..MAX_INPUTS_TO_SIGN)
1404                .map(|i| {
1405                    let mut s = create_shard(0);
1406                    let values = vec![1_000u64; 5];
1407                    add_btc_utxos_bulk(&mut s, &values);
1408                    // tweak vout base by index to make metas unique
1409                    if i > 0 {
1410                        // Already sequential in helper but fine
1411                    }
1412                    s
1413                })
1414                .collect();
1415
1416            let loaders = leak_loaders_from_vec(shards);
1417            let shard_refs =
1418                create_shard_refs_from_loaders(&loaders, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap();
1419
1420            let dist = plan_btc_distribution_among_shards::<
1421                MAX_MODIFIED_ACCOUNTS,
1422                MAX_INPUTS_TO_SIGN,
1423                SingleRuneSet,
1424                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1425                MockShardZc,
1426            >(&tx_builder, &shard_refs, 10_000u128)
1427            .unwrap();
1428
1429            assert_eq!(dist.iter().sum::<u128>(), 10_000u128);
1430        }
1431
1432        // ---- near-boundary dust split cases ----
1433        #[test]
1434        fn near_boundary_dust_splits_below() {
1435            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1436            const MAX_INPUTS_TO_SIGN: usize = 3;
1437            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1438
1439            let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1440            let loaders = leak_loaders_from_vec(shards);
1441            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1442
1443            let amount = (DUST_LIMIT as u128) * 3 - 1u128;
1444            let dist = plan_btc_distribution_among_shards::<
1445                MAX_MODIFIED_ACCOUNTS,
1446                MAX_INPUTS_TO_SIGN,
1447                SingleRuneSet,
1448                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1449                MockShardZc,
1450            >(&tx_builder, &shard_refs, amount)
1451            .unwrap();
1452
1453            assert!(dist.len() < 3);
1454            assert_eq!(dist.iter().sum::<u128>(), amount);
1455        }
1456
1457        #[test]
1458        fn near_boundary_dust_splits_above() {
1459            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1460            const MAX_INPUTS_TO_SIGN: usize = 3;
1461            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1462
1463            let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1464            let loaders = leak_loaders_from_vec(shards);
1465            let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1466
1467            let amount = (DUST_LIMIT as u128) * 3 + 1u128;
1468            let dist = plan_btc_distribution_among_shards::<
1469                MAX_MODIFIED_ACCOUNTS,
1470                MAX_INPUTS_TO_SIGN,
1471                SingleRuneSet,
1472                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1473                MockShardZc,
1474            >(&tx_builder, &shard_refs, amount)
1475            .unwrap();
1476
1477            assert_eq!(dist.len(), 3);
1478            assert!(dist.iter().all(|&x| x >= DUST_LIMIT as u128));
1479            assert_eq!(dist.iter().sum::<u128>(), amount);
1480        }
1481
1482        // ---- duplicate meta across shards ----
1483        #[test]
1484        fn duplicate_meta_utxos_across_shards() {
1485            const MAX_MODIFIED_ACCOUNTS: usize = 1;
1486            const MAX_INPUTS_TO_SIGN: usize = 2;
1487
1488            let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1489
1490            // Build two UTXOs with IDENTICAL meta but different values
1491            let shared_meta = random_utxo_meta(42);
1492            let utxo1 = create_btc_utxo(1_000, 42);
1493            let mut utxo2 = create_btc_utxo(2_000, 42); // same meta
1494            utxo2.meta = shared_meta; // ensure identical even if helper differs
1495
1496            let mut shard1 = create_shard(0);
1497            let mut shard2 = create_shard(0);
1498            shard1.add_btc_utxo(utxo1);
1499            shard2.add_btc_utxo(utxo2);
1500
1501            let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1502            // Load shard references directly (indices 0 and 1)
1503            let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1504
1505            // Spend the shared UTXO in the tx
1506            tx_builder.transaction.input.push(TxIn {
1507                previous_output: OutPoint::new(shared_meta.to_txid(), shared_meta.vout()),
1508                script_sig: ScriptBuf::new(),
1509                sequence: Sequence::MAX,
1510                witness: Witness::new(),
1511            });
1512
1513            let unsettled = compute_unsettled_btc_in_shards::<
1514                MAX_MODIFIED_ACCOUNTS,
1515                MAX_INPUTS_TO_SIGN,
1516                SingleRuneSet,
1517                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1518                MockShardZc,
1519            >(&tx_builder, &shard_refs, 0, &FeeRate(1.0))
1520            .unwrap();
1521
1522            assert_eq!(unsettled, 3_000);
1523        }
1524
1525        // ---- high fee overflow handling ----
1526        #[test]
1527        fn high_fee_scenario_overflow() {
1528            use arch_program::rune::{RuneAmount, RuneId};
1529            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1530            const MAX_INPUTS_TO_SIGN: usize = 1;
1531
1532            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1533
1534            let shard = create_shard(0);
1535            let loaders = leak_loaders_from_vec(vec![shard]);
1536            let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
1537
1538            // Add a huge rune amount -> expect overflow handled gracefully (Err)
1539            let rune_amount = RuneAmount {
1540                id: RuneId::BTC,
1541                amount: u128::MAX,
1542            };
1543            let result = balance_loader::<
1544                MAX_MODIFIED_ACCOUNTS,
1545                MAX_INPUTS_TO_SIGN,
1546                SingleRuneSet,
1547                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1548                MockShardZc,
1549            >(&tx_builder, &shard_refs, &rune_amount);
1550
1551            // Should succeed and return the full allocation for the single shard.
1552            assert_eq!(result.unwrap(), vec![u128::MAX]);
1553        }
1554
1555        // ---- empty amount optimisation ----
1556        #[test]
1557        fn empty_amount_optimization() {
1558            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1559            const MAX_INPUTS_TO_SIGN: usize = 2;
1560
1561            let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1562
1563            // preload some outputs
1564            let original_outputs = tx_builder.transaction.output.len();
1565
1566            let shards = vec![create_shard(1_000), create_shard(2_000)];
1567            let loaders = leak_loaders_from_vec(shards);
1568            let mut shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1569
1570            let dist = super::super::redistribute_remaining_btc_to_shards::<
1571                MAX_MODIFIED_ACCOUNTS,
1572                MAX_INPUTS_TO_SIGN,
1573                SingleRuneSet,
1574                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1575                MockShardZc,
1576            >(
1577                &mut tx_builder,
1578                &mut shard_refs,
1579                0,
1580                &ScriptBuf::new(),
1581                &FeeRate(1.0),
1582            )
1583            .unwrap();
1584
1585            assert!(dist.is_empty());
1586            assert_eq!(tx_builder.transaction.output.len(), original_outputs);
1587        }
1588
1589        // ---- overflow protection in balance_amount_across_shards ----
1590        #[test]
1591        fn balance_amount_overflow_protection() {
1592            use arch_program::rune::{RuneAmount, RuneId};
1593            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1594            const MAX_INPUTS_TO_SIGN: usize = 2;
1595            let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1596
1597            // shards with u64::MAX utxos
1598            let mut shard1 = create_shard(0);
1599            let mut shard2 = create_shard(0);
1600            shard1.add_btc_utxo(create_btc_utxo(u64::MAX, 1));
1601            shard2.add_btc_utxo(create_btc_utxo(u64::MAX, 2));
1602
1603            let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1604            let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1605
1606            let rune_amount = RuneAmount {
1607                id: RuneId::BTC,
1608                amount: u128::MAX,
1609            };
1610            let res = balance_loader::<
1611                MAX_MODIFIED_ACCOUNTS,
1612                MAX_INPUTS_TO_SIGN,
1613                SingleRuneSet,
1614                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1615                MockShardZc,
1616            >(&tx_builder, &shard_refs, &rune_amount);
1617
1618            assert!(res.is_err());
1619        }
1620
1621        // ---- runestone pointer update (Rune feature) ----
1622        #[cfg(feature = "runes")]
1623        #[test]
1624        fn runestone_pointer_update() {
1625            use bitcoin::{Amount, TxOut};
1626
1627            const MAX_MODIFIED_ACCOUNTS: usize = 0;
1628            const MAX_INPUTS_TO_SIGN: usize = 2;
1629
1630            let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1631
1632            // Pre-existing outputs to simulate prior transaction state.
1633            tx_builder.transaction.output.push(TxOut {
1634                value: Amount::from_sat(1_000),
1635                script_pubkey: ScriptBuf::new(),
1636            });
1637            tx_builder.transaction.output.push(TxOut {
1638                value: Amount::from_sat(2_000),
1639                script_pubkey: ScriptBuf::new(),
1640            });
1641
1642            let old_output_count = tx_builder.transaction.output.len();
1643
1644            // Two empty shards (no BTC / Rune UTXOs needed for this test)
1645            let shards = vec![create_shard(0), create_shard(0)];
1646            let loaders = leak_loaders_from_vec(shards);
1647            let mut shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1648
1649            // Invoke the rune redistribution helper (no runes to distribute)
1650            crate::split::redistribute_remaining_rune_to_shards::<
1651                MAX_MODIFIED_ACCOUNTS,
1652                MAX_INPUTS_TO_SIGN,
1653                SingleRuneSet,
1654                satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1655                MockShardZc,
1656            >(
1657                &mut tx_builder,
1658                &mut shard_refs,
1659                SingleRuneSet::default(),
1660                ScriptBuf::new(),
1661            )
1662            .unwrap();
1663
1664            // Pointer should now reference the first newly added output.
1665            assert_eq!(tx_builder.runestone.pointer, Some(old_output_count as u32));
1666
1667            // Any generated edicts (if present) must point to subsequent outputs.
1668            for (i, edict) in tx_builder.runestone.edicts.iter().enumerate() {
1669                if i > 0 {
1670                    assert_eq!(edict.output, (old_output_count + i) as u32);
1671                }
1672            }
1673        }
1674    }
1675}
1676
1677// -------------------------------------------------------------------------
1678// Rune-specific test suite (requires `--features runes`)
1679// -------------------------------------------------------------------------
1680#[cfg(all(test, feature = "runes"))]
1681mod rune_tests_loader {
1682    use super::*;
1683    // use crate::shard_set::ShardSet;
1684    use crate::tests::common::{
1685        create_rune_utxo, create_shard, leak_loaders_from_vec, MockShardZc,
1686    };
1687    use arch_program::rune::{RuneAmount, RuneId};
1688    use bitcoin::ScriptBuf;
1689    use satellite_bitcoin::utxo_info::SingleRuneSet;
1690    use satellite_bitcoin::TransactionBuilder as TB;
1691
1692    #[allow(unused_macros)]
1693    macro_rules! new_tb {
1694        ($max_utxos:expr, $max_shards:expr) => {
1695            TB::<$max_utxos, $max_shards, SingleRuneSet>::new()
1696        };
1697    }
1698
1699    // ---------------------------------------------------------------
1700    // compute_unsettled_rune_in_shards ------------------------------
1701    // ---------------------------------------------------------------
1702    #[test]
1703    fn compute_unsettled_rune_basic() {
1704        const MAX_MODIFIED_ACCOUNTS: usize = 0;
1705        const MAX_INPUTS_TO_SIGN: usize = 2;
1706
1707        // Two shards with 100 and 50 runes respectively
1708        let mut shard1 = create_shard(0);
1709        let mut shard2 = create_shard(0);
1710        shard1.set_rune_utxo(create_rune_utxo(100, 0));
1711        shard2.set_rune_utxo(create_rune_utxo(50, 1));
1712
1713        let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1714        let shard_refs =
1715            super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1716
1717        let unsettled = crate::split::compute_unsettled_rune_in_shards::<
1718            SingleRuneSet,
1719            satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1720            MockShardZc,
1721        >(&shard_refs, SingleRuneSet::default())
1722        .unwrap();
1723
1724        assert_eq!(unsettled.find(&RuneId::new(1, 1)).unwrap().amount, 150);
1725    }
1726
1727    // ---------------------------------------------------------------
1728    // plan_rune_distribution_among_shards ---------------------------
1729    // ---------------------------------------------------------------
1730    #[test]
1731    fn plan_rune_distribution_proportional() {
1732        const MAX_MODIFIED_ACCOUNTS: usize = 0;
1733        const MAX_INPUTS_TO_SIGN: usize = 3;
1734
1735        let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1736
1737        // Existing rune balances: 100, 200, 300
1738        let mut shard0 = create_shard(0);
1739        let mut shard1 = create_shard(0);
1740        let mut shard2 = create_shard(0);
1741        shard0.set_rune_utxo(create_rune_utxo(100, 0));
1742        shard1.set_rune_utxo(create_rune_utxo(200, 1));
1743        shard2.set_rune_utxo(create_rune_utxo(300, 2));
1744
1745        let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1746        let shard_refs =
1747            super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1748
1749        // Distribute 600 runes proportionally
1750        let mut target = SingleRuneSet::default();
1751        target
1752            .insert(RuneAmount {
1753                id: RuneId::new(1, 1),
1754                amount: 600,
1755            })
1756            .unwrap();
1757
1758        let dist = crate::split::plan_rune_distribution_among_shards::<
1759            MAX_MODIFIED_ACCOUNTS,
1760            MAX_INPUTS_TO_SIGN,
1761            SingleRuneSet,
1762            satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1763            MockShardZc,
1764        >(&mut tx_builder, &shard_refs, &target)
1765        .unwrap();
1766
1767        assert_eq!(dist.len(), 3);
1768        let allocs: Vec<u128> = dist
1769            .iter()
1770            .map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
1771            .collect();
1772        assert_eq!(allocs, vec![300, 200, 100]);
1773    }
1774
1775    #[test]
1776    fn plan_rune_distribution_zero_amount_inserts_zero_entries() {
1777        const MAX_MODIFIED_ACCOUNTS: usize = 0;
1778        const MAX_INPUTS_TO_SIGN: usize = 3;
1779
1780        let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1781
1782        // Existing rune balances: 100, 200, 300
1783        let mut shard0 = create_shard(0);
1784        let mut shard1 = create_shard(0);
1785        let mut shard2 = create_shard(0);
1786        shard0.set_rune_utxo(create_rune_utxo(100, 0));
1787        shard1.set_rune_utxo(create_rune_utxo(200, 1));
1788        shard2.set_rune_utxo(create_rune_utxo(300, 2));
1789
1790        let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1791        let shard_refs =
1792            super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1793
1794        // Distribute 0 runes; ensure each shard still gets an entry with amount 0
1795        let mut target = SingleRuneSet::default();
1796        target
1797            .insert(RuneAmount {
1798                id: RuneId::new(1, 1),
1799                amount: 0,
1800            })
1801            .unwrap();
1802
1803        let dist = crate::split::plan_rune_distribution_among_shards::<
1804            MAX_MODIFIED_ACCOUNTS,
1805            MAX_INPUTS_TO_SIGN,
1806            SingleRuneSet,
1807            satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1808            MockShardZc,
1809        >(&mut tx_builder, &shard_refs, &target)
1810        .unwrap();
1811
1812        assert_eq!(dist.len(), 3);
1813        for s in dist.iter() {
1814            let r = s.find(&RuneId::new(1, 1)).expect("rune entry present");
1815            assert_eq!(r.amount, 0);
1816            assert_eq!(s.len(), 1);
1817        }
1818    }
1819
1820    #[test]
1821    fn plan_rune_distribution_partial_creates_zero_entry_for_some_shards() {
1822        const MAX_MODIFIED_ACCOUNTS: usize = 0;
1823        const MAX_INPUTS_TO_SIGN: usize = 3;
1824
1825        let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1826
1827        // Existing rune balances: 100, 200, 300
1828        let mut shard0 = create_shard(0);
1829        let mut shard1 = create_shard(0);
1830        let mut shard2 = create_shard(0);
1831        shard0.set_rune_utxo(create_rune_utxo(100, 0));
1832        shard1.set_rune_utxo(create_rune_utxo(200, 1));
1833        shard2.set_rune_utxo(create_rune_utxo(300, 2));
1834
1835        let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1836        let shard_refs =
1837            super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1838
1839        // Distribute a small amount; at least one shard should receive 0
1840        let mut target = SingleRuneSet::default();
1841        target
1842            .insert(RuneAmount {
1843                id: RuneId::new(1, 1),
1844                amount: 10,
1845            })
1846            .unwrap();
1847
1848        let dist = crate::split::plan_rune_distribution_among_shards::<
1849            MAX_MODIFIED_ACCOUNTS,
1850            MAX_INPUTS_TO_SIGN,
1851            SingleRuneSet,
1852            satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1853            MockShardZc,
1854        >(&mut tx_builder, &shard_refs, &target)
1855        .unwrap();
1856
1857        assert_eq!(dist.len(), 3);
1858        let allocs: Vec<u128> = dist
1859            .iter()
1860            .map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
1861            .collect();
1862        assert!(allocs.contains(&0));
1863        for (i, amt) in allocs.iter().enumerate() {
1864            let r = dist[i].find(&RuneId::new(1, 1)).unwrap();
1865            assert_eq!(r.amount, *amt);
1866        }
1867    }
1868
1869    // ---------------------------------------------------------------
1870    // redistribute_remaining_rune_to_shards -------------------------
1871    // ---------------------------------------------------------------
1872    #[test]
1873    fn redistribute_remaining_rune_distribution() {
1874        const MAX_MODIFIED_ACCOUNTS: usize = 0;
1875        const MAX_INPUTS_TO_SIGN: usize = 3;
1876
1877        let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1878
1879        // Shards start with 100, 200, 300 runes
1880        let mut shard0 = create_shard(0);
1881        let mut shard1 = create_shard(0);
1882        let mut shard2 = create_shard(0);
1883        shard0.set_rune_utxo(create_rune_utxo(100, 0));
1884        shard1.set_rune_utxo(create_rune_utxo(200, 1));
1885        shard2.set_rune_utxo(create_rune_utxo(300, 2));
1886
1887        let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1888        let mut shard_refs =
1889            super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1890
1891        // Remove 150 runes total
1892        let mut removed = SingleRuneSet::default();
1893        removed
1894            .insert(RuneAmount {
1895                id: RuneId::new(1, 1),
1896                amount: 150,
1897            })
1898            .unwrap();
1899
1900        let dist = crate::split::redistribute_remaining_rune_to_shards::<
1901            MAX_MODIFIED_ACCOUNTS,
1902            MAX_INPUTS_TO_SIGN,
1903            SingleRuneSet,
1904            satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1905            MockShardZc,
1906        >(&mut tx_builder, &mut shard_refs, removed, ScriptBuf::new())
1907        .unwrap();
1908
1909        // Expect proportional (75, 150, 225) regardless of ordering
1910        let mut allocs: Vec<u128> = dist
1911            .iter()
1912            .map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
1913            .collect();
1914        allocs.sort_unstable();
1915        assert_eq!(allocs, vec![50, 150, 250]);
1916    }
1917}