use std::cell::Ref;
use arch_program::{
msg,
rune::{RuneAmount, RuneId},
utxo::UtxoMeta,
};
use bitcoin::{Amount, ScriptBuf, TxOut};
use satellite_bitcoin::generic::fixed_set::FixedCapacitySet;
use satellite_bitcoin::{
constants::DUST_LIMIT, fee_rate::FeeRate, utxo_info::UtxoInfoTrait, TransactionBuilder,
};
use satellite_bitcoin::{safe_add, safe_div, safe_mul, safe_sub, MathError};
use super::error::StateShardError;
use super::StateShard;
#[cfg(feature = "runes")]
use ordinals::Edict;
use satellite_lang::prelude::Owner;
use satellite_lang::ZeroCopy;
#[derive(Debug, PartialEq)]
pub enum DistributionError {
TotalBelowDustLimit,
Math(MathError),
}
impl From<MathError> for DistributionError {
fn from(value: MathError) -> Self {
DistributionError::Math(value)
}
}
#[allow(clippy::too_many_arguments)]
pub fn redistribute_remaining_btc_to_shards<
'info,
const MAX_MODIFIED_ACCOUNTS: usize,
const MAX_INPUTS_TO_SIGN: usize,
RS,
U,
S,
>(
tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
selected_shards: &[Ref<'info, S>],
removed_from_shards: u64,
program_script_pubkey: &ScriptBuf,
fee_rate: &FeeRate,
) -> Result<Vec<u128>, DistributionError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let remaining_amount = compute_unsettled_btc_in_shards(
tx_builder,
selected_shards,
removed_from_shards,
fee_rate,
)?;
let mut distribution =
plan_btc_distribution_among_shards(tx_builder, selected_shards, remaining_amount as u128)?;
distribution.sort_by(|a, b| b.cmp(a));
for amount in distribution.iter() {
let txout = TxOut {
value: Amount::from_sat(*amount as u64),
script_pubkey: program_script_pubkey.clone(),
};
tx_builder.transaction.output.push(txout);
}
Ok(distribution)
}
pub fn compute_unsettled_btc_in_shards<
'info,
const MAX_MODIFIED_ACCOUNTS: usize,
const MAX_INPUTS_TO_SIGN: usize,
RS,
U,
S,
>(
tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
selected_shards: &[Ref<'info, S>],
removed_from_shards: u64,
fee_rate: &FeeRate,
) -> Result<u64, MathError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let mut total_btc_amount: u64 = 0;
let non_state_inputs = tx_builder.get_non_state_transition_inputs();
for shard in selected_shards.iter() {
let mut sum: u64 = 0;
for utxo in shard.btc_utxos().iter() {
for input in non_state_inputs.iter() {
let spent_meta =
UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout);
if spent_meta == *utxo.meta() {
sum = sum.saturating_add(utxo.value());
}
}
}
total_btc_amount = safe_add(total_btc_amount, sum)?;
}
let fee_paid_by_program = {
#[cfg(feature = "utxo-consolidation")]
{
tx_builder.get_fee_paid_by_program(fee_rate)
}
#[cfg(not(feature = "utxo-consolidation"))]
{
0
}
};
let remaining_amount = safe_sub(
safe_sub(total_btc_amount, removed_from_shards)?,
fee_paid_by_program,
)?;
Ok(remaining_amount)
}
fn plan_btc_distribution_among_shards<
'info,
const MAX_MODIFIED_ACCOUNTS: usize,
const MAX_INPUTS_TO_SIGN: usize,
RS,
U,
S,
>(
tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
selected_shards: &[Ref<'info, S>],
amount: u128,
) -> Result<Vec<u128>, DistributionError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
if amount == 0 {
return Ok(Vec::new());
}
let mut result = balance_amount_across_shards(
tx_builder,
selected_shards,
&RuneAmount {
id: RuneId::BTC,
amount,
},
)?;
redistribute_sub_dust_values(&mut result, DUST_LIMIT as u128)?;
Ok(result)
}
fn balance_amount_across_shards<
'info,
const MAX_MODIFIED_ACCOUNTS: usize,
const MAX_INPUTS_TO_SIGN: usize,
RS,
U,
S,
>(
tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
selected_shards: &[Ref<'info, S>],
rune_amount: &RuneAmount,
) -> Result<Vec<u128>, MathError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let num_shards = selected_shards.len();
let mut assigned_amounts: Vec<u128> = Vec::with_capacity(num_shards);
let mut total_current_amount: u128 = 0;
let non_state_inputs = tx_builder.get_non_state_transition_inputs();
let is_meta_spent = |meta: &UtxoMeta| {
non_state_inputs.iter().any(|input| {
let spent_meta =
UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout);
spent_meta == *meta
})
};
for shard in selected_shards.iter() {
let current_res = match rune_amount.id {
RuneId::BTC => shard
.btc_utxos()
.iter()
.filter_map(|u| {
if is_meta_spent(u.meta()) {
None
} else {
Some(u.value() as u128)
}
})
.sum(),
_ => {
#[cfg(feature = "runes")]
{
shard
.rune_utxo()
.filter(|u| !is_meta_spent(u.meta()))
.and_then(|u| u.runes().find(&rune_amount.id).map(|r| r.amount))
.unwrap_or(0)
}
#[cfg(not(feature = "runes"))]
{
0
}
}
};
assigned_amounts.push(current_res);
total_current_amount = safe_add(total_current_amount, current_res)?;
}
msg!("total_current_amount: {}", total_current_amount);
let total_after = safe_add(total_current_amount, rune_amount.amount)?;
let desired_per_shard = safe_div(total_after, num_shards as u128)?;
msg!("desired_per_shard: {}", desired_per_shard);
let mut total_needed = 0u128;
for current in assigned_amounts.iter_mut() {
let needed = if desired_per_shard > *current {
safe_sub(desired_per_shard, *current)?
} else {
0
};
total_needed = safe_add(total_needed, needed)?;
*current = needed;
}
if total_needed <= rune_amount.amount {
let leftover = safe_sub(rune_amount.amount, total_needed)?;
let per_shard_extra = safe_div(leftover, num_shards as u128)?;
let mut extra_left = leftover % num_shards as u128;
for amt in assigned_amounts.iter_mut() {
*amt = safe_add(*amt, per_shard_extra)?;
if extra_left > 0 {
*amt = safe_add(*amt, 1)?;
extra_left -= 1;
}
}
} else {
let mut cumulative = 0u128;
let mut cumulative_needed = 0u128;
for i in 0..num_shards {
let needed = assigned_amounts[i];
cumulative_needed = safe_add(cumulative_needed, needed)?;
let proportional = safe_mul(rune_amount.amount, cumulative_needed)? / total_needed;
assigned_amounts[i] = safe_sub(proportional, cumulative)?;
cumulative = proportional;
}
}
msg!("assigned_amounts: {:?}", assigned_amounts);
Ok(assigned_amounts)
}
fn redistribute_sub_dust_values(
amounts: &mut Vec<u128>,
dust_limit: u128,
) -> Result<(), DistributionError> {
let mut total_sum: u128 = 0;
for &amt in amounts.iter() {
total_sum = safe_add(total_sum, amt)?;
}
if total_sum > 0 && total_sum < dust_limit {
return Err(DistributionError::TotalBelowDustLimit);
}
let sum_of_small_amounts: u128 = amounts.iter().filter(|&&amount| amount < dust_limit).sum();
amounts.retain(|&amount| amount >= dust_limit);
if amounts.is_empty() {
if sum_of_small_amounts >= dust_limit {
amounts.push(sum_of_small_amounts);
} else {
amounts.clear();
}
return Ok(());
}
let num_amounts = amounts.len() as u128;
let to_add = safe_div(sum_of_small_amounts, num_amounts)?;
let mut remainder = sum_of_small_amounts % num_amounts;
for amount in amounts.iter_mut() {
*amount = safe_add(*amount, to_add)?;
if remainder > 0 {
*amount = safe_add(*amount, 1)?;
remainder -= 1;
}
}
Ok(())
}
#[cfg(feature = "runes")]
pub fn compute_unsettled_rune_in_shards<'info, RS, U, S>(
selected_shards: &[Ref<'info, S>],
removed_from_shards: RS,
) -> Result<RS, StateShardError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let mut total_rune_amount = RS::default();
for shard in selected_shards.iter() {
if let Some(utxo) = shard.rune_utxo() {
for rune in utxo.runes().iter() {
let _ = total_rune_amount.insert_or_modify::<StateShardError, _>(
RuneAmount {
id: rune.id,
amount: rune.amount,
},
|r| {
r.amount = safe_add(r.amount, rune.amount)
.map_err(|_| StateShardError::RuneAmountAdditionOverflow)?;
Ok(())
},
);
}
};
}
for rune in removed_from_shards.iter() {
if let Some(output_rune) = total_rune_amount.find_mut(&rune.id) {
output_rune.amount = safe_sub(output_rune.amount, rune.amount)
.map_err(|_| StateShardError::RemovingMoreRunesThanPresentInShards)?;
}
}
Ok(total_rune_amount)
}
#[cfg(feature = "runes")]
#[allow(clippy::too_many_arguments)]
pub fn plan_rune_distribution_among_shards<
'info,
const MAX_MODIFIED_ACCOUNTS: usize,
const MAX_INPUTS_TO_SIGN: usize,
RS,
U,
S,
>(
tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
selected_shards: &[Ref<'info, S>],
amounts: &RS,
) -> Result<Vec<RS>, StateShardError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let num_shards = selected_shards.len();
let mut result: Vec<RS> = (0..num_shards).map(|_| RS::default()).collect();
for rune_amount in amounts.iter() {
let allocs = balance_amount_across_shards(tx_builder, selected_shards, rune_amount)
.map_err(|_| StateShardError::MathErrorInBalanceAmountAcrossShards)?;
for (i, amount) in allocs.iter().enumerate() {
result[i].insert_or_modify::<StateShardError, _>(
RuneAmount {
id: rune_amount.id,
amount: *amount,
},
|r| {
r.amount = safe_add(r.amount, *amount)
.map_err(|_| StateShardError::RuneAmountAdditionOverflow)?;
Ok(())
},
)?;
}
}
Ok(result)
}
#[cfg(feature = "runes")]
#[allow(clippy::too_many_arguments)]
pub fn redistribute_remaining_rune_to_shards<
'info,
const MAX_MODIFIED_ACCOUNTS: usize,
const MAX_INPUTS_TO_SIGN: usize,
RS,
U,
S,
>(
tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
selected_shards: &[Ref<'info, S>],
removed_from_shards: RS,
program_script_pubkey: ScriptBuf,
) -> Result<Vec<RS>, StateShardError>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let remaining_amount = compute_unsettled_rune_in_shards(selected_shards, removed_from_shards)?;
let mut distribution =
plan_rune_distribution_among_shards(tx_builder, selected_shards, &remaining_amount)?;
distribution.sort_by(|a, b| {
let total_a: u128 = a.iter().map(|r| r.amount).sum();
let total_b: u128 = b.iter().map(|r| r.amount).sum();
total_b.cmp(&total_a)
});
let current_output_index = tx_builder.transaction.output.len();
tx_builder.runestone.pointer = Some(current_output_index as u32);
let mut index = current_output_index;
for amount_set in distribution.iter() {
tx_builder.transaction.output.push(TxOut {
value: Amount::from_sat(DUST_LIMIT),
script_pubkey: program_script_pubkey.clone(),
});
if index > current_output_index {
for rune_amount in amount_set.iter() {
tx_builder.runestone.edicts.push(Edict {
id: ordinals::RuneId {
block: rune_amount.id.block,
tx: rune_amount.id.tx,
},
amount: rune_amount.amount,
output: index as u32,
});
}
}
index += 1;
}
Ok(distribution)
}
#[cfg(test)]
mod tests_loader {
use super::super::tests::common::{
create_btc_utxo, create_shard, leak_loaders_from_vec, MockShardZc,
};
use super::*;
use satellite_bitcoin::utxo_info::SingleRuneSet;
use satellite_lang::prelude::AccountLoader;
use std::cell::Ref;
use satellite_bitcoin::TransactionBuilder as TB;
#[allow(unused_macros)]
macro_rules! new_tb {
($max_modified_accounts:expr, $max_inputs_to_sign:expr) => {
TB::<$max_modified_accounts, $max_inputs_to_sign, SingleRuneSet>::new()
};
}
pub fn create_shard_refs_from_loaders<'info>(
loaders: &'info [AccountLoader<'info, MockShardZc>],
indices: &[usize],
) -> Result<Vec<Ref<'info, MockShardZc>>, arch_program::program_error::ProgramError> {
let mut refs = Vec::new();
for &idx in indices {
refs.push(loaders[idx].load()?);
}
Ok(refs)
}
mod plan_btc_distribution_among_shards {
use super::super::super::split;
use super::*;
use satellite_bitcoin::{constants::DUST_LIMIT, utxo_info::SingleRuneSet};
use split::plan_btc_distribution_among_shards;
#[test]
fn proportional_distribution_insufficient_remaining() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards: Vec<MockShardZc> =
vec![create_shard(100), create_shard(200), create_shard(300)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 150u128);
assert!(matches!(dist, Err(DistributionError::TotalBelowDustLimit)));
}
#[test]
fn zero_remaining_amount() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(1_000), create_shard(2_000)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 0u128)
.unwrap();
assert!(dist.is_empty());
}
#[test]
fn single_shard() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 1;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(500)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 1_000u128)
.unwrap();
assert_eq!(dist, vec![1_000]);
}
#[test]
fn empty_shards_all_zero_balances() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 1_500u128)
.unwrap();
assert_eq!(dist, vec![1_500]);
}
#[test]
fn remainder_distribution_sub_dust_merge() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let amount = 1_001u128;
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, amount)
.unwrap();
assert_eq!(dist.iter().sum::<u128>(), amount);
assert_eq!(dist, vec![amount]);
}
#[test]
fn used_utxos_excluded() {
use bitcoin::{transaction::Version, OutPoint, ScriptBuf, Sequence, TxIn, Witness};
const MAX_MODIFIED_ACCOUNTS: usize = 1;
const MAX_INPUTS_TO_SIGN: usize = 2;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shard1 = create_shard(1_000);
let shard2 = create_shard(1_000);
let used_meta = shard1.btc_utxos()[0].meta;
let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
tx_builder.transaction.version = Version::TWO;
tx_builder.transaction.input.push(TxIn {
previous_output: OutPoint::new(used_meta.to_txid(), used_meta.vout()),
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
});
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 1_000u128)
.unwrap();
assert_eq!(dist, vec![1_000]);
}
#[test]
fn partial_shard_selection() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 4;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![
create_shard(1_000),
create_shard(2_000),
create_shard(3_000),
create_shard(4_000),
];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[1, 2]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 2_000u128)
.unwrap();
assert_eq!(dist.iter().sum::<u128>(), 2_000);
assert_eq!(dist, vec![2_000]);
}
#[test]
fn large_numbers() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(u64::MAX), create_shard(u64::MAX)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 1_000u128)
.unwrap();
assert_eq!(dist, vec![1_000]);
}
#[test]
fn split_remaining_amount_even_and_odd() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let dist_odd = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 2_041u128)
.unwrap();
assert_eq!(dist_odd, vec![1_021, 1_020]);
assert_eq!(dist_odd.iter().sum::<u128>(), 2_041);
let dist_even = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 2_000u128)
.unwrap();
assert_eq!(dist_even, vec![1_000, 1_000]);
}
#[test]
fn split_remaining_amount_with_existing_balances() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(1_000), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 2_041u128)
.unwrap();
assert_eq!(dist.iter().sum::<u128>(), 2_041);
assert_eq!(dist, vec![2_041]);
}
#[test]
fn single_shard_sub_dust_amount() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 1;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, (DUST_LIMIT as u128) - 1u128);
assert!(matches!(dist, Err(DistributionError::TotalBelowDustLimit)));
}
#[test]
fn single_shard_exact_dust_limit() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 1;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, DUST_LIMIT as u128)
.unwrap();
assert_eq!(dist, vec![DUST_LIMIT as u128]);
}
#[test]
fn two_shards_each_exact_dust_limit() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let amount = (DUST_LIMIT as u128) * 2u128;
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, amount)
.unwrap();
assert_eq!(dist, vec![DUST_LIMIT as u128, DUST_LIMIT as u128]);
}
#[test]
fn mixed_dust_and_non_dust_allocations() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let amount = 1_600u128; let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, amount)
.unwrap();
assert_eq!(dist, vec![amount]);
}
}
mod compute_unsettled_btc_in_shards {
use super::super::compute_unsettled_btc_in_shards;
use super::*;
use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, Witness};
use satellite_bitcoin::fee_rate::FeeRate;
#[test]
fn basic_unsettled_calculation() {
const MAX_MODIFIED_ACCOUNTS: usize = 2;
const MAX_INPUTS_TO_SIGN: usize = 2;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shard1 = create_shard(1_000);
let shard2 = create_shard(500);
let spent_meta = shard1.btc_utxos()[0].meta;
let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
tx_builder.transaction.input.push(TxIn {
previous_output: OutPoint::new(spent_meta.to_txid(), spent_meta.vout()),
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
});
let unsettled = compute_unsettled_btc_in_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 1_000, &FeeRate(1.0))
.unwrap();
assert_eq!(unsettled, 500);
}
}
mod edge_cases {
use super::super::super::tests::common::add_btc_utxos_bulk;
use super::super::super::tests::common::random_utxo_meta;
use super::super::{
balance_amount_across_shards as balance_loader, compute_unsettled_btc_in_shards,
plan_btc_distribution_among_shards, redistribute_sub_dust_values,
};
use super::*;
use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, Witness};
use satellite_bitcoin::MathError;
use satellite_bitcoin::{constants::DUST_LIMIT, fee_rate::FeeRate};
use satellite_lang::prelude::AccountLoader;
#[test]
fn redistribute_sub_dust_all_above_dust() {
let mut amounts = vec![1000u128, 2000u128, 3000u128];
let original = amounts.clone();
redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
assert_eq!(amounts, original);
}
#[test]
fn redistribute_sub_dust_all_below_but_sum_above() {
let mut amounts = vec![200u128, 200u128, 200u128];
redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
assert_eq!(amounts, vec![600u128]);
}
#[test]
fn redistribute_sub_dust_mixed_with_remainder() {
let mut amounts = vec![1000u128, 200u128, 300u128, 2000u128]; redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
assert_eq!(amounts.len(), 2);
assert_eq!(amounts.iter().sum::<u128>(), 3500u128);
assert!(amounts.contains(&1250u128));
assert!(amounts.contains(&2250u128));
}
#[test]
fn redistribute_sub_dust_total_below_dust_returns_error() {
let mut amounts = vec![200u128, 300u128]; let res = redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128);
assert!(matches!(
res,
Err(super::super::DistributionError::TotalBelowDustLimit)
));
}
#[test]
fn plan_btc_distribution_zero_shards() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 0;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let loaders: &[AccountLoader<'static, MockShardZc>] = &[];
let shard_refs = create_shard_refs_from_loaders(&loaders, &[]).unwrap();
let result = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 1_000u128);
assert!(matches!(
result,
Err(DistributionError::Math(MathError::DivisionOverflow))
));
}
#[test]
fn max_capacity_stress() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 10;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards: Vec<MockShardZc> = (0..MAX_INPUTS_TO_SIGN)
.map(|i| {
let mut s = create_shard(0);
let values = vec![1_000u64; 5];
add_btc_utxos_bulk(&mut s, &values);
if i > 0 {
}
s
})
.collect();
let loaders = leak_loaders_from_vec(shards);
let shard_refs =
create_shard_refs_from_loaders(&loaders, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap();
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 10_000u128)
.unwrap();
assert_eq!(dist.iter().sum::<u128>(), 10_000u128);
}
#[test]
fn near_boundary_dust_splits_below() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let amount = (DUST_LIMIT as u128) * 3 - 1u128;
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, amount)
.unwrap();
assert!(dist.len() < 3);
assert_eq!(dist.iter().sum::<u128>(), amount);
}
#[test]
fn near_boundary_dust_splits_above() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let amount = (DUST_LIMIT as u128) * 3 + 1u128;
let dist = plan_btc_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, amount)
.unwrap();
assert_eq!(dist.len(), 3);
assert!(dist.iter().all(|&x| x >= DUST_LIMIT as u128));
assert_eq!(dist.iter().sum::<u128>(), amount);
}
#[test]
fn duplicate_meta_utxos_across_shards() {
const MAX_MODIFIED_ACCOUNTS: usize = 1;
const MAX_INPUTS_TO_SIGN: usize = 2;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shared_meta = random_utxo_meta(42);
let utxo1 = create_btc_utxo(1_000, 42);
let mut utxo2 = create_btc_utxo(2_000, 42); utxo2.meta = shared_meta;
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard1.add_btc_utxo(utxo1);
shard2.add_btc_utxo(utxo2);
let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
tx_builder.transaction.input.push(TxIn {
previous_output: OutPoint::new(shared_meta.to_txid(), shared_meta.vout()),
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
});
let unsettled = compute_unsettled_btc_in_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, 0, &FeeRate(1.0))
.unwrap();
assert_eq!(unsettled, 3_000);
}
#[test]
fn high_fee_scenario_overflow() {
use arch_program::rune::{RuneAmount, RuneId};
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 1;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let shard = create_shard(0);
let loaders = leak_loaders_from_vec(vec![shard]);
let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
let rune_amount = RuneAmount {
id: RuneId::BTC,
amount: u128::MAX,
};
let result = balance_loader::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, &rune_amount);
assert_eq!(result.unwrap(), vec![u128::MAX]);
}
#[test]
fn empty_amount_optimization() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let original_outputs = tx_builder.transaction.output.len();
let shards = vec![create_shard(1_000), create_shard(2_000)];
let loaders = leak_loaders_from_vec(shards);
let mut shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let dist = super::super::redistribute_remaining_btc_to_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut tx_builder,
&mut shard_refs,
0,
&ScriptBuf::new(),
&FeeRate(1.0),
)
.unwrap();
assert!(dist.is_empty());
assert_eq!(tx_builder.transaction.output.len(), original_outputs);
}
#[test]
fn balance_amount_overflow_protection() {
use arch_program::rune::{RuneAmount, RuneId};
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard1.add_btc_utxo(create_btc_utxo(u64::MAX, 1));
shard2.add_btc_utxo(create_btc_utxo(u64::MAX, 2));
let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let rune_amount = RuneAmount {
id: RuneId::BTC,
amount: u128::MAX,
};
let res = balance_loader::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&tx_builder, &shard_refs, &rune_amount);
assert!(res.is_err());
}
#[cfg(feature = "runes")]
#[test]
fn runestone_pointer_update() {
use bitcoin::{Amount, TxOut};
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
tx_builder.transaction.output.push(TxOut {
value: Amount::from_sat(1_000),
script_pubkey: ScriptBuf::new(),
});
tx_builder.transaction.output.push(TxOut {
value: Amount::from_sat(2_000),
script_pubkey: ScriptBuf::new(),
});
let old_output_count = tx_builder.transaction.output.len();
let shards = vec![create_shard(0), create_shard(0)];
let loaders = leak_loaders_from_vec(shards);
let mut shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
crate::split::redistribute_remaining_rune_to_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut tx_builder,
&mut shard_refs,
SingleRuneSet::default(),
ScriptBuf::new(),
)
.unwrap();
assert_eq!(tx_builder.runestone.pointer, Some(old_output_count as u32));
for (i, edict) in tx_builder.runestone.edicts.iter().enumerate() {
if i > 0 {
assert_eq!(edict.output, (old_output_count + i) as u32);
}
}
}
}
}
#[cfg(all(test, feature = "runes"))]
mod rune_tests_loader {
use super::*;
use crate::tests::common::{
create_rune_utxo, create_shard, leak_loaders_from_vec, MockShardZc,
};
use arch_program::rune::{RuneAmount, RuneId};
use bitcoin::ScriptBuf;
use satellite_bitcoin::utxo_info::SingleRuneSet;
use satellite_bitcoin::TransactionBuilder as TB;
#[allow(unused_macros)]
macro_rules! new_tb {
($max_utxos:expr, $max_shards:expr) => {
TB::<$max_utxos, $max_shards, SingleRuneSet>::new()
};
}
#[test]
fn compute_unsettled_rune_basic() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 2;
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard1.set_rune_utxo(create_rune_utxo(100, 0));
shard2.set_rune_utxo(create_rune_utxo(50, 1));
let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
let shard_refs =
super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
let unsettled = crate::split::compute_unsettled_rune_in_shards::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&shard_refs, SingleRuneSet::default())
.unwrap();
assert_eq!(unsettled.find(&RuneId::new(1, 1)).unwrap().amount, 150);
}
#[test]
fn plan_rune_distribution_proportional() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let mut shard0 = create_shard(0);
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard0.set_rune_utxo(create_rune_utxo(100, 0));
shard1.set_rune_utxo(create_rune_utxo(200, 1));
shard2.set_rune_utxo(create_rune_utxo(300, 2));
let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
let shard_refs =
super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let mut target = SingleRuneSet::default();
target
.insert(RuneAmount {
id: RuneId::new(1, 1),
amount: 600,
})
.unwrap();
let dist = crate::split::plan_rune_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut tx_builder, &shard_refs, &target)
.unwrap();
assert_eq!(dist.len(), 3);
let allocs: Vec<u128> = dist
.iter()
.map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
.collect();
assert_eq!(allocs, vec![300, 200, 100]);
}
#[test]
fn plan_rune_distribution_zero_amount_inserts_zero_entries() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let mut shard0 = create_shard(0);
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard0.set_rune_utxo(create_rune_utxo(100, 0));
shard1.set_rune_utxo(create_rune_utxo(200, 1));
shard2.set_rune_utxo(create_rune_utxo(300, 2));
let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
let shard_refs =
super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let mut target = SingleRuneSet::default();
target
.insert(RuneAmount {
id: RuneId::new(1, 1),
amount: 0,
})
.unwrap();
let dist = crate::split::plan_rune_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut tx_builder, &shard_refs, &target)
.unwrap();
assert_eq!(dist.len(), 3);
for s in dist.iter() {
let r = s.find(&RuneId::new(1, 1)).expect("rune entry present");
assert_eq!(r.amount, 0);
assert_eq!(s.len(), 1);
}
}
#[test]
fn plan_rune_distribution_partial_creates_zero_entry_for_some_shards() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let mut shard0 = create_shard(0);
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard0.set_rune_utxo(create_rune_utxo(100, 0));
shard1.set_rune_utxo(create_rune_utxo(200, 1));
shard2.set_rune_utxo(create_rune_utxo(300, 2));
let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
let shard_refs =
super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let mut target = SingleRuneSet::default();
target
.insert(RuneAmount {
id: RuneId::new(1, 1),
amount: 10,
})
.unwrap();
let dist = crate::split::plan_rune_distribution_among_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut tx_builder, &shard_refs, &target)
.unwrap();
assert_eq!(dist.len(), 3);
let allocs: Vec<u128> = dist
.iter()
.map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
.collect();
assert!(allocs.contains(&0));
for (i, amt) in allocs.iter().enumerate() {
let r = dist[i].find(&RuneId::new(1, 1)).unwrap();
assert_eq!(r.amount, *amt);
}
}
#[test]
fn redistribute_remaining_rune_distribution() {
const MAX_MODIFIED_ACCOUNTS: usize = 0;
const MAX_INPUTS_TO_SIGN: usize = 3;
let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
let mut shard0 = create_shard(0);
let mut shard1 = create_shard(0);
let mut shard2 = create_shard(0);
shard0.set_rune_utxo(create_rune_utxo(100, 0));
shard1.set_rune_utxo(create_rune_utxo(200, 1));
shard2.set_rune_utxo(create_rune_utxo(300, 2));
let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
let mut shard_refs =
super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
let mut removed = SingleRuneSet::default();
removed
.insert(RuneAmount {
id: RuneId::new(1, 1),
amount: 150,
})
.unwrap();
let dist = crate::split::redistribute_remaining_rune_to_shards::<
MAX_MODIFIED_ACCOUNTS,
MAX_INPUTS_TO_SIGN,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut tx_builder, &mut shard_refs, removed, ScriptBuf::new())
.unwrap();
let mut allocs: Vec<u128> = dist
.iter()
.map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
.collect();
allocs.sort_unstable();
assert_eq!(allocs, vec![50, 150, 250]);
}
}