|
@@ -26,6 +26,9 @@ pub enum Error {
|
|
|
/// Cannot convert units
|
|
/// Cannot convert units
|
|
|
#[error("Cannot convert units")]
|
|
#[error("Cannot convert units")]
|
|
|
CannotConvertUnits,
|
|
CannotConvertUnits,
|
|
|
|
|
+ /// Cannot perform operation on amounts with different units
|
|
|
|
|
+ #[error("Unit mismatch: cannot operate on {0} and {1}")]
|
|
|
|
|
+ UnitMismatch(CurrencyUnit, CurrencyUnit),
|
|
|
/// Invalid amount
|
|
/// Invalid amount
|
|
|
#[error("Invalid Amount: {0}")]
|
|
#[error("Invalid Amount: {0}")]
|
|
|
InvalidAmount(String),
|
|
InvalidAmount(String),
|
|
@@ -38,10 +41,15 @@ pub enum Error {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Amount can be any unit
|
|
/// Amount can be any unit
|
|
|
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
|
|
|
|
|
|
+///
|
|
|
|
|
+/// 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))]
|
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
|
|
-#[serde(transparent)]
|
|
|
|
|
-pub struct Amount(u64);
|
|
|
|
|
|
|
+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
|
|
/// 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
|
|
/// u64 which is the fee
|
|
@@ -77,23 +85,112 @@ impl FeeAndAmounts {
|
|
|
/// Fees and Amounts for each Keyset
|
|
/// Fees and Amounts for each Keyset
|
|
|
pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
|
|
pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
|
|
|
|
|
|
|
|
-impl FromStr for Amount {
|
|
|
|
|
|
|
+// 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;
|
|
type Err = Error;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
let value = s
|
|
let value = s
|
|
|
.parse::<u64>()
|
|
.parse::<u64>()
|
|
|
.map_err(|_| Error::InvalidAmount(s.to_owned()))?;
|
|
.map_err(|_| Error::InvalidAmount(s.to_owned()))?;
|
|
|
- Ok(Amount(value))
|
|
|
|
|
|
|
+ Ok(Amount { value, unit: () })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl Amount {
|
|
|
|
|
|
|
+impl Amount<()> {
|
|
|
/// Amount zero
|
|
/// Amount zero
|
|
|
- pub const ZERO: Amount = Amount(0);
|
|
|
|
|
|
|
+ pub const ZERO: Amount<()> = Amount { value: 0, unit: () };
|
|
|
|
|
|
|
|
/// Amount one
|
|
/// Amount one
|
|
|
- pub const ONE: Amount = Amount(1);
|
|
|
|
|
|
|
+ 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
|
|
/// Split into parts that are powers of two
|
|
|
pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
|
|
pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
|
|
@@ -101,7 +198,7 @@ impl Amount {
|
|
|
.amounts
|
|
.amounts
|
|
|
.iter()
|
|
.iter()
|
|
|
.rev()
|
|
.rev()
|
|
|
- .fold((Vec::new(), self.0), |(mut acc, total), &amount| {
|
|
|
|
|
|
|
+ .fold((Vec::new(), self.value), |(mut acc, total), &amount| {
|
|
|
if total >= amount {
|
|
if total >= amount {
|
|
|
acc.push(Self::from(amount));
|
|
acc.push(Self::from(amount));
|
|
|
}
|
|
}
|
|
@@ -131,10 +228,11 @@ impl Amount {
|
|
|
|
|
|
|
|
while parts_total.lt(self) {
|
|
while parts_total.lt(self) {
|
|
|
for part in parts_of_value.iter().copied() {
|
|
for part in parts_of_value.iter().copied() {
|
|
|
- if (part + parts_total).le(self) {
|
|
|
|
|
|
|
+ if (part.checked_add(parts_total).ok_or(Error::AmountOverflow)?).le(self) {
|
|
|
parts.push(part);
|
|
parts.push(part);
|
|
|
} else {
|
|
} else {
|
|
|
- let amount_left = *self - parts_total;
|
|
|
|
|
|
|
+ let amount_left =
|
|
|
|
|
+ self.checked_sub(parts_total).ok_or(Error::AmountOverflow)?;
|
|
|
parts.extend(amount_left.split(fee_and_amounts));
|
|
parts.extend(amount_left.split(fee_and_amounts));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -157,7 +255,9 @@ impl Amount {
|
|
|
return Err(Error::SplitValuesGreater);
|
|
return Err(Error::SplitValuesGreater);
|
|
|
}
|
|
}
|
|
|
Ordering::Greater => {
|
|
Ordering::Greater => {
|
|
|
- let extra = *self - values_total;
|
|
|
|
|
|
|
+ let extra = self
|
|
|
|
|
+ .checked_sub(values_total)
|
|
|
|
|
+ .ok_or(Error::AmountOverflow)?;
|
|
|
let mut extra_amount = extra.split(fee_and_amounts);
|
|
let mut extra_amount = extra.split(fee_and_amounts);
|
|
|
let mut values = values.clone();
|
|
let mut values = values.clone();
|
|
|
|
|
|
|
@@ -199,23 +299,31 @@ impl Amount {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Checked addition for Amount. Returns None if overflow occurs.
|
|
/// Checked addition for Amount. Returns None if overflow occurs.
|
|
|
- pub fn checked_add(self, other: Amount) -> Option<Amount> {
|
|
|
|
|
- self.0.checked_add(other.0).map(Amount)
|
|
|
|
|
|
|
+ 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.
|
|
/// Checked subtraction for Amount. Returns None if overflow occurs.
|
|
|
- pub fn checked_sub(self, other: Amount) -> Option<Amount> {
|
|
|
|
|
- self.0.checked_sub(other.0).map(Amount)
|
|
|
|
|
|
|
+ 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.
|
|
/// Checked multiplication for Amount. Returns None if overflow occurs.
|
|
|
- pub fn checked_mul(self, other: Amount) -> Option<Amount> {
|
|
|
|
|
- self.0.checked_mul(other.0).map(Amount)
|
|
|
|
|
|
|
+ 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.
|
|
/// Checked division for Amount. Returns None if overflow occurs.
|
|
|
- pub fn checked_div(self, other: Amount) -> Option<Amount> {
|
|
|
|
|
- self.0.checked_div(other.0).map(Amount)
|
|
|
|
|
|
|
+ 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
|
|
/// Try sum to check for overflow
|
|
@@ -233,19 +341,21 @@ impl Amount {
|
|
|
&self,
|
|
&self,
|
|
|
current_unit: &CurrencyUnit,
|
|
current_unit: &CurrencyUnit,
|
|
|
target_unit: &CurrencyUnit,
|
|
target_unit: &CurrencyUnit,
|
|
|
- ) -> Result<Amount, Error> {
|
|
|
|
|
- to_unit(self.0, current_unit, target_unit)
|
|
|
|
|
|
|
+ ) -> Result<Amount<()>, Error> {
|
|
|
|
|
+ Amount::new(self.value, current_unit.clone())
|
|
|
|
|
+ .convert_to(target_unit)
|
|
|
|
|
+ .map(Into::into)
|
|
|
}
|
|
}
|
|
|
- ///
|
|
|
|
|
|
|
+
|
|
|
/// Convert to u64
|
|
/// Convert to u64
|
|
|
pub fn to_u64(self) -> u64 {
|
|
pub fn to_u64(self) -> u64 {
|
|
|
- self.0
|
|
|
|
|
|
|
+ self.value
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Convert to i64
|
|
/// Convert to i64
|
|
|
pub fn to_i64(self) -> Option<i64> {
|
|
pub fn to_i64(self) -> Option<i64> {
|
|
|
- if self.0 <= i64::MAX as u64 {
|
|
|
|
|
- Some(self.0 as i64)
|
|
|
|
|
|
|
+ if self.value <= i64::MAX as u64 {
|
|
|
|
|
+ Some(self.value as i64)
|
|
|
} else {
|
|
} else {
|
|
|
None
|
|
None
|
|
|
}
|
|
}
|
|
@@ -254,69 +364,233 @@ impl Amount {
|
|
|
/// Create from i64, returning None if negative
|
|
/// Create from i64, returning None if negative
|
|
|
pub fn from_i64(value: i64) -> Option<Self> {
|
|
pub fn from_i64(value: i64) -> Option<Self> {
|
|
|
if value >= 0 {
|
|
if value >= 0 {
|
|
|
- Some(Amount(value as u64))
|
|
|
|
|
|
|
+ Some(Amount {
|
|
|
|
|
+ value: value as u64,
|
|
|
|
|
+ unit: (),
|
|
|
|
|
+ })
|
|
|
} else {
|
|
} else {
|
|
|
None
|
|
None
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl Default for Amount {
|
|
|
|
|
|
|
+impl Default for Amount<()> {
|
|
|
fn default() -> Self {
|
|
fn default() -> Self {
|
|
|
Amount::ZERO
|
|
Amount::ZERO
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl Default for &Amount {
|
|
|
|
|
|
|
+impl Default for &Amount<()> {
|
|
|
fn default() -> Self {
|
|
fn default() -> Self {
|
|
|
&Amount::ZERO
|
|
&Amount::ZERO
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl fmt::Display for Amount {
|
|
|
|
|
|
|
+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 {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
if let Some(width) = f.width() {
|
|
if let Some(width) = f.width() {
|
|
|
- write!(f, "{:width$}", self.0, width = width)
|
|
|
|
|
|
|
+ write!(f, "{:width$}", self.value, width = width)
|
|
|
} else {
|
|
} else {
|
|
|
- write!(f, "{}", self.0)
|
|
|
|
|
|
|
+ write!(f, "{}", self.value)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl From<u64> for Amount {
|
|
|
|
|
|
|
+impl From<u64> for Amount<()> {
|
|
|
fn from(value: u64) -> Self {
|
|
fn from(value: u64) -> Self {
|
|
|
- Self(value)
|
|
|
|
|
|
|
+ Amount { value, unit: () }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl From<&u64> for Amount {
|
|
|
|
|
|
|
+impl From<&u64> for Amount<()> {
|
|
|
fn from(value: &u64) -> Self {
|
|
fn from(value: &u64) -> Self {
|
|
|
- Self(*value)
|
|
|
|
|
|
|
+ Amount {
|
|
|
|
|
+ value: *value,
|
|
|
|
|
+ unit: (),
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl From<Amount> for u64 {
|
|
|
|
|
- fn from(value: Amount) -> Self {
|
|
|
|
|
- value.0
|
|
|
|
|
|
|
+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 {
|
|
|
|
|
|
|
+impl AsRef<u64> for Amount<()> {
|
|
|
fn as_ref(&self) -> &u64 {
|
|
fn as_ref(&self) -> &u64 {
|
|
|
- &self.0
|
|
|
|
|
|
|
+ &self.value
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl std::ops::Add for Amount {
|
|
|
|
|
- type Output = Amount;
|
|
|
|
|
|
|
+impl std::ops::Add for Amount<()> {
|
|
|
|
|
+ type Output = Amount<()>;
|
|
|
|
|
|
|
|
- fn add(self, rhs: Amount) -> Self::Output {
|
|
|
|
|
|
|
+ fn add(self, rhs: Amount<()>) -> Self::Output {
|
|
|
self.checked_add(rhs)
|
|
self.checked_add(rhs)
|
|
|
.expect("Addition overflow: the sum of the amounts exceeds the maximum value")
|
|
.expect("Addition overflow: the sum of the amounts exceeds the maximum value")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl std::ops::AddAssign for Amount {
|
|
|
|
|
|
|
+impl std::ops::AddAssign for Amount<()> {
|
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
fn add_assign(&mut self, rhs: Self) {
|
|
|
*self = self
|
|
*self = self
|
|
|
.checked_add(rhs)
|
|
.checked_add(rhs)
|
|
@@ -324,16 +598,16 @@ impl std::ops::AddAssign for Amount {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl std::ops::Sub for Amount {
|
|
|
|
|
- type Output = Amount;
|
|
|
|
|
|
|
+impl std::ops::Sub for Amount<()> {
|
|
|
|
|
+ type Output = Amount<()>;
|
|
|
|
|
|
|
|
- fn sub(self, rhs: Amount) -> Self::Output {
|
|
|
|
|
|
|
+ fn sub(self, rhs: Amount<()>) -> Self::Output {
|
|
|
self.checked_sub(rhs)
|
|
self.checked_sub(rhs)
|
|
|
.expect("Subtraction underflow: cannot subtract a larger amount from a smaller amount")
|
|
.expect("Subtraction underflow: cannot subtract a larger amount from a smaller amount")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl std::ops::SubAssign for Amount {
|
|
|
|
|
|
|
+impl std::ops::SubAssign for Amount<()> {
|
|
|
fn sub_assign(&mut self, other: Self) {
|
|
fn sub_assign(&mut self, other: Self) {
|
|
|
*self = self
|
|
*self = self
|
|
|
.checked_sub(other)
|
|
.checked_sub(other)
|
|
@@ -341,7 +615,7 @@ impl std::ops::SubAssign for Amount {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl std::ops::Mul for Amount {
|
|
|
|
|
|
|
+impl std::ops::Mul for Amount<()> {
|
|
|
type Output = Self;
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn mul(self, other: Self) -> Self::Output {
|
|
fn mul(self, other: Self) -> Self::Output {
|
|
@@ -350,7 +624,7 @@ impl std::ops::Mul for Amount {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-impl std::ops::Div for Amount {
|
|
|
|
|
|
|
+impl std::ops::Div for Amount<()> {
|
|
|
type Output = Self;
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn div(self, other: Self) -> Self::Output {
|
|
fn div(self, other: Self) -> Self::Output {
|
|
@@ -377,7 +651,10 @@ pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result<Amount, Er
|
|
|
),
|
|
),
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- to_unit(amount, ¤cy, unit).map_err(|_err| Error::CannotConvertUnits)
|
|
|
|
|
|
|
+ Amount::new(amount, currency)
|
|
|
|
|
+ .convert_to(unit)
|
|
|
|
|
+ .map(Into::into)
|
|
|
|
|
+ .map_err(|_err| Error::CannotConvertUnits)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Kinds of targeting that are supported
|
|
/// Kinds of targeting that are supported
|
|
@@ -395,30 +672,6 @@ pub enum SplitTarget {
|
|
|
/// Msats in sat
|
|
/// Msats in sat
|
|
|
pub const MSAT_IN_SAT: u64 = 1000;
|
|
pub const MSAT_IN_SAT: u64 = 1000;
|
|
|
|
|
|
|
|
-/// Helper function to convert units
|
|
|
|
|
-pub fn to_unit<T>(
|
|
|
|
|
- amount: T,
|
|
|
|
|
- current_unit: &CurrencyUnit,
|
|
|
|
|
- target_unit: &CurrencyUnit,
|
|
|
|
|
-) -> Result<Amount, Error>
|
|
|
|
|
-where
|
|
|
|
|
- T: Into<u64>,
|
|
|
|
|
-{
|
|
|
|
|
- let amount = amount.into();
|
|
|
|
|
- match (current_unit, target_unit) {
|
|
|
|
|
- (CurrencyUnit::Sat, CurrencyUnit::Sat) => Ok(amount.into()),
|
|
|
|
|
- (CurrencyUnit::Msat, CurrencyUnit::Msat) => Ok(amount.into()),
|
|
|
|
|
- (CurrencyUnit::Sat, CurrencyUnit::Msat) => amount
|
|
|
|
|
- .checked_mul(MSAT_IN_SAT)
|
|
|
|
|
- .map(Amount::from)
|
|
|
|
|
- .ok_or(Error::AmountOverflow),
|
|
|
|
|
- (CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok((amount / MSAT_IN_SAT).into()),
|
|
|
|
|
- (CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount.into()),
|
|
|
|
|
- (CurrencyUnit::Eur, CurrencyUnit::Eur) => Ok(amount.into()),
|
|
|
|
|
- _ => Err(Error::CannotConvertUnits),
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
#[cfg(test)]
|
|
#[cfg(test)]
|
|
|
mod tests {
|
|
mod tests {
|
|
|
use super::*;
|
|
use super::*;
|
|
@@ -451,29 +704,32 @@ mod tests {
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_split_target_amount() {
|
|
fn test_split_target_amount() {
|
|
|
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
- let amount = Amount(65);
|
|
|
|
|
|
|
+ let amount = Amount::from(65);
|
|
|
|
|
|
|
|
let split = amount
|
|
let split = amount
|
|
|
- .split_targeted(&SplitTarget::Value(Amount(32)), &fee_and_amounts)
|
|
|
|
|
|
|
+ .split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
|
|
|
.unwrap();
|
|
.unwrap();
|
|
|
- assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
|
|
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ vec![Amount::from(1), Amount::from(32), Amount::from(32)],
|
|
|
|
|
+ split
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
- let amount = Amount(150);
|
|
|
|
|
|
|
+ let amount = Amount::from(150);
|
|
|
|
|
|
|
|
let split = amount
|
|
let split = amount
|
|
|
.split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
|
|
.split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
|
|
|
.unwrap();
|
|
.unwrap();
|
|
|
assert_eq!(
|
|
assert_eq!(
|
|
|
vec![
|
|
vec![
|
|
|
- Amount(2),
|
|
|
|
|
- Amount(2),
|
|
|
|
|
- Amount(2),
|
|
|
|
|
- Amount(16),
|
|
|
|
|
- Amount(16),
|
|
|
|
|
- Amount(16),
|
|
|
|
|
- Amount(32),
|
|
|
|
|
- Amount(32),
|
|
|
|
|
- Amount(32)
|
|
|
|
|
|
|
+ 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
|
|
split
|
|
|
);
|
|
);
|
|
@@ -485,12 +741,12 @@ mod tests {
|
|
|
.unwrap();
|
|
.unwrap();
|
|
|
assert_eq!(
|
|
assert_eq!(
|
|
|
vec![
|
|
vec![
|
|
|
- Amount(1),
|
|
|
|
|
- Amount(2),
|
|
|
|
|
- Amount(4),
|
|
|
|
|
- Amount(8),
|
|
|
|
|
- Amount(16),
|
|
|
|
|
- Amount(32)
|
|
|
|
|
|
|
+ Amount::from(1),
|
|
|
|
|
+ Amount::from(2),
|
|
|
|
|
+ Amount::from(4),
|
|
|
|
|
+ Amount::from(8),
|
|
|
|
|
+ Amount::from(16),
|
|
|
|
|
+ Amount::from(32)
|
|
|
],
|
|
],
|
|
|
split
|
|
split
|
|
|
);
|
|
);
|
|
@@ -499,30 +755,30 @@ mod tests {
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_split_with_fee() {
|
|
fn test_split_with_fee() {
|
|
|
let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
- let amount = Amount(2);
|
|
|
|
|
|
|
+ let amount = Amount::from(2);
|
|
|
|
|
|
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
- assert_eq!(split, vec![Amount(2), Amount(1)]);
|
|
|
|
|
|
|
+ assert_eq!(split, vec![Amount::from(2), Amount::from(1)]);
|
|
|
|
|
|
|
|
- let amount = Amount(3);
|
|
|
|
|
|
|
+ let amount = Amount::from(3);
|
|
|
|
|
|
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
- assert_eq!(split, vec![Amount(4)]);
|
|
|
|
|
|
|
+ assert_eq!(split, vec![Amount::from(4)]);
|
|
|
|
|
|
|
|
- let amount = Amount(3);
|
|
|
|
|
|
|
+ let amount = Amount::from(3);
|
|
|
let fee_and_amounts = (1000, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
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();
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
// With fee_ppk=1000 (100%), amount 3 requires proofs totaling at least 5
|
|
// 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)
|
|
// to cover both the amount (3) and fees (~2 for 2 proofs)
|
|
|
- assert_eq!(split, vec![Amount(4), Amount(1)]);
|
|
|
|
|
|
|
+ assert_eq!(split, vec![Amount::from(4), Amount::from(1)]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_split_with_fee_reported_issue() {
|
|
fn test_split_with_fee_reported_issue() {
|
|
|
let fee_and_amounts = (100, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
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
|
|
// Test the reported issue: mint 600, send 300 with fee_ppk=100
|
|
|
- let amount = Amount(300);
|
|
|
|
|
|
|
+ let amount = Amount::from(300);
|
|
|
|
|
|
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
|
|
|
|
@@ -533,7 +789,7 @@ mod tests {
|
|
|
// The split should cover the amount plus fees
|
|
// The split should cover the amount plus fees
|
|
|
let split_total = Amount::try_sum(split.iter().copied()).unwrap();
|
|
let split_total = Amount::try_sum(split.iter().copied()).unwrap();
|
|
|
assert!(
|
|
assert!(
|
|
|
- split_total >= amount + total_fee,
|
|
|
|
|
|
|
+ split_total >= amount.checked_add(total_fee).unwrap(),
|
|
|
"Split total {} should be >= amount {} + fee {}",
|
|
"Split total {} should be >= amount {} + fee {}",
|
|
|
split_total,
|
|
split_total,
|
|
|
amount,
|
|
amount,
|
|
@@ -545,17 +801,17 @@ mod tests {
|
|
|
fn test_split_with_fee_edge_cases() {
|
|
fn test_split_with_fee_edge_cases() {
|
|
|
// Test various amounts with fee_ppk=100
|
|
// Test various amounts with fee_ppk=100
|
|
|
let test_cases = vec![
|
|
let test_cases = vec![
|
|
|
- (Amount(1), 100),
|
|
|
|
|
- (Amount(10), 100),
|
|
|
|
|
- (Amount(50), 100),
|
|
|
|
|
- (Amount(100), 100),
|
|
|
|
|
- (Amount(200), 100),
|
|
|
|
|
- (Amount(300), 100),
|
|
|
|
|
- (Amount(500), 100),
|
|
|
|
|
- (Amount(600), 100),
|
|
|
|
|
- (Amount(1000), 100),
|
|
|
|
|
- (Amount(1337), 100),
|
|
|
|
|
- (Amount(5000), 100),
|
|
|
|
|
|
|
+ (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 {
|
|
for (amount, fee_ppk) in test_cases {
|
|
@@ -600,12 +856,12 @@ mod tests {
|
|
|
fn test_split_with_fee_high_fees() {
|
|
fn test_split_with_fee_high_fees() {
|
|
|
// Test with very high fees
|
|
// Test with very high fees
|
|
|
let test_cases = vec![
|
|
let test_cases = vec![
|
|
|
- (Amount(10), 500), // 50% fee
|
|
|
|
|
- (Amount(10), 1000), // 100% fee
|
|
|
|
|
- (Amount(10), 2000), // 200% fee
|
|
|
|
|
- (Amount(100), 500),
|
|
|
|
|
- (Amount(100), 1000),
|
|
|
|
|
- (Amount(100), 2000),
|
|
|
|
|
|
|
+ (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 {
|
|
for (amount, fee_ppk) in test_cases {
|
|
@@ -638,7 +894,7 @@ mod tests {
|
|
|
fn test_split_with_fee_recursion_limit() {
|
|
fn test_split_with_fee_recursion_limit() {
|
|
|
// Test that the recursion doesn't go infinite
|
|
// Test that the recursion doesn't go infinite
|
|
|
// This tests the edge case where the method keeps adding Amount::ONE
|
|
// This tests the edge case where the method keeps adding Amount::ONE
|
|
|
- let amount = Amount(1);
|
|
|
|
|
|
|
+ let amount = Amount::from(1);
|
|
|
let fee_ppk = 10000;
|
|
let fee_ppk = 10000;
|
|
|
let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
|
|
|
|
@@ -652,9 +908,9 @@ mod tests {
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_split_values() {
|
|
fn test_split_values() {
|
|
|
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
- let amount = Amount(10);
|
|
|
|
|
|
|
+ let amount = Amount::from(10);
|
|
|
|
|
|
|
|
- let target = vec![Amount(2), Amount(4), Amount(4)];
|
|
|
|
|
|
|
+ let target = vec![Amount::from(2), Amount::from(4), Amount::from(4)];
|
|
|
|
|
|
|
|
let split_target = SplitTarget::Values(target.clone());
|
|
let split_target = SplitTarget::Values(target.clone());
|
|
|
|
|
|
|
@@ -664,9 +920,9 @@ mod tests {
|
|
|
|
|
|
|
|
assert_eq!(target, values);
|
|
assert_eq!(target, values);
|
|
|
|
|
|
|
|
- let target = vec![Amount(2), Amount(4), Amount(4)];
|
|
|
|
|
|
|
+ let target = vec![Amount::from(2), Amount::from(4), Amount::from(4)];
|
|
|
|
|
|
|
|
- let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
|
|
|
|
|
|
|
+ let split_target = SplitTarget::Values(vec![Amount::from(2), Amount::from(4)]);
|
|
|
|
|
|
|
|
let values = amount
|
|
let values = amount
|
|
|
.split_targeted(&split_target, &fee_and_amounts)
|
|
.split_targeted(&split_target, &fee_and_amounts)
|
|
@@ -674,7 +930,7 @@ mod tests {
|
|
|
|
|
|
|
|
assert_eq!(target, values);
|
|
assert_eq!(target, values);
|
|
|
|
|
|
|
|
- let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
|
|
|
|
|
|
|
+ let split_target = SplitTarget::Values(vec![Amount::from(2), Amount::from(10)]);
|
|
|
|
|
|
|
|
let values = amount.split_targeted(&split_target, &fee_and_amounts);
|
|
let values = amount.split_targeted(&split_target, &fee_and_amounts);
|
|
|
|
|
|
|
@@ -712,64 +968,55 @@ mod tests {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
#[test]
|
|
|
- fn test_amount_to_unit() {
|
|
|
|
|
- let amount = Amount::from(1000);
|
|
|
|
|
- let current_unit = CurrencyUnit::Sat;
|
|
|
|
|
- let target_unit = CurrencyUnit::Msat;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit).unwrap();
|
|
|
|
|
-
|
|
|
|
|
- assert_eq!(converted, 1000000.into());
|
|
|
|
|
-
|
|
|
|
|
- let amount = Amount::from(1000);
|
|
|
|
|
- let current_unit = CurrencyUnit::Msat;
|
|
|
|
|
- let target_unit = CurrencyUnit::Sat;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit).unwrap();
|
|
|
|
|
-
|
|
|
|
|
- assert_eq!(converted, 1.into());
|
|
|
|
|
-
|
|
|
|
|
- let amount = Amount::from(1);
|
|
|
|
|
- let current_unit = CurrencyUnit::Usd;
|
|
|
|
|
- let target_unit = CurrencyUnit::Usd;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit).unwrap();
|
|
|
|
|
-
|
|
|
|
|
- assert_eq!(converted, 1.into());
|
|
|
|
|
-
|
|
|
|
|
- let amount = Amount::from(1);
|
|
|
|
|
- let current_unit = CurrencyUnit::Eur;
|
|
|
|
|
- let target_unit = CurrencyUnit::Eur;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit).unwrap();
|
|
|
|
|
-
|
|
|
|
|
- assert_eq!(converted, 1.into());
|
|
|
|
|
-
|
|
|
|
|
- let amount = Amount::from(1);
|
|
|
|
|
- let current_unit = CurrencyUnit::Sat;
|
|
|
|
|
- let target_unit = CurrencyUnit::Eur;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ 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());
|
|
assert!(converted.is_err());
|
|
|
|
|
|
|
|
- // Test Sat -> Sat identity conversion
|
|
|
|
|
- let amount = Amount::from(500);
|
|
|
|
|
- let current_unit = CurrencyUnit::Sat;
|
|
|
|
|
- let target_unit = CurrencyUnit::Sat;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit).unwrap();
|
|
|
|
|
-
|
|
|
|
|
- assert_eq!(converted, 500.into());
|
|
|
|
|
-
|
|
|
|
|
- // Test Msat -> Msat identity conversion
|
|
|
|
|
- let amount = Amount::from(5000);
|
|
|
|
|
- let current_unit = CurrencyUnit::Msat;
|
|
|
|
|
- let target_unit = CurrencyUnit::Msat;
|
|
|
|
|
-
|
|
|
|
|
- let converted = to_unit(amount, ¤t_unit, &target_unit).unwrap();
|
|
|
|
|
|
|
+ // 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);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- assert_eq!(converted, 5000.into());
|
|
|
|
|
|
|
+ #[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.
|
|
/// Tests that the subtraction operator correctly computes the difference between amounts.
|
|
@@ -787,19 +1034,19 @@ mod tests {
|
|
|
let amount1 = Amount::from(100);
|
|
let amount1 = Amount::from(100);
|
|
|
let amount2 = Amount::from(30);
|
|
let amount2 = Amount::from(30);
|
|
|
|
|
|
|
|
- let result = amount1 - amount2;
|
|
|
|
|
|
|
+ let result = amount1.checked_sub(amount2).unwrap();
|
|
|
assert_eq!(result, Amount::from(70));
|
|
assert_eq!(result, Amount::from(70));
|
|
|
|
|
|
|
|
let amount1 = Amount::from(1000);
|
|
let amount1 = Amount::from(1000);
|
|
|
let amount2 = Amount::from(1);
|
|
let amount2 = Amount::from(1);
|
|
|
|
|
|
|
|
- let result = amount1 - amount2;
|
|
|
|
|
|
|
+ let result = amount1.checked_sub(amount2).unwrap();
|
|
|
assert_eq!(result, Amount::from(999));
|
|
assert_eq!(result, Amount::from(999));
|
|
|
|
|
|
|
|
let amount1 = Amount::from(255);
|
|
let amount1 = Amount::from(255);
|
|
|
let amount2 = Amount::from(128);
|
|
let amount2 = Amount::from(128);
|
|
|
|
|
|
|
|
- let result = amount1 - amount2;
|
|
|
|
|
|
|
+ let result = amount1.checked_sub(amount2).unwrap();
|
|
|
assert_eq!(result, Amount::from(127));
|
|
assert_eq!(result, Amount::from(127));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -970,11 +1217,11 @@ mod tests {
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_from_u64_returns_correct_value() {
|
|
fn test_from_u64_returns_correct_value() {
|
|
|
let amount = Amount::from(100u64);
|
|
let amount = Amount::from(100u64);
|
|
|
- assert_eq!(amount, Amount(100));
|
|
|
|
|
|
|
+ assert_eq!(amount, Amount::from(100));
|
|
|
assert_ne!(amount, Amount::ZERO);
|
|
assert_ne!(amount, Amount::ZERO);
|
|
|
|
|
|
|
|
let amount = Amount::from(1u64);
|
|
let amount = Amount::from(1u64);
|
|
|
- assert_eq!(amount, Amount(1));
|
|
|
|
|
|
|
+ assert_eq!(amount, Amount::from(1));
|
|
|
assert_eq!(amount, Amount::ONE);
|
|
assert_eq!(amount, Amount::ONE);
|
|
|
|
|
|
|
|
let amount = Amount::from(1337u64);
|
|
let amount = Amount::from(1337u64);
|
|
@@ -1283,4 +1530,390 @@ mod tests {
|
|
|
assert_eq!(amount, Amount::ZERO);
|
|
assert_eq!(amount, Amount::ZERO);
|
|
|
assert_ne!(amount, Amount::from(10)); // Should have changed
|
|
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),
|
|
|
|
|
+ ]
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|