use std::collections::HashMap;
use std::fmt::Debug;
use cdk_common::Id;
use tracing::instrument;
use uuid::Uuid;
use crate::fees::calculate_fee;
use crate::nuts::nut00::ProofsMethods;
use crate::nuts::{Proofs, Token};
use crate::{Amount, Error, Wallet};
pub(crate) mod saga;
use saga::SendSaga;
#[must_use = "must be confirmed or canceled to release reserved proofs"]
pub struct PreparedSend<'a> {
wallet: &'a Wallet,
operation_id: Uuid,
amount: Amount,
options: SendOptions,
proofs_to_swap: Proofs,
proofs_to_send: Proofs,
swap_fee: Amount,
send_fee: Amount,
}
impl PreparedSend<'_> {
pub fn operation_id(&self) -> Uuid {
self.operation_id
}
pub fn amount(&self) -> Amount {
self.amount
}
pub fn options(&self) -> &SendOptions {
&self.options
}
pub fn proofs_to_swap(&self) -> &Proofs {
&self.proofs_to_swap
}
pub fn swap_fee(&self) -> Amount {
self.swap_fee
}
pub fn proofs_to_send(&self) -> &Proofs {
&self.proofs_to_send
}
pub fn send_fee(&self) -> Amount {
self.send_fee
}
pub fn proofs(&self) -> Proofs {
let mut proofs = self.proofs_to_swap.clone();
proofs.extend(self.proofs_to_send.clone());
proofs
}
pub fn fee(&self) -> Amount {
self.swap_fee + self.send_fee
}
pub async fn confirm(self, memo: Option<SendMemo>) -> Result<Token, Error> {
self.wallet
.confirm_send(
self.operation_id,
self.amount,
self.options,
self.proofs_to_swap,
self.proofs_to_send,
self.swap_fee,
self.send_fee,
memo,
)
.await
}
pub async fn cancel(self) -> Result<(), Error> {
self.wallet
.cancel_send(self.operation_id, self.proofs_to_swap, self.proofs_to_send)
.await
}
}
impl Debug for PreparedSend<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreparedSend")
.field("operation_id", &self.operation_id)
.field("amount", &self.amount)
.field("options", &self.options)
.field(
"proofs_to_swap",
&self
.proofs_to_swap
.iter()
.map(|p| p.amount)
.collect::<Vec<_>>(),
)
.field("swap_fee", &self.swap_fee)
.field(
"proofs_to_send",
&self
.proofs_to_send
.iter()
.map(|p| p.amount)
.collect::<Vec<_>>(),
)
.field("send_fee", &self.send_fee)
.finish()
}
}
impl Wallet {
#[instrument(skip(self), err)]
pub async fn prepare_send(
&self,
amount: Amount,
opts: SendOptions,
) -> Result<PreparedSend<'_>, Error> {
let saga = SendSaga::new(self);
let prepared_saga = saga.prepare(amount, opts).await?;
let prepared = PreparedSend {
wallet: self,
operation_id: prepared_saga.operation_id(),
amount: prepared_saga.amount(),
options: prepared_saga.options().clone(),
proofs_to_swap: prepared_saga.proofs_to_swap().clone(),
proofs_to_send: prepared_saga.proofs_to_send().clone(),
swap_fee: prepared_saga.swap_fee(),
send_fee: prepared_saga.send_fee(),
};
Ok(prepared)
}
#[doc(hidden)]
#[instrument(skip(self, options, proofs_to_swap, proofs_to_send))]
#[allow(clippy::too_many_arguments)]
pub async fn confirm_send(
&self,
operation_id: Uuid,
amount: Amount,
options: SendOptions,
proofs_to_swap: Proofs,
proofs_to_send: Proofs,
swap_fee: Amount,
send_fee: Amount,
memo: Option<SendMemo>,
) -> Result<Token, Error> {
let db_saga = self
.localstore
.get_saga(&operation_id)
.await?
.ok_or(Error::Custom("Saga not found".to_string()))?;
let saga = SendSaga::from_prepared(
self,
operation_id,
amount,
options,
proofs_to_swap,
proofs_to_send,
swap_fee,
send_fee,
db_saga,
);
let (token, _saga) = saga.confirm(memo).await?;
Ok(token)
}
#[doc(hidden)]
#[instrument(skip(self, proofs_to_swap, proofs_to_send))]
pub async fn cancel_send(
&self,
operation_id: Uuid,
proofs_to_swap: Proofs,
proofs_to_send: Proofs,
) -> Result<(), Error> {
let db_saga = self
.localstore
.get_saga(&operation_id)
.await?
.ok_or(Error::Custom("Saga not found".to_string()))?;
let saga = SendSaga::from_prepared(
self,
operation_id,
Amount::ZERO, SendOptions::default(), proofs_to_swap,
proofs_to_send,
Amount::ZERO, Amount::ZERO, db_saga,
);
saga.cancel().await
}
#[instrument(skip(self))]
pub async fn get_pending_sends(&self) -> Result<Vec<Uuid>, Error> {
let incomplete = self.localstore.get_incomplete_sagas().await?;
Ok(incomplete
.into_iter()
.filter_map(|s| {
if s.mint_url != self.mint_url {
return None;
}
if let cdk_common::wallet::WalletSagaState::Send(
cdk_common::wallet::SendSagaState::TokenCreated,
) = s.state
{
Some(s.id)
} else {
None
}
})
.collect())
}
#[instrument(skip(self))]
pub async fn revoke_send(&self, operation_id: Uuid) -> Result<Amount, Error> {
let saga_record = self
.localstore
.get_saga(&operation_id)
.await?
.ok_or(Error::Custom("Saga not found".to_string()))?;
if let cdk_common::wallet::WalletSagaState::Send(
cdk_common::wallet::SendSagaState::TokenCreated,
) = saga_record.state
{
if let cdk_common::wallet::OperationData::Send(data) = saga_record.data.clone() {
let proofs = data.proofs.ok_or(Error::Custom(
"No proofs found in pending send saga".to_string(),
))?;
let saga = SendSaga {
wallet: self,
compensations: crate::wallet::saga::new_compensations(),
state_data: saga::state::TokenCreated {
operation_id,
proofs,
saga: saga_record,
},
};
return saga.revoke().await;
}
}
Err(Error::Custom("Operation is not a pending send".to_string()))
}
#[instrument(skip(self))]
pub async fn check_send_status(&self, operation_id: Uuid) -> Result<bool, Error> {
let saga_record = self
.localstore
.get_saga(&operation_id)
.await?
.ok_or(Error::Custom("Saga not found".to_string()))?;
if let cdk_common::wallet::WalletSagaState::Send(
cdk_common::wallet::SendSagaState::RollingBack,
) = saga_record.state
{
tracing::debug!(
"Operation {} is rolling back - returning pending status",
operation_id
);
return Ok(false);
}
if let cdk_common::wallet::WalletSagaState::Send(
cdk_common::wallet::SendSagaState::TokenCreated,
) = saga_record.state
{
if let cdk_common::wallet::OperationData::Send(data) = saga_record.data.clone() {
let proofs = data.proofs.ok_or(Error::Custom(
"No proofs found in pending send saga".to_string(),
))?;
let saga = SendSaga {
wallet: self,
compensations: crate::wallet::saga::new_compensations(),
state_data: saga::state::TokenCreated {
operation_id,
proofs,
saga: saga_record,
},
};
return saga.check_status().await;
}
}
Err(Error::Custom("Operation is not a pending send".to_string()))
}
}
pub use cdk_common::wallet::{SendMemo, SendOptions};
#[derive(Debug, Clone)]
pub struct ProofSplitResult {
pub proofs_to_send: Proofs,
pub proofs_to_swap: Proofs,
pub swap_fee: Amount,
}
pub(crate) fn split_proofs_for_send(
proofs: Proofs,
send_amounts: &[Amount],
amount: Amount,
send_fee: Amount,
keyset_fees: &HashMap<Id, u64>,
force_swap: bool,
is_exact_or_offline: bool,
) -> Result<ProofSplitResult, Error> {
let mut proofs_to_swap = Proofs::new();
let mut proofs_to_send = Proofs::new();
if force_swap {
proofs_to_swap = proofs;
} else if is_exact_or_offline {
proofs_to_send = proofs;
} else {
let mut remaining_send_amounts: Vec<Amount> = send_amounts.to_vec();
for proof in proofs {
if let Some(idx) = remaining_send_amounts
.iter()
.position(|a| a == &proof.amount)
{
proofs_to_send.push(proof);
remaining_send_amounts.remove(idx);
} else {
proofs_to_swap.push(proof);
}
}
if !proofs_to_swap.is_empty() {
let swap_output_needed = (amount + send_fee)
.checked_sub(proofs_to_send.total_amount()?)
.unwrap_or(Amount::ZERO);
if swap_output_needed == Amount::ZERO {
proofs_to_swap.clear();
} else {
loop {
let swap_input_fee =
calculate_fee(&proofs_to_swap.count_by_keyset(), keyset_fees)?.total;
let swap_total = proofs_to_swap.total_amount()?;
let swap_can_produce = swap_total.checked_sub(swap_input_fee);
match swap_can_produce {
Some(can_produce) if can_produce >= swap_output_needed => {
break;
}
_ => {
if proofs_to_send.is_empty() {
return Err(Error::InsufficientFunds);
}
proofs_to_send.sort_by_key(|a| a.amount);
let proof_to_move = proofs_to_send.remove(0);
proofs_to_swap.push(proof_to_move);
}
}
}
}
}
}
let swap_fee = calculate_fee(&proofs_to_swap.count_by_keyset(), keyset_fees)?.total;
Ok(ProofSplitResult {
proofs_to_send,
proofs_to_swap,
swap_fee,
})
}
#[cfg(test)]
mod tests {
use cdk_common::secret::Secret;
use cdk_common::{Amount, Id, Proof, PublicKey};
use super::*;
fn id() -> Id {
Id::from_bytes(&[0; 8]).unwrap()
}
fn proof(amount: u64) -> Proof {
Proof::new(
Amount::from(amount),
id(),
Secret::generate(),
PublicKey::from_hex(
"03deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
)
.unwrap(),
)
}
fn proofs(amounts: &[u64]) -> Proofs {
amounts.iter().map(|&a| proof(a)).collect()
}
fn keyset_fees_with_ppk(fee_ppk: u64) -> HashMap<Id, u64> {
let mut fees = HashMap::new();
fees.insert(id(), fee_ppk);
fees
}
fn amounts(values: &[u64]) -> Vec<Amount> {
values.iter().map(|&v| Amount::from(v)).collect()
}
#[test]
fn test_split_exact_match_simple() {
let input_proofs = proofs(&[8, 2]);
let send_amounts = amounts(&[8, 2]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(10),
Amount::from(1),
&keyset_fees,
false,
true, )
.unwrap();
assert_eq!(result.proofs_to_send.len(), 2);
assert!(result.proofs_to_swap.is_empty());
assert_eq!(result.swap_fee, Amount::ZERO);
}
#[test]
fn test_split_exact_match_six_proofs() {
let input_proofs = proofs(&[2048, 1024, 512, 256, 128, 32]);
let send_amounts = amounts(&[2048, 1024, 512, 256, 128, 32]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4000),
Amount::from(2),
&keyset_fees,
false,
true,
)
.unwrap();
assert_eq!(result.proofs_to_send.len(), 6);
assert!(result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_exact_match_ten_proofs() {
let input_proofs = proofs(&[4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8]);
let send_amounts = amounts(&[4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(8000),
Amount::from(2),
&keyset_fees,
false,
true,
)
.unwrap();
assert_eq!(result.proofs_to_send.len(), 10);
assert!(result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_exact_match_powers_of_two() {
let input_proofs = proofs(&[4096, 512, 256, 128, 8]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5000),
Amount::from(1),
&keyset_fees,
false,
true,
)
.unwrap();
assert_eq!(result.proofs_to_send.len(), 5);
assert!(result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_single_mismatch() {
let input_proofs = proofs(&[8, 4, 2, 1]);
let send_amounts = amounts(&[8, 2]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(10),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
let swap_amounts_result: Vec<u64> = result
.proofs_to_swap
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts_result.contains(&8));
assert!(send_amounts_result.contains(&2));
assert!(swap_amounts_result.contains(&4) || swap_amounts_result.contains(&1));
}
#[test]
fn test_split_multiple_mismatches() {
let input_proofs = proofs(&[4096, 1024, 512, 256, 64, 32, 16, 8]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5000),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts_result.contains(&4096));
assert!(send_amounts_result.contains(&512));
assert!(send_amounts_result.contains(&256));
assert!(send_amounts_result.contains(&8));
assert!(!result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_half_match() {
let input_proofs = proofs(&[2048, 2048, 1024, 512, 256, 128, 64, 32]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5000),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts_result.contains(&512));
assert!(send_amounts_result.contains(&256));
assert!(send_amounts_result.contains(&128));
assert!(!result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_large_swap_set() {
let input_proofs = proofs(&[1024, 1024, 1024, 1024, 1024, 512, 256, 128, 64, 32, 16, 8]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5000),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts_result.contains(&512));
assert!(send_amounts_result.contains(&256));
assert!(send_amounts_result.contains(&128));
assert!(send_amounts_result.contains(&8));
assert!(result.proofs_to_swap.len() >= 5);
}
#[test]
fn test_split_dense_small_proofs() {
let input_proofs = proofs(&[
512, 256, 256, 128, 128, 128, 64, 64, 64, 64, 32, 32, 16, 16, 8, 8, 4, 4, 2, 2,
]);
let send_amounts = amounts(&[1024, 256, 128, 64, 16, 8, 4]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(1500),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
assert!(!result.proofs_to_swap.is_empty());
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(
send_amounts_result.contains(&256)
|| send_amounts_result.contains(&128)
|| send_amounts_result.contains(&64)
);
}
#[test]
fn test_split_fragmented_no_match() {
let mut input_amounts = vec![];
input_amounts.extend(std::iter::repeat_n(64, 10));
input_amounts.extend(std::iter::repeat_n(32, 5));
input_amounts.extend(std::iter::repeat_n(16, 10));
input_amounts.extend(std::iter::repeat_n(8, 5));
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[512, 256, 128, 64, 32, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(1000),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(!result.proofs_to_swap.is_empty());
assert!(
send_amounts_result.contains(&64)
|| send_amounts_result.contains(&32)
|| send_amounts_result.contains(&8)
);
}
#[test]
fn test_split_large_fragmented() {
let mut input_amounts = vec![];
input_amounts.extend(std::iter::repeat_n(256, 8));
input_amounts.extend(std::iter::repeat_n(128, 4));
input_amounts.extend(std::iter::repeat_n(64, 8));
input_amounts.extend(std::iter::repeat_n(32, 4));
input_amounts.extend(std::iter::repeat_n(16, 8));
input_amounts.extend(std::iter::repeat_n(8, 4));
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[512, 256, 128, 64, 32, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(1000),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(
send_amounts_result.contains(&256)
|| send_amounts_result.contains(&128)
|| send_amounts_result.contains(&32)
);
assert!(result.proofs_to_swap.len() > 10);
}
#[test]
fn test_split_swap_sufficient() {
let input_proofs = proofs(&[4096, 512, 256, 128, 8, 64, 32]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5000),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_amounts: Vec<u64> = result
.proofs_to_swap
.iter()
.map(|p| p.amount.into())
.collect();
assert!(swap_amounts.contains(&64) || swap_amounts.contains(&32));
}
#[test]
fn test_split_swap_barely_sufficient() {
let input_proofs = proofs(&[2048, 1024, 256, 128, 32, 16, 8, 4, 2, 1]);
let send_amounts = amounts(&[2048, 1024, 256, 128, 64]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(3520),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
assert!(!result.proofs_to_swap.is_empty());
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
let swap_fee: u64 = result.swap_fee.into();
assert!(swap_total - swap_fee >= 65);
}
#[test]
fn test_split_move_one_proof() {
let input_proofs = proofs(&[4096, 512, 256, 128, 64, 32, 16, 8]);
let send_amounts = amounts(&[4096, 512, 256, 128, 64, 32]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5088),
Amount::from(50),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
assert!(swap_total >= 50);
}
#[test]
fn test_split_move_multiple_proofs() {
let input_proofs = proofs(&[2048, 1024, 512, 256, 128, 64, 8, 4, 2, 1]);
let send_amounts = amounts(&[2048, 1024, 512, 256, 128, 64]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4032),
Amount::from(100),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
let swap_fee: u64 = result.swap_fee.into();
assert!(swap_total - swap_fee >= 100);
}
#[test]
fn test_split_high_fee_many_proofs() {
let input_proofs = proofs(&[1024, 512, 256, 128, 64, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1]);
let send_amounts = amounts(&[1024, 512, 256, 128, 64, 32, 16, 8]);
let keyset_fees = keyset_fees_with_ppk(1000);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(2040),
Amount::from(10),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
let swap_fee: u64 = result.swap_fee.into();
assert!(swap_total - swap_fee >= 10);
}
#[test]
fn test_split_fee_eats_small_proofs() {
let input_proofs = proofs(&[4096, 512, 256, 128, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
let send_amounts = amounts(&[4096, 512, 256, 128]);
let keyset_fees = keyset_fees_with_ppk(1000);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4992),
Amount::from(5),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
let swap_fee: u64 = result.swap_fee.into();
assert!(swap_total - swap_fee >= 5);
assert!(swap_total > 10);
}
#[test]
fn test_split_cascading_fee_increase() {
let input_proofs = proofs(&[2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1]);
let send_amounts = amounts(&[2048, 1024, 512, 256, 128, 64]);
let keyset_fees = keyset_fees_with_ppk(500);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4032),
Amount::from(80),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
let swap_fee: u64 = result.swap_fee.into();
assert!(swap_total - swap_fee >= 80);
}
#[test]
fn test_split_20_proofs_mixed() {
let mut input_amounts = vec![2048, 1024, 512];
input_amounts.extend(vec![256; 2]);
input_amounts.extend(vec![128; 2]);
input_amounts.extend(vec![64; 4]);
input_amounts.extend(vec![32; 4]);
input_amounts.extend(vec![16; 4]);
input_amounts.push(8);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[2048, 1024, 512, 256, 128]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(3968), Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(
send_amounts_result.contains(&2048)
|| send_amounts_result.contains(&1024)
|| send_amounts_result.contains(&512)
);
assert!(!result.proofs_to_swap.is_empty());
assert_eq!(
result.proofs_to_send.len() + result.proofs_to_swap.len(),
20
);
}
#[test]
fn test_split_30_small_proofs() {
let mut input_amounts = vec![];
input_amounts.extend(vec![256; 2]);
input_amounts.extend(vec![128; 4]);
input_amounts.extend(vec![64; 6]);
input_amounts.extend(vec![32; 6]);
input_amounts.extend(vec![16; 6]);
input_amounts.extend(vec![8; 6]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[1024, 512, 256, 128, 64, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(2000),
Amount::from(6),
&keyset_fees,
false,
false,
)
.unwrap();
assert_eq!(
result.proofs_to_send.len() + result.proofs_to_swap.len(),
30
);
}
#[test]
fn test_split_15_proofs_high_fee() {
let mut input_amounts = vec![4096];
input_amounts.extend(vec![1024; 2]);
input_amounts.extend(vec![512; 2]);
input_amounts.extend(vec![256; 2]);
input_amounts.extend(vec![128; 2]);
input_amounts.extend(vec![64; 2]);
input_amounts.extend(vec![32; 2]);
input_amounts.extend(vec![16; 2]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[4096, 2048, 1024, 512, 256, 64]);
let keyset_fees = keyset_fees_with_ppk(500);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(8000),
Amount::from(8),
&keyset_fees,
false,
false,
)
.unwrap();
assert_eq!(
result.proofs_to_send.len() + result.proofs_to_swap.len(),
15
);
}
#[test]
fn test_split_uniform_25_proofs() {
let input_proofs = proofs(&[256; 25]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5000),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let send_count = result.proofs_to_send.len();
let swap_count = result.proofs_to_swap.len();
assert_eq!(send_count + swap_count, 25);
assert_eq!(send_count, 1); }
#[test]
fn test_split_tiered_18_proofs() {
let mut input_amounts = vec![4096, 2048];
input_amounts.extend(vec![1024; 2]);
input_amounts.extend(vec![512; 2]);
input_amounts.extend(vec![256; 4]);
input_amounts.extend(vec![128; 4]);
input_amounts.extend(vec![64; 4]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[8192, 1024, 512, 256, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(10000),
Amount::from(4), &keyset_fees,
false,
false,
)
.unwrap();
assert_eq!(
result.proofs_to_send.len() + result.proofs_to_swap.len(),
18
);
}
#[test]
fn test_split_dust_consolidation() {
let mut input_amounts = vec![];
input_amounts.extend(vec![16; 50]);
input_amounts.extend(vec![8; 50]);
input_amounts.extend(vec![4; 50]);
input_amounts.extend(vec![2; 50]);
input_amounts.extend(vec![1; 50]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[1024, 256, 128, 64, 16, 8, 4]);
let keyset_fees = keyset_fees_with_ppk(100);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(1500),
Amount::from(25), &keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(
send_amounts_result.contains(&16)
|| send_amounts_result.contains(&8)
|| send_amounts_result.contains(&4)
);
}
#[test]
fn test_split_force_swap_8_proofs() {
let input_proofs = proofs(&[2048, 1024, 512, 256, 128, 64, 32, 16]);
let send_amounts = amounts(&[2048, 1024, 512, 256, 128, 32]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(3000),
Amount::from(2),
&keyset_fees,
true, false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 8);
}
#[test]
fn test_split_force_swap_15_proofs() {
let mut input_amounts = vec![];
input_amounts.extend(vec![1024; 5]);
input_amounts.extend(vec![512; 5]);
input_amounts.extend(vec![256; 5]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[8000]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(8000),
Amount::from(3),
&keyset_fees,
true, false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 15);
}
#[test]
fn test_split_force_swap_fragmented() {
let mut input_amounts = vec![];
input_amounts.extend(vec![64; 10]);
input_amounts.extend(vec![32; 10]);
input_amounts.extend(vec![16; 10]);
input_amounts.extend(vec![8; 10]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[2000]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(2000),
Amount::from(8),
&keyset_fees,
true, false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 40);
}
#[test]
fn test_split_single_large_proof() {
let input_proofs = proofs(&[8192]);
let send_amounts = amounts(&[4096, 2048, 1024, 512, 256, 64]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(8000),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 1);
}
#[test]
fn test_split_many_1sat_proofs() {
let input_proofs = proofs(&[1; 100]);
let send_amounts = amounts(&[32, 16, 2]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(50),
Amount::from(20), &keyset_fees,
false,
false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 100);
}
#[test]
fn test_split_all_same_denomination() {
let input_proofs = proofs(&[512; 10]);
let send_amounts = amounts(&[4096, 512, 256, 128, 8]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4000),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
let send_count = result.proofs_to_send.len();
assert_eq!(send_count, 1);
assert_eq!(result.proofs_to_swap.len(), 9);
}
#[test]
fn test_split_alternating_sizes() {
let input_proofs = proofs(&[1024, 64, 1024, 64, 1024, 64, 1024, 64]);
let send_amounts = amounts(&[4096, 256, 128]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4000),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 8);
}
#[test]
fn test_split_power_of_two_boundary() {
let input_proofs = proofs(&[2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1]);
let send_amounts = amounts(&[2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4095),
Amount::from(3), &keyset_fees,
false,
false,
)
.unwrap();
assert_eq!(result.proofs_to_send.len(), 12);
assert!(result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_just_over_boundary() {
let input_proofs = proofs(&[2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 1, 64]);
let send_amounts = amounts(&[2048, 1024, 512, 1]);
let keyset_fees = keyset_fees_with_ppk(200);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(3585), Amount::from(3), &keyset_fees,
false,
false,
)
.unwrap();
let send_amounts_result: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts_result.contains(&1) || send_amounts_result.contains(&2048));
assert!(!result.proofs_to_swap.is_empty());
assert_eq!(
result.proofs_to_send.len() + result.proofs_to_swap.len(),
14
);
}
#[test]
fn test_split_regression_insufficient_swap_fee() {
let input_proofs = proofs(&[4096, 512, 256, 128, 1, 1]);
let send_amounts = amounts(&[4096, 512, 256, 128]);
let keyset_fees = keyset_fees_with_ppk(1000);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(4992),
Amount::from(1),
&keyset_fees,
false,
false,
)
.unwrap();
let swap_total: u64 = result
.proofs_to_swap
.iter()
.map(|p| u64::from(p.amount))
.sum();
let swap_fee: u64 = result.swap_fee.into();
assert!(swap_total > swap_fee || result.proofs_to_swap.is_empty());
}
#[test]
fn test_split_regression_many_small_in_swap() {
let mut input_amounts = vec![4096, 1024];
input_amounts.extend(vec![1; 20]);
let input_proofs = proofs(&input_amounts);
let send_amounts = amounts(&[4096, 1024]);
let keyset_fees = keyset_fees_with_ppk(500);
let result = split_proofs_for_send(
input_proofs,
&send_amounts,
Amount::from(5120),
Amount::from(5),
&keyset_fees,
false,
false,
)
.unwrap();
assert!(result.proofs_to_send.len() + result.proofs_to_swap.len() == 22);
}
#[test]
fn test_melt_exact_proofs_no_swap() {
let input_proofs = proofs(&[64, 32, 4, 2]);
let target_amounts = amounts(&[64, 32, 4, 2]); let keyset_fees = keyset_fees_with_ppk(500);
let result = split_proofs_for_send(
input_proofs,
&target_amounts,
Amount::from(100), Amount::from(2), &keyset_fees,
false,
false,
)
.unwrap();
assert_eq!(result.proofs_to_send.len(), 4);
assert!(result.proofs_to_swap.is_empty());
assert_eq!(result.swap_fee, Amount::ZERO);
}
#[test]
fn test_melt_excess_proofs_needs_swap() {
let input_proofs = proofs(&[128]);
let target_amounts = amounts(&[64, 32, 4, 2]); let keyset_fees = keyset_fees_with_ppk(500);
let result = split_proofs_for_send(
input_proofs,
&target_amounts,
Amount::from(100),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
assert!(result.proofs_to_send.is_empty());
assert_eq!(result.proofs_to_swap.len(), 1);
assert_eq!(result.proofs_to_swap[0].amount, Amount::from(128));
}
#[test]
fn test_melt_partial_match_with_swap() {
let input_proofs = proofs(&[64, 32, 16, 8]);
let target_amounts = amounts(&[64, 32, 4, 2]); let keyset_fees = keyset_fees_with_ppk(500);
let result = split_proofs_for_send(
input_proofs,
&target_amounts,
Amount::from(100),
Amount::from(2),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts.contains(&64));
assert!(send_amounts.contains(&32));
assert!(!result.proofs_to_swap.is_empty());
}
#[test]
fn test_melt_with_exact_target_match() {
let input_proofs = proofs(&[64, 32, 8, 4, 2]);
let target_amounts = amounts(&[64, 32, 8, 4, 2]); let keyset_fees = keyset_fees_with_ppk(1000);
let result = split_proofs_for_send(
input_proofs,
&target_amounts,
Amount::from(105), Amount::from(5), &keyset_fees,
false,
false,
)
.unwrap();
assert_eq!(result.proofs_to_send.len(), 5);
assert!(result.proofs_to_swap.is_empty());
}
#[test]
fn test_melt_swap_fee_calculated() {
let input_proofs = proofs(&[64, 32, 8, 4]); let target_amounts = amounts(&[64, 32, 4]); let keyset_fees = keyset_fees_with_ppk(1000);
let result = split_proofs_for_send(
input_proofs,
&target_amounts,
Amount::from(98),
Amount::from(2), &keyset_fees,
false,
false,
)
.unwrap();
if !result.proofs_to_swap.is_empty() {
assert_eq!(
result.swap_fee,
Amount::from(result.proofs_to_swap.len() as u64)
);
}
}
#[test]
fn test_melt_large_quote_partial_match() {
let input_proofs = proofs(&[512, 256, 128, 64, 32, 16]);
let target_amounts = amounts(&[512, 256, 128, 64, 32, 8, 4, 2, 1]);
let keyset_fees = keyset_fees_with_ppk(375);
let result = split_proofs_for_send(
input_proofs,
&target_amounts,
Amount::from(1004),
Amount::from(3),
&keyset_fees,
false,
false,
)
.unwrap();
let send_amounts: Vec<u64> = result
.proofs_to_send
.iter()
.map(|p| p.amount.into())
.collect();
assert!(send_amounts.contains(&512));
assert!(send_amounts.contains(&256));
assert!(send_amounts.contains(&128));
assert!(send_amounts.contains(&64));
assert!(send_amounts.contains(&32));
let swap_amounts: Vec<u64> = result
.proofs_to_swap
.iter()
.map(|p| p.amount.into())
.collect();
assert!(swap_amounts.contains(&16));
}
}