use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use lightning::offers::offer::Offer;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::nuts::CurrencyUnit;
use crate::Id;
#[derive(Debug, Error)]
pub enum Error {
#[error("Split Values must be less then or equal to amount")]
SplitValuesGreater,
#[error("Amount Overflow")]
AmountOverflow,
#[error("Cannot convert units")]
CannotConvertUnits,
#[error("Unit mismatch: cannot operate on {0} and {1}")]
UnitMismatch(CurrencyUnit, CurrencyUnit),
#[error("Invalid Amount: {0}")]
InvalidAmount(String),
#[error("Amount undefined")]
AmountUndefined,
#[error(transparent)]
Utf8ParseError(#[from] std::string::FromUtf8Error),
#[error("Cannot represent amount {0} with available denominations (got {1})")]
CannotSplitAmount(u64, u64),
}
#[derive(Debug, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct Amount<U = ()> {
value: u64,
unit: U,
}
#[derive(Debug, Clone)]
pub struct FeeAndAmounts {
fee: u64,
amounts: Vec<u64>,
}
impl From<(u64, Vec<u64>)> for FeeAndAmounts {
fn from(value: (u64, Vec<u64>)) -> Self {
Self {
fee: value.0,
amounts: value.1,
}
}
}
impl FeeAndAmounts {
#[inline(always)]
pub fn fee(&self) -> u64 {
self.fee
}
#[inline(always)]
pub fn amounts(&self) -> &[u64] {
&self.amounts
}
}
pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
impl Copy for Amount<()> {}
impl Clone for Amount<()> {
fn clone(&self) -> Self {
*self
}
}
impl Clone for Amount<CurrencyUnit> {
fn clone(&self) -> Self {
Self {
value: self.value,
unit: self.unit.clone(),
}
}
}
impl PartialOrd for Amount<()> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Amount<()> {
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
}
}
impl PartialOrd for Amount<CurrencyUnit> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.unit != other.unit {
None
} else {
Some(self.value.cmp(&other.value))
}
}
}
impl<U> Serialize for Amount<U> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Amount<()> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = u64::deserialize(deserializer)?;
Ok(Amount { value, unit: () })
}
}
impl FromStr for Amount<()> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s
.parse::<u64>()
.map_err(|_| Error::InvalidAmount(s.to_owned()))?;
Ok(Amount { value, unit: () })
}
}
impl Amount<()> {
pub const ZERO: Amount<()> = Amount { value: 0, unit: () };
pub const ONE: Amount<()> = Amount { value: 1, unit: () };
pub fn with_unit(self, unit: CurrencyUnit) -> Amount<CurrencyUnit> {
Amount {
value: self.value,
unit,
}
}
pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Result<Vec<Self>, Error> {
let parts: Vec<Self> = fee_and_amounts
.amounts
.iter()
.rev()
.fold((Vec::new(), self.value), |(mut acc, total), &amount| {
let count = total / amount;
for _ in 0..count {
acc.push(Self::from(amount));
}
(acc, total % amount)
})
.0;
let sum: u64 = parts.iter().map(|a| a.value).sum();
if sum != self.value {
return Err(Error::CannotSplitAmount(self.value, sum));
}
Ok(parts)
}
pub fn split_targeted(
&self,
target: &SplitTarget,
fee_and_amounts: &FeeAndAmounts,
) -> Result<Vec<Self>, Error> {
let mut parts = match target {
SplitTarget::None => self.split(fee_and_amounts)?,
SplitTarget::Value(amount) => {
if self.le(amount) {
return self.split(fee_and_amounts);
}
let mut parts_total = Amount::ZERO;
let mut parts = Vec::new();
let parts_of_value = amount.split(fee_and_amounts)?;
while parts_total.lt(self) {
for part in parts_of_value.iter().copied() {
if (part.checked_add(parts_total).ok_or(Error::AmountOverflow)?).le(self) {
parts.push(part);
} else {
let amount_left =
self.checked_sub(parts_total).ok_or(Error::AmountOverflow)?;
parts.extend(amount_left.split(fee_and_amounts)?);
}
parts_total = Amount::try_sum(parts.clone().iter().copied())?;
if parts_total.eq(self) {
break;
}
}
}
parts
}
SplitTarget::Values(values) => {
let values_total: Amount = Amount::try_sum(values.clone())?;
match self.cmp(&values_total) {
Ordering::Equal => values.clone(),
Ordering::Less => {
return Err(Error::SplitValuesGreater);
}
Ordering::Greater => {
let extra = self
.checked_sub(values_total)
.ok_or(Error::AmountOverflow)?;
let mut extra_amount = extra.split(fee_and_amounts)?;
let mut values = values.clone();
values.append(&mut extra_amount);
values
}
}
}
};
parts.sort();
Ok(parts)
}
pub fn split_with_fee(&self, fee_and_amounts: &FeeAndAmounts) -> Result<Vec<Self>, Error> {
let without_fee_amounts = self.split(fee_and_amounts)?;
let total_fee_ppk = fee_and_amounts
.fee
.checked_mul(without_fee_amounts.len() as u64)
.ok_or(Error::AmountOverflow)?;
let fee = Amount::from(total_fee_ppk.div_ceil(1000));
let new_amount = self.checked_add(fee).ok_or(Error::AmountOverflow)?;
let split = new_amount.split(fee_and_amounts)?;
let split_fee_ppk = (split.len() as u64)
.checked_mul(fee_and_amounts.fee)
.ok_or(Error::AmountOverflow)?;
let split_fee = Amount::from(split_fee_ppk.div_ceil(1000));
if let Some(net_amount) = new_amount.checked_sub(split_fee) {
if net_amount >= *self {
return Ok(split);
}
}
self.checked_add(Amount::ONE)
.ok_or(Error::AmountOverflow)?
.split_with_fee(fee_and_amounts)
}
pub fn checked_add(self, other: Amount<()>) -> Option<Amount<()>> {
self.value
.checked_add(other.value)
.map(|v| Amount { value: v, unit: () })
}
pub fn checked_sub(self, other: Amount<()>) -> Option<Amount<()>> {
self.value
.checked_sub(other.value)
.map(|v| Amount { value: v, unit: () })
}
pub fn checked_mul(self, other: Amount<()>) -> Option<Amount<()>> {
self.value
.checked_mul(other.value)
.map(|v| Amount { value: v, unit: () })
}
pub fn checked_div(self, other: Amount<()>) -> Option<Amount<()>> {
self.value
.checked_div(other.value)
.map(|v| Amount { value: v, unit: () })
}
pub fn saturating_sub(self, other: Self) -> Self {
if other > self {
Self::ZERO
} else {
self - other
}
}
pub fn try_sum<I>(iter: I) -> Result<Self, Error>
where
I: IntoIterator<Item = Self>,
{
iter.into_iter().try_fold(Amount::ZERO, |acc, x| {
acc.checked_add(x).ok_or(Error::AmountOverflow)
})
}
pub fn convert_unit(
&self,
current_unit: &CurrencyUnit,
target_unit: &CurrencyUnit,
) -> Result<Amount<()>, Error> {
Amount::new(self.value, current_unit.clone())
.convert_to(target_unit)
.map(Into::into)
}
pub fn to_u64(self) -> u64 {
self.value
}
pub fn to_i64(self) -> Option<i64> {
if self.value <= i64::MAX as u64 {
Some(self.value as i64)
} else {
None
}
}
pub fn from_i64(value: i64) -> Option<Self> {
if value >= 0 {
Some(Amount {
value: value as u64,
unit: (),
})
} else {
None
}
}
}
impl Default for Amount<()> {
fn default() -> Self {
Amount::ZERO
}
}
impl Default for &Amount<()> {
fn default() -> Self {
&Amount::ZERO
}
}
impl Amount<CurrencyUnit> {
pub fn new(value: u64, unit: CurrencyUnit) -> Self {
Self { value, unit }
}
pub fn value(&self) -> u64 {
self.value
}
pub fn to_u64(self) -> u64 {
self.value
}
pub fn to_i64(self) -> Option<i64> {
if self.value <= i64::MAX as u64 {
Some(self.value as i64)
} else {
None
}
}
pub fn unit(&self) -> &CurrencyUnit {
&self.unit
}
pub fn into_parts(self) -> (u64, CurrencyUnit) {
(self.value, self.unit)
}
pub fn checked_add(&self, other: &Self) -> Result<Self, Error> {
if self.unit != other.unit {
return Err(Error::UnitMismatch(self.unit.clone(), other.unit.clone()));
}
self.value
.checked_add(other.value)
.map(|v| Amount::new(v, self.unit.clone()))
.ok_or(Error::AmountOverflow)
}
pub fn checked_sub(&self, other: &Self) -> Result<Self, Error> {
if self.unit != other.unit {
return Err(Error::UnitMismatch(self.unit.clone(), other.unit.clone()));
}
self.value
.checked_sub(other.value)
.map(|v| Amount::new(v, self.unit.clone()))
.ok_or(Error::AmountOverflow)
}
pub fn convert_to(&self, target_unit: &CurrencyUnit) -> Result<Self, Error> {
if &self.unit == target_unit {
return Ok(self.clone());
}
let converted_value = match (&self.unit, target_unit) {
(CurrencyUnit::Sat, CurrencyUnit::Msat) => self
.value
.checked_mul(MSAT_IN_SAT)
.ok_or(Error::AmountOverflow)?,
(CurrencyUnit::Msat, CurrencyUnit::Sat) => self.value / MSAT_IN_SAT,
_ => return Err(Error::CannotConvertUnits),
};
Ok(Amount::new(converted_value, target_unit.clone()))
}
pub fn display_with_unit(&self) -> String {
format!("{} {}", self.value, self.unit)
}
pub fn to_msat(&self) -> Result<u64, Error> {
self.convert_to(&CurrencyUnit::Msat).map(|a| a.value())
}
pub fn to_sat(&self) -> Result<u64, Error> {
self.convert_to(&CurrencyUnit::Sat).map(|a| a.value())
}
}
impl<U> fmt::Display for Amount<U> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(width) = f.width() {
write!(f, "{:width$}", self.value, width = width)
} else {
write!(f, "{}", self.value)
}
}
}
impl From<u64> for Amount<()> {
fn from(value: u64) -> Self {
Amount { value, unit: () }
}
}
impl From<&u64> for Amount<()> {
fn from(value: &u64) -> Self {
Amount {
value: *value,
unit: (),
}
}
}
impl From<Amount<()>> for u64 {
fn from(value: Amount<()>) -> Self {
value.value
}
}
impl From<Amount<CurrencyUnit>> for Amount<()> {
fn from(value: Amount<CurrencyUnit>) -> Self {
Amount {
value: value.value,
unit: (),
}
}
}
impl AsRef<u64> for Amount<()> {
fn as_ref(&self) -> &u64 {
&self.value
}
}
impl std::ops::Add for Amount<()> {
type Output = Amount<()>;
fn add(self, rhs: Amount<()>) -> Self::Output {
self.checked_add(rhs)
.expect("Addition overflow: the sum of the amounts exceeds the maximum value")
}
}
impl std::ops::AddAssign for Amount<()> {
fn add_assign(&mut self, rhs: Self) {
*self = self
.checked_add(rhs)
.expect("AddAssign overflow: the sum of the amounts exceeds the maximum value");
}
}
impl std::ops::Sub for Amount<()> {
type Output = Amount<()>;
fn sub(self, rhs: Amount<()>) -> Self::Output {
self.checked_sub(rhs)
.expect("Subtraction underflow: cannot subtract a larger amount from a smaller amount")
}
}
impl std::ops::SubAssign for Amount<()> {
fn sub_assign(&mut self, other: Self) {
*self = self
.checked_sub(other)
.expect("SubAssign underflow: cannot subtract a larger amount from a smaller amount");
}
}
impl std::ops::Mul for Amount<()> {
type Output = Self;
fn mul(self, other: Self) -> Self::Output {
self.checked_mul(other)
.expect("Multiplication overflow: the product of the amounts exceeds the maximum value")
}
}
impl std::ops::Div for Amount<()> {
type Output = Self;
fn div(self, other: Self) -> Self::Output {
self.checked_div(other)
.expect("Division error: cannot divide by zero or overflow occurred")
}
}
pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result<Amount, Error> {
let offer_amount = offer.amount().ok_or(Error::AmountUndefined)?;
let (amount, currency) = match offer_amount {
lightning::offers::offer::Amount::Bitcoin { amount_msats } => {
(amount_msats, CurrencyUnit::Msat)
}
lightning::offers::offer::Amount::Currency {
iso4217_code,
amount,
} => (
amount,
CurrencyUnit::from_str(&String::from_utf8(iso4217_code.as_bytes().to_vec())?)
.map_err(|_| Error::CannotConvertUnits)?,
),
};
Amount::new(amount, currency)
.convert_to(unit)
.map(Into::into)
.map_err(|_err| Error::CannotConvertUnits)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub enum SplitTarget {
#[default]
None,
Value(Amount),
Values(Vec<Amount>),
}
pub const MSAT_IN_SAT: u64 = 1000;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_amount() {
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
assert_eq!(
Amount::from(1).split(&fee_and_amounts).unwrap(),
vec![Amount::from(1)]
);
assert_eq!(
Amount::from(2).split(&fee_and_amounts).unwrap(),
vec![Amount::from(2)]
);
assert_eq!(
Amount::from(3).split(&fee_and_amounts).unwrap(),
vec![Amount::from(2), Amount::from(1)]
);
let amounts: Vec<Amount> = [8, 2, 1].iter().map(|a| Amount::from(*a)).collect();
assert_eq!(Amount::from(11).split(&fee_and_amounts).unwrap(), amounts);
let amounts: Vec<Amount> = [128, 64, 32, 16, 8, 4, 2, 1]
.iter()
.map(|a| Amount::from(*a))
.collect();
assert_eq!(Amount::from(255).split(&fee_and_amounts).unwrap(), amounts);
}
#[test]
fn test_split_target_amount() {
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let amount = Amount::from(65);
let split = amount
.split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
.unwrap();
assert_eq!(
vec![Amount::from(1), Amount::from(32), Amount::from(32)],
split
);
let amount = Amount::from(150);
let split = amount
.split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
.unwrap();
assert_eq!(
vec![
Amount::from(2),
Amount::from(2),
Amount::from(2),
Amount::from(16),
Amount::from(16),
Amount::from(16),
Amount::from(32),
Amount::from(32),
Amount::from(32)
],
split
);
let amount = Amount::from(63);
let split = amount
.split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
.unwrap();
assert_eq!(
vec![
Amount::from(1),
Amount::from(2),
Amount::from(4),
Amount::from(8),
Amount::from(16),
Amount::from(32)
],
split
);
}
#[test]
fn test_split_with_fee() {
let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let amount = Amount::from(2);
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
assert_eq!(split, vec![Amount::from(2), Amount::from(1)]);
let amount = Amount::from(3);
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
assert_eq!(split, vec![Amount::from(4)]);
let amount = Amount::from(3);
let fee_and_amounts = (1000, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
assert_eq!(split, vec![Amount::from(4), Amount::from(1)]);
}
#[test]
fn test_split_with_fee_reported_issue() {
let fee_and_amounts = (100, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let amount = Amount::from(300);
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
let total_fee_ppk = (split.len() as u64) * fee_and_amounts.fee;
let total_fee = Amount::from(total_fee_ppk.div_ceil(1000));
let split_total = Amount::try_sum(split.iter().copied()).unwrap();
assert!(
split_total >= amount.checked_add(total_fee).unwrap(),
"Split total {} should be >= amount {} + fee {}",
split_total,
amount,
total_fee
);
}
#[test]
fn test_split_with_fee_edge_cases() {
let test_cases = vec![
(Amount::from(1), 100),
(Amount::from(10), 100),
(Amount::from(50), 100),
(Amount::from(100), 100),
(Amount::from(200), 100),
(Amount::from(300), 100),
(Amount::from(500), 100),
(Amount::from(600), 100),
(Amount::from(1000), 100),
(Amount::from(1337), 100),
(Amount::from(5000), 100),
];
for (amount, fee_ppk) in test_cases {
let fee_and_amounts =
(fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let result = amount.split_with_fee(&fee_and_amounts);
assert!(
result.is_ok(),
"split_with_fee failed for amount {} with fee_ppk {}: {:?}",
amount,
fee_ppk,
result.err()
);
let split = result.unwrap();
let split_total = Amount::try_sum(split.iter().copied()).unwrap();
let fee_for_split = (split.len() as u64) * fee_ppk;
let total_fee = Amount::from(fee_for_split.div_ceil(1000));
let net_amount = split_total.checked_sub(total_fee);
assert!(
net_amount.is_some(),
"Net amount calculation failed for amount {} with fee_ppk {}",
amount,
fee_ppk
);
assert!(
net_amount.unwrap() >= amount,
"Net amount {} is less than required {} for amount {} with fee_ppk {}",
net_amount.unwrap(),
amount,
amount,
fee_ppk
);
}
}
#[test]
fn test_split_with_fee_high_fees() {
let test_cases = vec![
(Amount::from(10), 500), (Amount::from(10), 1000), (Amount::from(10), 2000), (Amount::from(100), 500),
(Amount::from(100), 1000),
(Amount::from(100), 2000),
];
for (amount, fee_ppk) in test_cases {
let fee_and_amounts =
(fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let result = amount.split_with_fee(&fee_and_amounts);
assert!(
result.is_ok(),
"split_with_fee failed for amount {} with fee_ppk {}: {:?}",
amount,
fee_ppk,
result.err()
);
let split = result.unwrap();
let split_total = Amount::try_sum(split.iter().copied()).unwrap();
assert!(
split_total > amount,
"Split total {} should be greater than amount {} for fee_ppk {}",
split_total,
amount,
fee_ppk
);
}
}
#[test]
fn test_split_with_fee_recursion_limit() {
let amount = Amount::from(1);
let fee_ppk = 10000;
let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let result = amount.split_with_fee(&fee_and_amounts);
assert!(
result.is_ok(),
"split_with_fee should handle extreme fees without infinite recursion"
);
}
#[test]
fn test_split_values() {
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let amount = Amount::from(10);
let target = vec![Amount::from(2), Amount::from(4), Amount::from(4)];
let split_target = SplitTarget::Values(target.clone());
let values = amount
.split_targeted(&split_target, &fee_and_amounts)
.unwrap();
assert_eq!(target, values);
let target = vec![Amount::from(2), Amount::from(4), Amount::from(4)];
let split_target = SplitTarget::Values(vec![Amount::from(2), Amount::from(4)]);
let values = amount
.split_targeted(&split_target, &fee_and_amounts)
.unwrap();
assert_eq!(target, values);
let split_target = SplitTarget::Values(vec![Amount::from(2), Amount::from(10)]);
let values = amount.split_targeted(&split_target, &fee_and_amounts);
assert!(values.is_err())
}
#[test]
#[should_panic]
fn test_amount_addition() {
let amount_one: Amount = u64::MAX.into();
let amount_two: Amount = 1.into();
let amounts = vec![amount_one, amount_two];
let _total: Amount = Amount::try_sum(amounts).unwrap();
}
#[test]
fn test_try_amount_addition() {
let amount_one: Amount = u64::MAX.into();
let amount_two: Amount = 1.into();
let amounts = vec![amount_one, amount_two];
let total = Amount::try_sum(amounts);
assert!(total.is_err());
let amount_one: Amount = 10000.into();
let amount_two: Amount = 1.into();
let amounts = vec![amount_one, amount_two];
let total = Amount::try_sum(amounts).unwrap();
assert_eq!(total, 10001.into());
}
#[test]
fn test_amount_convert_to() {
let amount = Amount::new(1000, CurrencyUnit::Sat);
let converted = amount.convert_to(&CurrencyUnit::Msat).unwrap();
assert_eq!(converted.value(), 1000000);
assert_eq!(converted.unit(), &CurrencyUnit::Msat);
let amount = Amount::new(1000, CurrencyUnit::Msat);
let converted = amount.convert_to(&CurrencyUnit::Sat).unwrap();
assert_eq!(converted.value(), 1);
assert_eq!(converted.unit(), &CurrencyUnit::Sat);
let amount = Amount::new(1, CurrencyUnit::Usd);
let converted = amount.convert_to(&CurrencyUnit::Usd).unwrap();
assert_eq!(converted.value(), 1);
assert_eq!(converted.unit(), &CurrencyUnit::Usd);
let amount = Amount::new(1, CurrencyUnit::Eur);
let converted = amount.convert_to(&CurrencyUnit::Eur).unwrap();
assert_eq!(converted.value(), 1);
assert_eq!(converted.unit(), &CurrencyUnit::Eur);
let amount = Amount::new(1, CurrencyUnit::Sat);
let converted = amount.convert_to(&CurrencyUnit::Eur);
assert!(converted.is_err());
let amount = Amount::new(500, CurrencyUnit::Sat);
let converted = amount.convert_to(&CurrencyUnit::Sat).unwrap();
assert_eq!(converted.value(), 500);
assert_eq!(converted.unit(), &CurrencyUnit::Sat);
let amount = Amount::new(5000, CurrencyUnit::Msat);
let converted = amount.convert_to(&CurrencyUnit::Msat).unwrap();
assert_eq!(converted.value(), 5000);
assert_eq!(converted.unit(), &CurrencyUnit::Msat);
}
#[test]
fn test_amount_to_msat() {
let sat_amount = Amount::new(2, CurrencyUnit::Sat);
assert_eq!(sat_amount.to_msat().unwrap(), 2000);
let msat_amount = Amount::new(2500, CurrencyUnit::Msat);
assert_eq!(msat_amount.to_msat().unwrap(), 2500);
let usd_amount = Amount::new(1, CurrencyUnit::Usd);
assert!(usd_amount.to_msat().is_err());
}
#[test]
fn test_amount_to_sat() {
let msat_amount = Amount::new(2000, CurrencyUnit::Msat);
assert_eq!(msat_amount.to_sat().unwrap(), 2);
let sat_amount = Amount::new(5, CurrencyUnit::Sat);
assert_eq!(sat_amount.to_sat().unwrap(), 5);
let usd_amount = Amount::new(1, CurrencyUnit::Usd);
assert!(usd_amount.to_sat().is_err());
}
#[test]
fn test_amount_from_typed_to_untyped() {
let typed = Amount::new(1000, CurrencyUnit::Sat);
let untyped: Amount<()> = typed.into();
assert_eq!(u64::from(untyped), 1000);
}
#[test]
fn test_amount_sub_operator() {
let amount1 = Amount::from(100);
let amount2 = Amount::from(30);
let result = amount1.checked_sub(amount2).unwrap();
assert_eq!(result, Amount::from(70));
let amount1 = Amount::from(1000);
let amount2 = Amount::from(1);
let result = amount1.checked_sub(amount2).unwrap();
assert_eq!(result, Amount::from(999));
let amount1 = Amount::from(255);
let amount2 = Amount::from(128);
let result = amount1.checked_sub(amount2).unwrap();
assert_eq!(result, Amount::from(127));
}
#[test]
#[should_panic(expected = "Subtraction underflow")]
fn test_amount_sub_underflow() {
let amount1 = Amount::from(30);
let amount2 = Amount::from(100);
let _result = amount1 - amount2;
}
#[test]
fn test_checked_add_returns_correct_value() {
let amount1 = Amount::from(100);
let amount2 = Amount::from(50);
let result = amount1.checked_add(amount2);
assert_eq!(result, Some(Amount::from(150)));
let amount1 = Amount::from(1);
let amount2 = Amount::from(1);
let result = amount1.checked_add(amount2);
assert_eq!(result, Some(Amount::from(2)));
assert_ne!(result, Some(Amount::ZERO));
let amount1 = Amount::from(1000);
let amount2 = Amount::from(337);
let result = amount1.checked_add(amount2);
assert_eq!(result, Some(Amount::from(1337)));
}
#[test]
fn test_checked_add_overflow() {
let amount1 = Amount::from(u64::MAX);
let amount2 = Amount::from(1);
let result = amount1.checked_add(amount2);
assert!(result.is_none());
}
#[test]
fn test_try_sum_returns_correct_value() {
let amounts = vec![Amount::from(10), Amount::from(20), Amount::from(30)];
let result = Amount::try_sum(amounts).unwrap();
assert_eq!(result, Amount::from(60));
assert_ne!(result, Amount::ZERO);
let amounts = vec![Amount::from(1), Amount::from(1), Amount::from(1)];
let result = Amount::try_sum(amounts).unwrap();
assert_eq!(result, Amount::from(3));
let amounts = vec![Amount::from(100)];
let result = Amount::try_sum(amounts).unwrap();
assert_eq!(result, Amount::from(100));
let empty: Vec<Amount> = vec![];
let result = Amount::try_sum(empty).unwrap();
assert_eq!(result, Amount::ZERO);
}
#[test]
fn test_try_sum_overflow() {
let amounts = vec![Amount::from(u64::MAX), Amount::from(1)];
let result = Amount::try_sum(amounts);
assert!(result.is_err());
}
#[test]
fn test_split_returns_correct_values() {
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let amount = Amount::from(11);
let result = amount.split(&fee_and_amounts).unwrap();
assert!(!result.is_empty());
assert_eq!(Amount::try_sum(result.iter().copied()).unwrap(), amount);
let amount = Amount::from(255);
let result = amount.split(&fee_and_amounts).unwrap();
assert!(!result.is_empty());
assert_eq!(Amount::try_sum(result.iter().copied()).unwrap(), amount);
let amount = Amount::from(7);
let result = amount.split(&fee_and_amounts).unwrap();
assert_eq!(
result,
vec![Amount::from(4), Amount::from(2), Amount::from(1)]
);
for r in &result {
assert_ne!(*r, Amount::ZERO);
}
}
#[test]
fn test_split_modulo_operation() {
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let amount = Amount::from(15);
let result = amount.split(&fee_and_amounts).unwrap();
assert_eq!(
result,
vec![
Amount::from(8),
Amount::from(4),
Amount::from(2),
Amount::from(1)
]
);
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, amount);
}
#[test]
fn test_split_cannot_represent_amount() {
let fee_and_amounts: FeeAndAmounts = (0, vec![32]).into();
let amount = Amount::from(100);
let result = amount.split(&fee_and_amounts);
assert!(result.is_err());
match result {
Err(Error::CannotSplitAmount(requested, got)) => {
assert_eq!(requested, 100);
assert_eq!(got, 96); }
_ => panic!("Expected CannotSplitAmount error"),
}
let amount = Amount::from(32);
let result = amount.split(&fee_and_amounts);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![Amount::from(32)]);
let amount = Amount::from(64);
let result = amount.split(&fee_and_amounts);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![Amount::from(32), Amount::from(32)]);
let fee_and_amounts: FeeAndAmounts = (0, vec![32, 64]).into();
let amount = Amount::from(100);
let result = amount.split(&fee_and_amounts);
assert!(result.is_err());
match result {
Err(Error::CannotSplitAmount(requested, got)) => {
assert_eq!(requested, 100);
assert_eq!(got, 96);
}
_ => panic!("Expected CannotSplitAmount error"),
}
}
#[test]
fn test_split_amount_exceeds_keyset_capacity() {
let fee_and_amounts: FeeAndAmounts = (0, vec![32]).into();
let amount = Amount::from(100);
let result = amount.split(&fee_and_amounts);
assert!(result.is_err());
match result {
Err(Error::CannotSplitAmount(requested, got)) => {
assert_eq!(requested, 100);
assert_eq!(got, 96);
}
_ => panic!("Expected CannotSplitAmount error, got {:?}", result),
}
let fee_and_amounts: FeeAndAmounts = (0, vec![4, 16]).into();
let amount = Amount::from(50);
let result = amount.split(&fee_and_amounts);
assert!(result.is_err());
match result {
Err(Error::CannotSplitAmount(requested, got)) => {
assert_eq!(requested, 50);
assert_eq!(got, 48);
}
_ => panic!("Expected CannotSplitAmount error, got {:?}", result),
}
}
#[test]
fn test_from_u64_returns_correct_value() {
let amount = Amount::from(100u64);
assert_eq!(amount, Amount::from(100));
assert_ne!(amount, Amount::ZERO);
let amount = Amount::from(1u64);
assert_eq!(amount, Amount::from(1));
assert_eq!(amount, Amount::ONE);
let amount = Amount::from(1337u64);
assert_eq!(amount.to_u64(), 1337);
}
#[test]
fn test_checked_mul_returns_correct_value() {
let amount1 = Amount::from(10);
let amount2 = Amount::from(5);
let result = amount1.checked_mul(amount2);
assert_eq!(result, Some(Amount::from(50)));
assert_ne!(result, None);
assert_ne!(result, Some(Amount::ZERO));
let amount1 = Amount::from(100);
let amount2 = Amount::from(20);
let result = amount1.checked_mul(amount2);
assert_eq!(result, Some(Amount::from(2000)));
assert_ne!(result, Some(Amount::ZERO));
let amount1 = Amount::from(7);
let amount2 = Amount::from(13);
let result = amount1.checked_mul(amount2);
assert_eq!(result, Some(Amount::from(91)));
let amount1 = Amount::from(100);
let amount2 = Amount::ZERO;
let result = amount1.checked_mul(amount2);
assert_eq!(result, Some(Amount::ZERO));
let amount1 = Amount::from(42);
let amount2 = Amount::ONE;
let result = amount1.checked_mul(amount2);
assert_eq!(result, Some(Amount::from(42)));
let amount1 = Amount::from(u64::MAX);
let amount2 = Amount::from(2);
let result = amount1.checked_mul(amount2);
assert!(result.is_none());
}
#[test]
fn test_checked_div_returns_correct_value() {
let amount1 = Amount::from(100);
let amount2 = Amount::from(5);
let result = amount1.checked_div(amount2);
assert_eq!(result, Some(Amount::from(20)));
assert_ne!(result, None);
assert_ne!(result, Some(Amount::ZERO));
let amount1 = Amount::from(1000);
let amount2 = Amount::from(10);
let result = amount1.checked_div(amount2);
assert_eq!(result, Some(Amount::from(100)));
assert_ne!(result, Some(Amount::ZERO));
let amount1 = Amount::from(91);
let amount2 = Amount::from(7);
let result = amount1.checked_div(amount2);
assert_eq!(result, Some(Amount::from(13)));
let amount1 = Amount::from(42);
let amount2 = Amount::ONE;
let result = amount1.checked_div(amount2);
assert_eq!(result, Some(Amount::from(42)));
let amount1 = Amount::from(10);
let amount2 = Amount::from(3);
let result = amount1.checked_div(amount2);
assert_eq!(result, Some(Amount::from(3)));
let amount1 = Amount::from(100);
let amount2 = Amount::ZERO;
let result = amount1.checked_div(amount2);
assert!(result.is_none());
}
#[test]
fn test_convert_unit_returns_correct_value() {
let amount = Amount::from(1000);
let result = amount
.convert_unit(&CurrencyUnit::Sat, &CurrencyUnit::Msat)
.unwrap();
assert_eq!(result, Amount::from(1_000_000));
assert_ne!(result, Amount::ZERO);
let amount = Amount::from(5000);
let result = amount
.convert_unit(&CurrencyUnit::Msat, &CurrencyUnit::Sat)
.unwrap();
assert_eq!(result, Amount::from(5));
assert_ne!(result, Amount::ZERO);
let amount = Amount::from(123);
let result = amount
.convert_unit(&CurrencyUnit::Sat, &CurrencyUnit::Sat)
.unwrap();
assert_eq!(result, Amount::from(123));
let amount = Amount::from(456);
let result = amount
.convert_unit(&CurrencyUnit::Usd, &CurrencyUnit::Usd)
.unwrap();
assert_eq!(result, Amount::from(456));
let amount = Amount::from(789);
let result = amount
.convert_unit(&CurrencyUnit::Eur, &CurrencyUnit::Eur)
.unwrap();
assert_eq!(result, Amount::from(789));
let amount = Amount::from(100);
let result = amount.convert_unit(&CurrencyUnit::Sat, &CurrencyUnit::Eur);
assert!(result.is_err());
}
#[test]
fn test_amount_to_i64_returns_correct_value() {
let amount = Amount::from(100);
let result = amount.to_i64();
assert_eq!(result, Some(100));
assert!(result.is_some());
assert_ne!(result, Some(0));
assert_ne!(result, Some(1));
assert_ne!(result, Some(-1));
let amount = Amount::from(1000);
let result = amount.to_i64();
assert_eq!(result, Some(1000));
assert_ne!(result, None);
assert_ne!(result, Some(0));
assert_ne!(result, Some(1));
assert_ne!(result, Some(-1));
let amount = Amount::from(2);
let result = amount.to_i64();
assert_eq!(result, Some(2));
assert_ne!(result, Some(1));
let amount = Amount::from(i64::MAX as u64);
let result = amount.to_i64();
assert_eq!(result, Some(i64::MAX));
assert!(result.is_some());
let amount = Amount::from(i64::MAX as u64 + 1);
let result = amount.to_i64();
assert!(result.is_none());
let amount = Amount::from(u64::MAX);
let result = amount.to_i64();
assert!(result.is_none());
let amount = Amount::from(0);
let result = amount.to_i64();
assert_eq!(result, Some(0));
let amount = Amount::from(1);
let result = amount.to_i64();
assert_eq!(result, Some(1));
}
#[test]
fn test_amount_to_i64_boundary() {
let at_max = Amount::from(i64::MAX as u64);
assert!(at_max.to_i64().is_some());
assert_eq!(at_max.to_i64().unwrap(), i64::MAX);
let above_max = Amount::from(i64::MAX as u64 + 1);
assert!(above_max.to_i64().is_none());
let below_max = Amount::from(i64::MAX as u64 - 1);
assert!(below_max.to_i64().is_some());
assert_eq!(below_max.to_i64().unwrap(), i64::MAX - 1);
}
#[test]
fn test_amount_from_i64() {
let result = Amount::from_i64(100);
assert!(result.is_some());
assert_eq!(result.unwrap(), Amount::from(100));
assert_ne!(result, None);
assert_ne!(result, Some(Amount::ZERO));
let result = Amount::from_i64(0);
assert!(result.is_some());
assert_eq!(result.unwrap(), Amount::ZERO);
let result = Amount::from_i64(-1);
assert!(result.is_none());
let result = Amount::from_i64(-100);
assert!(result.is_none());
let result = Amount::from_i64(i64::MAX);
assert!(result.is_some());
assert_eq!(result.unwrap(), Amount::from(i64::MAX as u64));
assert_ne!(result, Some(Amount::ZERO));
let result = Amount::from_i64(1);
assert!(result.is_some());
assert_eq!(result.unwrap(), Amount::ONE);
assert_ne!(result, Some(Amount::ZERO));
}
#[test]
fn test_add_assign() {
let mut amount = Amount::from(100);
amount += Amount::from(50);
assert_eq!(amount, Amount::from(150));
assert_ne!(amount, Amount::from(100));
let mut amount = Amount::from(1);
amount += Amount::from(1);
assert_eq!(amount, Amount::from(2));
assert_ne!(amount, Amount::ONE);
let mut amount = Amount::ZERO;
amount += Amount::from(42);
assert_eq!(amount, Amount::from(42));
assert_ne!(amount, Amount::ZERO); }
#[test]
fn test_sub_assign() {
let mut amount = Amount::from(100);
amount -= Amount::from(30);
assert_eq!(amount, Amount::from(70));
assert_ne!(amount, Amount::from(100));
let mut amount = Amount::from(50);
amount -= Amount::from(1);
assert_eq!(amount, Amount::from(49));
assert_ne!(amount, Amount::from(50));
let mut amount = Amount::from(10);
amount -= Amount::from(10);
assert_eq!(amount, Amount::ZERO);
assert_ne!(amount, Amount::from(10)); }
#[test]
fn test_amount_with_currency_unit() {
let amount = Amount::new(1000, CurrencyUnit::Sat);
assert_eq!(amount.value(), 1000);
assert_eq!(amount.unit(), &CurrencyUnit::Sat);
}
#[test]
fn test_amount_new_with_custom_unit() {
let custom_unit = CurrencyUnit::Custom("BTC".to_string());
let amount = Amount::new(50, custom_unit.clone());
assert_eq!(amount.value(), 50);
assert_eq!(amount.unit(), &custom_unit);
}
#[test]
fn test_amount_into_parts() {
let amount = Amount::new(1234, CurrencyUnit::Msat);
let (value, unit) = amount.into_parts();
assert_eq!(value, 1234);
assert_eq!(unit, CurrencyUnit::Msat);
}
#[test]
fn test_amount_with_unit_conversion() {
let untyped: Amount<()> = Amount::from(100);
let typed = untyped.with_unit(CurrencyUnit::Sat);
assert_eq!(typed.value(), 100);
assert_eq!(typed.unit(), &CurrencyUnit::Sat);
}
#[test]
fn test_amount_with_unit_all_variants() {
let untyped = Amount::from(500);
let sat = untyped.with_unit(CurrencyUnit::Sat);
assert_eq!(sat.unit(), &CurrencyUnit::Sat);
let msat = untyped.with_unit(CurrencyUnit::Msat);
assert_eq!(msat.unit(), &CurrencyUnit::Msat);
let usd = untyped.with_unit(CurrencyUnit::Usd);
assert_eq!(usd.unit(), &CurrencyUnit::Usd);
let eur = untyped.with_unit(CurrencyUnit::Eur);
assert_eq!(eur.unit(), &CurrencyUnit::Eur);
let custom = untyped.with_unit(CurrencyUnit::Custom("TEST".into()));
assert_eq!(custom.unit(), &CurrencyUnit::Custom("TEST".into()));
}
#[test]
fn test_typed_amount_is_clone_not_copy() {
let amount = Amount::new(100, CurrencyUnit::Sat);
let cloned = amount.clone();
assert_eq!(cloned.value(), 100);
assert_eq!(cloned.unit(), &CurrencyUnit::Sat);
}
#[test]
fn test_untyped_amount_is_copy() {
let amount: Amount<()> = Amount::from(100);
let copy1 = amount;
let copy2 = amount; assert_eq!(copy1, copy2);
}
#[test]
fn test_amount_serialization_transparent() {
let amount = Amount::from(1234);
let json = serde_json::to_string(&amount).unwrap();
assert_eq!(json, "1234");
let deserialized: Amount<()> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, Amount::from(1234));
}
#[test]
fn test_typed_amount_serialization() {
let amount = Amount::new(5678, CurrencyUnit::Sat);
let json = serde_json::to_string(&amount).unwrap();
assert_eq!(json, "5678");
}
#[test]
fn test_protocol_type_pattern() {
let protocol_amount: Amount<()> = Amount::from(1000);
let _copied = protocol_amount;
let typed = protocol_amount.with_unit(CurrencyUnit::Sat);
assert_eq!(typed.value(), 1000);
let back_to_protocol = Amount::from(typed.value());
assert_eq!(back_to_protocol, protocol_amount);
}
#[test]
fn test_typed_amount_checked_add() {
let a = Amount::new(100, CurrencyUnit::Sat);
let b = Amount::new(50, CurrencyUnit::Sat);
let sum = a.checked_add(&b).unwrap();
assert_eq!(sum.value(), 150);
assert_eq!(sum.unit(), &CurrencyUnit::Sat);
}
#[test]
fn test_typed_amount_add_unit_mismatch() {
let sat = Amount::new(100, CurrencyUnit::Sat);
let msat = Amount::new(100, CurrencyUnit::Msat);
let result = sat.checked_add(&msat);
assert!(result.is_err());
match result.unwrap_err() {
Error::UnitMismatch(u1, u2) => {
assert_eq!(u1, CurrencyUnit::Sat);
assert_eq!(u2, CurrencyUnit::Msat);
}
_ => panic!("Expected UnitMismatch error"),
}
}
#[test]
fn test_typed_amount_checked_sub() {
let a = Amount::new(100, CurrencyUnit::Sat);
let b = Amount::new(30, CurrencyUnit::Sat);
let diff = a.checked_sub(&b).unwrap();
assert_eq!(diff.value(), 70);
assert_eq!(diff.unit(), &CurrencyUnit::Sat);
}
#[test]
fn test_typed_amount_sub_unit_mismatch() {
let sat = Amount::new(100, CurrencyUnit::Sat);
let usd = Amount::new(30, CurrencyUnit::Usd);
let result = sat.checked_sub(&usd);
assert!(result.is_err());
}
#[test]
fn test_typed_amount_convert_to() {
let sat = Amount::new(1000, CurrencyUnit::Sat);
let msat = sat.convert_to(&CurrencyUnit::Msat).unwrap();
assert_eq!(msat.value(), 1_000_000);
assert_eq!(msat.unit(), &CurrencyUnit::Msat);
let msat = Amount::new(5000, CurrencyUnit::Msat);
let sat = msat.convert_to(&CurrencyUnit::Sat).unwrap();
assert_eq!(sat.value(), 5);
assert_eq!(sat.unit(), &CurrencyUnit::Sat);
let sat = Amount::new(100, CurrencyUnit::Sat);
let same = sat.convert_to(&CurrencyUnit::Sat).unwrap();
assert_eq!(same.value(), 100);
assert_eq!(same.unit(), &CurrencyUnit::Sat);
}
#[test]
fn test_typed_amount_convert_invalid() {
let sat = Amount::new(100, CurrencyUnit::Sat);
let result = sat.convert_to(&CurrencyUnit::Eur);
assert!(result.is_err());
match result.unwrap_err() {
Error::CannotConvertUnits => {}
_ => panic!("Expected CannotConvertUnits error"),
}
}
#[test]
fn test_typed_amount_add_overflow() {
let a = Amount::new(u64::MAX, CurrencyUnit::Sat);
let b = Amount::new(1, CurrencyUnit::Sat);
let result = a.checked_add(&b);
assert!(result.is_err());
match result.unwrap_err() {
Error::AmountOverflow => {}
_ => panic!("Expected AmountOverflow error"),
}
}
#[test]
fn test_typed_amount_sub_underflow() {
let a = Amount::new(50, CurrencyUnit::Sat);
let b = Amount::new(100, CurrencyUnit::Sat);
let result = a.checked_sub(&b);
assert!(result.is_err());
match result.unwrap_err() {
Error::AmountOverflow => {} _ => panic!("Expected AmountOverflow error"),
}
}
#[test]
fn test_typed_amount_equality_same_unit() {
let a = Amount::new(100, CurrencyUnit::Sat);
let b = Amount::new(100, CurrencyUnit::Sat);
assert_eq!(a, b);
assert!(a == b);
let c = Amount::new(50, CurrencyUnit::Sat);
assert_ne!(a, c);
assert!(a != c);
}
#[test]
fn test_typed_amount_equality_different_units() {
let sat = Amount::new(100, CurrencyUnit::Sat);
let msat = Amount::new(100, CurrencyUnit::Msat);
assert_ne!(sat, msat);
assert!(sat != msat);
let usd = Amount::new(100, CurrencyUnit::Usd);
assert_ne!(sat, usd);
assert_ne!(msat, usd);
}
#[test]
fn test_typed_amount_comparison_same_unit() {
let small = Amount::new(50, CurrencyUnit::Sat);
let large = Amount::new(100, CurrencyUnit::Sat);
assert!(large > small);
assert!(small <= large);
assert!(small < large);
assert!(large >= small);
assert!(large >= small);
assert!(large >= Amount::new(100, CurrencyUnit::Sat));
assert!(small <= large);
assert!(small <= Amount::new(50, CurrencyUnit::Sat));
assert_eq!(large.partial_cmp(&small), Some(std::cmp::Ordering::Greater));
assert_eq!(small.partial_cmp(&large), Some(std::cmp::Ordering::Less));
assert_eq!(
small.partial_cmp(&Amount::new(50, CurrencyUnit::Sat)),
Some(std::cmp::Ordering::Equal)
);
}
#[test]
fn test_typed_amount_comparison_different_units_returns_none() {
let sat = Amount::new(100, CurrencyUnit::Sat);
let msat = Amount::new(50, CurrencyUnit::Msat);
assert_eq!(sat.partial_cmp(&msat), None);
assert_eq!(msat.partial_cmp(&sat), None);
let usd = Amount::new(100, CurrencyUnit::Usd);
assert_eq!(sat.partial_cmp(&usd), None);
assert_eq!(usd.partial_cmp(&sat), None);
let eur = Amount::new(100, CurrencyUnit::Eur);
assert_eq!(usd.partial_cmp(&eur), None);
let custom = Amount::new(100, CurrencyUnit::Custom("BTC".into()));
assert_eq!(sat.partial_cmp(&custom), None);
}
#[test]
fn test_typed_amount_comparison_operators_different_units() {
let sat = Amount::new(100, CurrencyUnit::Sat);
let msat = Amount::new(50, CurrencyUnit::Msat);
assert!(sat.partial_cmp(&msat).is_none());
assert!(msat.partial_cmp(&sat).is_none());
let sat100 = Amount::new(100, CurrencyUnit::Sat);
let msat100 = Amount::new(100, CurrencyUnit::Msat);
assert!(sat100.partial_cmp(&msat100).is_none());
}
#[test]
fn test_untyped_amount_has_total_ordering() {
use std::cmp::Ordering;
let a: Amount<()> = Amount::from(50);
let b: Amount<()> = Amount::from(100);
let c: Amount<()> = Amount::from(50);
assert_eq!(a.cmp(&b), Ordering::Less);
assert_eq!(b.cmp(&a), Ordering::Greater);
assert_eq!(a.cmp(&c), Ordering::Equal);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
assert_eq!(a.partial_cmp(&c), Some(Ordering::Equal));
}
#[test]
fn test_untyped_amount_sorting() {
let mut amounts: Vec<Amount<()>> = vec![
Amount::from(100),
Amount::from(25),
Amount::from(75),
Amount::from(50),
];
amounts.sort();
assert_eq!(
amounts,
vec![
Amount::from(25),
Amount::from(50),
Amount::from(75),
Amount::from(100),
]
);
}
#[test]
fn test_amount_currency_unit_to_i64() {
let amount = Amount::new(100, CurrencyUnit::Sat);
assert_eq!(amount.to_i64(), Some(100));
let amount = Amount::new(i64::MAX as u64, CurrencyUnit::Sat);
assert_eq!(amount.to_i64(), Some(i64::MAX));
let amount = Amount::new(i64::MAX as u64 + 1, CurrencyUnit::Sat);
assert_eq!(amount.to_i64(), None);
let amount = Amount::new(0, CurrencyUnit::Sat);
assert_eq!(amount.to_i64(), Some(0));
let amount = Amount::new(1, CurrencyUnit::Sat);
assert_eq!(amount.to_i64(), Some(1));
}
#[test]
fn test_display_with_unit() {
let amount = Amount::new(100, CurrencyUnit::Sat);
assert_eq!(amount.display_with_unit(), "100 sat");
let amount = Amount::new(50, CurrencyUnit::Msat);
assert_eq!(amount.display_with_unit(), "50 msat");
let amount = Amount::new(100, CurrencyUnit::Usd);
assert_eq!(amount.display_with_unit(), "100 usd");
let amount = Amount::new(123, CurrencyUnit::Custom("BTC".to_string()));
assert_eq!(amount.display_with_unit(), "123 btc");
}
#[test]
fn test_amount_add_operator() {
let a = Amount::from(100);
let b = Amount::from(50);
let sum = a + b;
assert_eq!(sum, Amount::from(150));
assert_ne!(sum, Amount::ZERO);
}
#[test]
fn test_saturating_sub_normal_case() {
let amount1 = Amount::from(100);
let amount2 = Amount::from(30);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::from(70));
assert_ne!(result, Amount::ZERO);
let amount1 = Amount::from(1000);
let amount2 = Amount::from(1);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::from(999));
let amount1 = Amount::from(2);
let amount2 = Amount::from(1);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::from(1));
assert_ne!(result, Amount::ZERO);
}
#[test]
fn test_saturating_sub_saturates_at_zero() {
let amount1 = Amount::from(30);
let amount2 = Amount::from(100);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
assert_ne!(result, Amount::from(30));
let amount1 = Amount::from(5);
let amount2 = Amount::from(10);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
let amount1 = Amount::ZERO;
let amount2 = Amount::from(1);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
}
#[test]
fn test_saturating_sub_equal_amounts() {
let amount1 = Amount::from(100);
let amount2 = Amount::from(100);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
let amount1 = Amount::from(1);
let amount2 = Amount::from(1);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
let result = Amount::ZERO.saturating_sub(Amount::ZERO);
assert_eq!(result, Amount::ZERO);
}
#[test]
fn test_saturating_sub_edge_case_other_greater_by_one() {
let amount1 = Amount::from(10);
let amount2 = Amount::from(11); let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
let amount1 = Amount::from(1);
let amount2 = Amount::from(2);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
let amount1 = Amount::ZERO;
let amount2 = Amount::from(1);
let result = amount1.saturating_sub(amount2);
assert_eq!(result, Amount::ZERO);
}
#[test]
fn test_split_single_denomination_keyset() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let result = Amount::from(1).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(1)]);
let result = Amount::from(2).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(1), Amount::from(1)]);
let result = Amount::from(5).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(1); 5]);
let result = Amount::from(10).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(1); 10]);
}
#[test]
fn test_split_denomination_reuse_required() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1, 5]).into();
let result = Amount::from(5).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(5)]);
let result = Amount::from(10).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(10));
assert_eq!(result, vec![Amount::from(5), Amount::from(5)]);
let result = Amount::from(12).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(12));
assert_eq!(
result,
vec![
Amount::from(5),
Amount::from(5),
Amount::from(1),
Amount::from(1),
]
);
}
#[test]
fn test_split_non_power_of_two_keyset() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1, 3, 5]).into();
let result = Amount::from(6).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(6));
let result = Amount::from(9).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(9));
let result = Amount::from(10).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(10));
let result = Amount::from(15).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(15));
}
#[test]
fn test_split_sparse_power_of_two_keyset() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1, 8]).into();
let result = Amount::from(3).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(1); 3]);
let result = Amount::from(9).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(9));
assert_eq!(result, vec![Amount::from(8), Amount::from(1)]);
let result = Amount::from(17).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(17));
assert_eq!(
result,
vec![Amount::from(8), Amount::from(8), Amount::from(1)]
);
}
#[test]
fn test_split_partial_power_of_two_keyset() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1, 4, 16]).into();
let result = Amount::from(2).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(1), Amount::from(1)]);
let result = Amount::from(5).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(4), Amount::from(1)]);
let result = Amount::from(6).split(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(6));
assert_eq!(
result,
vec![Amount::from(4), Amount::from(1), Amount::from(1)]
);
let result = Amount::from(20).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(16), Amount::from(4)]);
let result = Amount::from(8).split(&fee_and_amounts).unwrap();
assert_eq!(result, vec![Amount::from(4), Amount::from(4)]);
}
#[test]
fn test_split_large_amount_single_denomination() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let result = Amount::from(100).split(&fee_and_amounts).unwrap();
assert_eq!(result.len(), 100);
assert!(result.iter().all(|a| *a == Amount::from(1)));
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(100));
}
#[test]
fn test_split_zero_amount() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let result = Amount::from(0).split(&fee_and_amounts).unwrap();
assert!(result.is_empty());
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
let result = Amount::from(0).split(&fee_and_amounts).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_split_targeted_none_single_denomination() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let result = Amount::from(5)
.split_targeted(&SplitTarget::None, &fee_and_amounts)
.unwrap();
assert_eq!(result, vec![Amount::from(1); 5]);
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(5));
}
#[test]
fn test_split_targeted_value_single_denomination() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let result = Amount::from(5)
.split_targeted(&SplitTarget::Value(Amount::from(1)), &fee_and_amounts)
.unwrap();
assert_eq!(result, vec![Amount::from(1); 5]);
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(5));
}
#[test]
fn test_split_targeted_values_single_denomination() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let target = SplitTarget::Values(vec![Amount::from(1), Amount::from(1), Amount::from(1)]);
let result = Amount::from(5)
.split_targeted(&target, &fee_and_amounts)
.unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(5));
assert_eq!(result, vec![Amount::from(1); 5]);
}
#[test]
fn test_split_targeted_value_restricted_keyset() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1, 5]).into();
let result = Amount::from(15)
.split_targeted(&SplitTarget::Value(Amount::from(5)), &fee_and_amounts)
.unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
assert_eq!(total, Amount::from(15));
assert_eq!(
result,
vec![Amount::from(5), Amount::from(5), Amount::from(5)]
);
}
#[test]
fn test_split_with_fee_single_denomination() {
let fee_and_amounts: FeeAndAmounts = (100, vec![1]).into();
let amount = Amount::from(5);
let result = amount.split_with_fee(&fee_and_amounts).unwrap();
let total = Amount::try_sum(result.iter().copied()).unwrap();
let total_fee_ppk = (result.len() as u64) * fee_and_amounts.fee;
let total_fee = Amount::from(total_fee_ppk.div_ceil(1000));
assert!(
total >= amount.checked_add(total_fee).unwrap(),
"Split total {} should be >= amount {} + fee {}",
total,
amount,
total_fee
);
assert!(result.iter().all(|a| *a == Amount::from(1)));
}
#[test]
fn test_split_xsr_mint_keyset_regression() {
let fee_and_amounts: FeeAndAmounts = (0, vec![1]).into();
let amount = Amount::from(2);
let result = amount.split(&fee_and_amounts);
assert!(
result.is_ok(),
"split(2) with keyset {{1}} should succeed but got: {:?}",
result.err()
);
let proofs = result.unwrap();
assert_eq!(proofs, vec![Amount::from(1), Amount::from(1)]);
let amount = Amount::from(5);
let result = amount.split(&fee_and_amounts);
assert!(
result.is_ok(),
"split(5) with keyset {{1}} should succeed but got: {:?}",
result.err()
);
let proofs = result.unwrap();
assert_eq!(proofs.len(), 5);
assert_eq!(
Amount::try_sum(proofs.iter().copied()).unwrap(),
Amount::from(5)
);
}
}