Quellcode durchsuchen

feat: Generic unit on amount (#1470)

* feat: Add unit to the amount type

The Cashu protocol supports multiple currency units (Sat, Msat, Usd, Eur, etc.), but the previous Amount type was a simple u64 wrapper with no awareness of units. This created a subtle but dangerous class of bugs: nothing prevented code from accidentally adding 1000 satoshis to 500 millisatoshis, producing a nonsensical result of 1500 in an undefined unit. These bugs are especially insidious because the code compiles and runs without error—the incorrect math only manifests as wrong balances or failed payments in production.

This change makes unit mismatches impossible by encoding the currency unit into the type system. Amount<CurrencyUnit> now carries its unit at the type level, and arithmetic operations verify unit compatibility before proceeding. The compiler catches many mistakes statically, and runtime checks catch the rest with a clear UnitMismatch error rather than silent corruption.

The refactor maintains backwards compatibility by using Amount<()> (untyped) at serialization boundaries where the wire protocol expects a plain integer, while internal mint logic now uses Amount<CurrencyUnit> to get the safety guarantees. The with_unit() method provides a clear conversion point at the boundary between protocol parsing and application logic, making it explicit where unit context is being added.

This is particularly important for the mint's quote handling, where amounts flow between payment backends (which may use different units internally) and the core mint logic. By making MintQuote.amount_paid, MeltQuote.fee_reserve, and payment response types carry their units, we ensure that fee calculations, balance checks, and unit conversions are always performed correctly.
tsk vor 2 Wochen
Ursprung
Commit
352af3bf3f
35 geänderte Dateien mit 1697 neuen und 898 gelöschten Zeilen
  1. 819 186
      crates/cashu/src/amount.rs
  2. 33 33
      crates/cdk-cln/src/lib.rs
  3. 8 2
      crates/cdk-common/src/common.rs
  4. 4 4
      crates/cdk-common/src/database/mint/mod.rs
  5. 201 119
      crates/cdk-common/src/database/mint/test/mint.rs
  6. 131 78
      crates/cdk-common/src/mint.rs
  7. 44 29
      crates/cdk-common/src/payment.rs
  8. 2 4
      crates/cdk-common/src/pub_sub/remote_consumer.rs
  9. 43 37
      crates/cdk-fake-wallet/src/lib.rs
  10. 9 7
      crates/cdk-integration-tests/tests/mint.rs
  11. 26 29
      crates/cdk-ldk-node/src/lib.rs
  12. 19 18
      crates/cdk-lnbits/src/lib.rs
  13. 17 22
      crates/cdk-lnd/src/lib.rs
  14. 18 16
      crates/cdk-mint-rpc/src/proto/server.rs
  15. 19 20
      crates/cdk-payment-processor/src/proto/mod.rs
  16. 12 22
      crates/cdk-signatory/src/proto/server.rs
  17. 1 2
      crates/cdk-sql-common/src/mint/mod.rs
  18. 42 31
      crates/cdk-sql-common/src/mint/quotes.rs
  19. 15 14
      crates/cdk/src/mint/issue/mod.rs
  20. 6 7
      crates/cdk/src/mint/ln.rs
  21. 56 49
      crates/cdk/src/mint/melt/melt_saga/mod.rs
  22. 2 2
      crates/cdk/src/mint/melt/melt_saga/state.rs
  23. 2 3
      crates/cdk/src/mint/melt/melt_saga/tests.rs
  24. 41 49
      crates/cdk/src/mint/melt/mod.rs
  25. 52 23
      crates/cdk/src/mint/melt/shared.rs
  26. 7 10
      crates/cdk/src/mint/mod.rs
  27. 2 2
      crates/cdk/src/mint/start_up_check.rs
  28. 9 8
      crates/cdk/src/mint/subscription.rs
  29. 16 6
      crates/cdk/src/mint/swap/swap_saga/mod.rs
  30. 1 2
      crates/cdk/src/mint/swap/swap_saga/tests.rs
  31. 2 2
      crates/cdk/src/mint/swap/tests/p2pk_sigall_spending_conditions_tests.rs
  32. 2 2
      crates/cdk/src/mint/swap/tests/p2pk_spending_conditions_tests.rs
  33. 29 55
      crates/cdk/src/mint/verification.rs
  34. 3 2
      crates/cdk/src/wallet/melt/bolt11.rs
  35. 4 3
      crates/cdk/src/wallet/melt/bolt12.rs

+ 819 - 186
crates/cashu/src/amount.rs

@@ -26,6 +26,9 @@ pub enum Error {
     /// 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),
@@ -38,10 +41,15 @@ pub enum Error {
 }
 
 /// 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))]
-#[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
 /// u64 which is the fee
@@ -77,23 +85,112 @@ impl FeeAndAmounts {
 /// Fees and Amounts for each Keyset
 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;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         let value = s
             .parse::<u64>()
             .map_err(|_| Error::InvalidAmount(s.to_owned()))?;
-        Ok(Amount(value))
+        Ok(Amount { value, unit: () })
     }
 }
 
-impl Amount {
+impl Amount<()> {
     /// Amount zero
-    pub const ZERO: Amount = Amount(0);
+    pub const ZERO: Amount<()> = Amount { value: 0, unit: () };
 
     /// 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
     pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
@@ -101,7 +198,7 @@ impl Amount {
             .amounts
             .iter()
             .rev()
-            .fold((Vec::new(), self.0), |(mut acc, total), &amount| {
+            .fold((Vec::new(), self.value), |(mut acc, total), &amount| {
                 if total >= amount {
                     acc.push(Self::from(amount));
                 }
@@ -131,10 +228,11 @@ impl Amount {
 
                 while parts_total.lt(self) {
                     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);
                         } 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));
                         }
 
@@ -157,7 +255,9 @@ impl Amount {
                         return Err(Error::SplitValuesGreater);
                     }
                     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 values = values.clone();
 
@@ -199,23 +299,31 @@ impl Amount {
     }
 
     /// 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.
-    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.
-    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.
-    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
@@ -233,19 +341,21 @@ impl Amount {
         &self,
         current_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
     pub fn to_u64(self) -> u64 {
-        self.0
+        self.value
     }
 
     /// Convert to 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 {
             None
         }
@@ -254,69 +364,233 @@ impl Amount {
     /// Create from i64, returning None if negative
     pub fn from_i64(value: i64) -> Option<Self> {
         if value >= 0 {
-            Some(Amount(value as u64))
+            Some(Amount {
+                value: value as u64,
+                unit: (),
+            })
         } else {
             None
         }
     }
 }
 
-impl Default for Amount {
+impl Default for Amount<()> {
     fn default() -> Self {
         Amount::ZERO
     }
 }
 
-impl Default for &Amount {
+impl Default for &Amount<()> {
     fn default() -> Self {
         &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 {
         if let Some(width) = f.width() {
-            write!(f, "{:width$}", self.0, width = width)
+            write!(f, "{:width$}", self.value, width = width)
         } else {
-            write!(f, "{}", self.0)
+            write!(f, "{}", self.value)
         }
     }
 }
 
-impl From<u64> for Amount {
+impl From<u64> for Amount<()> {
     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 {
-        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 {
-        &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)
             .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) {
         *self = self
             .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)
             .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) {
         *self = self
             .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;
 
     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;
 
     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, &currency, 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
@@ -395,30 +672,6 @@ pub enum SplitTarget {
 /// Msats in sat
 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)]
 mod tests {
     use super::*;
@@ -451,29 +704,32 @@ mod tests {
     #[test]
     fn test_split_target_amount() {
         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
-            .split_targeted(&SplitTarget::Value(Amount(32)), &fee_and_amounts)
+            .split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
             .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
             .split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
             .unwrap();
         assert_eq!(
             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
         );
@@ -485,12 +741,12 @@ mod tests {
             .unwrap();
         assert_eq!(
             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
         );
@@ -499,30 +755,30 @@ mod tests {
     #[test]
     fn test_split_with_fee() {
         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();
-        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();
-        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 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(4), Amount(1)]);
+        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(300);
+        let amount = Amount::from(300);
 
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
 
@@ -533,7 +789,7 @@ mod tests {
         // The split should cover the amount plus fees
         let split_total = Amount::try_sum(split.iter().copied()).unwrap();
         assert!(
-            split_total >= amount + total_fee,
+            split_total >= amount.checked_add(total_fee).unwrap(),
             "Split total {} should be >= amount {} + fee {}",
             split_total,
             amount,
@@ -545,17 +801,17 @@ mod tests {
     fn test_split_with_fee_edge_cases() {
         // Test various amounts with fee_ppk=100
         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 {
@@ -600,12 +856,12 @@ mod tests {
     fn test_split_with_fee_high_fees() {
         // Test with very high fees
         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 {
@@ -638,7 +894,7 @@ mod tests {
     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(1);
+        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();
 
@@ -652,9 +908,9 @@ mod tests {
     #[test]
     fn test_split_values() {
         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());
 
@@ -664,9 +920,9 @@ mod tests {
 
         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
             .split_targeted(&split_target, &fee_and_amounts)
@@ -674,7 +930,7 @@ mod tests {
 
         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);
 
@@ -712,64 +968,55 @@ mod tests {
     }
 
     #[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, &current_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, &current_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, &current_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, &current_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, &current_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());
 
-        // 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, &current_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, &current_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.
@@ -787,19 +1034,19 @@ mod tests {
         let amount1 = Amount::from(100);
         let amount2 = Amount::from(30);
 
-        let result = amount1 - amount2;
+        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 - amount2;
+        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 - amount2;
+        let result = amount1.checked_sub(amount2).unwrap();
         assert_eq!(result, Amount::from(127));
     }
 
@@ -970,11 +1217,11 @@ mod tests {
     #[test]
     fn test_from_u64_returns_correct_value() {
         let amount = Amount::from(100u64);
-        assert_eq!(amount, Amount(100));
+        assert_eq!(amount, Amount::from(100));
         assert_ne!(amount, Amount::ZERO);
 
         let amount = Amount::from(1u64);
-        assert_eq!(amount, Amount(1));
+        assert_eq!(amount, Amount::from(1));
         assert_eq!(amount, Amount::ONE);
 
         let amount = Amount::from(1337u64);
@@ -1283,4 +1530,390 @@ mod tests {
         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),
+            ]
+        );
+    }
 }

+ 33 - 33
crates/cdk-cln/src/lib.rs

@@ -12,7 +12,7 @@ use std::time::Duration;
 
 use async_trait::async_trait;
 use bitcoin::hashes::sha256::Hash;
-use cdk_common::amount::{to_unit, Amount};
+use cdk_common::amount::Amount;
 use cdk_common::common::FeeReserve;
 use cdk_common::database::DynKVStore;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
@@ -271,9 +271,8 @@ impl MintPayment for Cln {
 
                             let response = WaitPaymentResponse {
                                 payment_identifier: request_lookup_id,
-                                payment_amount: amount_msats.msat().into(),
-                                unit: CurrencyUnit::Msat,
-                                payment_id: payment_hash.to_string()
+                                payment_amount: Amount::new(amount_msats.msat(), CurrencyUnit::Msat),
+                                payment_id: payment_hash.to_string(),
                             };
                             tracing::info!("CLN: Created WaitPaymentResponse with amount {} msats", amount_msats.msat());
                             let event = Event::PaymentReceived(response);
@@ -334,11 +333,12 @@ impl MintPayment for Cln {
                         .into()
                 };
                 // Convert to target unit
-                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+                let amount =
+                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
 
                 // Calculate fee
                 let relative_fee_reserve =
-                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
                 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
                 let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
@@ -347,9 +347,8 @@ impl MintPayment for Cln {
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                     )),
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -368,20 +367,19 @@ impl MintPayment for Cln {
                 };
 
                 // Convert to target unit
-                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+                let amount = Amount::new(amount_msat, CurrencyUnit::Msat).convert_to(unit)?;
 
                 // Calculate fee
                 let relative_fee_reserve =
-                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
                 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
                 let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
                 Ok(PaymentQuoteResponse {
                     request_lookup_id: None,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
             }
         }
@@ -521,15 +519,14 @@ impl MintPayment for Cln {
                 };
 
                 MakePaymentResponse {
-                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     payment_lookup_id: payment_identifier,
+                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     status,
-                    total_spent: to_unit(
+                    total_spent: Amount::new(
                         pay_response.amount_sent_msat.msat(),
-                        &CurrencyUnit::Msat,
-                        unit,
-                    )?,
-                    unit: unit.clone(),
+                        CurrencyUnit::Msat,
+                    )
+                    .convert_to(unit)?,
                 }
             }
             Err(err) => {
@@ -562,8 +559,10 @@ impl MintPayment for Cln {
 
                 let label = Uuid::new_v4().to_string();
 
-                let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
-                let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount.into()));
+                let amount_converted =
+                    Amount::new(amount.into(), unit.clone()).convert_to(&CurrencyUnit::Msat)?;
+                let amount_msat =
+                    AmountOrAny::Amount(CLN_Amount::from_msat(amount_converted.value()));
 
                 let invoice_response = cln_client
                     .call_typed(&InvoiceRequest {
@@ -604,9 +603,10 @@ impl MintPayment for Cln {
                 // Match like this until we change to option
                 let amount = match amount {
                     Some(amount) => {
-                        let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+                        let amount = Amount::new(amount.into(), unit.clone())
+                            .convert_to(&CurrencyUnit::Msat)?;
 
-                        amount.to_string()
+                        amount.value().to_string()
                     }
                     None => "any".to_string(),
                 };
@@ -711,13 +711,13 @@ impl MintPayment for Cln {
             .filter(|p| p.amount_msat.is_some()) // Filter out invoices without an amount
             .map(|p| WaitPaymentResponse {
                 payment_identifier: payment_identifier.clone(),
-                payment_amount: p
-                    .amount_msat
-                    // Safe to expect since we filtered for Some
-                    .expect("We have filter out those without amounts")
-                    .msat()
-                    .into(),
-                unit: CurrencyUnit::Msat,
+                payment_amount: Amount::new(
+                    p.amount_msat
+                        // Safe to expect since we filtered for Some
+                        .expect("We have filter out those without amounts")
+                        .msat(),
+                    CurrencyUnit::Msat,
+                ),
                 payment_id: p.payment_hash.to_string(),
             })
             .collect())
@@ -761,16 +761,16 @@ impl MintPayment for Cln {
                     status,
                     total_spent: pays_response
                         .amount_sent_msat
-                        .map_or(Amount::ZERO, |a| a.msat().into()),
-                    unit: CurrencyUnit::Msat,
+                        .map_or(Amount::new(0, CurrencyUnit::Msat), |a| {
+                            Amount::new(a.msat(), CurrencyUnit::Msat)
+                        }),
                 })
             }
             None => Ok(MakePaymentResponse {
                 payment_lookup_id: payment_identifier.clone(),
                 payment_proof: None,
                 status: MeltQuoteState::Unknown,
-                total_spent: Amount::ZERO,
-                unit: CurrencyUnit::Msat,
+                total_spent: Amount::new(0, CurrencyUnit::Msat),
             }),
         }
     }

+ 8 - 2
crates/cdk-common/src/common.rs

@@ -49,7 +49,11 @@ impl Melted {
         );
 
         let fee_paid = proofs_amount
-            .checked_sub(quote_amount + change_amount)
+            .checked_sub(
+                quote_amount
+                    .checked_add(change_amount)
+                    .ok_or(Error::AmountOverflow)?,
+            )
             .ok_or(Error::AmountOverflow)?;
 
         Ok(Self {
@@ -63,7 +67,9 @@ impl Melted {
 
     /// Total amount melted
     pub fn total_amount(&self) -> Amount {
-        self.amount + self.fee_paid
+        self.amount
+            .checked_add(self.fee_paid)
+            .expect("We check when calc fee paid")
     }
 }
 

+ 4 - 4
crates/cdk-common/src/database/mint/mod.rs

@@ -36,9 +36,9 @@ pub use super::kvstore::{
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct MeltRequestInfo {
     /// Total amount of all input proofs in the melt request
-    pub inputs_amount: Amount,
+    pub inputs_amount: Amount<CurrencyUnit>,
     /// Fee amount associated with the input proofs
-    pub inputs_fee: Amount,
+    pub inputs_fee: Amount<CurrencyUnit>,
     /// Blinded messages for change outputs
     pub change_outputs: Vec<BlindedMessage>,
 }
@@ -99,8 +99,8 @@ pub trait QuotesTransaction {
     async fn add_melt_request(
         &mut self,
         quote_id: &QuoteId,
-        inputs_amount: Amount,
-        inputs_fee: Amount,
+        inputs_amount: Amount<CurrencyUnit>,
+        inputs_fee: Amount<CurrencyUnit>,
     ) -> Result<(), Self::Err>;
 
     /// Add blinded_messages for a quote_id

+ 201 - 119
crates/cdk-common/src/database/mint/test/mint.rs

@@ -5,7 +5,7 @@ use std::str::FromStr;
 
 use cashu::nut00::KnownMethod;
 use cashu::quote_id::QuoteId;
-use cashu::{Amount, BlindSignature, Id, SecretKey};
+use cashu::{Amount, BlindSignature, CurrencyUnit, Id, SecretKey};
 
 use crate::database::mint::test::unique_string;
 use crate::database::mint::{Database, Error, KeysDatabase};
@@ -26,8 +26,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -53,8 +53,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -83,8 +83,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -99,18 +99,26 @@ where
     let p2 = unique_string();
 
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
-    assert_eq!(mint_quote.amount_paid(), 100.into());
+    assert_eq!(mint_quote.amount_paid().value(), 100);
 
     mint_quote
-        .add_payment(250.into(), p2.clone(), None)
+        .add_payment(
+            Amount::from(250).with_unit(CurrencyUnit::Sat),
+            p2.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
-    assert_eq!(mint_quote.amount_paid(), 350.into());
+    assert_eq!(mint_quote.amount_paid().value(), 350);
 
     tx.commit().await.unwrap();
 
@@ -119,14 +127,17 @@ where
         .await
         .unwrap()
         .expect("mint_quote_from_db");
-    assert_eq!(mint_quote_from_db.amount_paid(), 350.into());
+    assert_eq!(mint_quote_from_db.amount_paid().value(), 350);
     assert_eq!(
         mint_quote_from_db
             .payments
             .iter()
-            .map(|x| (x.payment_id.clone(), x.amount))
+            .map(|x| (x.payment_id.clone(), x.amount.clone()))
             .collect::<Vec<_>>(),
-        vec![(p1, 100.into()), (p2, 250.into())]
+        vec![
+            (p1, Amount::from(100).with_unit(CurrencyUnit::Sat)),
+            (p2, Amount::from(250).with_unit(CurrencyUnit::Sat))
+        ]
     );
 }
 
@@ -143,8 +154,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -158,17 +169,25 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
-    assert_eq!(mint_quote.amount_paid(), 100.into());
+    assert_eq!(mint_quote.amount_paid().value(), 100);
 
     mint_quote
-        .add_payment(250.into(), p2.clone(), None)
+        .add_payment(
+            Amount::from(250).with_unit(CurrencyUnit::Sat),
+            p2.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
-    assert_eq!(mint_quote.amount_paid(), 350.into());
+    assert_eq!(mint_quote.amount_paid().value(), 350);
     tx.commit().await.unwrap();
 
     let mint_quote_from_db = db
@@ -176,14 +195,17 @@ where
         .await
         .unwrap()
         .expect("mint_quote_from_db");
-    assert_eq!(mint_quote_from_db.amount_paid(), 350.into());
+    assert_eq!(mint_quote_from_db.amount_paid().value(), 350);
     assert_eq!(
         mint_quote_from_db
             .payments
             .iter()
-            .map(|x| (x.payment_id.clone(), x.amount))
+            .map(|x| (x.payment_id.clone(), x.amount.clone()))
             .collect::<Vec<_>>(),
-        vec![(p1, 100.into()), (p2, 250.into())]
+        vec![
+            (p1, Amount::from(100).with_unit(CurrencyUnit::Sat)),
+            (p2, Amount::from(250).with_unit(CurrencyUnit::Sat))
+        ]
     );
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -208,8 +230,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -222,12 +244,18 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
     // Duplicate payment should fail
-    assert!(mint_quote.add_payment(100.into(), p1, None).is_err());
+    assert!(mint_quote
+        .add_payment(Amount::from(100).with_unit(CurrencyUnit::Sat), p1, None)
+        .is_err());
     tx.commit().await.unwrap();
 
     let mint_quote_from_db = db
@@ -254,8 +282,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -266,7 +294,11 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
@@ -278,7 +310,9 @@ where
         .expect("no error")
         .expect("quote");
     // Duplicate payment should fail
-    assert!(mint_quote.add_payment(100.into(), p1, None).is_err());
+    assert!(mint_quote
+        .add_payment(Amount::from(100).with_unit(CurrencyUnit::Sat), p1, None)
+        .is_err());
     tx.commit().await.unwrap(); // although in theory nothing has changed, let's try it out
 
     let mint_quote_from_db = db
@@ -303,8 +337,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -315,7 +349,9 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     // Trying to issue without any payment should fail (over-issue)
-    assert!(mint_quote.add_issuance(100.into()).is_err());
+    assert!(mint_quote
+        .add_issuance(Amount::from(100).with_unit(CurrencyUnit::Sat))
+        .is_err());
 }
 
 /// Reject over issue
@@ -331,8 +367,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -351,7 +387,9 @@ where
         .expect("no error")
         .expect("quote");
     // Trying to issue without any payment should fail (over-issue)
-    assert!(mint_quote.add_issuance(100.into()).is_err());
+    assert!(mint_quote
+        .add_issuance(Amount::from(100).with_unit(CurrencyUnit::Sat))
+        .is_err());
 }
 
 /// Reject over issue with payment
@@ -367,8 +405,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -380,11 +418,17 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     // Trying to issue more than paid should fail (over-issue)
-    assert!(mint_quote.add_issuance(101.into()).is_err());
+    assert!(mint_quote
+        .add_issuance(Amount::from(101).with_unit(CurrencyUnit::Sat))
+        .is_err());
 }
 
 /// Reject over issue with payment
@@ -400,8 +444,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         vec![],
@@ -414,7 +458,11 @@ where
     let mut mint_quote = tx.add_mint_quote(mint_quote).await.unwrap();
     let quote_id = mint_quote.id.clone();
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
@@ -426,15 +474,17 @@ where
         .expect("no error")
         .expect("quote");
     // Trying to issue more than paid should fail (over-issue)
-    assert!(mint_quote.add_issuance(101.into()).is_err());
+    assert!(mint_quote
+        .add_issuance(Amount::from(101).with_unit(CurrencyUnit::Sat))
+        .is_err());
 }
 /// Successful melt with unique blinded messages
 pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
 where
     DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
 {
-    let inputs_amount = Amount::from(100u64);
-    let inputs_fee = Amount::from(1u64);
+    let inputs_amount = Amount::new(100, CurrencyUnit::Sat);
+    let inputs_fee = Amount::new(1, CurrencyUnit::Sat);
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
     // Create a dummy blinded message
@@ -448,9 +498,9 @@ where
     let blinded_messages = vec![blinded_message];
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, Amount::new(33, cashu::CurrencyUnit::Sat), Amount::new(0, cashu::CurrencyUnit::Sat), 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote.clone()).await.unwrap();
-    tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
+    tx.add_melt_request(&quote.id, inputs_amount.clone(), inputs_fee.clone())
         .await
         .unwrap();
     tx.add_blinded_messages(
@@ -486,8 +536,8 @@ where
     DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
 {
     let quote_id1 = QuoteId::new_uuid();
-    let inputs_amount = Amount::from(100u64);
-    let inputs_fee = Amount::from(1u64);
+    let inputs_amount = Amount::new(100, CurrencyUnit::Sat);
+    let inputs_fee = Amount::new(1, CurrencyUnit::Sat);
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
     // Create a dummy blinded message
@@ -517,7 +567,7 @@ where
 
     // Now try to add melt request with the same blinded message - should fail due to constraint
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
+    let quote2 = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, Amount::new(33, cashu::CurrencyUnit::Sat), Amount::new(0, cashu::CurrencyUnit::Sat), 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote2.clone()).await.unwrap();
     tx.add_melt_request(&quote2.id, inputs_amount, inputs_fee)
         .await
@@ -542,8 +592,8 @@ pub async fn reject_duplicate_blinded_message_db_constraint<DB>(db: DB)
 where
     DB: Database<Error> + KeysDatabase<Err = Error>,
 {
-    let inputs_amount = Amount::from(100u64);
-    let inputs_fee = Amount::from(1u64);
+    let inputs_amount = Amount::new(100, CurrencyUnit::Sat);
+    let inputs_fee = Amount::new(1, CurrencyUnit::Sat);
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
     // Create a dummy blinded message
@@ -558,9 +608,9 @@ where
 
     // First insert succeeds
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, Amount::new(33, cashu::CurrencyUnit::Sat), Amount::new(0, cashu::CurrencyUnit::Sat), 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote.clone()).await.unwrap();
-    tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
+    tx.add_melt_request(&quote.id, inputs_amount.clone(), inputs_fee.clone())
         .await
         .unwrap();
     assert!(tx
@@ -579,7 +629,7 @@ where
 
     // Second insert with same blinded_message but different quote_id should fail due to unique constraint on blinded_message
     let mut tx = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, Amount::new(33, cashu::CurrencyUnit::Sat), Amount::new(0, cashu::CurrencyUnit::Sat), 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx.add_melt_quote(quote.clone()).await.unwrap();
     tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
@@ -605,8 +655,8 @@ pub async fn cleanup_melt_request_after_processing<DB>(db: DB)
 where
     DB: Database<Error> + KeysDatabase<Err = Error>,
 {
-    let inputs_amount = Amount::from(100u64);
-    let inputs_fee = Amount::from(1u64);
+    let inputs_amount = Amount::new(100, CurrencyUnit::Sat);
+    let inputs_fee = Amount::new(1, CurrencyUnit::Sat);
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
     // Create dummy blinded message
@@ -621,7 +671,7 @@ where
 
     // Insert melt request
     let mut tx1 = Database::begin_transaction(&db).await.unwrap();
-    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, 33.into(), Amount::ZERO, 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
+    let quote = MeltQuote::new(MeltPaymentRequest::Bolt11 { bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap() }, cashu::CurrencyUnit::Sat, Amount::new(33, cashu::CurrencyUnit::Sat), Amount::new(0, cashu::CurrencyUnit::Sat), 0, None, None, cashu::PaymentMethod::Known(KnownMethod::Bolt11));
     tx1.add_melt_quote(quote.clone()).await.unwrap();
     tx1.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
@@ -669,8 +719,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         None,
         None,
@@ -687,8 +737,8 @@ where
     assert!(retrieved.is_some());
     let retrieved = retrieved.unwrap();
     assert_eq!(retrieved.id, melt_quote.id);
-    assert_eq!(retrieved.amount, melt_quote.amount);
-    assert_eq!(retrieved.fee_reserve, melt_quote.fee_reserve);
+    assert_eq!(retrieved.amount(), melt_quote.amount());
+    assert_eq!(retrieved.fee_reserve(), melt_quote.fee_reserve());
 }
 
 /// Test adding duplicate melt quotes fails
@@ -701,8 +751,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         None,
         None,
@@ -732,8 +782,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         None,
         None,
@@ -784,8 +834,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         Some(PaymentIdentifier::CustomId("old_lookup_id".to_string())),
         None,
@@ -826,8 +876,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -843,8 +893,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        200.into(),
-        0.into(),
+        Amount::new(200, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -875,8 +925,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         None,
         None,
@@ -888,8 +938,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        200.into(),
-        20.into(),
+        Amount::new(200, cashu::CurrencyUnit::Sat),
+        Amount::new(20, cashu::CurrencyUnit::Sat),
         0,
         None,
         None,
@@ -925,8 +975,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -963,8 +1013,8 @@ where
         0,
         lookup_id.clone(),
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1078,8 +1128,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1100,10 +1150,14 @@ where
         .expect("valid quote")
         .expect("valid result");
     mint_quote
-        .add_payment(300.into(), "payment_1".to_string(), None)
+        .add_payment(
+            Amount::from(300).with_unit(CurrencyUnit::Sat),
+            "payment_1".to_string(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
-    assert_eq!(mint_quote.amount_paid(), 300.into());
+    assert_eq!(mint_quote.amount_paid().value(), 300);
     tx.commit().await.unwrap();
 
     // Add payment second time
@@ -1114,15 +1168,19 @@ where
         .expect("valid quote")
         .expect("valid result");
     mint_quote
-        .add_payment(200.into(), "payment_2".to_string(), None)
+        .add_payment(
+            Amount::from(200).with_unit(CurrencyUnit::Sat),
+            "payment_2".to_string(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
-    assert_eq!(mint_quote.amount_paid(), 500.into());
+    assert_eq!(mint_quote.amount_paid().value(), 500);
     tx.commit().await.unwrap();
 
     // Verify final state
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
-    assert_eq!(retrieved.amount_paid(), 500.into());
+    assert_eq!(retrieved.amount_paid().value(), 500);
 }
 
 /// Test incrementing mint quote amount issued
@@ -1140,8 +1198,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1162,7 +1220,11 @@ where
         .expect("valid quote")
         .expect("valid result");
     mint_quote
-        .add_payment(1000.into(), "payment_1".to_string(), None)
+        .add_payment(
+            Amount::from(1000).with_unit(CurrencyUnit::Sat),
+            "payment_1".to_string(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
@@ -1174,9 +1236,11 @@ where
         .await
         .expect("valid quote")
         .expect("valid result");
-    mint_quote.add_issuance(400.into()).unwrap();
+    mint_quote
+        .add_issuance(Amount::from(400).with_unit(CurrencyUnit::Sat))
+        .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
-    assert_eq!(mint_quote.amount_issued(), 400.into());
+    assert_eq!(mint_quote.amount_issued().value(), 400);
     tx.commit().await.unwrap();
 
     // Add issuance second time
@@ -1186,14 +1250,16 @@ where
         .await
         .expect("valid quote")
         .expect("valid result");
-    mint_quote.add_issuance(300.into()).unwrap();
+    mint_quote
+        .add_issuance(Amount::from(300).with_unit(CurrencyUnit::Sat))
+        .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
-    assert_eq!(mint_quote.amount_issued(), 700.into());
+    assert_eq!(mint_quote.amount_issued().value(), 700);
     tx.commit().await.unwrap();
 
     // Verify final state
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
-    assert_eq!(retrieved.amount_issued(), 700.into());
+    assert_eq!(retrieved.amount_issued().value(), 700);
 }
 
 /// Test getting mint quote within transaction (with lock)
@@ -1211,8 +1277,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1245,8 +1311,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         None,
         None,
@@ -1264,7 +1330,7 @@ where
     assert!(retrieved.is_some());
     let retrieved = retrieved.unwrap();
     assert_eq!(retrieved.id, melt_quote.id);
-    assert_eq!(retrieved.amount, melt_quote.amount);
+    assert_eq!(retrieved.amount(), melt_quote.amount());
     tx.commit().await.unwrap();
 }
 
@@ -1284,8 +1350,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1324,8 +1390,8 @@ where
         0,
         lookup_id.clone(),
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1401,8 +1467,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1423,10 +1489,14 @@ where
         .expect("valid quote")
         .expect("valid result");
     mint_quote
-        .add_payment(300.into(), "payment_1".to_string(), None)
+        .add_payment(
+            Amount::from(300).with_unit(CurrencyUnit::Sat),
+            "payment_1".to_string(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
-    assert_eq!(mint_quote.amount_paid(), 300.into());
+    assert_eq!(mint_quote.amount_paid().value(), 300);
     tx.commit().await.unwrap();
 
     // Try to add the same payment_id again - should fail with DuplicatePaymentId error
@@ -1437,7 +1507,11 @@ where
         .expect("valid quote")
         .expect("valid result");
 
-    let result = mint_quote.add_payment(300.into(), "payment_1".to_string(), None);
+    let result = mint_quote.add_payment(
+        Amount::from(300).with_unit(CurrencyUnit::Sat),
+        "payment_1".to_string(),
+        None,
+    );
 
     assert!(
         matches!(result.unwrap_err(), crate::Error::DuplicatePaymentId),
@@ -1447,7 +1521,7 @@ where
 
     // Verify that the amount_paid is still 300 (not 600)
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
-    assert_eq!(retrieved.amount_paid(), 300.into());
+    assert_eq!(retrieved.amount_paid().value(), 300);
 
     // A different payment_id should succeed
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -1458,16 +1532,20 @@ where
         .expect("valid result");
 
     mint_quote
-        .add_payment(200.into(), "payment_2".to_string(), None)
+        .add_payment(
+            Amount::from(200).with_unit(CurrencyUnit::Sat),
+            "payment_2".to_string(),
+            None,
+        )
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
-    assert_eq!(mint_quote.amount_paid(), 500.into());
+    assert_eq!(mint_quote.amount_paid().value(), 500);
     tx.commit().await.unwrap();
 
     // Verify final state
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
-    assert_eq!(retrieved.amount_paid(), 500.into());
+    assert_eq!(retrieved.amount_paid().value(), 500);
 }
 
 /// Test that loading the quote first allows modifications
@@ -1485,8 +1563,8 @@ where
         0,
         PaymentIdentifier::CustomId(unique_string()),
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         vec![],
@@ -1510,7 +1588,11 @@ where
 
     // Now modification should succeed
     loaded_quote
-        .add_payment(100.into(), unique_string(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            unique_string(),
+            None,
+        )
         .unwrap();
     let result = tx.update_mint_quote(&mut loaded_quote).await;
 
@@ -1524,5 +1606,5 @@ where
 
     // Verify the modification was persisted
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
-    assert_eq!(retrieved.amount_paid(), 100.into());
+    assert_eq!(retrieved.amount_paid().value(), 100);
 }

+ 131 - 78
crates/cdk-common/src/mint.rs

@@ -439,12 +439,12 @@ pub struct MintQuoteChange {
 }
 
 /// Mint Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct MintQuote {
     /// Quote id
     pub id: QuoteId,
     /// Amount of quote
-    pub amount: Option<Amount>,
+    pub amount: Option<Amount<CurrencyUnit>>,
     /// Unit of quote
     pub unit: CurrencyUnit,
     /// Quote payment request e.g. bolt11
@@ -456,34 +456,24 @@ pub struct MintQuote {
     /// Pubkey
     pub pubkey: Option<PublicKey>,
     /// Unix time quote was created
-    #[serde(default)]
     pub created_time: u64,
-    /// Amount paid
-    #[serde(default)]
-    amount_paid: Amount,
-    /// Amount issued
-    #[serde(default)]
-    amount_issued: Amount,
+    /// Amount paid (typed for type safety)
+    amount_paid: Amount<CurrencyUnit>,
+    /// Amount issued (typed for type safety)
+    amount_issued: Amount<CurrencyUnit>,
     /// Payment of payment(s) that filled quote
-    #[serde(default)]
     pub payments: Vec<IncomingPayment>,
     /// Payment Method
     pub payment_method: PaymentMethod,
     /// Payment of payment(s) that filled quote
-    #[serde(default)]
     pub issuance: Vec<Issuance>,
     /// Extra payment-method-specific fields
-    ///
-    /// These fields are flattened into the JSON representation, allowing
-    /// custom payment methods to include additional data without nesting.
-    #[serde(flatten, default)]
     pub extra_json: Option<serde_json::Value>,
     /// Accumulated changes since this quote was loaded or created.
     ///
     /// This field is not serialized and is used internally to track modifications
     /// that need to be persisted. Use [`Self::take_changes`] to extract pending
     /// changes for persistence.
-    #[serde(skip)]
     changes: Option<MintQuoteChange>,
 }
 
@@ -494,12 +484,12 @@ impl MintQuote {
         id: Option<QuoteId>,
         request: String,
         unit: CurrencyUnit,
-        amount: Option<Amount>,
+        amount: Option<Amount<CurrencyUnit>>,
         expiry: u64,
         request_lookup_id: PaymentIdentifier,
         pubkey: Option<PublicKey>,
-        amount_paid: Amount,
-        amount_issued: Amount,
+        amount_paid: Amount<CurrencyUnit>,
+        amount_issued: Amount<CurrencyUnit>,
         payment_method: PaymentMethod,
         created_time: u64,
         payments: Vec<IncomingPayment>,
@@ -511,7 +501,7 @@ impl MintQuote {
         Self {
             id,
             amount,
-            unit,
+            unit: unit.clone(),
             request,
             expiry,
             request_lookup_id,
@@ -527,10 +517,23 @@ impl MintQuote {
         }
     }
 
+    /// Increment the amount paid on the mint quote by a given amount
+    #[instrument(skip(self))]
+    pub fn increment_amount_paid(
+        &mut self,
+        additional_amount: Amount<CurrencyUnit>,
+    ) -> Result<Amount, crate::Error> {
+        self.amount_paid = self
+            .amount_paid
+            .checked_add(&additional_amount)
+            .map_err(|_| crate::Error::AmountOverflow)?;
+        Ok(Amount::from(self.amount_paid.value()))
+    }
+
     /// Amount paid
     #[instrument(skip(self))]
-    pub fn amount_paid(&self) -> Amount {
-        self.amount_paid
+    pub fn amount_paid(&self) -> Amount<CurrencyUnit> {
+        self.amount_paid.clone()
     }
 
     /// Records tokens being issued against this mint quote.
@@ -556,11 +559,14 @@ impl MintQuote {
     /// Returns [`crate::Error::AmountOverflow`] if adding the issuance amount would
     /// cause an arithmetic overflow.
     #[instrument(skip(self))]
-    pub fn add_issuance(&mut self, additional_amount: Amount) -> Result<Amount, crate::Error> {
+    pub fn add_issuance(
+        &mut self,
+        additional_amount: Amount<CurrencyUnit>,
+    ) -> Result<Amount<CurrencyUnit>, crate::Error> {
         let new_amount_issued = self
             .amount_issued
-            .checked_add(additional_amount)
-            .ok_or(crate::Error::AmountOverflow)?;
+            .checked_add(&additional_amount)
+            .map_err(|_| crate::Error::AmountOverflow)?;
 
         // Can't issue more than what's been paid
         if new_amount_issued > self.amount_paid {
@@ -571,17 +577,17 @@ impl MintQuote {
             .get_or_insert_default()
             .issuances
             .get_or_insert_default()
-            .push(additional_amount);
+            .push(additional_amount.into());
 
         self.amount_issued = new_amount_issued;
 
-        Ok(self.amount_issued)
+        Ok(self.amount_issued.clone())
     }
 
     /// Amount issued
     #[instrument(skip(self))]
-    pub fn amount_issued(&self) -> Amount {
-        self.amount_issued
+    pub fn amount_issued(&self) -> Amount<CurrencyUnit> {
+        self.amount_issued.clone()
     }
 
     /// Get state of mint quote
@@ -601,8 +607,10 @@ impl MintQuote {
     /// The value is computed as the difference between the total amount that
     /// has been paid for this issuance (`self.amount_paid`) and the amount
     /// that has already been issued (`self.amount_issued`). In other words,
-    pub fn amount_mintable(&self) -> Amount {
-        self.amount_paid - self.amount_issued
+    pub fn amount_mintable(&self) -> Amount<CurrencyUnit> {
+        self.amount_paid
+            .checked_sub(&self.amount_issued)
+            .unwrap_or_else(|_| Amount::new(0, self.unit.clone()))
     }
 
     /// Extracts and returns all pending changes, leaving the internal change tracker empty.
@@ -640,7 +648,7 @@ impl MintQuote {
     #[instrument(skip(self))]
     pub fn add_payment(
         &mut self,
-        amount: Amount,
+        amount: Amount<CurrencyUnit>,
         payment_id: String,
         time: Option<u64>,
     ) -> Result<(), crate::Error> {
@@ -651,6 +659,11 @@ impl MintQuote {
             return Err(crate::Error::DuplicatePaymentId);
         }
 
+        self.amount_paid = self
+            .amount_paid
+            .checked_add(&amount)
+            .map_err(|_| crate::Error::AmountOverflow)?;
+
         let payment = IncomingPayment::new(amount, payment_id, time);
 
         self.payments.push(payment.clone());
@@ -661,48 +674,34 @@ impl MintQuote {
             .get_or_insert_default()
             .push(payment);
 
-        self.amount_paid = self
-            .amount_paid
-            .checked_add(amount)
-            .ok_or(crate::Error::AmountOverflow)?;
-
         Ok(())
     }
 
     /// Compute quote state
     #[instrument(skip(self))]
     fn compute_quote_state(&self) -> MintQuoteState {
-        if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
+        let zero_amount = Amount::new(0, self.unit.clone());
+
+        if self.amount_paid == zero_amount && self.amount_issued == zero_amount {
             return MintQuoteState::Unpaid;
         }
 
-        match self.amount_paid.cmp(&self.amount_issued) {
+        match self.amount_paid.value().cmp(&self.amount_issued.value()) {
             std::cmp::Ordering::Less => {
-                // self.amount_paid is less than other (amount issued)
-                // Handle case where paid amount is insufficient
                 tracing::error!("We should not have issued more then has been paid");
                 MintQuoteState::Issued
             }
-            std::cmp::Ordering::Equal => {
-                // We do this extra check for backwards compatibility for quotes where amount paid/issed was not tracked
-                // self.amount_paid equals other (amount issued)
-                // Handle case where paid amount exactly matches
-                MintQuoteState::Issued
-            }
-            std::cmp::Ordering::Greater => {
-                // self.amount_paid is greater than other (amount issued)
-                // Handle case where paid amount exceeds required amount
-                MintQuoteState::Paid
-            }
+            std::cmp::Ordering::Equal => MintQuoteState::Issued,
+            std::cmp::Ordering::Greater => MintQuoteState::Paid,
         }
     }
 }
 
 /// Mint Payments
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct IncomingPayment {
     /// Amount
-    pub amount: Amount,
+    pub amount: Amount<CurrencyUnit>,
     /// Pyament unix time
     pub time: u64,
     /// Payment id
@@ -711,7 +710,7 @@ pub struct IncomingPayment {
 
 impl IncomingPayment {
     /// New [`IncomingPayment`]
-    pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
+    pub fn new(amount: Amount<CurrencyUnit>, payment_id: String, time: u64) -> Self {
         Self {
             payment_id,
             time,
@@ -720,35 +719,35 @@ impl IncomingPayment {
     }
 }
 
-/// Informattion about issued quote
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+/// Information about issued quote
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct Issuance {
     /// Amount
-    pub amount: Amount,
+    pub amount: Amount<CurrencyUnit>,
     /// Time
     pub time: u64,
 }
 
 impl Issuance {
     /// Create new [`Issuance`]
-    pub fn new(amount: Amount, time: u64) -> Self {
+    pub fn new(amount: Amount<CurrencyUnit>, time: u64) -> Self {
         Self { amount, time }
     }
 }
 
 /// Melt Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct MeltQuote {
     /// Quote id
     pub id: QuoteId,
     /// Quote unit
     pub unit: CurrencyUnit,
-    /// Quote amount
-    pub amount: Amount,
     /// Quote Payment request e.g. bolt11
     pub request: MeltPaymentRequest,
-    /// Quote fee reserve
-    pub fee_reserve: Amount,
+    /// Quote amount (typed for type safety)
+    amount: Amount<CurrencyUnit>,
+    /// Quote fee reserve (typed for type safety)
+    fee_reserve: Amount<CurrencyUnit>,
     /// Quote state
     pub state: MeltQuoteState,
     /// Expiration time of quote
@@ -762,7 +761,6 @@ pub struct MeltQuote {
     /// Used for amountless invoices and MPP payments
     pub options: Option<MeltOptions>,
     /// Unix time quote was created
-    #[serde(default)]
     pub created_time: u64,
     /// Unix time quote was paid
     pub paid_time: Option<u64>,
@@ -776,8 +774,8 @@ impl MeltQuote {
     pub fn new(
         request: MeltPaymentRequest,
         unit: CurrencyUnit,
-        amount: Amount,
-        fee_reserve: Amount,
+        amount: Amount<CurrencyUnit>,
+        fee_reserve: Amount<CurrencyUnit>,
         expiry: u64,
         request_lookup_id: Option<PaymentIdentifier>,
         options: Option<MeltOptions>,
@@ -787,9 +785,9 @@ impl MeltQuote {
 
         Self {
             id: QuoteId::UUID(id),
-            amount,
-            unit,
+            unit: unit.clone(),
             request,
+            amount,
             fee_reserve,
             state: MeltQuoteState::Unpaid,
             expiry,
@@ -801,6 +799,61 @@ impl MeltQuote {
             payment_method,
         }
     }
+
+    /// Quote amount
+    #[inline]
+    pub fn amount(&self) -> Amount<CurrencyUnit> {
+        self.amount.clone()
+    }
+
+    /// Fee reserve
+    #[inline]
+    pub fn fee_reserve(&self) -> Amount<CurrencyUnit> {
+        self.fee_reserve.clone()
+    }
+
+    /// Total amount needed (amount + fee_reserve)
+    pub fn total_needed(&self) -> Result<Amount, crate::Error> {
+        let total = self
+            .amount
+            .checked_add(&self.fee_reserve)
+            .map_err(|_| crate::Error::AmountOverflow)?;
+        Ok(Amount::from(total.value()))
+    }
+
+    /// Create MeltQuote from database fields (for deserialization)
+    #[allow(clippy::too_many_arguments)]
+    pub fn from_db(
+        id: QuoteId,
+        unit: CurrencyUnit,
+        request: MeltPaymentRequest,
+        amount: u64,
+        fee_reserve: u64,
+        state: MeltQuoteState,
+        expiry: u64,
+        payment_preimage: Option<String>,
+        request_lookup_id: Option<PaymentIdentifier>,
+        options: Option<MeltOptions>,
+        created_time: u64,
+        paid_time: Option<u64>,
+        payment_method: PaymentMethod,
+    ) -> Self {
+        Self {
+            id,
+            unit: unit.clone(),
+            request,
+            amount: Amount::new(amount, unit.clone()),
+            fee_reserve: Amount::new(fee_reserve, unit),
+            state,
+            expiry,
+            payment_preimage,
+            request_lookup_id,
+            options,
+            created_time,
+            paid_time,
+            payment_method,
+        }
+    }
 }
 
 /// Mint Keyset Info
@@ -853,7 +906,7 @@ impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
             request: mint_quote.request,
             expiry: Some(mint_quote.expiry),
             pubkey: mint_quote.pubkey,
-            amount: mint_quote.amount,
+            amount: mint_quote.amount.map(Into::into),
             unit: Some(mint_quote.unit.clone()),
         }
     }
@@ -875,10 +928,10 @@ impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
             quote: mint_quote.id.clone(),
             request: mint_quote.request,
             expiry: Some(mint_quote.expiry),
-            amount_paid: mint_quote.amount_paid,
-            amount_issued: mint_quote.amount_issued,
+            amount_paid: Amount::from(mint_quote.amount_paid.value()),
+            amount_issued: Amount::from(mint_quote.amount_issued.value()),
             pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
-            amount: mint_quote.amount,
+            amount: mint_quote.amount.map(Into::into),
             unit: mint_quote.unit,
         })
     }
@@ -904,7 +957,7 @@ impl TryFrom<crate::mint::MintQuote> for crate::nuts::MintQuoteCustomResponse<Qu
             request: mint_quote.request,
             expiry: Some(mint_quote.expiry),
             pubkey: mint_quote.pubkey,
-            amount: mint_quote.amount,
+            amount: mint_quote.amount.map(Into::into),
             unit: Some(mint_quote.unit),
             extra: mint_quote.extra_json.unwrap_or_default(),
         })
@@ -929,8 +982,8 @@ impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
             change: None,
             state: melt_quote.state,
             expiry: melt_quote.expiry,
-            amount: melt_quote.amount,
-            fee_reserve: melt_quote.fee_reserve,
+            amount: melt_quote.amount().clone().into(),
+            fee_reserve: melt_quote.fee_reserve().clone().into(),
             request: None,
             unit: Some(melt_quote.unit.clone()),
         }
@@ -941,8 +994,8 @@ impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
     fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
         MeltQuoteBolt11Response {
             quote: melt_quote.id.clone(),
-            amount: melt_quote.amount,
-            fee_reserve: melt_quote.fee_reserve,
+            amount: melt_quote.amount().clone().into(),
+            fee_reserve: melt_quote.fee_reserve().clone().into(),
             state: melt_quote.state,
             expiry: melt_quote.expiry,
             payment_preimage: melt_quote.payment_preimage,

+ 44 - 29
crates/cdk-common/src/payment.rs

@@ -263,12 +263,13 @@ impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
     type Error = Error;
 
     fn try_from(melt_quote: crate::mint::MeltQuote) -> Result<Self, Self::Error> {
-        match melt_quote.request {
+        let fee_reserve = melt_quote.fee_reserve();
+        match &melt_quote.request {
             MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
                 Bolt11OutgoingPaymentOptions {
-                    max_fee_amount: Some(melt_quote.fee_reserve),
+                    max_fee_amount: Some(fee_reserve.to_owned().into()),
                     timeout_secs: None,
-                    bolt11,
+                    bolt11: bolt11.clone(),
                     melt_options: melt_quote.options,
                 },
             ))),
@@ -281,18 +282,18 @@ impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
 
                 Ok(OutgoingPaymentOptions::Bolt12(Box::new(
                     Bolt12OutgoingPaymentOptions {
-                        max_fee_amount: Some(melt_quote.fee_reserve),
+                        max_fee_amount: Some(fee_reserve.clone().into()),
                         timeout_secs: None,
-                        offer: *offer,
+                        offer: *offer.clone(),
                         melt_options,
                     },
                 )))
             }
             MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
                 Box::new(CustomOutgoingPaymentOptions {
-                    method,
-                    request,
-                    max_fee_amount: Some(melt_quote.fee_reserve),
+                    method: method.to_string(),
+                    request: request.to_string(),
+                    max_fee_amount: Some(melt_quote.fee_reserve().into()),
                     timeout_secs: None,
                     melt_options: melt_quote.options,
                     extra_json: None,
@@ -385,28 +386,32 @@ impl Default for Event {
         // The actual processing will filter these out
         Event::PaymentReceived(WaitPaymentResponse {
             payment_identifier: PaymentIdentifier::CustomId("default".to_string()),
-            payment_amount: Amount::from(0),
-            unit: CurrencyUnit::Msat,
+            payment_amount: Amount::new(0, CurrencyUnit::Msat),
             payment_id: "default".to_string(),
         })
     }
 }
 
 /// Wait any invoice response
-#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash)]
 pub struct WaitPaymentResponse {
     /// Request look up id
     /// Id that relates the quote and payment request
     pub payment_identifier: PaymentIdentifier,
-    /// Payment amount
-    pub payment_amount: Amount,
-    /// Unit
-    pub unit: CurrencyUnit,
+    /// Payment amount (typed with unit for compile-time safety)
+    pub payment_amount: Amount<CurrencyUnit>,
     /// Unique id of payment
     // Payment hash
     pub payment_id: String,
 }
 
+impl WaitPaymentResponse {
+    /// Get the currency unit
+    pub fn unit(&self) -> &CurrencyUnit {
+        self.payment_amount.unit()
+    }
+}
+
 /// Create incoming payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct CreateIncomingPaymentResponse {
@@ -425,7 +430,7 @@ pub struct CreateIncomingPaymentResponse {
 }
 
 /// Payment response
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct MakePaymentResponse {
     /// Payment hash
     pub payment_lookup_id: PaymentIdentifier,
@@ -433,27 +438,37 @@ pub struct MakePaymentResponse {
     pub payment_proof: Option<String>,
     /// Status
     pub status: MeltQuoteState,
-    /// Total Amount Spent
-    pub total_spent: Amount,
-    /// Unit of total spent
-    pub unit: CurrencyUnit,
+    /// Total Amount Spent (typed with unit for compile-time safety)
+    pub total_spent: Amount<CurrencyUnit>,
+}
+
+impl MakePaymentResponse {
+    /// Get the currency unit
+    pub fn unit(&self) -> &CurrencyUnit {
+        self.total_spent.unit()
+    }
 }
 
 /// Payment quote response
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct PaymentQuoteResponse {
     /// Request look up id
     pub request_lookup_id: Option<PaymentIdentifier>,
-    /// Amount
-    pub amount: Amount,
-    /// Fee required for melt
-    pub fee: Amount,
-    /// Currency unit of `amount` and `fee`
-    pub unit: CurrencyUnit,
+    /// Amount (typed with unit for compile-time safety)
+    pub amount: Amount<CurrencyUnit>,
+    /// Fee required for melt (typed with unit for compile-time safety)
+    pub fee: Amount<CurrencyUnit>,
     /// Status
     pub state: MeltQuoteState,
 }
 
+impl PaymentQuoteResponse {
+    /// Get the currency unit
+    pub fn unit(&self) -> &CurrencyUnit {
+        self.amount.unit()
+    }
+}
+
 /// BOLT11 settings
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
 pub struct Bolt11Settings {
@@ -591,8 +606,8 @@ where
         let success = result.is_ok();
 
         if let Ok(ref quote) = result {
-            let amount: f64 = u64::from(quote.amount) as f64;
-            let fee: f64 = u64::from(quote.fee) as f64;
+            let amount: f64 = quote.amount.value() as f64;
+            let fee: f64 = quote.fee.value() as f64;
             METRICS.record_lightning_payment(amount, fee);
         }
 

+ 2 - 4
crates/cdk-common/src/pub_sub/remote_consumer.rs

@@ -284,10 +284,8 @@ where
                     continue;
                 };
 
-            remote_subscription.total_subscribers = remote_subscription
-                .total_subscribers
-                .checked_sub(1)
-                .unwrap_or_default();
+            remote_subscription.total_subscribers =
+                remote_subscription.total_subscribers.saturating_sub(1);
 
             if remote_subscription.total_subscribers == 0 {
                 let mut cached_events = self.cached_events.write();

+ 43 - 37
crates/cdk-fake-wallet/src/lib.rs

@@ -21,7 +21,7 @@ use std::time::{Duration, Instant};
 use async_trait::async_trait;
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::secp256k1::{Secp256k1, SecretKey};
-use cdk_common::amount::{to_unit, Amount};
+use cdk_common::amount::Amount;
 use cdk_common::common::FeeReserve;
 use cdk_common::ensure_cdk;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
@@ -48,6 +48,9 @@ pub mod error;
 /// Default maximum size for the secondary repayment queue
 const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100;
 
+/// Payment state entry containing the melt quote state and amount spent
+type PaymentStateEntry = (MeltQuoteState, Amount<CurrencyUnit>);
+
 /// Cache duration for exchange rate (5 minutes)
 const RATE_CACHE_DURATION: Duration = Duration::from_secs(300);
 
@@ -139,11 +142,11 @@ async fn convert_currency_amount(
     from_unit: &CurrencyUnit,
     target_unit: &CurrencyUnit,
     rate_cache: &ExchangeRateCache,
-) -> Result<Amount, Error> {
+) -> Result<Amount<CurrencyUnit>, Error> {
     use CurrencyUnit::*;
 
     // Try basic unit conversion first (handles SAT/MSAT and same-unit conversions)
-    if let Ok(converted) = to_unit(amount, from_unit, target_unit) {
+    if let Ok(converted) = Amount::new(amount, from_unit.clone()).convert_to(target_unit) {
         return Ok(converted);
     }
 
@@ -153,15 +156,17 @@ async fn convert_currency_amount(
         (Usd | Eur, Sat) => {
             let rate = rate_cache.get_btc_rate(from_unit).await?;
             let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
-            Ok(Amount::from(
-                (fiat_amount / rate * 100_000_000.0).round() as u64
+            Ok(Amount::new(
+                (fiat_amount / rate * 100_000_000.0).round() as u64,
+                target_unit.clone(),
             )) // to sats
         }
         (Usd | Eur, Msat) => {
             let rate = rate_cache.get_btc_rate(from_unit).await?;
             let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
-            Ok(Amount::from(
+            Ok(Amount::new(
                 (fiat_amount / rate * 100_000_000_000.0).round() as u64,
+                target_unit.clone(),
             )) // to msats
         }
 
@@ -169,12 +174,18 @@ async fn convert_currency_amount(
         (Sat, Usd | Eur) => {
             let rate = rate_cache.get_btc_rate(target_unit).await?;
             let btc_amount = amount as f64 / 100_000_000.0; // sats to BTC
-            Ok(Amount::from((btc_amount * rate * 100.0).round() as u64)) // to cents
+            Ok(Amount::new(
+                (btc_amount * rate * 100.0).round() as u64,
+                target_unit.clone(),
+            )) // to cents
         }
         (Msat, Usd | Eur) => {
             let rate = rate_cache.get_btc_rate(target_unit).await?;
             let btc_amount = amount as f64 / 100_000_000_000.0; // msats to BTC
-            Ok(Amount::from((btc_amount * rate * 100.0).round() as u64)) // to cents
+            Ok(Amount::new(
+                (btc_amount * rate * 100.0).round() as u64,
+                target_unit.clone(),
+            )) // to cents
         }
 
         _ => Err(Error::UnknownInvoiceAmount), // Unsupported conversion
@@ -265,9 +276,11 @@ impl SecondaryRepaymentQueue {
 
                     // Create amount based on unit, ensuring minimum of 1 sat worth
                     let secondary_amount = match &unit {
-                        CurrencyUnit::Sat => Amount::from(random_amount),
-                        CurrencyUnit::Msat => Amount::from(u64::max(random_amount * 1000, 1000)),
-                        _ => Amount::from(u64::max(random_amount, 1)), // fallback
+                        CurrencyUnit::Sat => Amount::new(random_amount, unit.clone()),
+                        CurrencyUnit::Msat => {
+                            Amount::new(u64::max(random_amount * 1000, 1000), unit.clone())
+                        }
+                        _ => Amount::new(u64::max(random_amount, 1), unit.clone()), // fallback
                     };
 
                     // Generate a unique payment identifier for this secondary payment
@@ -301,7 +314,6 @@ impl SecondaryRepaymentQueue {
                     let secondary_response = WaitPaymentResponse {
                         payment_identifier: payment.clone(),
                         payment_amount: secondary_amount,
-                        unit: unit.clone(),
                         payment_id: unique_payment_id.to_string(),
                     };
 
@@ -324,7 +336,7 @@ pub struct FakeWallet {
     fee_reserve: FeeReserve,
     sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
     receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WaitPaymentResponse>>>>,
-    payment_states: Arc<Mutex<HashMap<String, (MeltQuoteState, Amount)>>>,
+    payment_states: Arc<Mutex<HashMap<String, PaymentStateEntry>>>,
     failed_payment_check: Arc<Mutex<HashSet<String>>>,
     payment_delay: u64,
     wait_invoice_cancel_token: CancellationToken,
@@ -339,7 +351,7 @@ impl FakeWallet {
     /// Create new [`FakeWallet`]
     pub fn new(
         fee_reserve: FeeReserve,
-        payment_states: HashMap<String, (MeltQuoteState, Amount)>,
+        payment_states: HashMap<String, PaymentStateEntry>,
         fail_payment_check: HashSet<String>,
         payment_delay: u64,
         unit: CurrencyUnit,
@@ -357,7 +369,7 @@ impl FakeWallet {
     /// Create new [`FakeWallet`] with custom secondary repayment queue size
     pub fn new_with_repay_queue_size(
         fee_reserve: FeeReserve,
-        payment_states: HashMap<String, (MeltQuoteState, Amount)>,
+        payment_states: HashMap<String, PaymentStateEntry>,
         fail_payment_check: HashSet<String>,
         payment_delay: u64,
         unit: CurrencyUnit,
@@ -519,7 +531,7 @@ impl MintPayment for FakeWallet {
         .await?;
 
         let relative_fee_reserve =
-            (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+            (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
 
         let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
@@ -528,9 +540,8 @@ impl MintPayment for FakeWallet {
         Ok(PaymentQuoteResponse {
             request_lookup_id,
             amount,
-            fee: fee.into(),
+            fee: Amount::new(fee, unit.clone()),
             state: MeltQuoteState::Unpaid,
-            unit: unit.clone(),
         })
     }
 
@@ -571,9 +582,9 @@ impl MintPayment for FakeWallet {
                 };
 
                 let amount_spent = if checkout_going_status == MeltQuoteState::Paid {
-                    amount_msat.into()
+                    Amount::new(amount_msat, CurrencyUnit::Msat)
                 } else {
-                    Amount::ZERO
+                    Amount::new(0, CurrencyUnit::Msat)
                 };
 
                 payment_states.insert(payment_hash.clone(), (checkout_going_status, amount_spent));
@@ -596,13 +607,12 @@ impl MintPayment for FakeWallet {
                 .await?;
 
                 Ok(MakePaymentResponse {
-                    payment_proof: Some("".to_string()),
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                         *bolt11.payment_hash().as_ref(),
                     ),
+                    payment_proof: Some("".to_string()),
                     status: payment_status,
-                    total_spent: total_spent + 1.into(),
-                    unit: unit.clone(),
+                    total_spent: Amount::new(total_spent.value() + 1, unit.clone()),
                 })
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -627,11 +637,10 @@ impl MintPayment for FakeWallet {
                 .await?;
 
                 Ok(MakePaymentResponse {
-                    payment_proof: Some("".to_string()),
                     payment_lookup_id: PaymentIdentifier::CustomId(Uuid::new_v4().to_string()),
+                    payment_proof: Some("".to_string()),
                     status: MeltQuoteState::Paid,
-                    total_spent: total_spent + 1.into(),
-                    unit: unit.clone(),
+                    total_spent: Amount::new(total_spent.value() + 1, unit.clone()),
                 })
             }
             OutgoingPaymentOptions::Custom(_) => {
@@ -668,7 +677,7 @@ impl MintPayment for FakeWallet {
                             &self.exchange_rate_cache,
                         )
                         .await?;
-                        offer_builder.amount_msats(amount_msat.into())
+                        offer_builder.amount_msats(amount_msat.value())
                     }
                     None => offer_builder,
                 };
@@ -693,10 +702,9 @@ impl MintPayment for FakeWallet {
                     &CurrencyUnit::Msat,
                     &self.exchange_rate_cache,
                 )
-                .await?
-                .into();
+                .await?;
 
-                let invoice = create_fake_invoice(amount_msat, description.clone());
+                let invoice = create_fake_invoice(amount_msat.value(), description.clone());
                 let payment_hash = invoice.payment_hash();
 
                 (
@@ -717,7 +725,6 @@ impl MintPayment for FakeWallet {
         let duration = time::Duration::from_secs(self.payment_delay);
         let payment_hash_clone = payment_hash.clone();
         let incoming_payment = self.incoming_payments.clone();
-        let unit_clone = self.unit.clone();
 
         let final_amount = if amount == Amount::ZERO {
             // For any-amount invoices, generate a random amount for the initial payment
@@ -726,9 +733,9 @@ impl MintPayment for FakeWallet {
             let mut rng = OsRng;
             let random_amount: u64 = rng.gen_range(1000..=10000);
             // Use the same unit as the wallet for any-amount invoices
-            Amount::from(random_amount)
+            Amount::new(random_amount, unit.clone())
         } else {
-            amount
+            Amount::new(u64::from(amount), unit.clone())
         };
 
         // Schedule the immediate payment (original behavior maintained)
@@ -739,7 +746,6 @@ impl MintPayment for FakeWallet {
             let response = WaitPaymentResponse {
                 payment_identifier: payment_hash_clone.clone(),
                 payment_amount: final_amount,
-                unit: unit_clone,
                 payment_id: payment_hash_clone.to_string(),
             };
             let mut incoming = incoming_payment.write().await;
@@ -797,7 +803,8 @@ impl MintPayment for FakeWallet {
         let states = self.payment_states.lock().await;
         let status = states.get(&request_lookup_id.to_string()).cloned();
 
-        let (status, total_spent) = status.unwrap_or((MeltQuoteState::Unknown, Amount::default()));
+        let (status, total_spent) =
+            status.unwrap_or((MeltQuoteState::Unknown, Amount::new(0, CurrencyUnit::Msat)));
 
         let fail_payments = self.failed_payment_check.lock().await;
 
@@ -806,11 +813,10 @@ impl MintPayment for FakeWallet {
         }
 
         Ok(MakePaymentResponse {
-            payment_proof: Some("".to_string()),
             payment_lookup_id: request_lookup_id.clone(),
+            payment_proof: Some("".to_string()),
             status,
             total_spent,
-            unit: CurrencyUnit::Msat,
         })
     }
 }

+ 9 - 7
crates/cdk-integration-tests/tests/mint.rs

@@ -175,12 +175,12 @@ async fn test_concurrent_duplicate_payment_handling() {
         None,
         "concurrent_test_invoice".to_string(),
         CurrencyUnit::Sat,
-        Some(Amount::from(1000)),
+        Some(Amount::from(1000).with_unit(CurrencyUnit::Sat)),
         current_time + 3600, // expires in 1 hour
         PaymentIdentifier::CustomId("test_lookup_id".to_string()),
         None,
-        Amount::ZERO,
-        Amount::ZERO,
+        Amount::ZERO.with_unit(CurrencyUnit::Sat),
+        Amount::ZERO.with_unit(CurrencyUnit::Sat),
         PaymentMethod::Known(KnownMethod::Bolt11),
         current_time,
         vec![],
@@ -212,9 +212,11 @@ async fn test_concurrent_duplicate_payment_handling() {
                 .expect("no error")
                 .expect("some value");
 
-            let result = if let Err(err) =
-                quote_from_db.add_payment(Amount::from(10), payment_id_clone, None)
-            {
+            let result = if let Err(err) = quote_from_db.add_payment(
+                Amount::from(10).with_unit(CurrencyUnit::Sat),
+                payment_id_clone,
+                None,
+            ) {
                 Err(err)
             } else {
                 tx.update_mint_quote(&mut quote_from_db)
@@ -275,7 +277,7 @@ async fn test_concurrent_duplicate_payment_handling() {
 
     assert_eq!(
         final_quote.amount_paid(),
-        Amount::from(10),
+        Amount::from(10).with_unit(CurrencyUnit::Sat),
         "Quote amount should be incremented exactly once"
     );
     assert_eq!(

+ 26 - 29
crates/cdk-ldk-node/src/lib.rs

@@ -8,7 +8,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
 use async_trait::async_trait;
-use cdk_common::amount::to_unit;
 use cdk_common::common::FeeReserve;
 use cdk_common::payment::{self, *};
 use cdk_common::util::{hex, unix_time};
@@ -343,8 +342,7 @@ impl CdkLdkNode {
 
         let wait_payment_response = WaitPaymentResponse {
             payment_identifier,
-            payment_amount: amount_msat.into(),
-            unit: CurrencyUnit::Msat,
+            payment_amount: Amount::new(amount_msat, CurrencyUnit::Msat),
             payment_id,
         };
 
@@ -477,7 +475,9 @@ impl MintPayment for CdkLdkNode {
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         match options {
             IncomingPaymentOptions::Bolt11(bolt11_options) => {
-                let amount_msat = to_unit(bolt11_options.amount, unit, &CurrencyUnit::Msat)?;
+                let amount_msat: Amount = Amount::new(bolt11_options.amount.into(), unit.clone())
+                    .convert_to(&CurrencyUnit::Msat)?
+                    .into();
                 let description = bolt11_options.description.unwrap_or_default();
                 let time = bolt11_options
                     .unix_expiry
@@ -519,7 +519,9 @@ impl MintPayment for CdkLdkNode {
 
                 let offer = match amount {
                     Some(amount) => {
-                        let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+                        let amount_msat: Amount = Amount::new(amount.into(), unit.clone())
+                            .convert_to(&CurrencyUnit::Msat)?
+                            .into();
 
                         self.inner
                             .bolt12_payment()
@@ -575,10 +577,11 @@ impl MintPayment for CdkLdkNode {
                         .into(),
                 };
 
-                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+                let amount =
+                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
 
                 let relative_fee_reserve =
-                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
 
                 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
@@ -595,9 +598,8 @@ impl MintPayment for CdkLdkNode {
                 Ok(PaymentQuoteResponse {
                     request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -616,10 +618,11 @@ impl MintPayment for CdkLdkNode {
                         }
                     }
                 };
-                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+                let amount =
+                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
 
                 let relative_fee_reserve =
-                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
 
                 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
@@ -631,9 +634,8 @@ impl MintPayment for CdkLdkNode {
                 Ok(PaymentQuoteResponse {
                     request_lookup_id: None,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
             }
         }
@@ -656,12 +658,12 @@ impl MintPayment for CdkLdkNode {
                 let send_params = match bolt11_options
                     .max_fee_amount
                     .map(|f| {
-                        to_unit(f, unit, &CurrencyUnit::Msat).map(|amount_msat| {
-                            RouteParametersConfig {
-                                max_total_routing_fee_msat: Some(amount_msat.into()),
+                        Amount::new(f.into(), unit.clone())
+                            .convert_to(&CurrencyUnit::Msat)
+                            .map(|amount_msat| RouteParametersConfig {
+                                max_total_routing_fee_msat: Some(amount_msat.value()),
                                 ..Default::default()
-                            }
-                        })
+                            })
                     })
                     .transpose()
                 {
@@ -735,7 +737,7 @@ impl MintPayment for CdkLdkNode {
                     .ok_or(Error::CouldNotGetAmountSpent)?
                     + payment_details.fee_paid_msat.unwrap_or_default();
 
-                let total_spent = to_unit(total_spent, &CurrencyUnit::Msat, unit)?;
+                let total_spent = Amount::new(total_spent, CurrencyUnit::Msat).convert_to(unit)?;
 
                 Ok(MakePaymentResponse {
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
@@ -744,7 +746,6 @@ impl MintPayment for CdkLdkNode {
                     payment_proof,
                     status,
                     total_spent,
-                    unit: unit.clone(),
                 })
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -810,14 +811,13 @@ impl MintPayment for CdkLdkNode {
                     .ok_or(Error::CouldNotGetAmountSpent)?
                     + payment_details.fee_paid_msat.unwrap_or_default();
 
-                let total_spent = to_unit(total_spent, &CurrencyUnit::Msat, unit)?;
+                let total_spent = Amount::new(total_spent, CurrencyUnit::Msat).convert_to(unit)?;
 
                 Ok(MakePaymentResponse {
                     payment_lookup_id: PaymentIdentifier::PaymentId(payment_id.0),
                     payment_proof,
                     status,
                     total_spent,
-                    unit: unit.clone(),
                 })
             }
         }
@@ -916,8 +916,7 @@ impl MintPayment for CdkLdkNode {
 
         let response = WaitPaymentResponse {
             payment_identifier: payment_identifier.clone(),
-            payment_amount: amount.into(),
-            unit: CurrencyUnit::Msat,
+            payment_amount: Amount::new(amount, CurrencyUnit::Msat),
             payment_id: payment_id_str,
         };
 
@@ -945,10 +944,9 @@ impl MintPayment for CdkLdkNode {
             _ => {
                 return Ok(MakePaymentResponse {
                     payment_lookup_id: request_lookup_id.clone(),
-                    status: MeltQuoteState::Unknown,
                     payment_proof: None,
-                    total_spent: Amount::ZERO,
-                    unit: CurrencyUnit::Msat,
+                    status: MeltQuoteState::Unknown,
+                    total_spent: Amount::new(0, CurrencyUnit::Msat),
                 });
             }
         }
@@ -982,8 +980,7 @@ impl MintPayment for CdkLdkNode {
             payment_lookup_id: request_lookup_id.clone(),
             payment_proof,
             status,
-            total_spent: total_spent.into(),
-            unit: CurrencyUnit::Msat,
+            total_spent: Amount::new(total_spent, CurrencyUnit::Msat),
         })
     }
 }

+ 19 - 18
crates/cdk-lnbits/src/lib.rs

@@ -9,7 +9,7 @@ use std::sync::Arc;
 
 use anyhow::anyhow;
 use async_trait::async_trait;
-use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
+use cdk_common::amount::{Amount, MSAT_IN_SAT};
 use cdk_common::common::FeeReserve;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
@@ -122,8 +122,7 @@ impl LNbits {
 
         Ok(Some(WaitPaymentResponse {
             payment_identifier: PaymentIdentifier::PaymentHash(hash),
-            payment_amount: Amount::from(amount.unsigned_abs()),
-            unit: CurrencyUnit::Msat,
+            payment_amount: Amount::new(amount.unsigned_abs(), CurrencyUnit::Msat),
             payment_id: msg.to_string(),
         }))
     }
@@ -253,10 +252,9 @@ impl MintPayment for LNbits {
                     request_lookup_id: Some(PaymentIdentifier::PaymentHash(
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                     )),
-                    amount: to_unit(amount_msat, &CurrencyUnit::Msat, unit)?,
-                    fee: to_unit(fee, &CurrencyUnit::Msat, unit)?,
+                    amount: Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?,
+                    fee: Amount::new(fee, CurrencyUnit::Msat).convert_to(unit)?,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
             }
             OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
@@ -268,7 +266,7 @@ impl MintPayment for LNbits {
 
     async fn make_payment(
         &self,
-        _unit: &CurrencyUnit,
+        unit: &CurrencyUnit,
         options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
         match options {
@@ -299,15 +297,18 @@ impl MintPayment for LNbits {
                     MeltQuoteState::Unpaid
                 };
 
-                let total_spent = Amount::from(
-                    (invoice_info
+                let total_spent_msat = Amount::new(
+                    invoice_info
                         .details
                         .amount
-                        .checked_add(invoice_info.details.fee)
-                        .ok_or(Error::AmountOverflow)?)
-                    .unsigned_abs(),
+                        .unsigned_abs()
+                        .checked_add(invoice_info.details.fee.unsigned_abs())
+                        .ok_or(Error::AmountOverflow)?,
+                    CurrencyUnit::Msat,
                 );
 
+                let total_spent = total_spent_msat.convert_to(unit)?;
+
                 Ok(MakePaymentResponse {
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                         hex::decode(pay_response.payment_hash)
@@ -318,7 +319,6 @@ impl MintPayment for LNbits {
                     payment_proof: Some(invoice_info.details.payment_hash),
                     status,
                     total_spent,
-                    unit: CurrencyUnit::Msat,
                 })
             }
             OutgoingPaymentOptions::Bolt12(_) => {
@@ -343,7 +343,9 @@ impl MintPayment for LNbits {
                 let expiry = unix_expiry.map(|t| t - time_now);
 
                 let invoice_request = CreateInvoiceRequest {
-                    amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(),
+                    amount: Amount::new(amount.into(), unit.clone())
+                        .convert_to(&CurrencyUnit::Sat)?
+                        .value(),
                     memo: Some(description),
                     unit: unit.to_string(),
                     expiry,
@@ -404,8 +406,7 @@ impl MintPayment for LNbits {
         match payment.paid {
             true => Ok(vec![WaitPaymentResponse {
                 payment_identifier: payment_identifier.clone(),
-                payment_amount: Amount::from(amount.unsigned_abs()),
-                unit: CurrencyUnit::Msat,
+                payment_amount: Amount::new(amount.unsigned_abs(), CurrencyUnit::Msat),
                 payment_id: payment.details.payment_hash,
             }]),
             false => Ok(vec![]),
@@ -430,10 +431,10 @@ impl MintPayment for LNbits {
             payment_lookup_id: payment_identifier.clone(),
             payment_proof: payment.preimage,
             status: lnbits_to_melt_status(&payment.details.status),
-            total_spent: Amount::from(
+            total_spent: Amount::new(
                 payment.details.amount.unsigned_abs() + payment.details.fee.unsigned_abs(),
+                CurrencyUnit::Msat,
             ),
-            unit: CurrencyUnit::Msat,
         };
 
         Ok(pay_response)

+ 17 - 22
crates/cdk-lnd/src/lib.rs

@@ -13,7 +13,7 @@ use std::sync::Arc;
 
 use anyhow::anyhow;
 use async_trait::async_trait;
-use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
+use cdk_common::amount::{Amount, MSAT_IN_SAT};
 use cdk_common::bitcoin::hashes::Hash;
 use cdk_common::common::FeeReserve;
 use cdk_common::database::DynKVStore;
@@ -305,8 +305,7 @@ impl MintPayment for Lnd {
 
                                             let wait_response = WaitPaymentResponse {
                                                 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
-                                                payment_amount: Amount::from(msg.amt_paid_msat as u64),
-                                                unit: CurrencyUnit::Msat,
+                                                payment_amount: Amount::new(msg.amt_paid_msat as u64, CurrencyUnit::Msat),
                                                 payment_id: hash,
                                             };
                                             let event = Event::PaymentReceived(wait_response);
@@ -362,10 +361,11 @@ impl MintPayment for Lnd {
                         .into(),
                 };
 
-                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
+                let amount =
+                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
 
                 let relative_fee_reserve =
-                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
+                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
 
                 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
@@ -376,9 +376,8 @@ impl MintPayment for Lnd {
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                     )),
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
             }
             OutgoingPaymentOptions::Bolt12(_) => {
@@ -511,8 +510,7 @@ impl MintPayment for Lnd {
                                     ),
                                     payment_proof: payment_preimage,
                                     status,
-                                    total_spent: total_amt.into(),
-                                    unit: CurrencyUnit::Sat,
+                                    total_spent: Amount::new(total_amt, CurrencyUnit::Sat),
                                 });
                             }
 
@@ -574,8 +572,7 @@ impl MintPayment for Lnd {
                             payment_lookup_id: payment_identifier,
                             payment_proof: payment_preimage,
                             status,
-                            total_spent: total_amount.into(),
-                            unit: CurrencyUnit::Sat,
+                            total_spent: Amount::new(total_amount, CurrencyUnit::Sat),
                         })
                     }
                 }
@@ -599,7 +596,9 @@ impl MintPayment for Lnd {
                 let amount = bolt11_options.amount;
                 let unix_expiry = bolt11_options.unix_expiry;
 
-                let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+                let amount_msat: Amount = Amount::new(amount.into(), unit.clone())
+                    .convert_to(&CurrencyUnit::Msat)?
+                    .into();
 
                 let invoice_request = lnrpc::Invoice {
                     value_msat: u64::from(amount_msat) as i64,
@@ -657,8 +656,7 @@ impl MintPayment for Lnd {
         if invoice.state() == InvoiceState::Settled {
             Ok(vec![WaitPaymentResponse {
                 payment_identifier: payment_identifier.clone(),
-                payment_amount: Amount::from(invoice.amt_paid_msat as u64),
-                unit: CurrencyUnit::Msat,
+                payment_amount: Amount::new(invoice.amt_paid_msat as u64, CurrencyUnit::Msat),
                 payment_id: hex::encode(invoice.r_hash),
             }])
         } else {
@@ -691,8 +689,7 @@ impl MintPayment for Lnd {
                         payment_lookup_id: payment_identifier.clone(),
                         payment_proof: None,
                         status: MeltQuoteState::Unknown,
-                        total_spent: Amount::ZERO,
-                        unit: self.unit.clone(),
+                        total_spent: Amount::new(0, self.unit.clone()),
                     });
                 } else {
                     return Err(payment::Error::UnknownPaymentState);
@@ -710,8 +707,7 @@ impl MintPayment for Lnd {
                             payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Unknown,
-                            total_spent: Amount::ZERO,
-                            unit: self.unit.clone(),
+                            total_spent: Amount::new(0, self.unit.clone()),
                         },
                         PaymentStatus::InFlight | PaymentStatus::Initiated => {
                             // Continue waiting for the next update
@@ -721,21 +717,20 @@ impl MintPayment for Lnd {
                             payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Paid,
-                            total_spent: Amount::from(
+                            total_spent: Amount::new(
                                 (update
                                     .value_sat
                                     .checked_add(update.fee_sat)
                                     .ok_or(Error::AmountOverflow)?)
                                     as u64,
+                                CurrencyUnit::Sat,
                             ),
-                            unit: CurrencyUnit::Sat,
                         },
                         PaymentStatus::Failed => MakePaymentResponse {
                             payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Failed,
-                            total_spent: Amount::ZERO,
-                            unit: self.unit.clone(),
+                            total_spent: Amount::new(0, self.unit.clone()),
                         },
                     };
 

+ 18 - 16
crates/cdk-mint-rpc/src/proto/server.rs

@@ -654,8 +654,10 @@ impl CdkMint for MintRPCServer {
                 // Create a dummy payment response
                 let response = WaitPaymentResponse {
                     payment_id: mint_quote.request_lookup_id.to_string(),
-                    payment_amount: mint_quote.amount.unwrap_or(mint_quote.amount_paid()),
-                    unit: mint_quote.unit.clone(),
+                    payment_amount: mint_quote.clone().amount.unwrap_or(cdk::Amount::new(
+                        mint_quote.amount_paid().value(),
+                        mint_quote.unit.clone(),
+                    )),
                     payment_identifier: mint_quote.request_lookup_id.clone(),
                 };
 
@@ -688,20 +690,20 @@ impl CdkMint for MintRPCServer {
             _ => {
                 // Create a new quote with the same values
                 let quote = MintQuote::new(
-                    Some(mint_quote.id.clone()),
-                    mint_quote.request.clone(),
-                    mint_quote.unit.clone(),
-                    mint_quote.amount,
-                    mint_quote.expiry,
-                    mint_quote.request_lookup_id.clone(),
-                    mint_quote.pubkey,
-                    mint_quote.amount_issued(),
-                    mint_quote.amount_paid(),
-                    mint_quote.payment_method.clone(),
-                    0,
-                    vec![],
-                    vec![],
-                    None,
+                    Some(mint_quote.id.clone()),          // id
+                    mint_quote.request.clone(),           // request
+                    mint_quote.unit.clone(),              // unit
+                    mint_quote.amount.clone(),            // amount
+                    mint_quote.expiry,                    // expiry
+                    mint_quote.request_lookup_id.clone(), // request_lookup_id
+                    mint_quote.pubkey,                    // pubkey
+                    mint_quote.amount_issued(),           // amount_issued
+                    mint_quote.amount_paid(),             // amount_paid
+                    mint_quote.payment_method.clone(),    // method
+                    0,                                    // created_at
+                    vec![],                               // blinded_messages
+                    vec![],                               // payment_ids
+                    None,                                 // extra_json
                 );
 
                 let mint_store = self.mint.localstore();

+ 19 - 20
crates/cdk-payment-processor/src/proto/mod.rs

@@ -2,9 +2,10 @@ use std::str::FromStr;
 
 use cdk_common::payment::{
     CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse,
-    PaymentIdentifier as CdkPaymentIdentifier, WaitPaymentResponse,
+    PaymentIdentifier as CdkPaymentIdentifier, PaymentQuoteResponse as CdkPaymentQuoteResponse,
+    WaitPaymentResponse,
 };
-use cdk_common::{CurrencyUnit, MeltOptions as CdkMeltOptions};
+use cdk_common::{Amount, CurrencyUnit, MeltOptions as CdkMeltOptions};
 
 mod client;
 mod server;
@@ -95,7 +96,6 @@ impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
         // as_str_name() returns "QUOTE_STATE_PAID" but MeltQuoteState::from_str expects "PAID"
         let status: cdk_common::nuts::MeltQuoteState = value.status().into();
         let payment_proof = value.payment_proof;
-        let total_spent = value.total_spent.into();
         let unit = CurrencyUnit::from_str(&value.unit)?;
         let payment_identifier = value
             .payment_identifier
@@ -104,8 +104,7 @@ impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
             payment_lookup_id: payment_identifier.try_into()?,
             payment_proof,
             status,
-            total_spent,
-            unit,
+            total_spent: Amount::new(value.total_spent, unit),
         })
     }
 }
@@ -116,8 +115,8 @@ impl From<CdkMakePaymentResponse> for MakePaymentResponse {
             payment_identifier: Some(value.payment_lookup_id.into()),
             payment_proof: value.payment_proof,
             status: QuoteState::from(value.status).into(),
-            total_spent: value.total_spent.into(),
-            unit: value.unit.to_string(),
+            total_spent: value.total_spent.value(),
+            unit: value.total_spent.unit().to_string(),
             extra_json: None,
         }
     }
@@ -152,30 +151,30 @@ impl TryFrom<CreatePaymentResponse> for CreateIncomingPaymentResponse {
         })
     }
 }
-impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
-    fn from(value: cdk_common::payment::PaymentQuoteResponse) -> Self {
+impl From<CdkPaymentQuoteResponse> for PaymentQuoteResponse {
+    fn from(value: CdkPaymentQuoteResponse) -> Self {
         Self {
             request_identifier: value.request_lookup_id.map(|i| i.into()),
-            amount: value.amount.into(),
-            fee: value.fee.into(),
-            unit: value.unit.to_string(),
+            amount: value.amount.value(),
+            fee: value.fee.value(),
+            unit: value.amount.unit().to_string(),
             state: QuoteState::from(value.state).into(),
             extra_json: None,
         }
     }
 }
 
-impl From<PaymentQuoteResponse> for cdk_common::payment::PaymentQuoteResponse {
+impl From<PaymentQuoteResponse> for CdkPaymentQuoteResponse {
     fn from(value: PaymentQuoteResponse) -> Self {
         let state_val = value.state();
         let request_identifier = value.request_identifier;
+        let unit = CurrencyUnit::from_str(&value.unit).unwrap_or_default();
 
         Self {
             request_lookup_id: request_identifier
                 .map(|i| i.try_into().expect("valid request identifier")),
-            amount: value.amount.into(),
-            fee: value.fee.into(),
-            unit: CurrencyUnit::from_str(&value.unit).unwrap_or_default(),
+            amount: Amount::new(value.amount, unit.clone()),
+            fee: Amount::new(value.fee, unit),
             state: state_val.into(),
         }
     }
@@ -255,8 +254,8 @@ impl From<WaitPaymentResponse> for WaitIncomingPaymentResponse {
     fn from(value: WaitPaymentResponse) -> Self {
         Self {
             payment_identifier: Some(value.payment_identifier.into()),
-            payment_amount: value.payment_amount.into(),
-            unit: value.unit.to_string(),
+            payment_amount: value.payment_amount.value(),
+            unit: value.payment_amount.unit().to_string(),
             payment_id: value.payment_id,
         }
     }
@@ -270,11 +269,11 @@ impl TryFrom<WaitIncomingPaymentResponse> for WaitPaymentResponse {
             .payment_identifier
             .ok_or(crate::error::Error::InvalidPaymentIdentifier)?
             .try_into()?;
+        let unit = CurrencyUnit::from_str(&value.unit)?;
 
         Ok(Self {
             payment_identifier,
-            payment_amount: value.payment_amount.into(),
-            unit: CurrencyUnit::from_str(&value.unit)?,
+            payment_amount: Amount::new(value.payment_amount, unit),
             payment_id: value.payment_id,
         })
     }

+ 12 - 22
crates/cdk-signatory/src/proto/server.rs

@@ -56,17 +56,12 @@ where
     ) -> Result<Response<proto::BlindSignResponse>, Status> {
         let metadata = request.metadata();
         let signatory = self.load_signatory(metadata).await?;
-        let result = match signatory
-            .blind_sign(
-                request
-                    .into_inner()
-                    .blinded_messages
-                    .into_iter()
-                    .map(|blind_message| blind_message.try_into())
-                    .collect::<Result<Vec<_>, _>>()?,
-            )
-            .await
-        {
+        let blinded_messages = request.into_inner().blinded_messages;
+        let mut converted_messages = Vec::with_capacity(blinded_messages.len());
+        for msg in blinded_messages {
+            converted_messages.push(msg.try_into()?);
+        }
+        let result = match signatory.blind_sign(converted_messages).await {
             Ok(blind_signatures) => proto::BlindSignResponse {
                 sigs: Some(proto::BlindSignatures {
                     blind_signatures: blind_signatures
@@ -92,17 +87,12 @@ where
     ) -> Result<Response<proto::BooleanResponse>, Status> {
         let metadata = request.metadata();
         let signatory = self.load_signatory(metadata).await?;
-        let result = match signatory
-            .verify_proofs(
-                request
-                    .into_inner()
-                    .proof
-                    .into_iter()
-                    .map(|x| x.try_into())
-                    .collect::<Result<Vec<_>, _>>()?,
-            )
-            .await
-        {
+        let proofs = request.into_inner().proof;
+        let mut converted_proofs = Vec::with_capacity(proofs.len());
+        for p in proofs {
+            converted_proofs.push(p.try_into()?);
+        }
+        let result = match signatory.verify_proofs(converted_proofs).await {
             Ok(()) => proto::BooleanResponse {
                 success: true,
                 ..Default::default()

+ 1 - 2
crates/cdk-sql-common/src/mint/mod.rs

@@ -13,8 +13,6 @@ use std::sync::Arc;
 
 use async_trait::async_trait;
 use cdk_common::database::{self, DbTransactionFinalizer, Error, MintDatabase};
-// Re-export for auth module
-use migrations::MIGRATIONS;
 
 use crate::common::migrate;
 use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
@@ -39,6 +37,7 @@ mod migrations {
 pub use auth::SQLMintAuthDatabase;
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
+use migrations::MIGRATIONS;
 
 /// Mint SQL Database
 #[derive(Debug, Clone)]

+ 42 - 31
crates/cdk-sql-common/src/mint/quotes.rs

@@ -42,13 +42,15 @@ where
     query(
         r#"
         SELECT
-            payment_id,
-            timestamp,
-            amount
+            p.payment_id,
+            p.timestamp,
+            p.amount,
+            q.unit
         FROM
-            mint_quote_payments
+            mint_quote_payments p
+        JOIN mint_quote q ON p.quote_id = q.id
         WHERE
-            quote_id=:quote_id
+            p.quote_id=:quote_id
         "#,
     )?
     .bind("quote_id", quote_id.to_string())
@@ -58,8 +60,9 @@ where
     .map(|row| {
         let amount: u64 = column_as_number!(row[2].clone());
         let time: u64 = column_as_number!(row[1].clone());
+        let unit = column_as_string!(&row[3], CurrencyUnit::from_str);
         Ok(IncomingPayment::new(
-            amount.into(),
+            Amount::from(amount).with_unit(unit),
             column_as_string!(&row[0]),
             time,
         ))
@@ -74,9 +77,10 @@ where
     // Get payment IDs and timestamps from the mint_quote_payments table
     query(
         r#"
-SELECT amount, timestamp
-FROM mint_quote_issued
-WHERE quote_id=:quote_id
+SELECT i.amount, i.timestamp, q.unit
+FROM mint_quote_issued i
+JOIN mint_quote q ON i.quote_id = q.id
+WHERE i.quote_id=:quote_id
             "#,
     )?
     .bind("quote_id", quote_id.to_string())
@@ -85,9 +89,11 @@ WHERE quote_id=:quote_id
     .into_iter()
     .map(|row| {
         let time: u64 = column_as_number!(row[1].clone());
+        let unit = column_as_string!(&row[2], CurrencyUnit::from_str);
         Ok(Issuance::new(
             Amount::from_i64(column_as_number!(row[0].clone()))
-                .expect("Is amount when put into db"),
+                .expect("Is amount when put into db")
+                .with_unit(unit),
             time,
         ))
     })
@@ -420,18 +426,19 @@ fn sql_row_to_mint_quote(
     let amount_paid: u64 = column_as_number!(amount_paid);
     let amount_issued: u64 = column_as_number!(amount_issued);
     let payment_method = column_as_string!(payment_method, PaymentMethod::from_str);
+    let unit = column_as_string!(unit, CurrencyUnit::from_str);
 
     Ok(MintQuote::new(
         Some(QuoteId::from_str(&id)?),
         request_str,
-        column_as_string!(unit, CurrencyUnit::from_str),
-        amount.map(Amount::from),
+        unit.clone(),
+        amount.map(|a| Amount::from(a).with_unit(unit.clone())),
         column_as_number!(expiry),
         PaymentIdentifier::new(&request_lookup_id_kind, &request_lookup_id)
             .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?,
         pubkey,
-        amount_paid.into(),
-        amount_issued.into(),
+        Amount::from(amount_paid).with_unit(unit.clone()),
+        Amount::from(amount_issued).with_unit(unit),
         payment_method,
         column_as_number!(created_time),
         payments,
@@ -511,21 +518,22 @@ fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<mint::MeltQuote, Error> {
         }
     };
 
-    Ok(MeltQuote {
-        id: QuoteId::from_str(&id)?,
-        unit: CurrencyUnit::from_str(&unit)?,
-        amount: Amount::from(amount),
+    let unit = CurrencyUnit::from_str(&unit)?;
+    Ok(MeltQuote::from_db(
+        QuoteId::from_str(&id)?,
+        unit,
         request,
-        fee_reserve: Amount::from(fee_reserve),
+        amount,
+        fee_reserve,
         state,
         expiry,
         payment_preimage,
         request_lookup_id,
         options,
-        created_time: created_time as u64,
+        created_time as u64,
         paid_time,
         payment_method,
-    })
+    ))
 }
 
 #[async_trait]
@@ -538,8 +546,8 @@ where
     async fn add_melt_request(
         &mut self,
         quote_id: &QuoteId,
-        inputs_amount: Amount,
-        inputs_fee: Amount,
+        inputs_amount: Amount<CurrencyUnit>,
+        inputs_fee: Amount<CurrencyUnit>,
     ) -> Result<(), Self::Err> {
         // Insert melt_request
         query(
@@ -641,9 +649,10 @@ where
     ) -> Result<Option<database::mint::MeltRequestInfo>, Self::Err> {
         let melt_request_row = query(
             r#"
-            SELECT inputs_amount, inputs_fee
-            FROM melt_request
-            WHERE quote_id = :quote_id
+            SELECT mr.inputs_amount, mr.inputs_fee, mq.unit
+            FROM melt_request mr
+            JOIN melt_quote mq ON mr.quote_id = mq.id
+            WHERE mr.quote_id = :quote_id
             FOR UPDATE
             "#,
         )?
@@ -654,6 +663,8 @@ where
         if let Some(row) = melt_request_row {
             let inputs_amount: u64 = column_as_number!(row[0].clone());
             let inputs_fee: u64 = column_as_number!(row[1].clone());
+            let unit_str = column_as_string!(&row[2]);
+            let unit = CurrencyUnit::from_str(&unit_str)?;
 
             // Get blinded messages from blind_signature table where c IS NULL
             let blinded_messages_rows = query(
@@ -686,8 +697,8 @@ where
             let blinded_messages = blinded_messages?;
 
             Ok(Some(database::mint::MeltRequestInfo {
-                inputs_amount: Amount::from(inputs_amount),
-                inputs_fee: Amount::from(inputs_fee),
+                inputs_amount: Amount::from(inputs_amount).with_unit(unit.clone()),
+                inputs_fee: Amount::from(inputs_fee).with_unit(unit),
                 change_outputs: blinded_messages,
             }))
         } else {
@@ -808,7 +819,7 @@ where
             "#,
         )?
         .bind("id", quote.id.to_string())
-        .bind("amount", quote.amount.map(|a| a.to_i64()))
+        .bind("amount", quote.amount.clone().map(|a| a.to_i64()))
         .bind("unit", quote.unit.to_string())
         .bind("request", quote.request.clone())
         .bind("expiry", quote.expiry as i64)
@@ -846,9 +857,9 @@ where
         )?
         .bind("id", quote.id.to_string())
         .bind("unit", quote.unit.to_string())
-        .bind("amount", quote.amount.to_i64())
+        .bind("amount", quote.amount().to_i64())
         .bind("request", serde_json::to_string(&quote.request)?)
-        .bind("fee_reserve", quote.fee_reserve.to_i64())
+        .bind("fee_reserve", quote.fee_reserve().to_i64())
         .bind("state", quote.state.to_string())
         .bind("expiry", quote.expiry as i64)
         .bind("payment_preimage", quote.payment_preimage)

+ 15 - 14
crates/cdk/src/mint/issue/mod.rs

@@ -341,12 +341,12 @@ impl Mint {
                 None,
                 create_invoice_response.request.to_string(),
                 unit.clone(),
-                amount,
+                amount.map(|a| a.with_unit(unit.clone())),
                 create_invoice_response.expiry.unwrap_or(0),
                 create_invoice_response.request_lookup_id.clone(),
                 pubkey,
-                Amount::ZERO,
-                Amount::ZERO,
+                Amount::new(0, unit.clone()),
+                Amount::new(0, unit.clone()),
                 payment_method.clone(),
                 unix_time(),
                 vec![],
@@ -440,7 +440,7 @@ impl Mint {
         #[cfg(feature = "prometheus")]
         METRICS.inc_in_flight_requests("pay_mint_quote_for_request_id");
         let result = async {
-            if wait_payment_response.payment_amount == Amount::ZERO {
+            if wait_payment_response.payment_amount.value() == 0 {
                 tracing::warn!(
                     "Received payment response with 0 amount with payment id {}.",
                     wait_payment_response.payment_id.to_string()
@@ -633,7 +633,7 @@ impl Mint {
         }
 
         let mint_amount = if mint_quote.payment_method.is_bolt11() {
-            let quote_amount = mint_quote.amount.ok_or(Error::AmountUndefined)?;
+            let quote_amount = mint_quote.amount.clone().ok_or(Error::AmountUndefined)?;
 
             if quote_amount != mint_quote.amount_mintable() {
                 tracing::error!("The quote amount {} does not equal the amount paid {}.", quote_amount, mint_quote.amount_mintable());
@@ -642,7 +642,8 @@ impl Mint {
 
             quote_amount
         } else if mint_quote.payment_method.is_bolt12() {
-            if mint_quote.amount_mintable() == Amount::ZERO{
+            let zero = Amount::new(0, mint_quote.unit.clone());
+            if mint_quote.amount_mintable() == zero {
                 tracing::error!(
                         "Quote state should not be issued if issued {} is => paid {}.",
                         mint_quote.amount_issued(),
@@ -664,7 +665,6 @@ impl Mint {
 
         let Verification {
             amount: outputs_amount,
-            unit,
         } = match self.verify_outputs(&mut tx, &mint_request.outputs).await {
             Ok(verification) => verification,
             Err(err) => {
@@ -674,11 +674,15 @@ impl Mint {
             }
         };
 
+        // Get unit from the typed outputs amount
+        let unit = outputs_amount.unit().clone();
+        ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
+
         if mint_quote.payment_method.is_bolt11() {
             // For bolt11 we enforce that mint amount == quote amount
             if outputs_amount != mint_amount {
                 return Err(Error::TransactionUnbalanced(
-                    mint_amount.into(),
+                    mint_amount.value(),
                     mint_request.total_amount()?.into(),
                     0,
                 ));
@@ -687,18 +691,15 @@ impl Mint {
             // For other payments we just make sure outputs is not more then mint amount
             if outputs_amount > mint_amount {
                 return Err(Error::TransactionUnbalanced(
-                    mint_amount.into(),
+                    mint_amount.value(),
                     mint_request.total_amount()?.into(),
                     0,
                 ));
             }
         }
 
-        let unit = unit.ok_or(Error::UnsupportedUnit)?;
-        ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
-
-        let amount_issued = mint_request.total_amount()?;
-        let operation = Operation::new_mint(amount_issued, mint_quote.payment_method.clone());
+        let amount_issued = mint_request.total_amount()?.with_unit(unit.clone());
+        let operation = Operation::new_mint(amount_issued.clone().into(), mint_quote.payment_method.clone());
 
         tx.add_blinded_messages(Some(&mint_request.quote), &mint_request.outputs, &operation).await?;
 

+ 6 - 7
crates/cdk/src/mint/ln.rs

@@ -1,12 +1,11 @@
 use std::collections::HashMap;
 use std::sync::Arc;
 
-use cdk_common::amount::to_unit;
 use cdk_common::common::PaymentProcessorKey;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::MintQuote;
 use cdk_common::payment::DynMintPayment;
-use cdk_common::{Amount, MintQuoteState};
+use cdk_common::MintQuoteState;
 use tracing::instrument;
 
 use super::subscription::PubSubManager;
@@ -71,16 +70,16 @@ impl Mint {
 
         for payment in ln_status {
             if !new_quote.payment_ids().contains(&&payment.payment_id)
-                && payment.payment_amount > Amount::ZERO
+                && payment.payment_amount.value() > 0
             {
                 tracing::debug!(
-                    "Found payment of {} {} for quote {} when checking.",
-                    payment.payment_amount,
-                    payment.unit,
+                    "Found payment of {} {:?} for quote {} when checking.",
+                    payment.payment_amount.value(),
+                    payment.unit(),
                     new_quote.id
                 );
 
-                let amount_paid = to_unit(payment.payment_amount, &payment.unit, &new_quote.unit)?;
+                let amount_paid = payment.payment_amount.convert_to(&new_quote.unit)?;
 
                 match new_quote.add_payment(amount_paid, payment.payment_id.clone(), None) {
                     Ok(()) => {

+ 56 - 49
crates/cdk/src/mint/melt/melt_saga/mod.rs

@@ -1,13 +1,12 @@
 use std::collections::VecDeque;
 use std::sync::Arc;
 
-use cdk_common::amount::to_unit;
 use cdk_common::database::mint::MeltRequestInfo;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::{MeltSagaState, Operation, Saga, SagaStateEnum};
 use cdk_common::nut00::KnownMethod;
 use cdk_common::nuts::MeltQuoteState;
-use cdk_common::{Amount, Error, ProofsMethods, PublicKey, QuoteId, State};
+use cdk_common::{Amount, CurrencyUnit, Error, ProofsMethods, PublicKey, QuoteId, State};
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
 use tokio::sync::Mutex;
@@ -197,8 +196,8 @@ impl MeltSaga<Initial> {
     ) -> Result<MeltSaga<SetupComplete>, Error> {
         let Verification {
             amount: input_amount,
-            unit: input_unit,
         } = input_verification;
+        let input_unit = Some(input_amount.unit().clone());
 
         let mut tx = self.db.begin_transaction().await?;
 
@@ -220,10 +219,10 @@ impl MeltSaga<Initial> {
         let operation = Operation::new(
             self.state_data.operation_id,
             cdk_common::mint::OperationKind::Melt,
-            Amount::ZERO,         // total_issued (change will be calculated later)
-            input_amount,         // total_redeemed
-            fee_breakdown.total,  // fee_collected
-            None,                 // complete_at
+            Amount::ZERO, // total_issued (change will be calculated later)
+            input_amount.clone().into(), // total_redeemed (convert to untyped)
+            fee_breakdown.total, // fee_collected
+            None,         // complete_at
             Some(payment_method), // payment_method
         );
 
@@ -297,23 +296,28 @@ impl MeltSaga<Initial> {
             .melt_quote_status(&*quote, None, None, MeltQuoteState::Pending);
 
         let inputs_fee_breakdown = self.mint.get_proofs_fee(melt_request.inputs()).await?;
+        let inputs_fee = inputs_fee_breakdown.total.with_unit(quote.unit.clone());
+        let fee_reserve = quote.fee_reserve();
 
-        let required_total = quote.amount + quote.fee_reserve + inputs_fee_breakdown.total;
+        let required_total = quote
+            .amount()
+            .checked_add(&fee_reserve)?
+            .checked_add(&inputs_fee)?;
 
-        if input_amount < required_total {
+        if input_amount < required_total.clone() {
             tracing::info!(
                 "Melt request unbalanced: inputs {}, amount {}, fee_reserve {}, input_fee {}, required {}",
                 input_amount,
-                quote.amount,
-                quote.fee_reserve,
-                inputs_fee_breakdown.total,
+                quote.amount(),
+                fee_reserve,
+                inputs_fee,
                 required_total
             );
             tx.rollback().await?;
             return Err(Error::TransactionUnbalanced(
-                input_amount.into(),
-                quote.amount.into(),
-                (inputs_fee_breakdown.total + quote.fee_reserve).into(),
+                input_amount.to_u64(),
+                quote.amount().value(),
+                inputs_fee.checked_add(&fee_reserve)?.value(),
             ));
         }
 
@@ -328,7 +332,7 @@ impl MeltSaga<Initial> {
                     }
                 };
 
-                if input_unit != output_verification.unit {
+                if input_unit.as_ref() != Some(output_verification.amount.unit()) {
                     tx.rollback().await?;
                     return Err(Error::UnitMismatch);
                 }
@@ -338,8 +342,8 @@ impl MeltSaga<Initial> {
         // Add melt request tracking record
         tx.add_melt_request(
             melt_request.quote_id(),
-            melt_request.inputs_amount()?,
-            inputs_fee_breakdown.total,
+            melt_request.inputs_amount()?.with_unit(quote.unit.clone()),
+            inputs_fee.clone(),
         )
         .await?;
 
@@ -479,13 +483,16 @@ impl MeltSaga<SetupComplete> {
             return Err(Error::RequestAlreadyPaid);
         }
 
-        let inputs_amount_quote_unit = melt_request.inputs_amount().map_err(|_| {
-            tracing::error!("Proof inputs in melt quote overflowed");
-            Error::AmountOverflow
-        })?;
+        let inputs_amount_quote_unit = melt_request
+            .inputs_amount()
+            .map_err(|_| {
+                tracing::error!("Proof inputs in melt quote overflowed");
+                Error::AmountOverflow
+            })?
+            .with_unit(mint_quote.unit.clone());
 
-        if let Some(amount) = mint_quote.amount {
-            if amount > inputs_amount_quote_unit {
+        if let Some(ref amount) = mint_quote.amount {
+            if amount > &inputs_amount_quote_unit {
                 tracing::debug!(
                     "Not enough inputs provided: {} needed {}",
                     inputs_amount_quote_unit,
@@ -497,7 +504,7 @@ impl MeltSaga<SetupComplete> {
             }
         }
 
-        let amount = self.state_data.quote.amount;
+        let amount = self.state_data.quote.amount();
 
         tracing::info!(
             "Mint quote {} paid {} from internal payment.",
@@ -513,7 +520,7 @@ impl MeltSaga<SetupComplete> {
         )
         .await?;
 
-        mint_quote.add_payment(amount, self.state_data.quote.id.to_string(), None)?;
+        mint_quote.add_payment(amount.clone(), self.state_data.quote.id.to_string(), None)?;
         tx.update_mint_quote(&mut mint_quote).await?;
 
         self.pubsub
@@ -590,7 +597,6 @@ impl MeltSaga<SetupComplete> {
                 MakePaymentResponse {
                     status: MeltQuoteState::Paid,
                     total_spent: amount,
-                    unit: self.state_data.quote.unit.clone(),
                     payment_proof: None,
                     payment_lookup_id: self
                         .state_data
@@ -650,7 +656,7 @@ impl MeltSaga<SetupComplete> {
                             "Got {} status when paying melt quote {} for {} {}. Verifying with backend...",
                             pay.status,
                             self.state_data.quote.id,
-                            self.state_data.quote.amount,
+                            self.state_data.quote.amount(),
                             self.state_data.quote.unit
                         );
 
@@ -825,15 +831,17 @@ impl MeltSaga<PaymentConfirmed> {
     /// - `UnitMismatch`: Failed to convert payment amount to quote unit
     #[instrument(skip_all)]
     pub async fn finalize(self) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
-        let total_spent = to_unit(
-            self.state_data.payment_result.total_spent,
-            &self.state_data.payment_result.unit,
-            &self.state_data.quote.unit,
-        )
-        .map_err(|e| {
-            tracing::error!("Failed to convert total_spent to quote unit: {:?}", e);
-            Error::UnitMismatch
-        })?;
+        tracing::info!("TX2: Finalizing melt (mark spent + change)");
+
+        let total_spent: Amount<CurrencyUnit> = self
+            .state_data
+            .payment_result
+            .total_spent
+            .convert_to(&self.state_data.quote.unit)
+            .map_err(|e| {
+                tracing::error!("Failed to convert total_spent to quote unit: {:?}", e);
+                Error::UnitMismatch
+            })?;
 
         let payment_preimage = self.state_data.payment_result.payment_proof.clone();
         let payment_lookup_id = &self.state_data.payment_result.payment_lookup_id;
@@ -860,9 +868,9 @@ impl MeltSaga<PaymentConfirmed> {
             &self.pubsub,
             &mut quote,
             &self.state_data.input_ys,
-            inputs_amount,
-            inputs_fee,
-            total_spent,
+            inputs_amount.clone(),
+            inputs_fee.clone(),
+            total_spent.clone(),
             payment_preimage.clone(),
             payment_lookup_id,
         )
@@ -895,8 +903,8 @@ impl MeltSaga<PaymentConfirmed> {
                 &self.mint,
                 &self.db,
                 &self.state_data.quote.id,
-                inputs_amount,
-                total_spent,
+                inputs_amount.clone(),
+                total_spent.clone(),
                 inputs_fee,
                 change_outputs,
             )
@@ -922,10 +930,9 @@ impl MeltSaga<PaymentConfirmed> {
         // Set payment details for melt operation
         // payment_amount = the Lightning invoice amount
         // payment_fee = actual fee paid (total_spent - invoice_amount)
-        let payment_fee = total_spent
-            .checked_sub(self.state_data.quote.amount)
-            .unwrap_or(Amount::ZERO);
-        operation.set_payment_details(self.state_data.quote.amount, payment_fee);
+        let payment_fee = total_spent.checked_sub(&self.state_data.quote.amount())?;
+
+        operation.set_payment_details(self.state_data.quote.amount().into(), payment_fee.into());
 
         tx.add_completed_operation(&operation, &self.state_data.fee_breakdown.per_keyset)
             .await?;
@@ -956,11 +963,11 @@ impl MeltSaga<PaymentConfirmed> {
         }
 
         let response = MeltQuoteBolt11Response {
-            amount: self.state_data.quote.amount,
+            amount: self.state_data.quote.amount().into(),
             payment_preimage,
             change,
-            quote: self.state_data.quote.id,
-            fee_reserve: self.state_data.quote.fee_reserve,
+            quote: self.state_data.quote.id.clone(),
+            fee_reserve: self.state_data.quote.fee_reserve().into(),
             state: MeltQuoteState::Paid,
             expiry: self.state_data.quote.expiry,
             request: Some(self.state_data.quote.request.to_string()),

+ 2 - 2
crates/cdk/src/mint/melt/melt_saga/state.rs

@@ -1,5 +1,5 @@
 use cdk_common::mint::Operation;
-use cdk_common::nuts::BlindedMessage;
+use cdk_common::nuts::{BlindedMessage, CurrencyUnit};
 use cdk_common::{Amount, PublicKey};
 use uuid::Uuid;
 
@@ -49,7 +49,7 @@ pub struct PaymentConfirmed {
 pub enum SettlementDecision {
     /// Payment was settled internally (melt-to-mint on the same mint).
     /// Contains the amount that was settled.
-    Internal { amount: Amount },
+    Internal { amount: Amount<CurrencyUnit> },
     /// Payment requires external Lightning Network settlement.
     RequiresExternalPayment,
 }

+ 2 - 3
crates/cdk/src/mint/melt/melt_saga/tests.rs

@@ -582,7 +582,7 @@ async fn test_crash_recovery_internal_settlement() {
         crate::mint::melt::melt_saga::state::SettlementDecision::Internal { amount } => {
             assert_eq!(
                 amount,
-                Amount::from(4_000),
+                Amount::from(4_000).with_unit(cdk_common::CurrencyUnit::Sat),
                 "Internal settlement amount should match"
             );
         }
@@ -1460,8 +1460,7 @@ async fn test_get_incomplete_sagas_filters_by_kind() {
     // STEP 3: Create a swap saga
     let swap_proofs = mint_test_proofs(&mint, Amount::from(5_000)).await.unwrap();
     let swap_verification = crate::mint::Verification {
-        amount: Amount::from(5_000),
-        unit: Some(cdk_common::nuts::CurrencyUnit::Sat),
+        amount: Amount::from(5_000).with_unit(cdk_common::nuts::CurrencyUnit::Sat),
     };
 
     let (swap_outputs, _) = create_test_blinded_messages(&mint, Amount::from(5_000))

+ 41 - 49
crates/cdk/src/mint/melt/mod.rs

@@ -1,6 +1,5 @@
 use std::str::FromStr;
 
-use cdk_common::amount::amount_for_offer;
 use cdk_common::melt::MeltQuoteRequest;
 use cdk_common::mint::MeltPaymentRequest;
 use cdk_common::nut00::KnownMethod;
@@ -22,7 +21,6 @@ use super::{
     CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, Mint,
     PaymentMethod,
 };
-use crate::amount::to_unit;
 use crate::nuts::MeltQuoteState;
 use crate::types::PaymentProcessorKey;
 use crate::util::unix_time;
@@ -40,12 +38,12 @@ impl Mint {
     #[instrument(skip_all)]
     async fn check_melt_request_acceptable(
         &self,
-        amount: Amount,
-        unit: CurrencyUnit,
+        amount: Amount<CurrencyUnit>,
         method: PaymentMethod,
         request: String,
         options: Option<MeltOptions>,
     ) -> Result<(), Error> {
+        let unit = amount.unit().clone();
         let mint_info = self.mint_info().await?;
         let nut05 = mint_info.nuts.nut05;
 
@@ -55,7 +53,7 @@ impl Mint {
             .get_settings(&unit, &method)
             .ok_or(Error::UnsupportedUnit)?;
 
-        let amount = match options {
+        match options {
             Some(MeltOptions::Mpp { mpp: _ }) => {
                 let nut15 = mint_info.nuts.nut15;
                 // Verify there is no corresponding mint quote.
@@ -72,9 +70,6 @@ impl Mint {
                 {
                     return Err(Error::MppUnitMethodNotSupported(unit, method));
                 }
-                // Assign `amount`
-                // because should have already been converted to the partial amount
-                amount
             }
             Some(MeltOptions::Amountless { amountless: _ }) => {
                 if method.is_bolt11()
@@ -85,14 +80,14 @@ impl Mint {
                 {
                     return Err(Error::AmountlessInvoiceNotSupported(unit, method));
                 }
-
-                amount
             }
-            None => amount,
+            None => {}
         };
 
-        let is_above_max = matches!(settings.max_amount, Some(max) if amount > max);
-        let is_below_min = matches!(settings.min_amount, Some(min) if amount < min);
+        // Compare using raw values since settings use Amount without unit
+        let amount_value = amount.value();
+        let is_above_max = matches!(settings.max_amount, Some(max) if amount_value > max.into());
+        let is_below_min = matches!(settings.min_amount, Some(min) if amount_value < min.into());
         match is_above_max || is_below_min {
             true => {
                 tracing::error!(
@@ -104,7 +99,7 @@ impl Mint {
                 Err(Error::AmountOutofLimitRange(
                     settings.min_amount.unwrap_or_default(),
                     settings.max_amount.unwrap_or_default(),
-                    amount,
+                    amount.into(),
                 ))
             }
             false => Ok(()),
@@ -187,20 +182,23 @@ impl Mint {
                 err
             })?;
 
-        if &payment_quote.unit != unit {
+        if payment_quote.unit() != unit {
             return Err(Error::UnitMismatch);
         }
 
         // Validate using processor quote amount for currency conversion
         self.check_melt_request_acceptable(
-            payment_quote.amount,
-            unit.clone(),
+            payment_quote.amount.clone(),
             PaymentMethod::Known(KnownMethod::Bolt11),
             request.to_string(),
             *options,
         )
         .await?;
 
+        // Extract values for quote creation
+        let quote_amount = payment_quote.amount;
+        let quote_fee = payment_quote.fee;
+
         let melt_ttl = self.quote_ttl().await?.melt_ttl;
 
         let quote = MeltQuote::new(
@@ -208,8 +206,8 @@ impl Mint {
                 bolt11: request.clone(),
             },
             unit.clone(),
-            payment_quote.amount,
-            payment_quote.fee,
+            quote_amount.clone(),
+            quote_fee,
             unix_time() + melt_ttl,
             payment_quote.request_lookup_id.clone(),
             *options,
@@ -220,7 +218,7 @@ impl Mint {
             "New {} melt quote {} for {} {} with request id {:?}",
             quote.payment_method,
             quote.id,
-            payment_quote.amount,
+            quote_amount,
             unit,
             payment_quote.request_lookup_id
         );
@@ -244,18 +242,6 @@ impl Mint {
             options,
         } = melt_request;
 
-        let offer = Offer::from_str(request).map_err(|_| Error::InvalidPaymentRequest)?;
-
-        let amount = match options {
-            Some(options) => match options {
-                MeltOptions::Amountless { amountless } => {
-                    to_unit(amountless.amount_msat, &CurrencyUnit::Msat, unit)?
-                }
-                _ => return Err(Error::UnsupportedUnit),
-            },
-            None => amount_for_offer(&offer, unit).map_err(|_| Error::UnsupportedUnit)?,
-        };
-
         let ln = self
             .payment_processors
             .get(&PaymentProcessorKey::new(
@@ -293,20 +279,23 @@ impl Mint {
                 err
             })?;
 
-        if &payment_quote.unit != unit {
+        if payment_quote.unit() != unit {
             return Err(Error::UnitMismatch);
         }
 
         // Validate using processor quote amount for currency conversion
         self.check_melt_request_acceptable(
-            payment_quote.amount,
-            unit.clone(),
+            payment_quote.amount.clone(),
             PaymentMethod::Known(KnownMethod::Bolt12),
             request.clone(),
             *options,
         )
         .await?;
 
+        // Extract values for quote creation
+        let quote_amount = payment_quote.amount;
+        let quote_fee = payment_quote.fee;
+
         let payment_request = MeltPaymentRequest::Bolt12 {
             offer: Box::new(offer),
         };
@@ -314,8 +303,8 @@ impl Mint {
         let quote = MeltQuote::new(
             payment_request,
             unit.clone(),
-            payment_quote.amount,
-            payment_quote.fee,
+            quote_amount.clone(),
+            quote_fee,
             unix_time() + self.quote_ttl().await?.melt_ttl,
             payment_quote.request_lookup_id.clone(),
             *options,
@@ -326,7 +315,7 @@ impl Mint {
             "New {} melt quote {} for {} {} with request id {:?}",
             quote.payment_method,
             quote.id,
-            amount,
+            quote_amount,
             unit,
             payment_quote.request_lookup_id
         );
@@ -408,15 +397,14 @@ impl Mint {
                 Error::UnsupportedUnit
             })?;
 
-        if &payment_quote.unit != unit {
+        if payment_quote.unit() != unit {
             return Err(Error::UnitMismatch);
         }
 
         // For custom methods, we don't validate amount limits upfront since
         // the payment processor handles method-specific validation
         self.check_melt_request_acceptable(
-            payment_quote.amount,
-            unit.clone(),
+            payment_quote.amount.clone(),
             PaymentMethod::from(method.as_str()),
             request.clone(),
             None, // Custom methods don't use options
@@ -425,14 +413,18 @@ impl Mint {
 
         let melt_ttl = self.quote_ttl().await?.melt_ttl;
 
+        // Extract values for quote creation
+        let quote_amount = payment_quote.amount;
+        let quote_fee = payment_quote.fee;
+
         let quote = MeltQuote::new(
             MeltPaymentRequest::Custom {
                 method: method.to_string(),
                 request: request.clone(),
             },
             unit.clone(),
-            payment_quote.amount,
-            payment_quote.fee,
+            quote_amount.clone(),
+            quote_fee,
             unix_time() + melt_ttl,
             payment_quote.request_lookup_id.clone(),
             None, // Custom methods don't use options
@@ -443,7 +435,7 @@ impl Mint {
             "New {} melt quote {} for {} {} with request id {:?}",
             method,
             quote.id,
-            payment_quote.amount,
+            quote_amount,
             unit,
             payment_quote.request_lookup_id
         );
@@ -511,11 +503,11 @@ impl Mint {
         let change = (!blind_signatures.is_empty()).then_some(blind_signatures);
 
         let response = MeltQuoteBolt11Response {
-            quote: quote.id,
+            quote: quote.id.clone(),
             state: quote.state,
             expiry: quote.expiry,
-            amount: quote.amount,
-            fee_reserve: quote.fee_reserve,
+            amount: quote.amount().into(),
+            fee_reserve: quote.fee_reserve().into(),
             payment_preimage: quote.payment_preimage,
             change,
             request: Some(quote.request.to_string()),
@@ -672,8 +664,8 @@ impl Mint {
         // Return immediately with the quote in PENDING state
         Ok(MeltQuoteBolt11Response {
             quote: quote_id,
-            amount: quote.amount,
-            fee_reserve: quote.fee_reserve,
+            amount: quote.amount().into(),
+            fee_reserve: quote.fee_reserve().into(),
             state: quote.state,
             expiry: quote.expiry,
             payment_preimage: None,

+ 52 - 23
crates/cdk/src/mint/melt/shared.rs

@@ -8,7 +8,7 @@
 
 use cdk_common::database::{self, Acquired, DynMintDatabase};
 use cdk_common::nuts::{BlindSignature, BlindedMessage, MeltQuoteState, State};
-use cdk_common::{Amount, Error, PublicKey, QuoteId};
+use cdk_common::{Amount, CurrencyUnit, Error, PublicKey, QuoteId};
 use cdk_signatory::signatory::SignatoryKeySet;
 
 use crate::mint::subscription::PubSubManager;
@@ -183,9 +183,9 @@ pub async fn process_melt_change(
     mint: &super::super::Mint,
     db: &DynMintDatabase,
     quote_id: &QuoteId,
-    inputs_amount: Amount,
-    total_spent: Amount,
-    inputs_fee: Amount,
+    inputs_amount: Amount<CurrencyUnit>,
+    total_spent: Amount<CurrencyUnit>,
+    inputs_fee: Amount<CurrencyUnit>,
     change_outputs: Vec<BlindedMessage>,
 ) -> Result<
     (
@@ -203,13 +203,16 @@ pub async fn process_melt_change(
         return Ok((None, tx));
     }
 
-    let change_target = inputs_amount - total_spent - inputs_fee;
+    let change_target: Amount = inputs_amount
+        .checked_sub(&total_spent)?
+        .checked_sub(&inputs_fee)?
+        .into();
 
     // Get keyset configuration
     let fee_and_amounts = get_keyset_fee_and_amounts(&mint.keysets, &change_outputs);
 
     // Split change into denominations
-    let mut amounts = change_target.split(&fee_and_amounts);
+    let mut amounts: Vec<Amount> = change_target.split(&fee_and_amounts);
 
     if change_outputs.len() < amounts.len() {
         tracing::debug!(
@@ -363,27 +366,58 @@ pub async fn finalize_melt_core(
     pubsub: &PubSubManager,
     quote: &mut Acquired<MeltQuote>,
     input_ys: &[PublicKey],
-    inputs_amount: Amount,
-    inputs_fee: Amount,
-    total_spent: Amount,
+    inputs_amount: Amount<CurrencyUnit>,
+    inputs_fee: Amount<CurrencyUnit>,
+    total_spent: Amount<CurrencyUnit>,
     payment_preimage: Option<String>,
     payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
 ) -> Result<(), Error> {
     // Validate quote amount vs payment amount
-    if quote.amount > total_spent {
+    if quote.amount() > total_spent {
         tracing::error!(
             "Payment amount {} is less than quote amount {} for quote {}",
             total_spent,
-            quote.amount,
+            quote.amount(),
             quote.id
         );
         return Err(Error::IncorrectQuoteAmount);
     }
 
     // Validate inputs amount
-    if inputs_amount - inputs_fee < total_spent {
-        tracing::error!("Over paid melt quote {}", quote.id);
-        return Err(Error::IncorrectQuoteAmount);
+    let net_inputs = inputs_amount.checked_sub(&inputs_fee)?;
+
+    // Convert total_spent to the same unit as net_inputs for comparison.
+    // Backends should return total_spent in the quote's unit, but we convert defensively.
+    let total_spent = total_spent.convert_to(net_inputs.unit())?;
+
+    tracing::debug!(
+        "Melt validation for quote {}: inputs_amount={}, inputs_fee={}, net_inputs={}, total_spent={}, quote_amount={}, fee_reserve={}",
+        quote.id,
+        inputs_amount.display_with_unit(),
+        inputs_fee.display_with_unit(),
+        net_inputs.display_with_unit(),
+        total_spent.display_with_unit(),
+        quote.amount().display_with_unit(),
+        quote.fee_reserve().display_with_unit(),
+    );
+
+    // This can only happen on backends where we cannot set the max fee (e.g., LNbits).
+    // LNbits does not allow setting a fee limit, so payments can exceed the fee reserve.
+    debug_assert!(
+        net_inputs >= total_spent,
+        "Over paid melt quote {}: net_inputs ({}) < total_spent ({}). Payment already complete, finalizing with no change.",
+        quote.id,
+        net_inputs.display_with_unit(),
+        total_spent.display_with_unit(),
+    );
+    if net_inputs < total_spent {
+        tracing::error!(
+            "Over paid melt quote {}: net_inputs ({}) < total_spent ({}). Payment already complete, finalizing with no change.",
+            quote.id,
+            net_inputs.display_with_unit(),
+            total_spent.display_with_unit(),
+        );
+        // Payment is already done - continue finalization but no change will be returned
     }
 
     // Update quote state to Paid
@@ -442,17 +476,12 @@ pub async fn finalize_melt_quote(
     db: &DynMintDatabase,
     pubsub: &PubSubManager,
     quote: &MeltQuote,
-    total_spent: Amount,
+    total_spent: Amount<CurrencyUnit>,
     payment_preimage: Option<String>,
     payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
 ) -> Result<Option<Vec<BlindSignature>>, Error> {
-    use cdk_common::amount::to_unit;
-
     tracing::info!("Finalizing melt quote {}", quote.id);
 
-    // Convert total_spent to quote unit
-    let total_spent = to_unit(total_spent, &quote.unit, &quote.unit).unwrap_or(total_spent);
-
     let mut tx = db.begin_transaction().await?;
 
     // Acquire lock on the quote for safe state update
@@ -490,9 +519,9 @@ pub async fn finalize_melt_quote(
         pubsub,
         &mut locked_quote,
         &input_ys,
-        melt_request_info.inputs_amount,
-        melt_request_info.inputs_fee,
-        total_spent,
+        melt_request_info.inputs_amount.clone(),
+        melt_request_info.inputs_fee.clone(),
+        total_spent.clone(),
         payment_preimage.clone(),
         payment_lookup_id,
     )

+ 7 - 10
crates/cdk/src/mint/mod.rs

@@ -5,7 +5,6 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use arc_swap::ArcSwap;
-use cdk_common::amount::to_unit;
 use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 #[cfg(feature = "auth")]
 use cdk_common::database::DynMintAuthDatabase;
@@ -707,7 +706,7 @@ impl Mint {
         pubsub_manager: &Arc<PubSubManager>,
         wait_payment_response: WaitPaymentResponse,
     ) -> Result<(), Error> {
-        if wait_payment_response.payment_amount == Amount::ZERO {
+        if wait_payment_response.payment_amount.value() == 0 {
             tracing::warn!(
                 "Received payment response with 0 amount with payment id {}.",
                 wait_payment_response.payment_id
@@ -748,9 +747,9 @@ impl Mint {
         pubsub_manager: &Arc<PubSubManager>,
     ) -> Result<(), Error> {
         tracing::debug!(
-            "Received payment notification of {} {} for mint quote {} with payment id {}",
+            "Received payment notification of {} {:?} for mint quote {} with payment id {}",
             wait_payment_response.payment_amount,
-            wait_payment_response.unit,
+            wait_payment_response.unit(),
             mint_quote.id,
             wait_payment_response.payment_id.to_string()
         );
@@ -765,13 +764,11 @@ impl Mint {
             {
                 tracing::info!("Received payment notification for already issued quote.");
             } else {
-                let payment_amount_quote_unit = to_unit(
-                    wait_payment_response.payment_amount,
-                    &wait_payment_response.unit,
-                    &mint_quote.unit,
-                )?;
+                let payment_amount_quote_unit: Amount<CurrencyUnit> = wait_payment_response
+                    .payment_amount
+                    .convert_to(&mint_quote.unit)?;
 
-                if payment_amount_quote_unit == Amount::ZERO {
+                if payment_amount_quote_unit.value() == 0 {
                     tracing::error!("Zero amount payments should not be recorded.");
                     return Err(Error::AmountUndefined);
                 }

+ 2 - 2
crates/cdk/src/mint/start_up_check.rs

@@ -75,7 +75,7 @@ impl Mint {
     async fn finalize_paid_melt_quote(
         &self,
         quote: &MeltQuote,
-        total_spent: cdk_common::Amount,
+        total_spent: cdk_common::Amount<cdk_common::CurrencyUnit>,
         payment_preimage: Option<String>,
         payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
     ) -> Result<(), Error> {
@@ -381,7 +381,7 @@ impl Mint {
                                 );
 
                                 // Get payment info for finalization
-                                let total_spent = quote.amount;
+                                let total_spent = quote.amount();
                                 let payment_lookup_id =
                                     quote.request_lookup_id.clone().unwrap_or_else(|| {
                                         cdk_common::payment::PaymentIdentifier::CustomId(

+ 9 - 8
crates/cdk/src/mint/subscription.rs

@@ -12,8 +12,9 @@ use cdk_common::payment::DynMintPayment;
 use cdk_common::pub_sub::{Pubsub, Spec, Subscriber};
 use cdk_common::subscription::SubId;
 use cdk_common::{
-    Amount, BlindSignature, MeltQuoteBolt11Response, MeltQuoteState, MintQuoteBolt11Response,
-    MintQuoteBolt12Response, MintQuoteState, ProofState, PublicKey, QuoteId,
+    Amount, BlindSignature, CurrencyUnit, MeltQuoteBolt11Response, MeltQuoteState,
+    MintQuoteBolt11Response, MintQuoteBolt12Response, MintQuoteState, ProofState, PublicKey,
+    QuoteId,
 };
 
 use super::Mint;
@@ -193,7 +194,7 @@ impl PubSubManager {
     }
 
     /// Helper function to publish even of a mint quote being paid
-    pub fn mint_quote_issue(&self, mint_quote: &MintQuote, total_issued: Amount) {
+    pub fn mint_quote_issue(&self, mint_quote: &MintQuote, total_issued: Amount<CurrencyUnit>) {
         match mint_quote.payment_method {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Issued);
@@ -201,8 +202,8 @@ impl PubSubManager {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.mint_quote_bolt12_status(
                     mint_quote.clone(),
-                    mint_quote.amount_paid(),
-                    total_issued,
+                    mint_quote.amount_paid().into(),
+                    total_issued.into(),
                 );
             }
             _ => {
@@ -212,7 +213,7 @@ impl PubSubManager {
     }
 
     /// Helper function to publish even of a mint quote being paid
-    pub fn mint_quote_payment(&self, mint_quote: &MintQuote, total_paid: Amount) {
+    pub fn mint_quote_payment(&self, mint_quote: &MintQuote, total_paid: Amount<CurrencyUnit>) {
         match mint_quote.payment_method {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
@@ -220,8 +221,8 @@ impl PubSubManager {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.mint_quote_bolt12_status(
                     mint_quote.clone(),
-                    total_paid,
-                    mint_quote.amount_issued(),
+                    total_paid.into(),
+                    mint_quote.amount_issued().into(),
                 );
             }
             _ => {

+ 16 - 6
crates/cdk/src/mint/swap/swap_saga/mod.rs

@@ -4,7 +4,7 @@ use std::sync::Arc;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::{Operation, Saga, SwapSagaState};
 use cdk_common::nuts::BlindedMessage;
-use cdk_common::{database, Amount, Error, Proofs, ProofsMethods, PublicKey, QuoteId, State};
+use cdk_common::{database, Error, Proofs, ProofsMethods, PublicKey, QuoteId, State};
 use tokio::sync::Mutex;
 use tracing::instrument;
 
@@ -150,27 +150,37 @@ impl<'a> SwapSaga<'a, Initial> {
     ) -> Result<SwapSaga<'a, SetupComplete>, Error> {
         let mut tx = self.db.begin_transaction().await?;
 
+        let output_verification = self
+            .mint
+            .verify_outputs(&mut tx, blinded_messages)
+            .await
+            .map_err(|err| {
+                tracing::debug!("Output verification failed: {:?}", err);
+                err
+            })?;
+
         // Verify balance within the transaction
         self.mint
             .verify_transaction_balanced(
-                &mut tx,
                 input_verification.clone(),
+                output_verification.clone(),
                 input_proofs,
-                blinded_messages,
             )
             .await?;
 
         // Calculate amounts to create Operation
         let total_redeemed = input_verification.amount;
-        let total_issued = Amount::try_sum(blinded_messages.iter().map(|bm| bm.amount))?;
+        let total_issued = output_verification.amount;
+
         let fee_breakdown = self.mint.get_proofs_fee(input_proofs).await?;
 
         // Create Operation with actual amounts now that we know them
+        // Convert typed amounts to untyped for Operation::new
         let operation = Operation::new(
             self.state_data.operation_id,
             cdk_common::mint::OperationKind::Swap,
-            total_issued,
-            total_redeemed,
+            total_issued.clone().into(),
+            total_redeemed.clone().into(),
             fee_breakdown.total,
             None, // complete_at
             None, // payment_method (not applicable for swap)

+ 1 - 2
crates/cdk/src/mint/swap/swap_saga/tests.rs

@@ -18,8 +18,7 @@ use crate::test_helpers::mint::{
 /// Helper to create a verification result for testing
 fn create_verification(amount: Amount) -> Verification {
     Verification {
-        amount,
-        unit: Some(cdk_common::nuts::CurrencyUnit::Sat),
+        amount: amount.with_unit(cdk_common::nuts::CurrencyUnit::Sat),
     }
 }
 

+ 2 - 2
crates/cdk/src/mint/swap/tests/p2pk_sigall_spending_conditions_tests.rs

@@ -559,7 +559,7 @@ async fn test_p2pk_sig_all_locktime_after_expiry() {
     let mint = test_mint.mint();
 
     let (alice_secret, alice_pubkey) = create_test_keypair();
-    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_bob_secret, bob_pubkey) = create_test_keypair();
 
     // Set locktime in the past (already expired)
     let locktime = unix_time() - 3600;
@@ -726,7 +726,7 @@ async fn test_p2pk_sig_all_multisig_locktime() {
     let (_carol_secret, carol_pubkey) = create_test_keypair();
 
     // After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
-    let (dave_secret, dave_pubkey) = create_test_keypair();
+    let (_dave_secret, dave_pubkey) = create_test_keypair();
     let (_eve_secret, eve_pubkey) = create_test_keypair();
 
     let locktime = unix_time() - 100; // Already expired

+ 2 - 2
crates/cdk/src/mint/swap/tests/p2pk_spending_conditions_tests.rs

@@ -375,7 +375,7 @@ async fn test_p2pk_locktime_after_expiry() {
     let mint = test_mint.mint();
 
     let (alice_secret, alice_pubkey) = create_test_keypair();
-    let (bob_secret, bob_pubkey) = create_test_keypair();
+    let (_bob_secret, bob_pubkey) = create_test_keypair();
 
     // Set locktime in the past (already expired)
     let locktime = unix_time() - 3600;
@@ -544,7 +544,7 @@ async fn test_p2pk_multisig_locktime() {
     let (_carol_secret, carol_pubkey) = create_test_keypair();
 
     // After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
-    let (dave_secret, dave_pubkey) = create_test_keypair();
+    let (_dave_secret, dave_pubkey) = create_test_keypair();
     let (_eve_secret, eve_pubkey) = create_test_keypair();
 
     let locktime = unix_time() - 100; // Already expired

+ 29 - 55
crates/cdk/src/mint/verification.rs

@@ -6,13 +6,11 @@ use tracing::instrument;
 use super::{Error, Mint};
 use crate::cdk_database;
 
-/// Verification result
+/// Verification result with typed amount
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct Verification {
-    /// Value in request
-    pub amount: Amount,
-    /// Unit of request
-    pub unit: Option<CurrencyUnit>,
+    /// Verified amount with unit
+    pub amount: Amount<CurrencyUnit>,
 }
 
 impl Mint {
@@ -169,7 +167,10 @@ impl Mint {
     }
 
     /// Verifies outputs
-    /// Checks outputs are unique, of the same unit and not signed before
+    ///
+    /// Checks outputs are unique, of the same unit and not signed before.
+    /// Returns an error if outputs are empty - callers should guard against
+    /// empty outputs before calling this function.
     #[instrument(skip_all)]
     pub async fn verify_outputs(
         &self,
@@ -177,10 +178,8 @@ impl Mint {
         outputs: &[BlindedMessage],
     ) -> Result<Verification, Error> {
         if outputs.is_empty() {
-            return Ok(Verification {
-                amount: Amount::ZERO,
-                unit: None,
-            });
+            tracing::debug!("verify_outputs called with empty outputs");
+            return Err(Error::TransactionUnbalanced(0, 0, 0));
         }
 
         Mint::check_outputs_unique(outputs)?;
@@ -188,81 +187,56 @@ impl Mint {
 
         let unit = self.verify_outputs_keyset(outputs)?;
 
-        let amount = Amount::try_sum(outputs.iter().map(|o| o.amount).collect::<Vec<Amount>>())?;
+        let amount = Amount::try_sum(outputs.iter().map(|o| o.amount))?.with_unit(unit);
 
-        Ok(Verification {
-            amount,
-            unit: Some(unit),
-        })
+        Ok(Verification { amount })
     }
 
     /// Verifies inputs
-    /// Checks that inputs are unique and of the same unit
+    ///
+    /// Checks that inputs are unique and of the same unit.
     /// **NOTE: This does not check if inputs have been spent
     #[instrument(skip_all)]
     pub async fn verify_inputs(&self, inputs: &Proofs) -> Result<Verification, Error> {
         Mint::check_inputs_unique(inputs)?;
         let unit = self.verify_inputs_keyset(inputs).await?;
-        let amount = inputs.total_amount()?;
+        let amount = inputs.total_amount()?.with_unit(unit);
 
         self.verify_proofs(inputs.clone()).await?;
 
-        Ok(Verification {
-            amount,
-            unit: Some(unit),
-        })
+        Ok(Verification { amount })
     }
 
     /// Verify that inputs and outputs are valid and balanced
     #[instrument(skip_all)]
     pub async fn verify_transaction_balanced(
         &self,
-        tx: &mut Box<dyn cdk_database::MintTransaction<cdk_database::Error> + Send + Sync>,
         input_verification: Verification,
+        output_verification: Verification,
         inputs: &Proofs,
-        outputs: &[BlindedMessage],
     ) -> Result<(), Error> {
-        let output_verification = self.verify_outputs(tx, outputs).await.map_err(|err| {
-            tracing::debug!("Output verification failed: {:?}", err);
-            err
-        })?;
-
         let fee_breakdown = self.get_proofs_fee(inputs).await?;
 
-        if output_verification
-            .unit
-            .as_ref()
-            .ok_or(Error::TransactionUnbalanced(
-                input_verification.amount.into(),
-                output_verification.amount.into(),
-                fee_breakdown.total.into(),
-            ))?
-            != input_verification
-                .unit
-                .as_ref()
-                .ok_or(Error::TransactionUnbalanced(
-                    input_verification.amount.to_u64(),
-                    output_verification.amount.to_u64(),
-                    0,
-                ))?
-        {
+        // Units are now embedded in the typed amounts - check they match
+        if output_verification.amount.unit() != input_verification.amount.unit() {
             tracing::debug!(
                 "Output unit {:?} does not match input unit {:?}",
-                output_verification.unit,
-                input_verification.unit
+                output_verification.amount.unit(),
+                input_verification.amount.unit()
             );
             return Err(Error::UnitMismatch);
         }
 
-        if output_verification.amount
-            != input_verification
-                .amount
-                .checked_sub(fee_breakdown.total)
-                .ok_or(Error::AmountOverflow)?
-        {
+        // Check amounts are balanced (inputs = outputs + fee)
+        let fee_typed = fee_breakdown
+            .total
+            .with_unit(input_verification.amount.unit().clone());
+        let expected_output = input_verification.amount.checked_sub(&fee_typed)?;
+
+        if output_verification.amount != expected_output {
             return Err(Error::TransactionUnbalanced(
-                input_verification.amount.into(),
-                output_verification.amount.into(),
+                input_verification.amount.value(),
+                output_verification.amount.value(),
                 fee_breakdown.total.into(),
             ));
         }

+ 3 - 2
crates/cdk/src/wallet/melt/bolt11.rs

@@ -8,7 +8,6 @@ use cdk_common::PaymentMethod;
 use lightning_invoice::Bolt11Invoice;
 use tracing::instrument;
 
-use crate::amount::to_unit;
 use crate::dhke::construct_proofs;
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::{
@@ -68,7 +67,9 @@ impl Wallet {
                 .or_else(|| invoice.amount_milli_satoshis())
                 .ok_or(Error::InvoiceAmountUndefined)?;
 
-            let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit)?;
+            let amount_quote_unit = Amount::new(amount_msat, CurrencyUnit::Msat)
+                .convert_to(&self.unit)?
+                .into();
 
             if quote_res.amount != amount_quote_unit {
                 tracing::warn!(

+ 4 - 3
crates/cdk/src/wallet/melt/bolt12.rs

@@ -11,9 +11,8 @@ use cdk_common::PaymentMethod;
 use lightning::offers::offer::Offer;
 use tracing::instrument;
 
-use crate::amount::to_unit;
 use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request};
-use crate::{Error, Wallet};
+use crate::{Amount, Error, Wallet};
 
 impl Wallet {
     /// Melt Quote for BOLT12 offer
@@ -38,7 +37,9 @@ impl Wallet {
                 .map(|opt| opt.amount_msat())
                 .or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok())
                 .ok_or(Error::AmountUndefined)?;
-            let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit)?;
+            let amount_quote_unit = Amount::new(amount_msat.into(), CurrencyUnit::Msat)
+                .convert_to(&self.unit)?
+                .into();
 
             if quote_res.amount != amount_quote_unit {
                 tracing::warn!(