| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037 |
- //! CDK Amount
- //!
- //! Is any unit and will be treated as the unit of the wallet
- 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;
- /// Amount Error
- #[derive(Debug, Error)]
- pub enum Error {
- /// Split Values must be less then or equal to amount
- #[error("Split Values must be less then or equal to amount")]
- SplitValuesGreater,
- /// Amount overflow
- #[error("Amount Overflow")]
- AmountOverflow,
- /// Cannot convert units
- #[error("Cannot convert units")]
- CannotConvertUnits,
- /// Cannot perform operation on amounts with different units
- #[error("Unit mismatch: cannot operate on {0} and {1}")]
- UnitMismatch(CurrencyUnit, CurrencyUnit),
- /// Invalid amount
- #[error("Invalid Amount: {0}")]
- InvalidAmount(String),
- /// Amount undefined
- #[error("Amount undefined")]
- AmountUndefined,
- /// Utf8 parse error
- #[error(transparent)]
- Utf8ParseError(#[from] std::string::FromUtf8Error),
- /// Cannot represent amount with available denominations
- #[error("Cannot represent amount {0} with available denominations (got {1})")]
- CannotSplitAmount(u64, u64),
- }
- /// Amount can be any unit
- ///
- /// Note: `PartialOrd` is implemented manually for `Amount<CurrencyUnit>` to return `None`
- /// when comparing amounts with different units. `Ord` is only implemented for `Amount<()>`.
- #[derive(Debug, Hash, PartialEq, Eq)]
- #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
- pub struct Amount<U = ()> {
- value: u64,
- unit: U,
- }
- /// Fees and and amount type, it can be casted just as a reference to the inner amounts, or a single
- /// u64 which is the fee
- #[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 {
- /// Fees
- #[inline(always)]
- pub fn fee(&self) -> u64 {
- self.fee
- }
- /// Amounts
- #[inline(always)]
- pub fn amounts(&self) -> &[u64] {
- &self.amounts
- }
- }
- /// Fees and Amounts for each Keyset
- pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
- // Copy and Clone implementations for Amount<()>
- impl Copy for Amount<()> {}
- impl Clone for Amount<()> {
- fn clone(&self) -> Self {
- *self
- }
- }
- // Clone implementation for Amount<CurrencyUnit>
- impl Clone for Amount<CurrencyUnit> {
- fn clone(&self) -> Self {
- Self {
- value: self.value,
- unit: self.unit.clone(),
- }
- }
- }
- // PartialOrd implementation for Amount<()> - always comparable
- impl PartialOrd for Amount<()> {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
- }
- // Ord implementation for Amount<()> - total ordering on value
- impl Ord for Amount<()> {
- fn cmp(&self, other: &Self) -> Ordering {
- self.value.cmp(&other.value)
- }
- }
- // PartialOrd implementation for Amount<CurrencyUnit> - returns None if units differ
- impl PartialOrd for Amount<CurrencyUnit> {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- if self.unit != other.unit {
- // Different units are not comparable
- None
- } else {
- Some(self.value.cmp(&other.value))
- }
- }
- }
- // Note: We intentionally do NOT implement Ord for Amount<CurrencyUnit>
- // because amounts with different units cannot have a total ordering.
- // Serialization - both variants serialize as just the u64 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<()> {
- /// Amount zero
- pub const ZERO: Amount<()> = Amount { value: 0, unit: () };
- /// Amount one
- pub const ONE: Amount<()> = Amount { value: 1, unit: () };
- /// Convert an untyped amount to a typed one by adding a unit
- ///
- /// This is used at the boundary between protocol and application layers.
- /// Protocol types use `Amount<()>` (no unit), while application types
- /// use `Amount<CurrencyUnit>` (with unit from keyset).
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// let untyped = Amount::from(100);
- /// let typed = untyped.with_unit(CurrencyUnit::Sat);
- /// assert_eq!(typed.value(), 100);
- /// assert_eq!(typed.unit(), &CurrencyUnit::Sat);
- /// ```
- pub fn with_unit(self, unit: CurrencyUnit) -> Amount<CurrencyUnit> {
- Amount {
- value: self.value,
- unit,
- }
- }
- /// Split into parts that are powers of two
- ///
- /// Returns an error if the amount cannot be fully represented
- /// with the available denominations.
- 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| {
- if total >= amount {
- 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)
- }
- /// Split into parts that are powers of two by target
- 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();
- // The powers of two that are need to create target value
- 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().into_iter())?;
- 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)
- }
- /// Splits amount into powers of two while accounting for the swap fee
- 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)
- }
- /// Checked addition for Amount. Returns None if overflow occurs.
- pub fn checked_add(self, other: Amount<()>) -> Option<Amount<()>> {
- self.value
- .checked_add(other.value)
- .map(|v| Amount { value: v, unit: () })
- }
- /// Checked subtraction for Amount. Returns None if overflow occurs.
- pub fn checked_sub(self, other: Amount<()>) -> Option<Amount<()>> {
- self.value
- .checked_sub(other.value)
- .map(|v| Amount { value: v, unit: () })
- }
- /// Checked multiplication for Amount. Returns None if overflow occurs.
- pub fn checked_mul(self, other: Amount<()>) -> Option<Amount<()>> {
- self.value
- .checked_mul(other.value)
- .map(|v| Amount { value: v, unit: () })
- }
- /// Checked division for Amount. Returns None if overflow occurs.
- pub fn checked_div(self, other: Amount<()>) -> Option<Amount<()>> {
- self.value
- .checked_div(other.value)
- .map(|v| Amount { value: v, unit: () })
- }
- /// Try sum to check for overflow
- 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)
- })
- }
- /// Convert unit
- 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)
- }
- /// Convert to u64
- pub fn to_u64(self) -> u64 {
- self.value
- }
- /// Convert to i64
- pub fn to_i64(self) -> Option<i64> {
- if self.value <= i64::MAX as u64 {
- Some(self.value as i64)
- } else {
- None
- }
- }
- /// Create from i64, returning None if negative
- 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> {
- /// Create a new Amount with an explicit unit
- ///
- /// This is the primary constructor for typed amounts. It works with all
- /// CurrencyUnit variants including Custom.
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// let sat_amount = Amount::new(1000, CurrencyUnit::Sat);
- /// let custom = Amount::new(50, CurrencyUnit::Custom("BTC".into()));
- /// ```
- pub fn new(value: u64, unit: CurrencyUnit) -> Self {
- Self { value, unit }
- }
- /// Get the numeric value
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// let amount = Amount::new(1000, CurrencyUnit::Sat);
- /// assert_eq!(amount.value(), 1000);
- /// ```
- pub fn value(&self) -> u64 {
- self.value
- }
- /// Convert to u64
- pub fn to_u64(self) -> u64 {
- self.value
- }
- /// Convert to i64
- pub fn to_i64(self) -> Option<i64> {
- if self.value <= i64::MAX as u64 {
- Some(self.value as i64)
- } else {
- None
- }
- }
- /// Get a reference to the unit
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// let amount = Amount::new(1000, CurrencyUnit::Sat);
- /// assert_eq!(amount.unit(), &CurrencyUnit::Sat);
- /// ```
- pub fn unit(&self) -> &CurrencyUnit {
- &self.unit
- }
- /// Consume self and return both value and unit
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// let amount = Amount::new(1000, CurrencyUnit::Sat);
- /// let (value, unit) = amount.into_parts();
- /// assert_eq!(value, 1000);
- /// assert_eq!(unit, CurrencyUnit::Sat);
- /// ```
- pub fn into_parts(self) -> (u64, CurrencyUnit) {
- (self.value, self.unit)
- }
- /// Checked addition with unit verification
- ///
- /// Returns an error if units don't match or if overflow occurs.
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// 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);
- ///
- /// // Different units cause an error
- /// let c = Amount::new(100, CurrencyUnit::Msat);
- /// assert!(a.checked_add(&c).is_err());
- /// ```
- 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)
- }
- /// Checked subtraction with unit verification
- ///
- /// Returns an error if units don't match or if underflow occurs.
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// 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);
- /// ```
- 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)
- }
- /// Convert to a different unit
- ///
- /// # Example
- /// ```
- /// # use cashu::{Amount, nuts::CurrencyUnit};
- /// 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);
- /// ```
- 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()))
- }
- /// Returns a string representation that includes the unit
- pub fn display_with_unit(&self) -> String {
- format!("{} {}", self.value, self.unit)
- }
- }
- 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")
- }
- }
- /// Convert offer to amount in unit
- 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)
- }
- /// Kinds of targeting that are supported
- #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
- pub enum SplitTarget {
- /// Default target; least amount of proofs
- #[default]
- None,
- /// Target amount for wallet to have most proofs that add up to value
- Value(Amount),
- /// Specific amounts to split into **MUST** equal amount being split
- Values(Vec<Amount>),
- }
- /// Msats in sat
- 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();
- // With fee_ppk=1000 (100%), amount 3 requires proofs totaling at least 5
- // to cover both the amount (3) and fees (~2 for 2 proofs)
- 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();
- // Test the reported issue: mint 600, send 300 with fee_ppk=100
- let amount = Amount::from(300);
- let split = amount.split_with_fee(&fee_and_amounts).unwrap();
- // Calculate the total fee for the split
- let total_fee_ppk = (split.len() as u64) * fee_and_amounts.fee;
- let total_fee = Amount::from(total_fee_ppk.div_ceil(1000));
- // The split should cover the amount plus fees
- 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() {
- // Test various amounts with fee_ppk=100
- 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();
- // Verify the split covers the required amount
- 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));
- // The net amount after fees should be at least the original amount
- 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() {
- // Test with very high fees
- let test_cases = vec![
- (Amount::from(10), 500), // 50% fee
- (Amount::from(10), 1000), // 100% fee
- (Amount::from(10), 2000), // 200% fee
- (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();
- // With high fees, we just need to ensure we can cover the amount
- 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() {
- // Test that the recursion doesn't go infinite
- // This tests the edge case where the method keeps adding Amount::ONE
- 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() {
- // Sat -> Msat
- 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);
- // Msat -> Sat
- 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);
- // Usd -> Usd identity conversion
- 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);
- // Eur -> Eur identity conversion
- 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);
- // Sat -> Eur should fail (no conversion path)
- let amount = Amount::new(1, CurrencyUnit::Sat);
- let converted = amount.convert_to(&CurrencyUnit::Eur);
- assert!(converted.is_err());
- // Sat -> Sat identity conversion
- 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);
- // Msat -> Msat identity conversion
- 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_from_typed_to_untyped() {
- // Test From<Amount<CurrencyUnit>> for Amount<()>
- let typed = Amount::new(1000, CurrencyUnit::Sat);
- let untyped: Amount<()> = typed.into();
- assert_eq!(u64::from(untyped), 1000);
- }
- /// Tests that the subtraction operator correctly computes the difference between amounts.
- ///
- /// This test verifies that the `-` operator for Amount produces the expected result.
- /// It's particularly important because the subtraction operation is used in critical
- /// code paths like `split_targeted`, where incorrect subtraction could lead to
- /// infinite loops or wrong calculations.
- ///
- /// Mutant testing: Catches mutations that replace the subtraction implementation
- /// with `Default::default()` (returning Amount::ZERO), which would cause infinite
- /// loops in `split_targeted` at line 138 where `*self - parts_total` is computed.
- #[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));
- }
- /// Tests that the subtraction operator panics when attempting to subtract
- /// a larger amount from a smaller amount (underflow).
- ///
- /// This test verifies the safety property that Amount subtraction will panic
- /// rather than wrap around on underflow. This is critical for preventing
- /// bugs where negative amounts could be interpreted as very large positive amounts.
- ///
- /// Mutant testing: Catches mutations that remove the panic behavior or return
- /// default values instead of properly handling underflow.
- #[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;
- }
- /// Tests that checked_add correctly computes the sum and returns the actual value.
- ///
- /// This is critical because checked_add is used in recursive functions like
- /// split_with_fee. If it returns Some(Amount::ZERO) instead of the actual sum,
- /// the recursion would never terminate.
- ///
- /// Mutant testing: Kills mutations that replace the implementation with
- /// `Some(Default::default())`, which would cause infinite loops in split_with_fee
- /// at line 198 where it recursively calls itself with incremented amounts.
- #[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)));
- }
- /// Tests that checked_add returns None on overflow.
- #[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());
- }
- /// Tests that try_sum correctly computes the total sum of amounts.
- ///
- /// This is critical because try_sum is used in loops like split_targeted at line 130
- /// to track progress. If it returns Ok(Amount::ZERO) instead of the actual sum,
- /// the loop condition `parts_total.eq(self)` would never be true, causing an infinite loop.
- ///
- /// Mutant testing: Kills mutations that replace the implementation with
- /// `Ok(Default::default())`, which would cause infinite loops.
- #[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);
- }
- /// Tests that try_sum returns error on overflow.
- #[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());
- }
- /// Tests that split returns a non-empty vec with actual values, not defaults.
- ///
- /// The split function is used in split_targeted's while loop (line 122).
- /// If split returns an empty vec or vec with Amount::ZERO when it shouldn't,
- /// the loop that extends parts with split results would never make progress,
- /// causing an infinite loop.
- ///
- /// Mutant testing: Kills mutations that replace split with `vec![]` or
- /// `vec![Default::default()]` which would cause infinite loops.
- #[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);
- }
- }
- /// Tests that the modulo operation in split works correctly.
- ///
- /// At line 108, split uses modulo (%) to compute the remainder.
- /// If this is mutated to division (/), it would produce wrong results
- /// that could cause infinite loops in code that depends on split.
- ///
- /// Mutant testing: Kills mutations that replace `%` with `/`.
- #[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);
- }
- /// Tests that split returns an error when the amount cannot be represented
- /// with the available denominations.
- #[test]
- fn test_split_cannot_represent_amount() {
- // Only denomination 32 available - the split algorithm can only use each denomination once
- let fee_and_amounts: FeeAndAmounts = (0, vec![32]).into();
- // 100 cannot be exactly represented: 100 >= 32, push(32), 100 % 32 = 4, result = [32]
- 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, 32); // Only one 32 can be taken
- }
- _ => panic!("Expected CannotSplitAmount error"),
- }
- // 32 can be exactly represented
- let amount = Amount::from(32);
- let result = amount.split(&fee_and_amounts);
- assert!(result.is_ok());
- assert_eq!(result.unwrap(), vec![Amount::from(32)]);
- // Missing denominations: only have 32 and 64, trying to split 100
- // 100 >= 64, push(64), 100 % 64 = 36
- // 36 >= 32, push(32), 36 % 32 = 4
- // Result: [64, 32] = 96, missing 4
- 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() {
- // Keyset with denominations 2^0 to 2^31
- let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
- // Attempt to split 2^63 (way larger than sum of keyset)
- let amount = Amount::from(2u64.pow(63));
- let result = amount.split(&fee_and_amounts);
- assert!(result.is_err());
- match result {
- Err(Error::CannotSplitAmount(requested, got)) => {
- assert_eq!(requested, 2u64.pow(63));
- // The algorithm greedily takes 2^31, and since 2^63 % 2^31 == 0, it stops there.
- // So "got" should be 2^31.
- assert_eq!(got, 2u64.pow(31));
- }
- _ => panic!("Expected CannotSplitAmount error, got {:?}", result),
- }
- }
- /// Tests that From<u64> correctly converts values to Amount.
- ///
- /// This conversion is used throughout the codebase including in loops and split operations.
- /// If it returns Default::default() (Amount::ZERO) instead of the actual value,
- /// it can cause infinite loops where amounts are being accumulated or compared.
- ///
- /// Mutant testing: Kills mutations that replace From<u64> with `Default::default()`.
- #[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);
- }
- /// Tests that checked_mul returns the correct product value.
- ///
- /// This is critical for any multiplication operations. If it returns None
- /// or Some(Amount::ZERO) instead of the actual product, calculations will be wrong.
- ///
- /// Mutant testing: Kills mutations that replace checked_mul with None or Some(Default::default()).
- #[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)));
- // Test multiplication by zero
- let amount1 = Amount::from(100);
- let amount2 = Amount::ZERO;
- let result = amount1.checked_mul(amount2);
- assert_eq!(result, Some(Amount::ZERO));
- // Test multiplication by one
- let amount1 = Amount::from(42);
- let amount2 = Amount::ONE;
- let result = amount1.checked_mul(amount2);
- assert_eq!(result, Some(Amount::from(42)));
- // Test overflow
- let amount1 = Amount::from(u64::MAX);
- let amount2 = Amount::from(2);
- let result = amount1.checked_mul(amount2);
- assert!(result.is_none());
- }
- /// Tests that checked_div returns the correct quotient value.
- ///
- /// This is critical for division operations. If it returns None or
- /// Some(Amount::ZERO) instead of the actual quotient, calculations will be wrong.
- ///
- /// Mutant testing: Kills mutations that replace checked_div with None or Some(Default::default()).
- #[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)));
- // Test division by one
- let amount1 = Amount::from(42);
- let amount2 = Amount::ONE;
- let result = amount1.checked_div(amount2);
- assert_eq!(result, Some(Amount::from(42)));
- // Test integer division (truncation)
- let amount1 = Amount::from(10);
- let amount2 = Amount::from(3);
- let result = amount1.checked_div(amount2);
- assert_eq!(result, Some(Amount::from(3)));
- // Test division by zero
- let amount1 = Amount::from(100);
- let amount2 = Amount::ZERO;
- let result = amount1.checked_div(amount2);
- assert!(result.is_none());
- }
- /// Tests that Amount::convert_unit returns the correct converted value.
- ///
- /// This is critical for unit conversions. If it returns Ok(Amount::ZERO)
- /// instead of the actual converted value, all conversions will be wrong.
- ///
- /// Mutant testing: Kills mutations that replace convert_unit with Ok(Default::default()).
- #[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));
- // Test invalid conversion
- let amount = Amount::from(100);
- let result = amount.convert_unit(&CurrencyUnit::Sat, &CurrencyUnit::Eur);
- assert!(result.is_err());
- }
- /// Tests that Amount::to_i64() returns the correct value.
- ///
- /// Mutant testing: Kills mutations that replace the return value with:
- /// - None
- /// - Some(0)
- /// - Some(1)
- /// - Some(-1)
- /// Also catches mutation that replaces <= with > in the comparison.
- #[test]
- fn test_amount_to_i64_returns_correct_value() {
- // Test with value 100 (catches None, Some(0), Some(1), Some(-1) mutations)
- 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));
- // Test with value 1000 (catches all constant mutations)
- 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));
- // Test with value 2 (specifically catches Some(1) mutation)
- let amount = Amount::from(2);
- let result = amount.to_i64();
- assert_eq!(result, Some(2));
- assert_ne!(result, Some(1));
- // Test with i64::MAX (should return Some(i64::MAX))
- // This catches the <= vs > mutation: if <= becomes >, this would return None
- let amount = Amount::from(i64::MAX as u64);
- let result = amount.to_i64();
- assert_eq!(result, Some(i64::MAX));
- assert!(result.is_some());
- // Test with i64::MAX + 1 (should return None)
- // This is the boundary case for the <= comparison
- let amount = Amount::from(i64::MAX as u64 + 1);
- let result = amount.to_i64();
- assert!(result.is_none());
- // Test with u64::MAX (should return None)
- let amount = Amount::from(u64::MAX);
- let result = amount.to_i64();
- assert!(result.is_none());
- // Edge case: 0 should return Some(0)
- let amount = Amount::from(0);
- let result = amount.to_i64();
- assert_eq!(result, Some(0));
- // Edge case: 1 should return Some(1)
- let amount = Amount::from(1);
- let result = amount.to_i64();
- assert_eq!(result, Some(1));
- }
- /// Tests the boundary condition for Amount::to_i64() at i64::MAX.
- ///
- /// This specifically tests the <= vs > mutation in the condition
- /// `if self.0 <= i64::MAX as u64`.
- #[test]
- fn test_amount_to_i64_boundary() {
- // Exactly at i64::MAX - should succeed
- 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);
- // One above i64::MAX - should fail
- let above_max = Amount::from(i64::MAX as u64 + 1);
- assert!(above_max.to_i64().is_none());
- // One below i64::MAX - should succeed
- 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);
- }
- /// Tests Amount::from_i64 returns the correct value.
- ///
- /// Mutant testing: Catches mutations that:
- /// - Replace return with None
- /// - Replace return with Some(Default::default())
- /// - Replace >= with < in the condition
- #[test]
- fn test_amount_from_i64() {
- // Positive value - should return Some with correct value
- 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));
- // Zero - boundary case for >= vs <
- // If >= becomes <, this would return None instead of Some
- let result = Amount::from_i64(0);
- assert!(result.is_some());
- assert_eq!(result.unwrap(), Amount::ZERO);
- // Negative value - should return None
- let result = Amount::from_i64(-1);
- assert!(result.is_none());
- let result = Amount::from_i64(-100);
- assert!(result.is_none());
- // Large positive value
- 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));
- // Value 1 - catches Some(Default::default()) mutation
- let result = Amount::from_i64(1);
- assert!(result.is_some());
- assert_eq!(result.unwrap(), Amount::ONE);
- assert_ne!(result, Some(Amount::ZERO));
- }
- /// Tests AddAssign actually modifies the value.
- ///
- /// Mutant testing: Catches mutation that replaces add_assign with ().
- #[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)); // Should have changed
- let mut amount = Amount::from(1);
- amount += Amount::from(1);
- assert_eq!(amount, Amount::from(2));
- assert_ne!(amount, Amount::ONE); // Should have changed
- let mut amount = Amount::ZERO;
- amount += Amount::from(42);
- assert_eq!(amount, Amount::from(42));
- assert_ne!(amount, Amount::ZERO); // Should have changed
- }
- /// Tests SubAssign actually modifies the value.
- ///
- /// Mutant testing: Catches mutation that replaces sub_assign with ().
- #[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)); // Should have changed
- let mut amount = Amount::from(50);
- amount -= Amount::from(1);
- assert_eq!(amount, Amount::from(49));
- assert_ne!(amount, Amount::from(50)); // Should have changed
- let mut amount = Amount::from(10);
- amount -= Amount::from(10);
- assert_eq!(amount, Amount::ZERO);
- assert_ne!(amount, Amount::from(10)); // Should have changed
- }
- // Phase 2 tests: Amount<CurrencyUnit> methods
- #[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();
- // If this compiles, Clone works. Cannot test Copy directly without moving.
- assert_eq!(cloned.value(), 100);
- assert_eq!(cloned.unit(), &CurrencyUnit::Sat);
- }
- // Phase 3 tests: Protocol types verification
- #[test]
- fn test_untyped_amount_is_copy() {
- // Verify Amount<()> is Copy (required for protocol types)
- let amount: Amount<()> = Amount::from(100);
- let copy1 = amount;
- let copy2 = amount; // Should not move - verifies Copy
- assert_eq!(copy1, copy2);
- }
- #[test]
- fn test_amount_serialization_transparent() {
- // Verify Amount<()> serializes as just the number (protocol compatibility)
- let amount = Amount::from(1234);
- let json = serde_json::to_string(&amount).unwrap();
- assert_eq!(json, "1234");
- // Verify deserialization works
- let deserialized: Amount<()> = serde_json::from_str(&json).unwrap();
- assert_eq!(deserialized, Amount::from(1234));
- }
- #[test]
- fn test_typed_amount_serialization() {
- // Verify Amount<CurrencyUnit> also serializes as just the number
- let amount = Amount::new(5678, CurrencyUnit::Sat);
- let json = serde_json::to_string(&amount).unwrap();
- assert_eq!(json, "5678");
- // Note: Cannot deserialize Amount<CurrencyUnit> directly
- // Unit must come from context (e.g., keyset)
- }
- #[test]
- fn test_protocol_type_pattern() {
- // Simulate protocol type usage pattern
- // Protocol layer: Amount<()> is Copy and serializes transparently
- let protocol_amount: Amount<()> = Amount::from(1000);
- let _copied = protocol_amount; // Copy works
- // Application layer: Convert to typed when needed
- let typed = protocol_amount.with_unit(CurrencyUnit::Sat);
- assert_eq!(typed.value(), 1000);
- // Back to protocol: Extract value
- let back_to_protocol = Amount::from(typed.value());
- assert_eq!(back_to_protocol, protocol_amount);
- }
- // Phase 4 tests: Unit-aware arithmetic
- #[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() {
- // Sat to Msat
- 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);
- // Msat to Sat
- 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);
- // Same unit (optimization check)
- 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 => {} // Underflow also returns AmountOverflow
- _ => panic!("Expected AmountOverflow error"),
- }
- }
- // Phase 5 tests: PartialOrd behavior for Amount<CurrencyUnit>
- /// Tests that equality works correctly for typed amounts with the same unit.
- #[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);
- }
- /// Tests that equality returns false for typed amounts with different units.
- #[test]
- fn test_typed_amount_equality_different_units() {
- let sat = Amount::new(100, CurrencyUnit::Sat);
- let msat = Amount::new(100, CurrencyUnit::Msat);
- // Same value, different units - should NOT be equal
- assert_ne!(sat, msat);
- assert!(sat != msat);
- let usd = Amount::new(100, CurrencyUnit::Usd);
- assert_ne!(sat, usd);
- assert_ne!(msat, usd);
- }
- /// Tests that comparison operators work correctly for typed amounts with the same unit.
- #[test]
- fn test_typed_amount_comparison_same_unit() {
- let small = Amount::new(50, CurrencyUnit::Sat);
- let large = Amount::new(100, CurrencyUnit::Sat);
- // Greater than
- assert!(large > small);
- assert!(!(small > large));
- // Less than
- assert!(small < large);
- assert!(!(large < small));
- // Greater than or equal
- assert!(large >= small);
- assert!(large >= Amount::new(100, CurrencyUnit::Sat));
- // Less than or equal
- assert!(small <= large);
- assert!(small <= Amount::new(50, CurrencyUnit::Sat));
- // partial_cmp returns Some
- 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)
- );
- }
- /// Tests that partial_cmp returns None for typed amounts with different units.
- /// This ensures that comparisons between different units are not accidentally valid.
- #[test]
- fn test_typed_amount_comparison_different_units_returns_none() {
- let sat = Amount::new(100, CurrencyUnit::Sat);
- let msat = Amount::new(50, CurrencyUnit::Msat);
- // partial_cmp should return None for different units
- assert_eq!(sat.partial_cmp(&msat), None);
- assert_eq!(msat.partial_cmp(&sat), None);
- // Different unit combinations
- 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);
- }
- /// Tests that comparison operators return false when comparing different units.
- /// Since partial_cmp returns None, all comparisons should be false.
- #[test]
- fn test_typed_amount_comparison_operators_different_units() {
- let sat = Amount::new(100, CurrencyUnit::Sat);
- let msat = Amount::new(50, CurrencyUnit::Msat);
- // When partial_cmp returns None:
- // - > returns false
- // - < returns false
- // - >= returns false
- // - <= returns false
- assert!(!(sat > msat));
- assert!(!(sat < msat));
- assert!(!(sat >= msat));
- assert!(!(sat <= msat));
- assert!(!(msat > sat));
- assert!(!(msat < sat));
- assert!(!(msat >= sat));
- assert!(!(msat <= sat));
- // Even with same value, different units should return false
- let sat100 = Amount::new(100, CurrencyUnit::Sat);
- let msat100 = Amount::new(100, CurrencyUnit::Msat);
- assert!(!(sat100 > msat100));
- assert!(!(sat100 < msat100));
- assert!(!(sat100 >= msat100));
- assert!(!(sat100 <= msat100));
- }
- /// Tests that Amount<()> (untyped) has total ordering and implements Ord.
- #[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);
- // Ord::cmp is available for Amount<()>
- assert_eq!(a.cmp(&b), Ordering::Less);
- assert_eq!(b.cmp(&a), Ordering::Greater);
- assert_eq!(a.cmp(&c), Ordering::Equal);
- // PartialOrd returns Some (total ordering)
- 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));
- }
- /// Tests that Amount<()> can be sorted (requires Ord).
- #[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);
- }
- }
|