Просмотр исходного кода

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 2 недель назад
Родитель
Сommit
352af3bf3f
35 измененных файлов с 1697 добавлено и 898 удалено
  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
     /// Cannot convert units
     #[error("Cannot convert units")]
     #[error("Cannot convert units")]
     CannotConvertUnits,
     CannotConvertUnits,
+    /// Cannot perform operation on amounts with different units
+    #[error("Unit mismatch: cannot operate on {0} and {1}")]
+    UnitMismatch(CurrencyUnit, CurrencyUnit),
     /// Invalid amount
     /// Invalid amount
     #[error("Invalid Amount: {0}")]
     #[error("Invalid Amount: {0}")]
     InvalidAmount(String),
     InvalidAmount(String),
@@ -38,10 +41,15 @@ pub enum Error {
 }
 }
 
 
 /// Amount can be any unit
 /// Amount can be any unit
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+///
+/// Note: `PartialOrd` is implemented manually for `Amount<CurrencyUnit>` to return `None`
+/// when comparing amounts with different units. `Ord` is only implemented for `Amount<()>`.
+#[derive(Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-#[serde(transparent)]
-pub struct Amount(u64);
+pub struct Amount<U = ()> {
+    value: u64,
+    unit: U,
+}
 
 
 /// Fees and and amount type, it can be casted just as a reference to the inner amounts, or a single
 /// Fees and and amount type, it can be casted just as a reference to the inner amounts, or a single
 /// u64 which is the fee
 /// u64 which is the fee
@@ -77,23 +85,112 @@ impl FeeAndAmounts {
 /// Fees and Amounts for each Keyset
 /// Fees and Amounts for each Keyset
 pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
 pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
 
 
-impl FromStr for Amount {
+// Copy and Clone implementations for Amount<()>
+impl Copy for Amount<()> {}
+
+impl Clone for Amount<()> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+// Clone implementation for Amount<CurrencyUnit>
+impl Clone for Amount<CurrencyUnit> {
+    fn clone(&self) -> Self {
+        Self {
+            value: self.value,
+            unit: self.unit.clone(),
+        }
+    }
+}
+
+// PartialOrd implementation for Amount<()> - always comparable
+impl PartialOrd for Amount<()> {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+// Ord implementation for Amount<()> - total ordering on value
+impl Ord for Amount<()> {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.value.cmp(&other.value)
+    }
+}
+
+// PartialOrd implementation for Amount<CurrencyUnit> - returns None if units differ
+impl PartialOrd for Amount<CurrencyUnit> {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        if self.unit != other.unit {
+            // Different units are not comparable
+            None
+        } else {
+            Some(self.value.cmp(&other.value))
+        }
+    }
+}
+
+// Note: We intentionally do NOT implement Ord for Amount<CurrencyUnit>
+// because amounts with different units cannot have a total ordering.
+
+// Serialization - both variants serialize as just the u64 value
+impl<U> Serialize for Amount<U> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        self.value.serialize(serializer)
+    }
+}
+
+impl<'de> Deserialize<'de> for Amount<()> {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = u64::deserialize(deserializer)?;
+        Ok(Amount { value, unit: () })
+    }
+}
+
+impl FromStr for Amount<()> {
     type Err = Error;
     type Err = Error;
 
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         let value = s
         let value = s
             .parse::<u64>()
             .parse::<u64>()
             .map_err(|_| Error::InvalidAmount(s.to_owned()))?;
             .map_err(|_| Error::InvalidAmount(s.to_owned()))?;
-        Ok(Amount(value))
+        Ok(Amount { value, unit: () })
     }
     }
 }
 }
 
 
-impl Amount {
+impl Amount<()> {
     /// Amount zero
     /// Amount zero
-    pub const ZERO: Amount = Amount(0);
+    pub const ZERO: Amount<()> = Amount { value: 0, unit: () };
 
 
     /// Amount one
     /// Amount one
-    pub const ONE: Amount = Amount(1);
+    pub const ONE: Amount<()> = Amount { value: 1, unit: () };
+
+    /// Convert an untyped amount to a typed one by adding a unit
+    ///
+    /// This is used at the boundary between protocol and application layers.
+    /// Protocol types use `Amount<()>` (no unit), while application types
+    /// use `Amount<CurrencyUnit>` (with unit from keyset).
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let untyped = Amount::from(100);
+    /// let typed = untyped.with_unit(CurrencyUnit::Sat);
+    /// assert_eq!(typed.value(), 100);
+    /// assert_eq!(typed.unit(), &CurrencyUnit::Sat);
+    /// ```
+    pub fn with_unit(self, unit: CurrencyUnit) -> Amount<CurrencyUnit> {
+        Amount {
+            value: self.value,
+            unit,
+        }
+    }
 
 
     /// Split into parts that are powers of two
     /// Split into parts that are powers of two
     pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
     pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
@@ -101,7 +198,7 @@ impl Amount {
             .amounts
             .amounts
             .iter()
             .iter()
             .rev()
             .rev()
-            .fold((Vec::new(), self.0), |(mut acc, total), &amount| {
+            .fold((Vec::new(), self.value), |(mut acc, total), &amount| {
                 if total >= amount {
                 if total >= amount {
                     acc.push(Self::from(amount));
                     acc.push(Self::from(amount));
                 }
                 }
@@ -131,10 +228,11 @@ impl Amount {
 
 
                 while parts_total.lt(self) {
                 while parts_total.lt(self) {
                     for part in parts_of_value.iter().copied() {
                     for part in parts_of_value.iter().copied() {
-                        if (part + parts_total).le(self) {
+                        if (part.checked_add(parts_total).ok_or(Error::AmountOverflow)?).le(self) {
                             parts.push(part);
                             parts.push(part);
                         } else {
                         } else {
-                            let amount_left = *self - parts_total;
+                            let amount_left =
+                                self.checked_sub(parts_total).ok_or(Error::AmountOverflow)?;
                             parts.extend(amount_left.split(fee_and_amounts));
                             parts.extend(amount_left.split(fee_and_amounts));
                         }
                         }
 
 
@@ -157,7 +255,9 @@ impl Amount {
                         return Err(Error::SplitValuesGreater);
                         return Err(Error::SplitValuesGreater);
                     }
                     }
                     Ordering::Greater => {
                     Ordering::Greater => {
-                        let extra = *self - values_total;
+                        let extra = self
+                            .checked_sub(values_total)
+                            .ok_or(Error::AmountOverflow)?;
                         let mut extra_amount = extra.split(fee_and_amounts);
                         let mut extra_amount = extra.split(fee_and_amounts);
                         let mut values = values.clone();
                         let mut values = values.clone();
 
 
@@ -199,23 +299,31 @@ impl Amount {
     }
     }
 
 
     /// Checked addition for Amount. Returns None if overflow occurs.
     /// Checked addition for Amount. Returns None if overflow occurs.
-    pub fn checked_add(self, other: Amount) -> Option<Amount> {
-        self.0.checked_add(other.0).map(Amount)
+    pub fn checked_add(self, other: Amount<()>) -> Option<Amount<()>> {
+        self.value
+            .checked_add(other.value)
+            .map(|v| Amount { value: v, unit: () })
     }
     }
 
 
     /// Checked subtraction for Amount. Returns None if overflow occurs.
     /// Checked subtraction for Amount. Returns None if overflow occurs.
-    pub fn checked_sub(self, other: Amount) -> Option<Amount> {
-        self.0.checked_sub(other.0).map(Amount)
+    pub fn checked_sub(self, other: Amount<()>) -> Option<Amount<()>> {
+        self.value
+            .checked_sub(other.value)
+            .map(|v| Amount { value: v, unit: () })
     }
     }
 
 
     /// Checked multiplication for Amount. Returns None if overflow occurs.
     /// Checked multiplication for Amount. Returns None if overflow occurs.
-    pub fn checked_mul(self, other: Amount) -> Option<Amount> {
-        self.0.checked_mul(other.0).map(Amount)
+    pub fn checked_mul(self, other: Amount<()>) -> Option<Amount<()>> {
+        self.value
+            .checked_mul(other.value)
+            .map(|v| Amount { value: v, unit: () })
     }
     }
 
 
     /// Checked division for Amount. Returns None if overflow occurs.
     /// Checked division for Amount. Returns None if overflow occurs.
-    pub fn checked_div(self, other: Amount) -> Option<Amount> {
-        self.0.checked_div(other.0).map(Amount)
+    pub fn checked_div(self, other: Amount<()>) -> Option<Amount<()>> {
+        self.value
+            .checked_div(other.value)
+            .map(|v| Amount { value: v, unit: () })
     }
     }
 
 
     /// Try sum to check for overflow
     /// Try sum to check for overflow
@@ -233,19 +341,21 @@ impl Amount {
         &self,
         &self,
         current_unit: &CurrencyUnit,
         current_unit: &CurrencyUnit,
         target_unit: &CurrencyUnit,
         target_unit: &CurrencyUnit,
-    ) -> Result<Amount, Error> {
-        to_unit(self.0, current_unit, target_unit)
+    ) -> Result<Amount<()>, Error> {
+        Amount::new(self.value, current_unit.clone())
+            .convert_to(target_unit)
+            .map(Into::into)
     }
     }
-    ///
+
     /// Convert to u64
     /// Convert to u64
     pub fn to_u64(self) -> u64 {
     pub fn to_u64(self) -> u64 {
-        self.0
+        self.value
     }
     }
 
 
     /// Convert to i64
     /// Convert to i64
     pub fn to_i64(self) -> Option<i64> {
     pub fn to_i64(self) -> Option<i64> {
-        if self.0 <= i64::MAX as u64 {
-            Some(self.0 as i64)
+        if self.value <= i64::MAX as u64 {
+            Some(self.value as i64)
         } else {
         } else {
             None
             None
         }
         }
@@ -254,69 +364,233 @@ impl Amount {
     /// Create from i64, returning None if negative
     /// Create from i64, returning None if negative
     pub fn from_i64(value: i64) -> Option<Self> {
     pub fn from_i64(value: i64) -> Option<Self> {
         if value >= 0 {
         if value >= 0 {
-            Some(Amount(value as u64))
+            Some(Amount {
+                value: value as u64,
+                unit: (),
+            })
         } else {
         } else {
             None
             None
         }
         }
     }
     }
 }
 }
 
 
-impl Default for Amount {
+impl Default for Amount<()> {
     fn default() -> Self {
     fn default() -> Self {
         Amount::ZERO
         Amount::ZERO
     }
     }
 }
 }
 
 
-impl Default for &Amount {
+impl Default for &Amount<()> {
     fn default() -> Self {
     fn default() -> Self {
         &Amount::ZERO
         &Amount::ZERO
     }
     }
 }
 }
 
 
-impl fmt::Display for Amount {
+impl Amount<CurrencyUnit> {
+    /// Create a new Amount with an explicit unit
+    ///
+    /// This is the primary constructor for typed amounts. It works with all
+    /// CurrencyUnit variants including Custom.
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let sat_amount = Amount::new(1000, CurrencyUnit::Sat);
+    /// let custom = Amount::new(50, CurrencyUnit::Custom("BTC".into()));
+    /// ```
+    pub fn new(value: u64, unit: CurrencyUnit) -> Self {
+        Self { value, unit }
+    }
+
+    /// Get the numeric value
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let amount = Amount::new(1000, CurrencyUnit::Sat);
+    /// assert_eq!(amount.value(), 1000);
+    /// ```
+    pub fn value(&self) -> u64 {
+        self.value
+    }
+
+    /// Convert to u64
+    pub fn to_u64(self) -> u64 {
+        self.value
+    }
+
+    /// Convert to i64
+    pub fn to_i64(self) -> Option<i64> {
+        if self.value <= i64::MAX as u64 {
+            Some(self.value as i64)
+        } else {
+            None
+        }
+    }
+
+    /// Get a reference to the unit
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let amount = Amount::new(1000, CurrencyUnit::Sat);
+    /// assert_eq!(amount.unit(), &CurrencyUnit::Sat);
+    /// ```
+    pub fn unit(&self) -> &CurrencyUnit {
+        &self.unit
+    }
+
+    /// Consume self and return both value and unit
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let amount = Amount::new(1000, CurrencyUnit::Sat);
+    /// let (value, unit) = amount.into_parts();
+    /// assert_eq!(value, 1000);
+    /// assert_eq!(unit, CurrencyUnit::Sat);
+    /// ```
+    pub fn into_parts(self) -> (u64, CurrencyUnit) {
+        (self.value, self.unit)
+    }
+
+    /// Checked addition with unit verification
+    ///
+    /// Returns an error if units don't match or if overflow occurs.
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let a = Amount::new(100, CurrencyUnit::Sat);
+    /// let b = Amount::new(50, CurrencyUnit::Sat);
+    /// let sum = a.checked_add(&b).unwrap();
+    /// assert_eq!(sum.value(), 150);
+    ///
+    /// // Different units cause an error
+    /// let c = Amount::new(100, CurrencyUnit::Msat);
+    /// assert!(a.checked_add(&c).is_err());
+    /// ```
+    pub fn checked_add(&self, other: &Self) -> Result<Self, Error> {
+        if self.unit != other.unit {
+            return Err(Error::UnitMismatch(self.unit.clone(), other.unit.clone()));
+        }
+        self.value
+            .checked_add(other.value)
+            .map(|v| Amount::new(v, self.unit.clone()))
+            .ok_or(Error::AmountOverflow)
+    }
+
+    /// Checked subtraction with unit verification
+    ///
+    /// Returns an error if units don't match or if underflow occurs.
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let a = Amount::new(100, CurrencyUnit::Sat);
+    /// let b = Amount::new(30, CurrencyUnit::Sat);
+    /// let diff = a.checked_sub(&b).unwrap();
+    /// assert_eq!(diff.value(), 70);
+    /// ```
+    pub fn checked_sub(&self, other: &Self) -> Result<Self, Error> {
+        if self.unit != other.unit {
+            return Err(Error::UnitMismatch(self.unit.clone(), other.unit.clone()));
+        }
+        self.value
+            .checked_sub(other.value)
+            .map(|v| Amount::new(v, self.unit.clone()))
+            .ok_or(Error::AmountOverflow)
+    }
+
+    /// Convert to a different unit
+    ///
+    /// # Example
+    /// ```
+    /// # use cashu::{Amount, nuts::CurrencyUnit};
+    /// let sat = Amount::new(1000, CurrencyUnit::Sat);
+    /// let msat = sat.convert_to(&CurrencyUnit::Msat).unwrap();
+    /// assert_eq!(msat.value(), 1_000_000);
+    /// assert_eq!(msat.unit(), &CurrencyUnit::Msat);
+    /// ```
+    pub fn convert_to(&self, target_unit: &CurrencyUnit) -> Result<Self, Error> {
+        if &self.unit == target_unit {
+            return Ok(self.clone());
+        }
+
+        let converted_value = match (&self.unit, target_unit) {
+            (CurrencyUnit::Sat, CurrencyUnit::Msat) => self
+                .value
+                .checked_mul(MSAT_IN_SAT)
+                .ok_or(Error::AmountOverflow)?,
+            (CurrencyUnit::Msat, CurrencyUnit::Sat) => self.value / MSAT_IN_SAT,
+            _ => return Err(Error::CannotConvertUnits),
+        };
+
+        Ok(Amount::new(converted_value, target_unit.clone()))
+    }
+
+    /// Returns a string representation that includes the unit
+    pub fn display_with_unit(&self) -> String {
+        format!("{} {}", self.value, self.unit)
+    }
+}
+
+impl<U> fmt::Display for Amount<U> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         if let Some(width) = f.width() {
         if let Some(width) = f.width() {
-            write!(f, "{:width$}", self.0, width = width)
+            write!(f, "{:width$}", self.value, width = width)
         } else {
         } else {
-            write!(f, "{}", self.0)
+            write!(f, "{}", self.value)
         }
         }
     }
     }
 }
 }
 
 
-impl From<u64> for Amount {
+impl From<u64> for Amount<()> {
     fn from(value: u64) -> Self {
     fn from(value: u64) -> Self {
-        Self(value)
+        Amount { value, unit: () }
     }
     }
 }
 }
 
 
-impl From<&u64> for Amount {
+impl From<&u64> for Amount<()> {
     fn from(value: &u64) -> Self {
     fn from(value: &u64) -> Self {
-        Self(*value)
+        Amount {
+            value: *value,
+            unit: (),
+        }
     }
     }
 }
 }
 
 
-impl From<Amount> for u64 {
-    fn from(value: Amount) -> Self {
-        value.0
+impl From<Amount<()>> for u64 {
+    fn from(value: Amount<()>) -> Self {
+        value.value
+    }
+}
+
+impl From<Amount<CurrencyUnit>> for Amount<()> {
+    fn from(value: Amount<CurrencyUnit>) -> Self {
+        Amount {
+            value: value.value,
+            unit: (),
+        }
     }
     }
 }
 }
 
 
-impl AsRef<u64> for Amount {
+impl AsRef<u64> for Amount<()> {
     fn as_ref(&self) -> &u64 {
     fn as_ref(&self) -> &u64 {
-        &self.0
+        &self.value
     }
     }
 }
 }
 
 
-impl std::ops::Add for Amount {
-    type Output = Amount;
+impl std::ops::Add for Amount<()> {
+    type Output = Amount<()>;
 
 
-    fn add(self, rhs: Amount) -> Self::Output {
+    fn add(self, rhs: Amount<()>) -> Self::Output {
         self.checked_add(rhs)
         self.checked_add(rhs)
             .expect("Addition overflow: the sum of the amounts exceeds the maximum value")
             .expect("Addition overflow: the sum of the amounts exceeds the maximum value")
     }
     }
 }
 }
 
 
-impl std::ops::AddAssign for Amount {
+impl std::ops::AddAssign for Amount<()> {
     fn add_assign(&mut self, rhs: Self) {
     fn add_assign(&mut self, rhs: Self) {
         *self = self
         *self = self
             .checked_add(rhs)
             .checked_add(rhs)
@@ -324,16 +598,16 @@ impl std::ops::AddAssign for Amount {
     }
     }
 }
 }
 
 
-impl std::ops::Sub for Amount {
-    type Output = Amount;
+impl std::ops::Sub for Amount<()> {
+    type Output = Amount<()>;
 
 
-    fn sub(self, rhs: Amount) -> Self::Output {
+    fn sub(self, rhs: Amount<()>) -> Self::Output {
         self.checked_sub(rhs)
         self.checked_sub(rhs)
             .expect("Subtraction underflow: cannot subtract a larger amount from a smaller amount")
             .expect("Subtraction underflow: cannot subtract a larger amount from a smaller amount")
     }
     }
 }
 }
 
 
-impl std::ops::SubAssign for Amount {
+impl std::ops::SubAssign for Amount<()> {
     fn sub_assign(&mut self, other: Self) {
     fn sub_assign(&mut self, other: Self) {
         *self = self
         *self = self
             .checked_sub(other)
             .checked_sub(other)
@@ -341,7 +615,7 @@ impl std::ops::SubAssign for Amount {
     }
     }
 }
 }
 
 
-impl std::ops::Mul for Amount {
+impl std::ops::Mul for Amount<()> {
     type Output = Self;
     type Output = Self;
 
 
     fn mul(self, other: Self) -> Self::Output {
     fn mul(self, other: Self) -> Self::Output {
@@ -350,7 +624,7 @@ impl std::ops::Mul for Amount {
     }
     }
 }
 }
 
 
-impl std::ops::Div for Amount {
+impl std::ops::Div for Amount<()> {
     type Output = Self;
     type Output = Self;
 
 
     fn div(self, other: Self) -> Self::Output {
     fn div(self, other: Self) -> Self::Output {
@@ -377,7 +651,10 @@ pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result<Amount, Er
         ),
         ),
     };
     };
 
 
-    to_unit(amount, &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
 /// Kinds of targeting that are supported
@@ -395,30 +672,6 @@ pub enum SplitTarget {
 /// Msats in sat
 /// Msats in sat
 pub const MSAT_IN_SAT: u64 = 1000;
 pub const MSAT_IN_SAT: u64 = 1000;
 
 
-/// Helper function to convert units
-pub fn to_unit<T>(
-    amount: T,
-    current_unit: &CurrencyUnit,
-    target_unit: &CurrencyUnit,
-) -> Result<Amount, Error>
-where
-    T: Into<u64>,
-{
-    let amount = amount.into();
-    match (current_unit, target_unit) {
-        (CurrencyUnit::Sat, CurrencyUnit::Sat) => Ok(amount.into()),
-        (CurrencyUnit::Msat, CurrencyUnit::Msat) => Ok(amount.into()),
-        (CurrencyUnit::Sat, CurrencyUnit::Msat) => amount
-            .checked_mul(MSAT_IN_SAT)
-            .map(Amount::from)
-            .ok_or(Error::AmountOverflow),
-        (CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok((amount / MSAT_IN_SAT).into()),
-        (CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount.into()),
-        (CurrencyUnit::Eur, CurrencyUnit::Eur) => Ok(amount.into()),
-        _ => Err(Error::CannotConvertUnits),
-    }
-}
-
 #[cfg(test)]
 #[cfg(test)]
 mod tests {
 mod tests {
     use super::*;
     use super::*;
@@ -451,29 +704,32 @@ mod tests {
     #[test]
     #[test]
     fn test_split_target_amount() {
     fn test_split_target_amount() {
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
-        let amount = Amount(65);
+        let amount = Amount::from(65);
 
 
         let split = amount
         let split = amount
-            .split_targeted(&SplitTarget::Value(Amount(32)), &fee_and_amounts)
+            .split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
             .unwrap();
             .unwrap();
-        assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
+        assert_eq!(
+            vec![Amount::from(1), Amount::from(32), Amount::from(32)],
+            split
+        );
 
 
-        let amount = Amount(150);
+        let amount = Amount::from(150);
 
 
         let split = amount
         let split = amount
             .split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
             .split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
             .unwrap();
             .unwrap();
         assert_eq!(
         assert_eq!(
             vec![
             vec![
-                Amount(2),
-                Amount(2),
-                Amount(2),
-                Amount(16),
-                Amount(16),
-                Amount(16),
-                Amount(32),
-                Amount(32),
-                Amount(32)
+                Amount::from(2),
+                Amount::from(2),
+                Amount::from(2),
+                Amount::from(16),
+                Amount::from(16),
+                Amount::from(16),
+                Amount::from(32),
+                Amount::from(32),
+                Amount::from(32)
             ],
             ],
             split
             split
         );
         );
@@ -485,12 +741,12 @@ mod tests {
             .unwrap();
             .unwrap();
         assert_eq!(
         assert_eq!(
             vec![
             vec![
-                Amount(1),
-                Amount(2),
-                Amount(4),
-                Amount(8),
-                Amount(16),
-                Amount(32)
+                Amount::from(1),
+                Amount::from(2),
+                Amount::from(4),
+                Amount::from(8),
+                Amount::from(16),
+                Amount::from(32)
             ],
             ],
             split
             split
         );
         );
@@ -499,30 +755,30 @@ mod tests {
     #[test]
     #[test]
     fn test_split_with_fee() {
     fn test_split_with_fee() {
         let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
-        let amount = Amount(2);
+        let amount = Amount::from(2);
 
 
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
-        assert_eq!(split, vec![Amount(2), Amount(1)]);
+        assert_eq!(split, vec![Amount::from(2), Amount::from(1)]);
 
 
-        let amount = Amount(3);
+        let amount = Amount::from(3);
 
 
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
-        assert_eq!(split, vec![Amount(4)]);
+        assert_eq!(split, vec![Amount::from(4)]);
 
 
-        let amount = Amount(3);
+        let amount = Amount::from(3);
         let fee_and_amounts = (1000, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let fee_and_amounts = (1000, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
 
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         // With fee_ppk=1000 (100%), amount 3 requires proofs totaling at least 5
         // With fee_ppk=1000 (100%), amount 3 requires proofs totaling at least 5
         // to cover both the amount (3) and fees (~2 for 2 proofs)
         // to cover both the amount (3) and fees (~2 for 2 proofs)
-        assert_eq!(split, vec![Amount(4), Amount(1)]);
+        assert_eq!(split, vec![Amount::from(4), Amount::from(1)]);
     }
     }
 
 
     #[test]
     #[test]
     fn test_split_with_fee_reported_issue() {
     fn test_split_with_fee_reported_issue() {
         let fee_and_amounts = (100, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let fee_and_amounts = (100, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         // Test the reported issue: mint 600, send 300 with fee_ppk=100
         // Test the reported issue: mint 600, send 300 with fee_ppk=100
-        let amount = Amount(300);
+        let amount = Amount::from(300);
 
 
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         let split = amount.split_with_fee(&fee_and_amounts).unwrap();
 
 
@@ -533,7 +789,7 @@ mod tests {
         // The split should cover the amount plus fees
         // The split should cover the amount plus fees
         let split_total = Amount::try_sum(split.iter().copied()).unwrap();
         let split_total = Amount::try_sum(split.iter().copied()).unwrap();
         assert!(
         assert!(
-            split_total >= amount + total_fee,
+            split_total >= amount.checked_add(total_fee).unwrap(),
             "Split total {} should be >= amount {} + fee {}",
             "Split total {} should be >= amount {} + fee {}",
             split_total,
             split_total,
             amount,
             amount,
@@ -545,17 +801,17 @@ mod tests {
     fn test_split_with_fee_edge_cases() {
     fn test_split_with_fee_edge_cases() {
         // Test various amounts with fee_ppk=100
         // Test various amounts with fee_ppk=100
         let test_cases = vec![
         let test_cases = vec![
-            (Amount(1), 100),
-            (Amount(10), 100),
-            (Amount(50), 100),
-            (Amount(100), 100),
-            (Amount(200), 100),
-            (Amount(300), 100),
-            (Amount(500), 100),
-            (Amount(600), 100),
-            (Amount(1000), 100),
-            (Amount(1337), 100),
-            (Amount(5000), 100),
+            (Amount::from(1), 100),
+            (Amount::from(10), 100),
+            (Amount::from(50), 100),
+            (Amount::from(100), 100),
+            (Amount::from(200), 100),
+            (Amount::from(300), 100),
+            (Amount::from(500), 100),
+            (Amount::from(600), 100),
+            (Amount::from(1000), 100),
+            (Amount::from(1337), 100),
+            (Amount::from(5000), 100),
         ];
         ];
 
 
         for (amount, fee_ppk) in test_cases {
         for (amount, fee_ppk) in test_cases {
@@ -600,12 +856,12 @@ mod tests {
     fn test_split_with_fee_high_fees() {
     fn test_split_with_fee_high_fees() {
         // Test with very high fees
         // Test with very high fees
         let test_cases = vec![
         let test_cases = vec![
-            (Amount(10), 500),  // 50% fee
-            (Amount(10), 1000), // 100% fee
-            (Amount(10), 2000), // 200% fee
-            (Amount(100), 500),
-            (Amount(100), 1000),
-            (Amount(100), 2000),
+            (Amount::from(10), 500),  // 50% fee
+            (Amount::from(10), 1000), // 100% fee
+            (Amount::from(10), 2000), // 200% fee
+            (Amount::from(100), 500),
+            (Amount::from(100), 1000),
+            (Amount::from(100), 2000),
         ];
         ];
 
 
         for (amount, fee_ppk) in test_cases {
         for (amount, fee_ppk) in test_cases {
@@ -638,7 +894,7 @@ mod tests {
     fn test_split_with_fee_recursion_limit() {
     fn test_split_with_fee_recursion_limit() {
         // Test that the recursion doesn't go infinite
         // Test that the recursion doesn't go infinite
         // This tests the edge case where the method keeps adding Amount::ONE
         // This tests the edge case where the method keeps adding Amount::ONE
-        let amount = Amount(1);
+        let amount = Amount::from(1);
         let fee_ppk = 10000;
         let fee_ppk = 10000;
         let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
 
@@ -652,9 +908,9 @@ mod tests {
     #[test]
     #[test]
     fn test_split_values() {
     fn test_split_values() {
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
-        let amount = Amount(10);
+        let amount = Amount::from(10);
 
 
-        let target = vec![Amount(2), Amount(4), Amount(4)];
+        let target = vec![Amount::from(2), Amount::from(4), Amount::from(4)];
 
 
         let split_target = SplitTarget::Values(target.clone());
         let split_target = SplitTarget::Values(target.clone());
 
 
@@ -664,9 +920,9 @@ mod tests {
 
 
         assert_eq!(target, values);
         assert_eq!(target, values);
 
 
-        let target = vec![Amount(2), Amount(4), Amount(4)];
+        let target = vec![Amount::from(2), Amount::from(4), Amount::from(4)];
 
 
-        let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
+        let split_target = SplitTarget::Values(vec![Amount::from(2), Amount::from(4)]);
 
 
         let values = amount
         let values = amount
             .split_targeted(&split_target, &fee_and_amounts)
             .split_targeted(&split_target, &fee_and_amounts)
@@ -674,7 +930,7 @@ mod tests {
 
 
         assert_eq!(target, values);
         assert_eq!(target, values);
 
 
-        let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
+        let split_target = SplitTarget::Values(vec![Amount::from(2), Amount::from(10)]);
 
 
         let values = amount.split_targeted(&split_target, &fee_and_amounts);
         let values = amount.split_targeted(&split_target, &fee_and_amounts);
 
 
@@ -712,64 +968,55 @@ mod tests {
     }
     }
 
 
     #[test]
     #[test]
-    fn test_amount_to_unit() {
-        let amount = Amount::from(1000);
-        let current_unit = CurrencyUnit::Sat;
-        let target_unit = CurrencyUnit::Msat;
-
-        let converted = to_unit(amount, &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());
         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.
     /// Tests that the subtraction operator correctly computes the difference between amounts.
@@ -787,19 +1034,19 @@ mod tests {
         let amount1 = Amount::from(100);
         let amount1 = Amount::from(100);
         let amount2 = Amount::from(30);
         let amount2 = Amount::from(30);
 
 
-        let result = amount1 - amount2;
+        let result = amount1.checked_sub(amount2).unwrap();
         assert_eq!(result, Amount::from(70));
         assert_eq!(result, Amount::from(70));
 
 
         let amount1 = Amount::from(1000);
         let amount1 = Amount::from(1000);
         let amount2 = Amount::from(1);
         let amount2 = Amount::from(1);
 
 
-        let result = amount1 - amount2;
+        let result = amount1.checked_sub(amount2).unwrap();
         assert_eq!(result, Amount::from(999));
         assert_eq!(result, Amount::from(999));
 
 
         let amount1 = Amount::from(255);
         let amount1 = Amount::from(255);
         let amount2 = Amount::from(128);
         let amount2 = Amount::from(128);
 
 
-        let result = amount1 - amount2;
+        let result = amount1.checked_sub(amount2).unwrap();
         assert_eq!(result, Amount::from(127));
         assert_eq!(result, Amount::from(127));
     }
     }
 
 
@@ -970,11 +1217,11 @@ mod tests {
     #[test]
     #[test]
     fn test_from_u64_returns_correct_value() {
     fn test_from_u64_returns_correct_value() {
         let amount = Amount::from(100u64);
         let amount = Amount::from(100u64);
-        assert_eq!(amount, Amount(100));
+        assert_eq!(amount, Amount::from(100));
         assert_ne!(amount, Amount::ZERO);
         assert_ne!(amount, Amount::ZERO);
 
 
         let amount = Amount::from(1u64);
         let amount = Amount::from(1u64);
-        assert_eq!(amount, Amount(1));
+        assert_eq!(amount, Amount::from(1));
         assert_eq!(amount, Amount::ONE);
         assert_eq!(amount, Amount::ONE);
 
 
         let amount = Amount::from(1337u64);
         let amount = Amount::from(1337u64);
@@ -1283,4 +1530,390 @@ mod tests {
         assert_eq!(amount, Amount::ZERO);
         assert_eq!(amount, Amount::ZERO);
         assert_ne!(amount, Amount::from(10)); // Should have changed
         assert_ne!(amount, Amount::from(10)); // Should have changed
     }
     }
+
+    // Phase 2 tests: Amount<CurrencyUnit> methods
+
+    #[test]
+    fn test_amount_with_currency_unit() {
+        let amount = Amount::new(1000, CurrencyUnit::Sat);
+        assert_eq!(amount.value(), 1000);
+        assert_eq!(amount.unit(), &CurrencyUnit::Sat);
+    }
+
+    #[test]
+    fn test_amount_new_with_custom_unit() {
+        let custom_unit = CurrencyUnit::Custom("BTC".to_string());
+        let amount = Amount::new(50, custom_unit.clone());
+
+        assert_eq!(amount.value(), 50);
+        assert_eq!(amount.unit(), &custom_unit);
+    }
+
+    #[test]
+    fn test_amount_into_parts() {
+        let amount = Amount::new(1234, CurrencyUnit::Msat);
+        let (value, unit) = amount.into_parts();
+
+        assert_eq!(value, 1234);
+        assert_eq!(unit, CurrencyUnit::Msat);
+    }
+
+    #[test]
+    fn test_amount_with_unit_conversion() {
+        let untyped: Amount<()> = Amount::from(100);
+        let typed = untyped.with_unit(CurrencyUnit::Sat);
+
+        assert_eq!(typed.value(), 100);
+        assert_eq!(typed.unit(), &CurrencyUnit::Sat);
+    }
+
+    #[test]
+    fn test_amount_with_unit_all_variants() {
+        let untyped = Amount::from(500);
+
+        let sat = untyped.with_unit(CurrencyUnit::Sat);
+        assert_eq!(sat.unit(), &CurrencyUnit::Sat);
+
+        let msat = untyped.with_unit(CurrencyUnit::Msat);
+        assert_eq!(msat.unit(), &CurrencyUnit::Msat);
+
+        let usd = untyped.with_unit(CurrencyUnit::Usd);
+        assert_eq!(usd.unit(), &CurrencyUnit::Usd);
+
+        let eur = untyped.with_unit(CurrencyUnit::Eur);
+        assert_eq!(eur.unit(), &CurrencyUnit::Eur);
+
+        let custom = untyped.with_unit(CurrencyUnit::Custom("TEST".into()));
+        assert_eq!(custom.unit(), &CurrencyUnit::Custom("TEST".into()));
+    }
+
+    #[test]
+    fn test_typed_amount_is_clone_not_copy() {
+        let amount = Amount::new(100, CurrencyUnit::Sat);
+        let cloned = amount.clone();
+        // If this compiles, Clone works. Cannot test Copy directly without moving.
+        assert_eq!(cloned.value(), 100);
+        assert_eq!(cloned.unit(), &CurrencyUnit::Sat);
+    }
+
+    // Phase 3 tests: Protocol types verification
+
+    #[test]
+    fn test_untyped_amount_is_copy() {
+        // Verify Amount<()> is Copy (required for protocol types)
+        let amount: Amount<()> = Amount::from(100);
+        let copy1 = amount;
+        let copy2 = amount; // Should not move - verifies Copy
+        assert_eq!(copy1, copy2);
+    }
+
+    #[test]
+    fn test_amount_serialization_transparent() {
+        // Verify Amount<()> serializes as just the number (protocol compatibility)
+        let amount = Amount::from(1234);
+        let json = serde_json::to_string(&amount).unwrap();
+        assert_eq!(json, "1234");
+
+        // Verify deserialization works
+        let deserialized: Amount<()> = serde_json::from_str(&json).unwrap();
+        assert_eq!(deserialized, Amount::from(1234));
+    }
+
+    #[test]
+    fn test_typed_amount_serialization() {
+        // Verify Amount<CurrencyUnit> also serializes as just the number
+        let amount = Amount::new(5678, CurrencyUnit::Sat);
+        let json = serde_json::to_string(&amount).unwrap();
+        assert_eq!(json, "5678");
+
+        // Note: Cannot deserialize Amount<CurrencyUnit> directly
+        // Unit must come from context (e.g., keyset)
+    }
+
+    #[test]
+    fn test_protocol_type_pattern() {
+        // Simulate protocol type usage pattern
+
+        // Protocol layer: Amount<()> is Copy and serializes transparently
+        let protocol_amount: Amount<()> = Amount::from(1000);
+        let _copied = protocol_amount; // Copy works
+
+        // Application layer: Convert to typed when needed
+        let typed = protocol_amount.with_unit(CurrencyUnit::Sat);
+        assert_eq!(typed.value(), 1000);
+
+        // Back to protocol: Extract value
+        let back_to_protocol = Amount::from(typed.value());
+        assert_eq!(back_to_protocol, protocol_amount);
+    }
+
+    // Phase 4 tests: Unit-aware arithmetic
+
+    #[test]
+    fn test_typed_amount_checked_add() {
+        let a = Amount::new(100, CurrencyUnit::Sat);
+        let b = Amount::new(50, CurrencyUnit::Sat);
+
+        let sum = a.checked_add(&b).unwrap();
+        assert_eq!(sum.value(), 150);
+        assert_eq!(sum.unit(), &CurrencyUnit::Sat);
+    }
+
+    #[test]
+    fn test_typed_amount_add_unit_mismatch() {
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let msat = Amount::new(100, CurrencyUnit::Msat);
+
+        let result = sat.checked_add(&msat);
+        assert!(result.is_err());
+
+        match result.unwrap_err() {
+            Error::UnitMismatch(u1, u2) => {
+                assert_eq!(u1, CurrencyUnit::Sat);
+                assert_eq!(u2, CurrencyUnit::Msat);
+            }
+            _ => panic!("Expected UnitMismatch error"),
+        }
+    }
+
+    #[test]
+    fn test_typed_amount_checked_sub() {
+        let a = Amount::new(100, CurrencyUnit::Sat);
+        let b = Amount::new(30, CurrencyUnit::Sat);
+
+        let diff = a.checked_sub(&b).unwrap();
+        assert_eq!(diff.value(), 70);
+        assert_eq!(diff.unit(), &CurrencyUnit::Sat);
+    }
+
+    #[test]
+    fn test_typed_amount_sub_unit_mismatch() {
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let usd = Amount::new(30, CurrencyUnit::Usd);
+
+        let result = sat.checked_sub(&usd);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn test_typed_amount_convert_to() {
+        // Sat to Msat
+        let sat = Amount::new(1000, CurrencyUnit::Sat);
+        let msat = sat.convert_to(&CurrencyUnit::Msat).unwrap();
+        assert_eq!(msat.value(), 1_000_000);
+        assert_eq!(msat.unit(), &CurrencyUnit::Msat);
+
+        // Msat to Sat
+        let msat = Amount::new(5000, CurrencyUnit::Msat);
+        let sat = msat.convert_to(&CurrencyUnit::Sat).unwrap();
+        assert_eq!(sat.value(), 5);
+        assert_eq!(sat.unit(), &CurrencyUnit::Sat);
+
+        // Same unit (optimization check)
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let same = sat.convert_to(&CurrencyUnit::Sat).unwrap();
+        assert_eq!(same.value(), 100);
+        assert_eq!(same.unit(), &CurrencyUnit::Sat);
+    }
+
+    #[test]
+    fn test_typed_amount_convert_invalid() {
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let result = sat.convert_to(&CurrencyUnit::Eur);
+        assert!(result.is_err());
+
+        match result.unwrap_err() {
+            Error::CannotConvertUnits => {}
+            _ => panic!("Expected CannotConvertUnits error"),
+        }
+    }
+
+    #[test]
+    fn test_typed_amount_add_overflow() {
+        let a = Amount::new(u64::MAX, CurrencyUnit::Sat);
+        let b = Amount::new(1, CurrencyUnit::Sat);
+
+        let result = a.checked_add(&b);
+        assert!(result.is_err());
+
+        match result.unwrap_err() {
+            Error::AmountOverflow => {}
+            _ => panic!("Expected AmountOverflow error"),
+        }
+    }
+
+    #[test]
+    fn test_typed_amount_sub_underflow() {
+        let a = Amount::new(50, CurrencyUnit::Sat);
+        let b = Amount::new(100, CurrencyUnit::Sat);
+
+        let result = a.checked_sub(&b);
+        assert!(result.is_err());
+
+        match result.unwrap_err() {
+            Error::AmountOverflow => {} // Underflow also returns AmountOverflow
+            _ => panic!("Expected AmountOverflow error"),
+        }
+    }
+
+    // Phase 5 tests: PartialOrd behavior for Amount<CurrencyUnit>
+
+    /// Tests that equality works correctly for typed amounts with the same unit.
+    #[test]
+    fn test_typed_amount_equality_same_unit() {
+        let a = Amount::new(100, CurrencyUnit::Sat);
+        let b = Amount::new(100, CurrencyUnit::Sat);
+
+        assert_eq!(a, b);
+        assert!(a == b);
+
+        let c = Amount::new(50, CurrencyUnit::Sat);
+        assert_ne!(a, c);
+        assert!(a != c);
+    }
+
+    /// Tests that equality returns false for typed amounts with different units.
+    #[test]
+    fn test_typed_amount_equality_different_units() {
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let msat = Amount::new(100, CurrencyUnit::Msat);
+
+        // Same value, different units - should NOT be equal
+        assert_ne!(sat, msat);
+        assert!(sat != msat);
+
+        let usd = Amount::new(100, CurrencyUnit::Usd);
+        assert_ne!(sat, usd);
+        assert_ne!(msat, usd);
+    }
+
+    /// Tests that comparison operators work correctly for typed amounts with the same unit.
+    #[test]
+    fn test_typed_amount_comparison_same_unit() {
+        let small = Amount::new(50, CurrencyUnit::Sat);
+        let large = Amount::new(100, CurrencyUnit::Sat);
+
+        // Greater than
+        assert!(large > small);
+        assert!(!(small > large));
+
+        // Less than
+        assert!(small < large);
+        assert!(!(large < small));
+
+        // Greater than or equal
+        assert!(large >= small);
+        assert!(large >= Amount::new(100, CurrencyUnit::Sat));
+
+        // Less than or equal
+        assert!(small <= large);
+        assert!(small <= Amount::new(50, CurrencyUnit::Sat));
+
+        // partial_cmp returns Some
+        assert_eq!(large.partial_cmp(&small), Some(std::cmp::Ordering::Greater));
+        assert_eq!(small.partial_cmp(&large), Some(std::cmp::Ordering::Less));
+        assert_eq!(
+            small.partial_cmp(&Amount::new(50, CurrencyUnit::Sat)),
+            Some(std::cmp::Ordering::Equal)
+        );
+    }
+
+    /// Tests that partial_cmp returns None for typed amounts with different units.
+    /// This ensures that comparisons between different units are not accidentally valid.
+    #[test]
+    fn test_typed_amount_comparison_different_units_returns_none() {
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let msat = Amount::new(50, CurrencyUnit::Msat);
+
+        // partial_cmp should return None for different units
+        assert_eq!(sat.partial_cmp(&msat), None);
+        assert_eq!(msat.partial_cmp(&sat), None);
+
+        // Different unit combinations
+        let usd = Amount::new(100, CurrencyUnit::Usd);
+        assert_eq!(sat.partial_cmp(&usd), None);
+        assert_eq!(usd.partial_cmp(&sat), None);
+
+        let eur = Amount::new(100, CurrencyUnit::Eur);
+        assert_eq!(usd.partial_cmp(&eur), None);
+
+        let custom = Amount::new(100, CurrencyUnit::Custom("BTC".into()));
+        assert_eq!(sat.partial_cmp(&custom), None);
+    }
+
+    /// Tests that comparison operators return false when comparing different units.
+    /// Since partial_cmp returns None, all comparisons should be false.
+    #[test]
+    fn test_typed_amount_comparison_operators_different_units() {
+        let sat = Amount::new(100, CurrencyUnit::Sat);
+        let msat = Amount::new(50, CurrencyUnit::Msat);
+
+        // When partial_cmp returns None:
+        // - > returns false
+        // - < returns false
+        // - >= returns false
+        // - <= returns false
+
+        assert!(!(sat > msat));
+        assert!(!(sat < msat));
+        assert!(!(sat >= msat));
+        assert!(!(sat <= msat));
+
+        assert!(!(msat > sat));
+        assert!(!(msat < sat));
+        assert!(!(msat >= sat));
+        assert!(!(msat <= sat));
+
+        // Even with same value, different units should return false
+        let sat100 = Amount::new(100, CurrencyUnit::Sat);
+        let msat100 = Amount::new(100, CurrencyUnit::Msat);
+
+        assert!(!(sat100 > msat100));
+        assert!(!(sat100 < msat100));
+        assert!(!(sat100 >= msat100));
+        assert!(!(sat100 <= msat100));
+    }
+
+    /// Tests that Amount<()> (untyped) has total ordering and implements Ord.
+    #[test]
+    fn test_untyped_amount_has_total_ordering() {
+        use std::cmp::Ordering;
+
+        let a: Amount<()> = Amount::from(50);
+        let b: Amount<()> = Amount::from(100);
+        let c: Amount<()> = Amount::from(50);
+
+        // Ord::cmp is available for Amount<()>
+        assert_eq!(a.cmp(&b), Ordering::Less);
+        assert_eq!(b.cmp(&a), Ordering::Greater);
+        assert_eq!(a.cmp(&c), Ordering::Equal);
+
+        // PartialOrd returns Some (total ordering)
+        assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
+        assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
+        assert_eq!(a.partial_cmp(&c), Some(Ordering::Equal));
+    }
+
+    /// Tests that Amount<()> can be sorted (requires Ord).
+    #[test]
+    fn test_untyped_amount_sorting() {
+        let mut amounts: Vec<Amount<()>> = vec![
+            Amount::from(100),
+            Amount::from(25),
+            Amount::from(75),
+            Amount::from(50),
+        ];
+
+        amounts.sort();
+
+        assert_eq!(
+            amounts,
+            vec![
+                Amount::from(25),
+                Amount::from(50),
+                Amount::from(75),
+                Amount::from(100),
+            ]
+        );
+    }
 }
 }

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

@@ -12,7 +12,7 @@ use std::time::Duration;
 
 
 use async_trait::async_trait;
 use async_trait::async_trait;
 use bitcoin::hashes::sha256::Hash;
 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::common::FeeReserve;
 use cdk_common::database::DynKVStore;
 use cdk_common::database::DynKVStore;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
@@ -271,9 +271,8 @@ impl MintPayment for Cln {
 
 
                             let response = WaitPaymentResponse {
                             let response = WaitPaymentResponse {
                                 payment_identifier: request_lookup_id,
                                 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());
                             tracing::info!("CLN: Created WaitPaymentResponse with amount {} msats", amount_msats.msat());
                             let event = Event::PaymentReceived(response);
                             let event = Event::PaymentReceived(response);
@@ -334,11 +333,12 @@ impl MintPayment for Cln {
                         .into()
                         .into()
                 };
                 };
                 // Convert to target unit
                 // 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
                 // Calculate fee
                 let relative_fee_reserve =
                 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 absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
                 let fee = max(relative_fee_reserve, absolute_fee_reserve);
                 let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
 
@@ -347,9 +347,8 @@ impl MintPayment for Cln {
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                     )),
                     )),
                     amount,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -368,20 +367,19 @@ impl MintPayment for Cln {
                 };
                 };
 
 
                 // Convert to target unit
                 // 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
                 // Calculate fee
                 let relative_fee_reserve =
                 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 absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
                 let fee = max(relative_fee_reserve, absolute_fee_reserve);
                 let fee = max(relative_fee_reserve, absolute_fee_reserve);
 
 
                 Ok(PaymentQuoteResponse {
                 Ok(PaymentQuoteResponse {
                     request_lookup_id: None,
                     request_lookup_id: None,
                     amount,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
         }
         }
@@ -521,15 +519,14 @@ impl MintPayment for Cln {
                 };
                 };
 
 
                 MakePaymentResponse {
                 MakePaymentResponse {
-                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     payment_lookup_id: payment_identifier,
                     payment_lookup_id: payment_identifier,
+                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     status,
                     status,
-                    total_spent: to_unit(
+                    total_spent: Amount::new(
                         pay_response.amount_sent_msat.msat(),
                         pay_response.amount_sent_msat.msat(),
-                        &CurrencyUnit::Msat,
-                        unit,
-                    )?,
-                    unit: unit.clone(),
+                        CurrencyUnit::Msat,
+                    )
+                    .convert_to(unit)?,
                 }
                 }
             }
             }
             Err(err) => {
             Err(err) => {
@@ -562,8 +559,10 @@ impl MintPayment for Cln {
 
 
                 let label = Uuid::new_v4().to_string();
                 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
                 let invoice_response = cln_client
                     .call_typed(&InvoiceRequest {
                     .call_typed(&InvoiceRequest {
@@ -604,9 +603,10 @@ impl MintPayment for Cln {
                 // Match like this until we change to option
                 // Match like this until we change to option
                 let amount = match amount {
                 let amount = match amount {
                     Some(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(),
                     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
             .filter(|p| p.amount_msat.is_some()) // Filter out invoices without an amount
             .map(|p| WaitPaymentResponse {
             .map(|p| WaitPaymentResponse {
                 payment_identifier: payment_identifier.clone(),
                 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(),
                 payment_id: p.payment_hash.to_string(),
             })
             })
             .collect())
             .collect())
@@ -761,16 +761,16 @@ impl MintPayment for Cln {
                     status,
                     status,
                     total_spent: pays_response
                     total_spent: pays_response
                         .amount_sent_msat
                         .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 {
             None => Ok(MakePaymentResponse {
                 payment_lookup_id: payment_identifier.clone(),
                 payment_lookup_id: payment_identifier.clone(),
                 payment_proof: None,
                 payment_proof: None,
                 status: MeltQuoteState::Unknown,
                 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
         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_or(Error::AmountOverflow)?;
 
 
         Ok(Self {
         Ok(Self {
@@ -63,7 +67,9 @@ impl Melted {
 
 
     /// Total amount melted
     /// Total amount melted
     pub fn total_amount(&self) -> Amount {
     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)]
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct MeltRequestInfo {
 pub struct MeltRequestInfo {
     /// Total amount of all input proofs in the melt request
     /// 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
     /// Fee amount associated with the input proofs
-    pub inputs_fee: Amount,
+    pub inputs_fee: Amount<CurrencyUnit>,
     /// Blinded messages for change outputs
     /// Blinded messages for change outputs
     pub change_outputs: Vec<BlindedMessage>,
     pub change_outputs: Vec<BlindedMessage>,
 }
 }
@@ -99,8 +99,8 @@ pub trait QuotesTransaction {
     async fn add_melt_request(
     async fn add_melt_request(
         &mut self,
         &mut self,
         quote_id: &QuoteId,
         quote_id: &QuoteId,
-        inputs_amount: Amount,
-        inputs_fee: Amount,
+        inputs_amount: Amount<CurrencyUnit>,
+        inputs_fee: Amount<CurrencyUnit>,
     ) -> Result<(), Self::Err>;
     ) -> Result<(), Self::Err>;
 
 
     /// Add blinded_messages for a quote_id
     /// 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::nut00::KnownMethod;
 use cashu::quote_id::QuoteId;
 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::test::unique_string;
 use crate::database::mint::{Database, Error, KeysDatabase};
 use crate::database::mint::{Database, Error, KeysDatabase};
@@ -26,8 +26,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -53,8 +53,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -83,8 +83,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -99,18 +99,26 @@ where
     let p2 = unique_string();
     let p2 = unique_string();
 
 
     mint_quote
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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
     mint_quote
-        .add_payment(250.into(), p2.clone(), None)
+        .add_payment(
+            Amount::from(250).with_unit(CurrencyUnit::Sat),
+            p2.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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();
     tx.commit().await.unwrap();
 
 
@@ -119,14 +127,17 @@ where
         .await
         .await
         .unwrap()
         .unwrap()
         .expect("mint_quote_from_db");
         .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!(
     assert_eq!(
         mint_quote_from_db
         mint_quote_from_db
             .payments
             .payments
             .iter()
             .iter()
-            .map(|x| (x.payment_id.clone(), x.amount))
+            .map(|x| (x.payment_id.clone(), x.amount.clone()))
             .collect::<Vec<_>>(),
             .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,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -158,17 +169,25 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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
     mint_quote
-        .add_payment(250.into(), p2.clone(), None)
+        .add_payment(
+            Amount::from(250).with_unit(CurrencyUnit::Sat),
+            p2.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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();
     tx.commit().await.unwrap();
 
 
     let mint_quote_from_db = db
     let mint_quote_from_db = db
@@ -176,14 +195,17 @@ where
         .await
         .await
         .unwrap()
         .unwrap()
         .expect("mint_quote_from_db");
         .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!(
     assert_eq!(
         mint_quote_from_db
         mint_quote_from_db
             .payments
             .payments
             .iter()
             .iter()
-            .map(|x| (x.payment_id.clone(), x.amount))
+            .map(|x| (x.payment_id.clone(), x.amount.clone()))
             .collect::<Vec<_>>(),
             .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();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -208,8 +230,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -222,12 +244,18 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
 
 
     // Duplicate payment should fail
     // 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();
     tx.commit().await.unwrap();
 
 
     let mint_quote_from_db = db
     let mint_quote_from_db = db
@@ -254,8 +282,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -266,7 +294,11 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
     tx.commit().await.unwrap();
@@ -278,7 +310,9 @@ where
         .expect("no error")
         .expect("no error")
         .expect("quote");
         .expect("quote");
     // Duplicate payment should fail
     // 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
     tx.commit().await.unwrap(); // although in theory nothing has changed, let's try it out
 
 
     let mint_quote_from_db = db
     let mint_quote_from_db = db
@@ -303,8 +337,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -315,7 +349,9 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).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)
     // 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
 /// Reject over issue
@@ -331,8 +367,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -351,7 +387,9 @@ where
         .expect("no error")
         .expect("no error")
         .expect("quote");
         .expect("quote");
     // Trying to issue without any payment should fail (over-issue)
     // 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
 /// Reject over issue with payment
@@ -367,8 +405,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -380,11 +418,17 @@ where
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote.clone()).await.unwrap();
     mint_quote
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     // Trying to issue more than paid should fail (over-issue)
     // 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
 /// Reject over issue with payment
@@ -400,8 +444,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        0.into(),
-        0.into(),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         cashu::PaymentMethod::Known(KnownMethod::Bolt12),
         0,
         0,
         vec![],
         vec![],
@@ -414,7 +458,11 @@ where
     let mut mint_quote = tx.add_mint_quote(mint_quote).await.unwrap();
     let mut mint_quote = tx.add_mint_quote(mint_quote).await.unwrap();
     let quote_id = mint_quote.id.clone();
     let quote_id = mint_quote.id.clone();
     mint_quote
     mint_quote
-        .add_payment(100.into(), p1.clone(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            p1.clone(),
+            None,
+        )
         .unwrap();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
     tx.commit().await.unwrap();
@@ -426,15 +474,17 @@ where
         .expect("no error")
         .expect("no error")
         .expect("quote");
         .expect("quote");
     // Trying to issue more than paid should fail (over-issue)
     // 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
 /// Successful melt with unique blinded messages
 pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
 pub async fn add_melt_request_unique_blinded_messages<DB>(db: DB)
 where
 where
     DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
     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();
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
 
     // Create a dummy blinded message
     // Create a dummy blinded message
@@ -448,9 +498,9 @@ where
     let blinded_messages = vec![blinded_message];
     let blinded_messages = vec![blinded_message];
 
 
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     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_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
         .await
         .unwrap();
         .unwrap();
     tx.add_blinded_messages(
     tx.add_blinded_messages(
@@ -486,8 +536,8 @@ where
     DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
     DB: Database<Error> + KeysDatabase<Err = Error> + MintSignaturesDatabase<Err = Error>,
 {
 {
     let quote_id1 = QuoteId::new_uuid();
     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();
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
 
     // Create a dummy blinded message
     // 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
     // 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 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_quote(quote2.clone()).await.unwrap();
     tx.add_melt_request(&quote2.id, inputs_amount, inputs_fee)
     tx.add_melt_request(&quote2.id, inputs_amount, inputs_fee)
         .await
         .await
@@ -542,8 +592,8 @@ pub async fn reject_duplicate_blinded_message_db_constraint<DB>(db: DB)
 where
 where
     DB: Database<Error> + KeysDatabase<Err = Error>,
     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();
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
 
     // Create a dummy blinded message
     // Create a dummy blinded message
@@ -558,9 +608,9 @@ where
 
 
     // First insert succeeds
     // First insert succeeds
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     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_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
         .await
         .unwrap();
         .unwrap();
     assert!(tx
     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
     // 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 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_quote(quote.clone()).await.unwrap();
     tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
     tx.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
         .await
@@ -605,8 +655,8 @@ pub async fn cleanup_melt_request_after_processing<DB>(db: DB)
 where
 where
     DB: Database<Error> + KeysDatabase<Err = Error>,
     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();
     let keyset_id = Id::from_str("001711afb1de20cb").unwrap();
 
 
     // Create dummy blinded message
     // Create dummy blinded message
@@ -621,7 +671,7 @@ where
 
 
     // Insert melt request
     // Insert melt request
     let mut tx1 = Database::begin_transaction(&db).await.unwrap();
     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_quote(quote.clone()).await.unwrap();
     tx1.add_melt_request(&quote.id, inputs_amount, inputs_fee)
     tx1.add_melt_request(&quote.id, inputs_amount, inputs_fee)
         .await
         .await
@@ -669,8 +719,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         0,
         None,
         None,
         None,
         None,
@@ -687,8 +737,8 @@ where
     assert!(retrieved.is_some());
     assert!(retrieved.is_some());
     let retrieved = retrieved.unwrap();
     let retrieved = retrieved.unwrap();
     assert_eq!(retrieved.id, melt_quote.id);
     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
 /// Test adding duplicate melt quotes fails
@@ -701,8 +751,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         0,
         None,
         None,
         None,
         None,
@@ -732,8 +782,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         0,
         None,
         None,
         None,
         None,
@@ -784,8 +834,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         0,
         Some(PaymentIdentifier::CustomId("old_lookup_id".to_string())),
         Some(PaymentIdentifier::CustomId("old_lookup_id".to_string())),
         None,
         None,
@@ -826,8 +876,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -843,8 +893,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        200.into(),
-        0.into(),
+        Amount::new(200, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -875,8 +925,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         0,
         None,
         None,
         None,
         None,
@@ -888,8 +938,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        200.into(),
-        20.into(),
+        Amount::new(200, cashu::CurrencyUnit::Sat),
+        Amount::new(20, cashu::CurrencyUnit::Sat),
         0,
         0,
         None,
         None,
         None,
         None,
@@ -925,8 +975,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -963,8 +1013,8 @@ where
         0,
         0,
         lookup_id.clone(),
         lookup_id.clone(),
         None,
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1078,8 +1128,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1100,10 +1150,14 @@ where
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .expect("valid result");
     mint_quote
     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();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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();
     tx.commit().await.unwrap();
 
 
     // Add payment second time
     // Add payment second time
@@ -1114,15 +1168,19 @@ where
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .expect("valid result");
     mint_quote
     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();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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();
     tx.commit().await.unwrap();
 
 
     // Verify final state
     // Verify final state
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
     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
 /// Test incrementing mint quote amount issued
@@ -1140,8 +1198,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1162,7 +1220,11 @@ where
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .expect("valid result");
     mint_quote
     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();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.update_mint_quote(&mut mint_quote).await.unwrap();
     tx.commit().await.unwrap();
     tx.commit().await.unwrap();
@@ -1174,9 +1236,11 @@ where
         .await
         .await
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .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();
     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();
     tx.commit().await.unwrap();
 
 
     // Add issuance second time
     // Add issuance second time
@@ -1186,14 +1250,16 @@ where
         .await
         .await
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .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();
     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();
     tx.commit().await.unwrap();
 
 
     // Verify final state
     // Verify final state
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
     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)
 /// Test getting mint quote within transaction (with lock)
@@ -1211,8 +1277,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1245,8 +1311,8 @@ where
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
             bolt11: "lnbc330n1p5d85skpp5344v3ktclujsjl3h09wgsfm7zytumr7h7zhrl857f5w8nv0a52zqdqqcqzzsxqyz5vqrzjqvueefmrckfdwyyu39m0lf24sqzcr9vcrmxrvgfn6empxz7phrjxvrttncqq0lcqqyqqqqlgqqqqqqgq2qsp5j3rrg8kvpemqxtf86j8tjm90wq77c7ende4e5qmrerq4xsg02vhq9qxpqysgqjltywgyk6uc5qcgwh8xnzmawl2tjlhz8d28tgp3yx8xwtz76x0jqkfh6mmq70hervjxs0keun7ur0spldgll29l0dnz3md50d65sfqqqwrwpsu".parse().unwrap()
         },
         },
         cashu::CurrencyUnit::Sat,
         cashu::CurrencyUnit::Sat,
-        100.into(),
-        10.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(10, cashu::CurrencyUnit::Sat),
         0,
         0,
         None,
         None,
         None,
         None,
@@ -1264,7 +1330,7 @@ where
     assert!(retrieved.is_some());
     assert!(retrieved.is_some());
     let retrieved = retrieved.unwrap();
     let retrieved = retrieved.unwrap();
     assert_eq!(retrieved.id, melt_quote.id);
     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();
     tx.commit().await.unwrap();
 }
 }
 
 
@@ -1284,8 +1350,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1324,8 +1390,8 @@ where
         0,
         0,
         lookup_id.clone(),
         lookup_id.clone(),
         None,
         None,
-        100.into(),
-        0.into(),
+        Amount::new(100, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1401,8 +1467,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1423,10 +1489,14 @@ where
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .expect("valid result");
     mint_quote
     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();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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();
     tx.commit().await.unwrap();
 
 
     // Try to add the same payment_id again - should fail with DuplicatePaymentId error
     // Try to add the same payment_id again - should fail with DuplicatePaymentId error
@@ -1437,7 +1507,11 @@ where
         .expect("valid quote")
         .expect("valid quote")
         .expect("valid result");
         .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!(
     assert!(
         matches!(result.unwrap_err(), crate::Error::DuplicatePaymentId),
         matches!(result.unwrap_err(), crate::Error::DuplicatePaymentId),
@@ -1447,7 +1521,7 @@ where
 
 
     // Verify that the amount_paid is still 300 (not 600)
     // Verify that the amount_paid is still 300 (not 600)
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
     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
     // A different payment_id should succeed
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut tx = Database::begin_transaction(&db).await.unwrap();
@@ -1458,16 +1532,20 @@ where
         .expect("valid result");
         .expect("valid result");
 
 
     mint_quote
     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();
         .unwrap();
     tx.update_mint_quote(&mut mint_quote).await.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();
     tx.commit().await.unwrap();
 
 
     // Verify final state
     // Verify final state
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
     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
 /// Test that loading the quote first allows modifications
@@ -1485,8 +1563,8 @@ where
         0,
         0,
         PaymentIdentifier::CustomId(unique_string()),
         PaymentIdentifier::CustomId(unique_string()),
         None,
         None,
-        1000.into(),
-        0.into(),
+        Amount::new(1000, cashu::CurrencyUnit::Sat),
+        Amount::new(0, cashu::CurrencyUnit::Sat),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         cashu::PaymentMethod::Known(KnownMethod::Bolt11),
         0,
         0,
         vec![],
         vec![],
@@ -1510,7 +1588,11 @@ where
 
 
     // Now modification should succeed
     // Now modification should succeed
     loaded_quote
     loaded_quote
-        .add_payment(100.into(), unique_string(), None)
+        .add_payment(
+            Amount::from(100).with_unit(CurrencyUnit::Sat),
+            unique_string(),
+            None,
+        )
         .unwrap();
         .unwrap();
     let result = tx.update_mint_quote(&mut loaded_quote).await;
     let result = tx.update_mint_quote(&mut loaded_quote).await;
 
 
@@ -1524,5 +1606,5 @@ where
 
 
     // Verify the modification was persisted
     // Verify the modification was persisted
     let retrieved = db.get_mint_quote(&mint_quote.id).await.unwrap().unwrap();
     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
 /// Mint Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct MintQuote {
 pub struct MintQuote {
     /// Quote id
     /// Quote id
     pub id: QuoteId,
     pub id: QuoteId,
     /// Amount of quote
     /// Amount of quote
-    pub amount: Option<Amount>,
+    pub amount: Option<Amount<CurrencyUnit>>,
     /// Unit of quote
     /// Unit of quote
     pub unit: CurrencyUnit,
     pub unit: CurrencyUnit,
     /// Quote payment request e.g. bolt11
     /// Quote payment request e.g. bolt11
@@ -456,34 +456,24 @@ pub struct MintQuote {
     /// Pubkey
     /// Pubkey
     pub pubkey: Option<PublicKey>,
     pub pubkey: Option<PublicKey>,
     /// Unix time quote was created
     /// Unix time quote was created
-    #[serde(default)]
     pub created_time: u64,
     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
     /// Payment of payment(s) that filled quote
-    #[serde(default)]
     pub payments: Vec<IncomingPayment>,
     pub payments: Vec<IncomingPayment>,
     /// Payment Method
     /// Payment Method
     pub payment_method: PaymentMethod,
     pub payment_method: PaymentMethod,
     /// Payment of payment(s) that filled quote
     /// Payment of payment(s) that filled quote
-    #[serde(default)]
     pub issuance: Vec<Issuance>,
     pub issuance: Vec<Issuance>,
     /// Extra payment-method-specific fields
     /// 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>,
     pub extra_json: Option<serde_json::Value>,
     /// Accumulated changes since this quote was loaded or created.
     /// Accumulated changes since this quote was loaded or created.
     ///
     ///
     /// This field is not serialized and is used internally to track modifications
     /// This field is not serialized and is used internally to track modifications
     /// that need to be persisted. Use [`Self::take_changes`] to extract pending
     /// that need to be persisted. Use [`Self::take_changes`] to extract pending
     /// changes for persistence.
     /// changes for persistence.
-    #[serde(skip)]
     changes: Option<MintQuoteChange>,
     changes: Option<MintQuoteChange>,
 }
 }
 
 
@@ -494,12 +484,12 @@ impl MintQuote {
         id: Option<QuoteId>,
         id: Option<QuoteId>,
         request: String,
         request: String,
         unit: CurrencyUnit,
         unit: CurrencyUnit,
-        amount: Option<Amount>,
+        amount: Option<Amount<CurrencyUnit>>,
         expiry: u64,
         expiry: u64,
         request_lookup_id: PaymentIdentifier,
         request_lookup_id: PaymentIdentifier,
         pubkey: Option<PublicKey>,
         pubkey: Option<PublicKey>,
-        amount_paid: Amount,
-        amount_issued: Amount,
+        amount_paid: Amount<CurrencyUnit>,
+        amount_issued: Amount<CurrencyUnit>,
         payment_method: PaymentMethod,
         payment_method: PaymentMethod,
         created_time: u64,
         created_time: u64,
         payments: Vec<IncomingPayment>,
         payments: Vec<IncomingPayment>,
@@ -511,7 +501,7 @@ impl MintQuote {
         Self {
         Self {
             id,
             id,
             amount,
             amount,
-            unit,
+            unit: unit.clone(),
             request,
             request,
             expiry,
             expiry,
             request_lookup_id,
             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
     /// Amount paid
     #[instrument(skip(self))]
     #[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.
     /// Records tokens being issued against this mint quote.
@@ -556,11 +559,14 @@ impl MintQuote {
     /// Returns [`crate::Error::AmountOverflow`] if adding the issuance amount would
     /// Returns [`crate::Error::AmountOverflow`] if adding the issuance amount would
     /// cause an arithmetic overflow.
     /// cause an arithmetic overflow.
     #[instrument(skip(self))]
     #[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
         let new_amount_issued = self
             .amount_issued
             .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
         // Can't issue more than what's been paid
         if new_amount_issued > self.amount_paid {
         if new_amount_issued > self.amount_paid {
@@ -571,17 +577,17 @@ impl MintQuote {
             .get_or_insert_default()
             .get_or_insert_default()
             .issuances
             .issuances
             .get_or_insert_default()
             .get_or_insert_default()
-            .push(additional_amount);
+            .push(additional_amount.into());
 
 
         self.amount_issued = new_amount_issued;
         self.amount_issued = new_amount_issued;
 
 
-        Ok(self.amount_issued)
+        Ok(self.amount_issued.clone())
     }
     }
 
 
     /// Amount issued
     /// Amount issued
     #[instrument(skip(self))]
     #[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
     /// Get state of mint quote
@@ -601,8 +607,10 @@ impl MintQuote {
     /// The value is computed as the difference between the total amount that
     /// The value is computed as the difference between the total amount that
     /// has been paid for this issuance (`self.amount_paid`) and the amount
     /// has been paid for this issuance (`self.amount_paid`) and the amount
     /// that has already been issued (`self.amount_issued`). In other words,
     /// 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.
     /// Extracts and returns all pending changes, leaving the internal change tracker empty.
@@ -640,7 +648,7 @@ impl MintQuote {
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     pub fn add_payment(
     pub fn add_payment(
         &mut self,
         &mut self,
-        amount: Amount,
+        amount: Amount<CurrencyUnit>,
         payment_id: String,
         payment_id: String,
         time: Option<u64>,
         time: Option<u64>,
     ) -> Result<(), crate::Error> {
     ) -> Result<(), crate::Error> {
@@ -651,6 +659,11 @@ impl MintQuote {
             return Err(crate::Error::DuplicatePaymentId);
             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);
         let payment = IncomingPayment::new(amount, payment_id, time);
 
 
         self.payments.push(payment.clone());
         self.payments.push(payment.clone());
@@ -661,48 +674,34 @@ impl MintQuote {
             .get_or_insert_default()
             .get_or_insert_default()
             .push(payment);
             .push(payment);
 
 
-        self.amount_paid = self
-            .amount_paid
-            .checked_add(amount)
-            .ok_or(crate::Error::AmountOverflow)?;
-
         Ok(())
         Ok(())
     }
     }
 
 
     /// Compute quote state
     /// Compute quote state
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     fn compute_quote_state(&self) -> MintQuoteState {
     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;
             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 => {
             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");
                 tracing::error!("We should not have issued more then has been paid");
                 MintQuoteState::Issued
                 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
 /// Mint Payments
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct IncomingPayment {
 pub struct IncomingPayment {
     /// Amount
     /// Amount
-    pub amount: Amount,
+    pub amount: Amount<CurrencyUnit>,
     /// Pyament unix time
     /// Pyament unix time
     pub time: u64,
     pub time: u64,
     /// Payment id
     /// Payment id
@@ -711,7 +710,7 @@ pub struct IncomingPayment {
 
 
 impl IncomingPayment {
 impl IncomingPayment {
     /// New [`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 {
         Self {
             payment_id,
             payment_id,
             time,
             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 {
 pub struct Issuance {
     /// Amount
     /// Amount
-    pub amount: Amount,
+    pub amount: Amount<CurrencyUnit>,
     /// Time
     /// Time
     pub time: u64,
     pub time: u64,
 }
 }
 
 
 impl Issuance {
 impl Issuance {
     /// Create new [`Issuance`]
     /// Create new [`Issuance`]
-    pub fn new(amount: Amount, time: u64) -> Self {
+    pub fn new(amount: Amount<CurrencyUnit>, time: u64) -> Self {
         Self { amount, time }
         Self { amount, time }
     }
     }
 }
 }
 
 
 /// Melt Quote Info
 /// Melt Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct MeltQuote {
 pub struct MeltQuote {
     /// Quote id
     /// Quote id
     pub id: QuoteId,
     pub id: QuoteId,
     /// Quote unit
     /// Quote unit
     pub unit: CurrencyUnit,
     pub unit: CurrencyUnit,
-    /// Quote amount
-    pub amount: Amount,
     /// Quote Payment request e.g. bolt11
     /// Quote Payment request e.g. bolt11
     pub request: MeltPaymentRequest,
     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
     /// Quote state
     pub state: MeltQuoteState,
     pub state: MeltQuoteState,
     /// Expiration time of quote
     /// Expiration time of quote
@@ -762,7 +761,6 @@ pub struct MeltQuote {
     /// Used for amountless invoices and MPP payments
     /// Used for amountless invoices and MPP payments
     pub options: Option<MeltOptions>,
     pub options: Option<MeltOptions>,
     /// Unix time quote was created
     /// Unix time quote was created
-    #[serde(default)]
     pub created_time: u64,
     pub created_time: u64,
     /// Unix time quote was paid
     /// Unix time quote was paid
     pub paid_time: Option<u64>,
     pub paid_time: Option<u64>,
@@ -776,8 +774,8 @@ impl MeltQuote {
     pub fn new(
     pub fn new(
         request: MeltPaymentRequest,
         request: MeltPaymentRequest,
         unit: CurrencyUnit,
         unit: CurrencyUnit,
-        amount: Amount,
-        fee_reserve: Amount,
+        amount: Amount<CurrencyUnit>,
+        fee_reserve: Amount<CurrencyUnit>,
         expiry: u64,
         expiry: u64,
         request_lookup_id: Option<PaymentIdentifier>,
         request_lookup_id: Option<PaymentIdentifier>,
         options: Option<MeltOptions>,
         options: Option<MeltOptions>,
@@ -787,9 +785,9 @@ impl MeltQuote {
 
 
         Self {
         Self {
             id: QuoteId::UUID(id),
             id: QuoteId::UUID(id),
-            amount,
-            unit,
+            unit: unit.clone(),
             request,
             request,
+            amount,
             fee_reserve,
             fee_reserve,
             state: MeltQuoteState::Unpaid,
             state: MeltQuoteState::Unpaid,
             expiry,
             expiry,
@@ -801,6 +799,61 @@ impl MeltQuote {
             payment_method,
             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
 /// Mint Keyset Info
@@ -853,7 +906,7 @@ impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
             request: mint_quote.request,
             request: mint_quote.request,
             expiry: Some(mint_quote.expiry),
             expiry: Some(mint_quote.expiry),
             pubkey: mint_quote.pubkey,
             pubkey: mint_quote.pubkey,
-            amount: mint_quote.amount,
+            amount: mint_quote.amount.map(Into::into),
             unit: Some(mint_quote.unit.clone()),
             unit: Some(mint_quote.unit.clone()),
         }
         }
     }
     }
@@ -875,10 +928,10 @@ impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
             quote: mint_quote.id.clone(),
             quote: mint_quote.id.clone(),
             request: mint_quote.request,
             request: mint_quote.request,
             expiry: Some(mint_quote.expiry),
             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)?,
             pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
-            amount: mint_quote.amount,
+            amount: mint_quote.amount.map(Into::into),
             unit: mint_quote.unit,
             unit: mint_quote.unit,
         })
         })
     }
     }
@@ -904,7 +957,7 @@ impl TryFrom<crate::mint::MintQuote> for crate::nuts::MintQuoteCustomResponse<Qu
             request: mint_quote.request,
             request: mint_quote.request,
             expiry: Some(mint_quote.expiry),
             expiry: Some(mint_quote.expiry),
             pubkey: mint_quote.pubkey,
             pubkey: mint_quote.pubkey,
-            amount: mint_quote.amount,
+            amount: mint_quote.amount.map(Into::into),
             unit: Some(mint_quote.unit),
             unit: Some(mint_quote.unit),
             extra: mint_quote.extra_json.unwrap_or_default(),
             extra: mint_quote.extra_json.unwrap_or_default(),
         })
         })
@@ -929,8 +982,8 @@ impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
             change: None,
             change: None,
             state: melt_quote.state,
             state: melt_quote.state,
             expiry: melt_quote.expiry,
             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,
             request: None,
             unit: Some(melt_quote.unit.clone()),
             unit: Some(melt_quote.unit.clone()),
         }
         }
@@ -941,8 +994,8 @@ impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
     fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
     fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
         MeltQuoteBolt11Response {
         MeltQuoteBolt11Response {
             quote: melt_quote.id.clone(),
             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,
             state: melt_quote.state,
             expiry: melt_quote.expiry,
             expiry: melt_quote.expiry,
             payment_preimage: melt_quote.payment_preimage,
             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;
     type Error = Error;
 
 
     fn try_from(melt_quote: crate::mint::MeltQuote) -> Result<Self, Self::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(
             MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
                 Bolt11OutgoingPaymentOptions {
                 Bolt11OutgoingPaymentOptions {
-                    max_fee_amount: Some(melt_quote.fee_reserve),
+                    max_fee_amount: Some(fee_reserve.to_owned().into()),
                     timeout_secs: None,
                     timeout_secs: None,
-                    bolt11,
+                    bolt11: bolt11.clone(),
                     melt_options: melt_quote.options,
                     melt_options: melt_quote.options,
                 },
                 },
             ))),
             ))),
@@ -281,18 +282,18 @@ impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
 
 
                 Ok(OutgoingPaymentOptions::Bolt12(Box::new(
                 Ok(OutgoingPaymentOptions::Bolt12(Box::new(
                     Bolt12OutgoingPaymentOptions {
                     Bolt12OutgoingPaymentOptions {
-                        max_fee_amount: Some(melt_quote.fee_reserve),
+                        max_fee_amount: Some(fee_reserve.clone().into()),
                         timeout_secs: None,
                         timeout_secs: None,
-                        offer: *offer,
+                        offer: *offer.clone(),
                         melt_options,
                         melt_options,
                     },
                     },
                 )))
                 )))
             }
             }
             MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
             MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
                 Box::new(CustomOutgoingPaymentOptions {
                 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,
                     timeout_secs: None,
                     melt_options: melt_quote.options,
                     melt_options: melt_quote.options,
                     extra_json: None,
                     extra_json: None,
@@ -385,28 +386,32 @@ impl Default for Event {
         // The actual processing will filter these out
         // The actual processing will filter these out
         Event::PaymentReceived(WaitPaymentResponse {
         Event::PaymentReceived(WaitPaymentResponse {
             payment_identifier: PaymentIdentifier::CustomId("default".to_string()),
             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(),
             payment_id: "default".to_string(),
         })
         })
     }
     }
 }
 }
 
 
 /// Wait any invoice response
 /// Wait any invoice response
-#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash)]
 pub struct WaitPaymentResponse {
 pub struct WaitPaymentResponse {
     /// Request look up id
     /// Request look up id
     /// Id that relates the quote and payment request
     /// Id that relates the quote and payment request
     pub payment_identifier: PaymentIdentifier,
     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
     /// Unique id of payment
     // Payment hash
     // Payment hash
     pub payment_id: String,
     pub payment_id: String,
 }
 }
 
 
+impl WaitPaymentResponse {
+    /// Get the currency unit
+    pub fn unit(&self) -> &CurrencyUnit {
+        self.payment_amount.unit()
+    }
+}
+
 /// Create incoming payment response
 /// Create incoming payment response
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct CreateIncomingPaymentResponse {
 pub struct CreateIncomingPaymentResponse {
@@ -425,7 +430,7 @@ pub struct CreateIncomingPaymentResponse {
 }
 }
 
 
 /// Payment response
 /// Payment response
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct MakePaymentResponse {
 pub struct MakePaymentResponse {
     /// Payment hash
     /// Payment hash
     pub payment_lookup_id: PaymentIdentifier,
     pub payment_lookup_id: PaymentIdentifier,
@@ -433,27 +438,37 @@ pub struct MakePaymentResponse {
     pub payment_proof: Option<String>,
     pub payment_proof: Option<String>,
     /// Status
     /// Status
     pub status: MeltQuoteState,
     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
 /// Payment quote response
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct PaymentQuoteResponse {
 pub struct PaymentQuoteResponse {
     /// Request look up id
     /// Request look up id
     pub request_lookup_id: Option<PaymentIdentifier>,
     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
     /// Status
     pub state: MeltQuoteState,
     pub state: MeltQuoteState,
 }
 }
 
 
+impl PaymentQuoteResponse {
+    /// Get the currency unit
+    pub fn unit(&self) -> &CurrencyUnit {
+        self.amount.unit()
+    }
+}
+
 /// BOLT11 settings
 /// BOLT11 settings
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
 pub struct Bolt11Settings {
 pub struct Bolt11Settings {
@@ -591,8 +606,8 @@ where
         let success = result.is_ok();
         let success = result.is_ok();
 
 
         if let Ok(ref quote) = result {
         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);
             METRICS.record_lightning_payment(amount, fee);
         }
         }
 
 

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

@@ -284,10 +284,8 @@ where
                     continue;
                     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 {
             if remote_subscription.total_subscribers == 0 {
                 let mut cached_events = self.cached_events.write();
                 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 async_trait::async_trait;
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::secp256k1::{Secp256k1, SecretKey};
 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::common::FeeReserve;
 use cdk_common::ensure_cdk;
 use cdk_common::ensure_cdk;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
@@ -48,6 +48,9 @@ pub mod error;
 /// Default maximum size for the secondary repayment queue
 /// Default maximum size for the secondary repayment queue
 const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100;
 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)
 /// Cache duration for exchange rate (5 minutes)
 const RATE_CACHE_DURATION: Duration = Duration::from_secs(300);
 const RATE_CACHE_DURATION: Duration = Duration::from_secs(300);
 
 
@@ -139,11 +142,11 @@ async fn convert_currency_amount(
     from_unit: &CurrencyUnit,
     from_unit: &CurrencyUnit,
     target_unit: &CurrencyUnit,
     target_unit: &CurrencyUnit,
     rate_cache: &ExchangeRateCache,
     rate_cache: &ExchangeRateCache,
-) -> Result<Amount, Error> {
+) -> Result<Amount<CurrencyUnit>, Error> {
     use CurrencyUnit::*;
     use CurrencyUnit::*;
 
 
     // Try basic unit conversion first (handles SAT/MSAT and same-unit conversions)
     // 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);
         return Ok(converted);
     }
     }
 
 
@@ -153,15 +156,17 @@ async fn convert_currency_amount(
         (Usd | Eur, Sat) => {
         (Usd | Eur, Sat) => {
             let rate = rate_cache.get_btc_rate(from_unit).await?;
             let rate = rate_cache.get_btc_rate(from_unit).await?;
             let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
             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
             )) // to sats
         }
         }
         (Usd | Eur, Msat) => {
         (Usd | Eur, Msat) => {
             let rate = rate_cache.get_btc_rate(from_unit).await?;
             let rate = rate_cache.get_btc_rate(from_unit).await?;
             let fiat_amount = amount as f64 / 100.0; // cents to dollars/euros
             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,
                 (fiat_amount / rate * 100_000_000_000.0).round() as u64,
+                target_unit.clone(),
             )) // to msats
             )) // to msats
         }
         }
 
 
@@ -169,12 +174,18 @@ async fn convert_currency_amount(
         (Sat, Usd | Eur) => {
         (Sat, Usd | Eur) => {
             let rate = rate_cache.get_btc_rate(target_unit).await?;
             let rate = rate_cache.get_btc_rate(target_unit).await?;
             let btc_amount = amount as f64 / 100_000_000.0; // sats to BTC
             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) => {
         (Msat, Usd | Eur) => {
             let rate = rate_cache.get_btc_rate(target_unit).await?;
             let rate = rate_cache.get_btc_rate(target_unit).await?;
             let btc_amount = amount as f64 / 100_000_000_000.0; // msats to BTC
             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
         _ => Err(Error::UnknownInvoiceAmount), // Unsupported conversion
@@ -265,9 +276,11 @@ impl SecondaryRepaymentQueue {
 
 
                     // Create amount based on unit, ensuring minimum of 1 sat worth
                     // Create amount based on unit, ensuring minimum of 1 sat worth
                     let secondary_amount = match &unit {
                     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
                     // Generate a unique payment identifier for this secondary payment
@@ -301,7 +314,6 @@ impl SecondaryRepaymentQueue {
                     let secondary_response = WaitPaymentResponse {
                     let secondary_response = WaitPaymentResponse {
                         payment_identifier: payment.clone(),
                         payment_identifier: payment.clone(),
                         payment_amount: secondary_amount,
                         payment_amount: secondary_amount,
-                        unit: unit.clone(),
                         payment_id: unique_payment_id.to_string(),
                         payment_id: unique_payment_id.to_string(),
                     };
                     };
 
 
@@ -324,7 +336,7 @@ pub struct FakeWallet {
     fee_reserve: FeeReserve,
     fee_reserve: FeeReserve,
     sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
     sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
     receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<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>>>,
     failed_payment_check: Arc<Mutex<HashSet<String>>>,
     payment_delay: u64,
     payment_delay: u64,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_cancel_token: CancellationToken,
@@ -339,7 +351,7 @@ impl FakeWallet {
     /// Create new [`FakeWallet`]
     /// Create new [`FakeWallet`]
     pub fn new(
     pub fn new(
         fee_reserve: FeeReserve,
         fee_reserve: FeeReserve,
-        payment_states: HashMap<String, (MeltQuoteState, Amount)>,
+        payment_states: HashMap<String, PaymentStateEntry>,
         fail_payment_check: HashSet<String>,
         fail_payment_check: HashSet<String>,
         payment_delay: u64,
         payment_delay: u64,
         unit: CurrencyUnit,
         unit: CurrencyUnit,
@@ -357,7 +369,7 @@ impl FakeWallet {
     /// Create new [`FakeWallet`] with custom secondary repayment queue size
     /// Create new [`FakeWallet`] with custom secondary repayment queue size
     pub fn new_with_repay_queue_size(
     pub fn new_with_repay_queue_size(
         fee_reserve: FeeReserve,
         fee_reserve: FeeReserve,
-        payment_states: HashMap<String, (MeltQuoteState, Amount)>,
+        payment_states: HashMap<String, PaymentStateEntry>,
         fail_payment_check: HashSet<String>,
         fail_payment_check: HashSet<String>,
         payment_delay: u64,
         payment_delay: u64,
         unit: CurrencyUnit,
         unit: CurrencyUnit,
@@ -519,7 +531,7 @@ impl MintPayment for FakeWallet {
         .await?;
         .await?;
 
 
         let relative_fee_reserve =
         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 absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
 
@@ -528,9 +540,8 @@ impl MintPayment for FakeWallet {
         Ok(PaymentQuoteResponse {
         Ok(PaymentQuoteResponse {
             request_lookup_id,
             request_lookup_id,
             amount,
             amount,
-            fee: fee.into(),
+            fee: Amount::new(fee, unit.clone()),
             state: MeltQuoteState::Unpaid,
             state: MeltQuoteState::Unpaid,
-            unit: unit.clone(),
         })
         })
     }
     }
 
 
@@ -571,9 +582,9 @@ impl MintPayment for FakeWallet {
                 };
                 };
 
 
                 let amount_spent = if checkout_going_status == MeltQuoteState::Paid {
                 let amount_spent = if checkout_going_status == MeltQuoteState::Paid {
-                    amount_msat.into()
+                    Amount::new(amount_msat, CurrencyUnit::Msat)
                 } else {
                 } else {
-                    Amount::ZERO
+                    Amount::new(0, CurrencyUnit::Msat)
                 };
                 };
 
 
                 payment_states.insert(payment_hash.clone(), (checkout_going_status, amount_spent));
                 payment_states.insert(payment_hash.clone(), (checkout_going_status, amount_spent));
@@ -596,13 +607,12 @@ impl MintPayment for FakeWallet {
                 .await?;
                 .await?;
 
 
                 Ok(MakePaymentResponse {
                 Ok(MakePaymentResponse {
-                    payment_proof: Some("".to_string()),
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                         *bolt11.payment_hash().as_ref(),
                         *bolt11.payment_hash().as_ref(),
                     ),
                     ),
+                    payment_proof: Some("".to_string()),
                     status: payment_status,
                     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) => {
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -627,11 +637,10 @@ impl MintPayment for FakeWallet {
                 .await?;
                 .await?;
 
 
                 Ok(MakePaymentResponse {
                 Ok(MakePaymentResponse {
-                    payment_proof: Some("".to_string()),
                     payment_lookup_id: PaymentIdentifier::CustomId(Uuid::new_v4().to_string()),
                     payment_lookup_id: PaymentIdentifier::CustomId(Uuid::new_v4().to_string()),
+                    payment_proof: Some("".to_string()),
                     status: MeltQuoteState::Paid,
                     status: MeltQuoteState::Paid,
-                    total_spent: total_spent + 1.into(),
-                    unit: unit.clone(),
+                    total_spent: Amount::new(total_spent.value() + 1, unit.clone()),
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Custom(_) => {
             OutgoingPaymentOptions::Custom(_) => {
@@ -668,7 +677,7 @@ impl MintPayment for FakeWallet {
                             &self.exchange_rate_cache,
                             &self.exchange_rate_cache,
                         )
                         )
                         .await?;
                         .await?;
-                        offer_builder.amount_msats(amount_msat.into())
+                        offer_builder.amount_msats(amount_msat.value())
                     }
                     }
                     None => offer_builder,
                     None => offer_builder,
                 };
                 };
@@ -693,10 +702,9 @@ impl MintPayment for FakeWallet {
                     &CurrencyUnit::Msat,
                     &CurrencyUnit::Msat,
                     &self.exchange_rate_cache,
                     &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();
                 let payment_hash = invoice.payment_hash();
 
 
                 (
                 (
@@ -717,7 +725,6 @@ impl MintPayment for FakeWallet {
         let duration = time::Duration::from_secs(self.payment_delay);
         let duration = time::Duration::from_secs(self.payment_delay);
         let payment_hash_clone = payment_hash.clone();
         let payment_hash_clone = payment_hash.clone();
         let incoming_payment = self.incoming_payments.clone();
         let incoming_payment = self.incoming_payments.clone();
-        let unit_clone = self.unit.clone();
 
 
         let final_amount = if amount == Amount::ZERO {
         let final_amount = if amount == Amount::ZERO {
             // For any-amount invoices, generate a random amount for the initial payment
             // 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 mut rng = OsRng;
             let random_amount: u64 = rng.gen_range(1000..=10000);
             let random_amount: u64 = rng.gen_range(1000..=10000);
             // Use the same unit as the wallet for any-amount invoices
             // Use the same unit as the wallet for any-amount invoices
-            Amount::from(random_amount)
+            Amount::new(random_amount, unit.clone())
         } else {
         } else {
-            amount
+            Amount::new(u64::from(amount), unit.clone())
         };
         };
 
 
         // Schedule the immediate payment (original behavior maintained)
         // Schedule the immediate payment (original behavior maintained)
@@ -739,7 +746,6 @@ impl MintPayment for FakeWallet {
             let response = WaitPaymentResponse {
             let response = WaitPaymentResponse {
                 payment_identifier: payment_hash_clone.clone(),
                 payment_identifier: payment_hash_clone.clone(),
                 payment_amount: final_amount,
                 payment_amount: final_amount,
-                unit: unit_clone,
                 payment_id: payment_hash_clone.to_string(),
                 payment_id: payment_hash_clone.to_string(),
             };
             };
             let mut incoming = incoming_payment.write().await;
             let mut incoming = incoming_payment.write().await;
@@ -797,7 +803,8 @@ impl MintPayment for FakeWallet {
         let states = self.payment_states.lock().await;
         let states = self.payment_states.lock().await;
         let status = states.get(&request_lookup_id.to_string()).cloned();
         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;
         let fail_payments = self.failed_payment_check.lock().await;
 
 
@@ -806,11 +813,10 @@ impl MintPayment for FakeWallet {
         }
         }
 
 
         Ok(MakePaymentResponse {
         Ok(MakePaymentResponse {
-            payment_proof: Some("".to_string()),
             payment_lookup_id: request_lookup_id.clone(),
             payment_lookup_id: request_lookup_id.clone(),
+            payment_proof: Some("".to_string()),
             status,
             status,
             total_spent,
             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,
         None,
         "concurrent_test_invoice".to_string(),
         "concurrent_test_invoice".to_string(),
         CurrencyUnit::Sat,
         CurrencyUnit::Sat,
-        Some(Amount::from(1000)),
+        Some(Amount::from(1000).with_unit(CurrencyUnit::Sat)),
         current_time + 3600, // expires in 1 hour
         current_time + 3600, // expires in 1 hour
         PaymentIdentifier::CustomId("test_lookup_id".to_string()),
         PaymentIdentifier::CustomId("test_lookup_id".to_string()),
         None,
         None,
-        Amount::ZERO,
-        Amount::ZERO,
+        Amount::ZERO.with_unit(CurrencyUnit::Sat),
+        Amount::ZERO.with_unit(CurrencyUnit::Sat),
         PaymentMethod::Known(KnownMethod::Bolt11),
         PaymentMethod::Known(KnownMethod::Bolt11),
         current_time,
         current_time,
         vec![],
         vec![],
@@ -212,9 +212,11 @@ async fn test_concurrent_duplicate_payment_handling() {
                 .expect("no error")
                 .expect("no error")
                 .expect("some value");
                 .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)
                 Err(err)
             } else {
             } else {
                 tx.update_mint_quote(&mut quote_from_db)
                 tx.update_mint_quote(&mut quote_from_db)
@@ -275,7 +277,7 @@ async fn test_concurrent_duplicate_payment_handling() {
 
 
     assert_eq!(
     assert_eq!(
         final_quote.amount_paid(),
         final_quote.amount_paid(),
-        Amount::from(10),
+        Amount::from(10).with_unit(CurrencyUnit::Sat),
         "Quote amount should be incremented exactly once"
         "Quote amount should be incremented exactly once"
     );
     );
     assert_eq!(
     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 std::sync::Arc;
 
 
 use async_trait::async_trait;
 use async_trait::async_trait;
-use cdk_common::amount::to_unit;
 use cdk_common::common::FeeReserve;
 use cdk_common::common::FeeReserve;
 use cdk_common::payment::{self, *};
 use cdk_common::payment::{self, *};
 use cdk_common::util::{hex, unix_time};
 use cdk_common::util::{hex, unix_time};
@@ -343,8 +342,7 @@ impl CdkLdkNode {
 
 
         let wait_payment_response = WaitPaymentResponse {
         let wait_payment_response = WaitPaymentResponse {
             payment_identifier,
             payment_identifier,
-            payment_amount: amount_msat.into(),
-            unit: CurrencyUnit::Msat,
+            payment_amount: Amount::new(amount_msat, CurrencyUnit::Msat),
             payment_id,
             payment_id,
         };
         };
 
 
@@ -477,7 +475,9 @@ impl MintPayment for CdkLdkNode {
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
     ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
         match options {
         match options {
             IncomingPaymentOptions::Bolt11(bolt11_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 description = bolt11_options.description.unwrap_or_default();
                 let time = bolt11_options
                 let time = bolt11_options
                     .unix_expiry
                     .unix_expiry
@@ -519,7 +519,9 @@ impl MintPayment for CdkLdkNode {
 
 
                 let offer = match amount {
                 let offer = match amount {
                     Some(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
                         self.inner
                             .bolt12_payment()
                             .bolt12_payment()
@@ -575,10 +577,11 @@ impl MintPayment for CdkLdkNode {
                         .into(),
                         .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 =
                 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 absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
 
@@ -595,9 +598,8 @@ impl MintPayment for CdkLdkNode {
                 Ok(PaymentQuoteResponse {
                 Ok(PaymentQuoteResponse {
                     request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
                     request_lookup_id: Some(PaymentIdentifier::PaymentHash(payment_hash_bytes)),
                     amount,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
             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 =
                 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 absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
 
 
@@ -631,9 +634,8 @@ impl MintPayment for CdkLdkNode {
                 Ok(PaymentQuoteResponse {
                 Ok(PaymentQuoteResponse {
                     request_lookup_id: None,
                     request_lookup_id: None,
                     amount,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
         }
         }
@@ -656,12 +658,12 @@ impl MintPayment for CdkLdkNode {
                 let send_params = match bolt11_options
                 let send_params = match bolt11_options
                     .max_fee_amount
                     .max_fee_amount
                     .map(|f| {
                     .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()
                                 ..Default::default()
-                            }
-                        })
+                            })
                     })
                     })
                     .transpose()
                     .transpose()
                 {
                 {
@@ -735,7 +737,7 @@ impl MintPayment for CdkLdkNode {
                     .ok_or(Error::CouldNotGetAmountSpent)?
                     .ok_or(Error::CouldNotGetAmountSpent)?
                     + payment_details.fee_paid_msat.unwrap_or_default();
                     + 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 {
                 Ok(MakePaymentResponse {
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
@@ -744,7 +746,6 @@ impl MintPayment for CdkLdkNode {
                     payment_proof,
                     payment_proof,
                     status,
                     status,
                     total_spent,
                     total_spent,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
             OutgoingPaymentOptions::Bolt12(bolt12_options) => {
@@ -810,14 +811,13 @@ impl MintPayment for CdkLdkNode {
                     .ok_or(Error::CouldNotGetAmountSpent)?
                     .ok_or(Error::CouldNotGetAmountSpent)?
                     + payment_details.fee_paid_msat.unwrap_or_default();
                     + 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 {
                 Ok(MakePaymentResponse {
                     payment_lookup_id: PaymentIdentifier::PaymentId(payment_id.0),
                     payment_lookup_id: PaymentIdentifier::PaymentId(payment_id.0),
                     payment_proof,
                     payment_proof,
                     status,
                     status,
                     total_spent,
                     total_spent,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
         }
         }
@@ -916,8 +916,7 @@ impl MintPayment for CdkLdkNode {
 
 
         let response = WaitPaymentResponse {
         let response = WaitPaymentResponse {
             payment_identifier: payment_identifier.clone(),
             payment_identifier: payment_identifier.clone(),
-            payment_amount: amount.into(),
-            unit: CurrencyUnit::Msat,
+            payment_amount: Amount::new(amount, CurrencyUnit::Msat),
             payment_id: payment_id_str,
             payment_id: payment_id_str,
         };
         };
 
 
@@ -945,10 +944,9 @@ impl MintPayment for CdkLdkNode {
             _ => {
             _ => {
                 return Ok(MakePaymentResponse {
                 return Ok(MakePaymentResponse {
                     payment_lookup_id: request_lookup_id.clone(),
                     payment_lookup_id: request_lookup_id.clone(),
-                    status: MeltQuoteState::Unknown,
                     payment_proof: None,
                     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_lookup_id: request_lookup_id.clone(),
             payment_proof,
             payment_proof,
             status,
             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 anyhow::anyhow;
 use async_trait::async_trait;
 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::common::FeeReserve;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
 use cdk_common::payment::{
@@ -122,8 +122,7 @@ impl LNbits {
 
 
         Ok(Some(WaitPaymentResponse {
         Ok(Some(WaitPaymentResponse {
             payment_identifier: PaymentIdentifier::PaymentHash(hash),
             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(),
             payment_id: msg.to_string(),
         }))
         }))
     }
     }
@@ -253,10 +252,9 @@ impl MintPayment for LNbits {
                     request_lookup_id: Some(PaymentIdentifier::PaymentHash(
                     request_lookup_id: Some(PaymentIdentifier::PaymentHash(
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                         *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,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
             OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
@@ -268,7 +266,7 @@ impl MintPayment for LNbits {
 
 
     async fn make_payment(
     async fn make_payment(
         &self,
         &self,
-        _unit: &CurrencyUnit,
+        unit: &CurrencyUnit,
         options: OutgoingPaymentOptions,
         options: OutgoingPaymentOptions,
     ) -> Result<MakePaymentResponse, Self::Err> {
     ) -> Result<MakePaymentResponse, Self::Err> {
         match options {
         match options {
@@ -299,15 +297,18 @@ impl MintPayment for LNbits {
                     MeltQuoteState::Unpaid
                     MeltQuoteState::Unpaid
                 };
                 };
 
 
-                let total_spent = Amount::from(
-                    (invoice_info
+                let total_spent_msat = Amount::new(
+                    invoice_info
                         .details
                         .details
                         .amount
                         .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 {
                 Ok(MakePaymentResponse {
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                     payment_lookup_id: PaymentIdentifier::PaymentHash(
                         hex::decode(pay_response.payment_hash)
                         hex::decode(pay_response.payment_hash)
@@ -318,7 +319,6 @@ impl MintPayment for LNbits {
                     payment_proof: Some(invoice_info.details.payment_hash),
                     payment_proof: Some(invoice_info.details.payment_hash),
                     status,
                     status,
                     total_spent,
                     total_spent,
-                    unit: CurrencyUnit::Msat,
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Bolt12(_) => {
             OutgoingPaymentOptions::Bolt12(_) => {
@@ -343,7 +343,9 @@ impl MintPayment for LNbits {
                 let expiry = unix_expiry.map(|t| t - time_now);
                 let expiry = unix_expiry.map(|t| t - time_now);
 
 
                 let invoice_request = CreateInvoiceRequest {
                 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),
                     memo: Some(description),
                     unit: unit.to_string(),
                     unit: unit.to_string(),
                     expiry,
                     expiry,
@@ -404,8 +406,7 @@ impl MintPayment for LNbits {
         match payment.paid {
         match payment.paid {
             true => Ok(vec![WaitPaymentResponse {
             true => Ok(vec![WaitPaymentResponse {
                 payment_identifier: payment_identifier.clone(),
                 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,
                 payment_id: payment.details.payment_hash,
             }]),
             }]),
             false => Ok(vec![]),
             false => Ok(vec![]),
@@ -430,10 +431,10 @@ impl MintPayment for LNbits {
             payment_lookup_id: payment_identifier.clone(),
             payment_lookup_id: payment_identifier.clone(),
             payment_proof: payment.preimage,
             payment_proof: payment.preimage,
             status: lnbits_to_melt_status(&payment.details.status),
             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(),
                 payment.details.amount.unsigned_abs() + payment.details.fee.unsigned_abs(),
+                CurrencyUnit::Msat,
             ),
             ),
-            unit: CurrencyUnit::Msat,
         };
         };
 
 
         Ok(pay_response)
         Ok(pay_response)

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

@@ -13,7 +13,7 @@ use std::sync::Arc;
 
 
 use anyhow::anyhow;
 use anyhow::anyhow;
 use async_trait::async_trait;
 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::bitcoin::hashes::Hash;
 use cdk_common::common::FeeReserve;
 use cdk_common::common::FeeReserve;
 use cdk_common::database::DynKVStore;
 use cdk_common::database::DynKVStore;
@@ -305,8 +305,7 @@ impl MintPayment for Lnd {
 
 
                                             let wait_response = WaitPaymentResponse {
                                             let wait_response = WaitPaymentResponse {
                                                 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
                                                 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,
                                                 payment_id: hash,
                                             };
                                             };
                                             let event = Event::PaymentReceived(wait_response);
                                             let event = Event::PaymentReceived(wait_response);
@@ -362,10 +361,11 @@ impl MintPayment for Lnd {
                         .into(),
                         .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 =
                 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 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(),
                         *bolt11_options.bolt11.payment_hash().as_ref(),
                     )),
                     )),
                     amount,
                     amount,
-                    fee: fee.into(),
+                    fee: Amount::new(fee, unit.clone()),
                     state: MeltQuoteState::Unpaid,
                     state: MeltQuoteState::Unpaid,
-                    unit: unit.clone(),
                 })
                 })
             }
             }
             OutgoingPaymentOptions::Bolt12(_) => {
             OutgoingPaymentOptions::Bolt12(_) => {
@@ -511,8 +510,7 @@ impl MintPayment for Lnd {
                                     ),
                                     ),
                                     payment_proof: payment_preimage,
                                     payment_proof: payment_preimage,
                                     status,
                                     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_lookup_id: payment_identifier,
                             payment_proof: payment_preimage,
                             payment_proof: payment_preimage,
                             status,
                             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 amount = bolt11_options.amount;
                 let unix_expiry = bolt11_options.unix_expiry;
                 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 {
                 let invoice_request = lnrpc::Invoice {
                     value_msat: u64::from(amount_msat) as i64,
                     value_msat: u64::from(amount_msat) as i64,
@@ -657,8 +656,7 @@ impl MintPayment for Lnd {
         if invoice.state() == InvoiceState::Settled {
         if invoice.state() == InvoiceState::Settled {
             Ok(vec![WaitPaymentResponse {
             Ok(vec![WaitPaymentResponse {
                 payment_identifier: payment_identifier.clone(),
                 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),
                 payment_id: hex::encode(invoice.r_hash),
             }])
             }])
         } else {
         } else {
@@ -691,8 +689,7 @@ impl MintPayment for Lnd {
                         payment_lookup_id: payment_identifier.clone(),
                         payment_lookup_id: payment_identifier.clone(),
                         payment_proof: None,
                         payment_proof: None,
                         status: MeltQuoteState::Unknown,
                         status: MeltQuoteState::Unknown,
-                        total_spent: Amount::ZERO,
-                        unit: self.unit.clone(),
+                        total_spent: Amount::new(0, self.unit.clone()),
                     });
                     });
                 } else {
                 } else {
                     return Err(payment::Error::UnknownPaymentState);
                     return Err(payment::Error::UnknownPaymentState);
@@ -710,8 +707,7 @@ impl MintPayment for Lnd {
                             payment_lookup_id: payment_identifier.clone(),
                             payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Unknown,
                             status: MeltQuoteState::Unknown,
-                            total_spent: Amount::ZERO,
-                            unit: self.unit.clone(),
+                            total_spent: Amount::new(0, self.unit.clone()),
                         },
                         },
                         PaymentStatus::InFlight | PaymentStatus::Initiated => {
                         PaymentStatus::InFlight | PaymentStatus::Initiated => {
                             // Continue waiting for the next update
                             // Continue waiting for the next update
@@ -721,21 +717,20 @@ impl MintPayment for Lnd {
                             payment_lookup_id: payment_identifier.clone(),
                             payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Paid,
                             status: MeltQuoteState::Paid,
-                            total_spent: Amount::from(
+                            total_spent: Amount::new(
                                 (update
                                 (update
                                     .value_sat
                                     .value_sat
                                     .checked_add(update.fee_sat)
                                     .checked_add(update.fee_sat)
                                     .ok_or(Error::AmountOverflow)?)
                                     .ok_or(Error::AmountOverflow)?)
                                     as u64,
                                     as u64,
+                                CurrencyUnit::Sat,
                             ),
                             ),
-                            unit: CurrencyUnit::Sat,
                         },
                         },
                         PaymentStatus::Failed => MakePaymentResponse {
                         PaymentStatus::Failed => MakePaymentResponse {
                             payment_lookup_id: payment_identifier.clone(),
                             payment_lookup_id: payment_identifier.clone(),
                             payment_proof: Some(update.payment_preimage),
                             payment_proof: Some(update.payment_preimage),
                             status: MeltQuoteState::Failed,
                             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
                 // Create a dummy payment response
                 let response = WaitPaymentResponse {
                 let response = WaitPaymentResponse {
                     payment_id: mint_quote.request_lookup_id.to_string(),
                     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(),
                     payment_identifier: mint_quote.request_lookup_id.clone(),
                 };
                 };
 
 
@@ -688,20 +690,20 @@ impl CdkMint for MintRPCServer {
             _ => {
             _ => {
                 // Create a new quote with the same values
                 // Create a new quote with the same values
                 let quote = MintQuote::new(
                 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();
                 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::{
 use cdk_common::payment::{
     CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse,
     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 client;
 mod server;
 mod server;
@@ -95,7 +96,6 @@ impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
         // as_str_name() returns "QUOTE_STATE_PAID" but MeltQuoteState::from_str expects "PAID"
         // as_str_name() returns "QUOTE_STATE_PAID" but MeltQuoteState::from_str expects "PAID"
         let status: cdk_common::nuts::MeltQuoteState = value.status().into();
         let status: cdk_common::nuts::MeltQuoteState = value.status().into();
         let payment_proof = value.payment_proof;
         let payment_proof = value.payment_proof;
-        let total_spent = value.total_spent.into();
         let unit = CurrencyUnit::from_str(&value.unit)?;
         let unit = CurrencyUnit::from_str(&value.unit)?;
         let payment_identifier = value
         let payment_identifier = value
             .payment_identifier
             .payment_identifier
@@ -104,8 +104,7 @@ impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
             payment_lookup_id: payment_identifier.try_into()?,
             payment_lookup_id: payment_identifier.try_into()?,
             payment_proof,
             payment_proof,
             status,
             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_identifier: Some(value.payment_lookup_id.into()),
             payment_proof: value.payment_proof,
             payment_proof: value.payment_proof,
             status: QuoteState::from(value.status).into(),
             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,
             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 {
         Self {
             request_identifier: value.request_lookup_id.map(|i| i.into()),
             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(),
             state: QuoteState::from(value.state).into(),
             extra_json: None,
             extra_json: None,
         }
         }
     }
     }
 }
 }
 
 
-impl From<PaymentQuoteResponse> for cdk_common::payment::PaymentQuoteResponse {
+impl From<PaymentQuoteResponse> for CdkPaymentQuoteResponse {
     fn from(value: PaymentQuoteResponse) -> Self {
     fn from(value: PaymentQuoteResponse) -> Self {
         let state_val = value.state();
         let state_val = value.state();
         let request_identifier = value.request_identifier;
         let request_identifier = value.request_identifier;
+        let unit = CurrencyUnit::from_str(&value.unit).unwrap_or_default();
 
 
         Self {
         Self {
             request_lookup_id: request_identifier
             request_lookup_id: request_identifier
                 .map(|i| i.try_into().expect("valid 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(),
             state: state_val.into(),
         }
         }
     }
     }
@@ -255,8 +254,8 @@ impl From<WaitPaymentResponse> for WaitIncomingPaymentResponse {
     fn from(value: WaitPaymentResponse) -> Self {
     fn from(value: WaitPaymentResponse) -> Self {
         Self {
         Self {
             payment_identifier: Some(value.payment_identifier.into()),
             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,
             payment_id: value.payment_id,
         }
         }
     }
     }
@@ -270,11 +269,11 @@ impl TryFrom<WaitIncomingPaymentResponse> for WaitPaymentResponse {
             .payment_identifier
             .payment_identifier
             .ok_or(crate::error::Error::InvalidPaymentIdentifier)?
             .ok_or(crate::error::Error::InvalidPaymentIdentifier)?
             .try_into()?;
             .try_into()?;
+        let unit = CurrencyUnit::from_str(&value.unit)?;
 
 
         Ok(Self {
         Ok(Self {
             payment_identifier,
             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,
             payment_id: value.payment_id,
         })
         })
     }
     }

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

@@ -56,17 +56,12 @@ where
     ) -> Result<Response<proto::BlindSignResponse>, Status> {
     ) -> Result<Response<proto::BlindSignResponse>, Status> {
         let metadata = request.metadata();
         let metadata = request.metadata();
         let signatory = self.load_signatory(metadata).await?;
         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 {
             Ok(blind_signatures) => proto::BlindSignResponse {
                 sigs: Some(proto::BlindSignatures {
                 sigs: Some(proto::BlindSignatures {
                     blind_signatures: blind_signatures
                     blind_signatures: blind_signatures
@@ -92,17 +87,12 @@ where
     ) -> Result<Response<proto::BooleanResponse>, Status> {
     ) -> Result<Response<proto::BooleanResponse>, Status> {
         let metadata = request.metadata();
         let metadata = request.metadata();
         let signatory = self.load_signatory(metadata).await?;
         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 {
             Ok(()) => proto::BooleanResponse {
                 success: true,
                 success: true,
                 ..Default::default()
                 ..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 async_trait::async_trait;
 use cdk_common::database::{self, DbTransactionFinalizer, Error, MintDatabase};
 use cdk_common::database::{self, DbTransactionFinalizer, Error, MintDatabase};
-// Re-export for auth module
-use migrations::MIGRATIONS;
 
 
 use crate::common::migrate;
 use crate::common::migrate;
 use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
 use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
@@ -39,6 +37,7 @@ mod migrations {
 pub use auth::SQLMintAuthDatabase;
 pub use auth::SQLMintAuthDatabase;
 #[cfg(feature = "prometheus")]
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
 use cdk_prometheus::METRICS;
+use migrations::MIGRATIONS;
 
 
 /// Mint SQL Database
 /// Mint SQL Database
 #[derive(Debug, Clone)]
 #[derive(Debug, Clone)]

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

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

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

@@ -341,12 +341,12 @@ impl Mint {
                 None,
                 None,
                 create_invoice_response.request.to_string(),
                 create_invoice_response.request.to_string(),
                 unit.clone(),
                 unit.clone(),
-                amount,
+                amount.map(|a| a.with_unit(unit.clone())),
                 create_invoice_response.expiry.unwrap_or(0),
                 create_invoice_response.expiry.unwrap_or(0),
                 create_invoice_response.request_lookup_id.clone(),
                 create_invoice_response.request_lookup_id.clone(),
                 pubkey,
                 pubkey,
-                Amount::ZERO,
-                Amount::ZERO,
+                Amount::new(0, unit.clone()),
+                Amount::new(0, unit.clone()),
                 payment_method.clone(),
                 payment_method.clone(),
                 unix_time(),
                 unix_time(),
                 vec![],
                 vec![],
@@ -440,7 +440,7 @@ impl Mint {
         #[cfg(feature = "prometheus")]
         #[cfg(feature = "prometheus")]
         METRICS.inc_in_flight_requests("pay_mint_quote_for_request_id");
         METRICS.inc_in_flight_requests("pay_mint_quote_for_request_id");
         let result = async {
         let result = async {
-            if wait_payment_response.payment_amount == Amount::ZERO {
+            if wait_payment_response.payment_amount.value() == 0 {
                 tracing::warn!(
                 tracing::warn!(
                     "Received payment response with 0 amount with payment id {}.",
                     "Received payment response with 0 amount with payment id {}.",
                     wait_payment_response.payment_id.to_string()
                     wait_payment_response.payment_id.to_string()
@@ -633,7 +633,7 @@ impl Mint {
         }
         }
 
 
         let mint_amount = if mint_quote.payment_method.is_bolt11() {
         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() {
             if quote_amount != mint_quote.amount_mintable() {
                 tracing::error!("The quote amount {} does not equal the amount paid {}.", 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
             quote_amount
         } else if mint_quote.payment_method.is_bolt12() {
         } 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!(
                 tracing::error!(
                         "Quote state should not be issued if issued {} is => paid {}.",
                         "Quote state should not be issued if issued {} is => paid {}.",
                         mint_quote.amount_issued(),
                         mint_quote.amount_issued(),
@@ -664,7 +665,6 @@ impl Mint {
 
 
         let Verification {
         let Verification {
             amount: outputs_amount,
             amount: outputs_amount,
-            unit,
         } = match self.verify_outputs(&mut tx, &mint_request.outputs).await {
         } = match self.verify_outputs(&mut tx, &mint_request.outputs).await {
             Ok(verification) => verification,
             Ok(verification) => verification,
             Err(err) => {
             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() {
         if mint_quote.payment_method.is_bolt11() {
             // For bolt11 we enforce that mint amount == quote amount
             // For bolt11 we enforce that mint amount == quote amount
             if outputs_amount != mint_amount {
             if outputs_amount != mint_amount {
                 return Err(Error::TransactionUnbalanced(
                 return Err(Error::TransactionUnbalanced(
-                    mint_amount.into(),
+                    mint_amount.value(),
                     mint_request.total_amount()?.into(),
                     mint_request.total_amount()?.into(),
                     0,
                     0,
                 ));
                 ));
@@ -687,18 +691,15 @@ impl Mint {
             // For other payments we just make sure outputs is not more then mint amount
             // For other payments we just make sure outputs is not more then mint amount
             if outputs_amount > mint_amount {
             if outputs_amount > mint_amount {
                 return Err(Error::TransactionUnbalanced(
                 return Err(Error::TransactionUnbalanced(
-                    mint_amount.into(),
+                    mint_amount.value(),
                     mint_request.total_amount()?.into(),
                     mint_request.total_amount()?.into(),
                     0,
                     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?;
         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::collections::HashMap;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
-use cdk_common::amount::to_unit;
 use cdk_common::common::PaymentProcessorKey;
 use cdk_common::common::PaymentProcessorKey;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::MintQuote;
 use cdk_common::mint::MintQuote;
 use cdk_common::payment::DynMintPayment;
 use cdk_common::payment::DynMintPayment;
-use cdk_common::{Amount, MintQuoteState};
+use cdk_common::MintQuoteState;
 use tracing::instrument;
 use tracing::instrument;
 
 
 use super::subscription::PubSubManager;
 use super::subscription::PubSubManager;
@@ -71,16 +70,16 @@ impl Mint {
 
 
         for payment in ln_status {
         for payment in ln_status {
             if !new_quote.payment_ids().contains(&&payment.payment_id)
             if !new_quote.payment_ids().contains(&&payment.payment_id)
-                && payment.payment_amount > Amount::ZERO
+                && payment.payment_amount.value() > 0
             {
             {
                 tracing::debug!(
                 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
                     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) {
                 match new_quote.add_payment(amount_paid, payment.payment_id.clone(), None) {
                     Ok(()) => {
                     Ok(()) => {

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

@@ -1,13 +1,12 @@
 use std::collections::VecDeque;
 use std::collections::VecDeque;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
-use cdk_common::amount::to_unit;
 use cdk_common::database::mint::MeltRequestInfo;
 use cdk_common::database::mint::MeltRequestInfo;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::{MeltSagaState, Operation, Saga, SagaStateEnum};
 use cdk_common::mint::{MeltSagaState, Operation, Saga, SagaStateEnum};
 use cdk_common::nut00::KnownMethod;
 use cdk_common::nut00::KnownMethod;
 use cdk_common::nuts::MeltQuoteState;
 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")]
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
 use cdk_prometheus::METRICS;
 use tokio::sync::Mutex;
 use tokio::sync::Mutex;
@@ -197,8 +196,8 @@ impl MeltSaga<Initial> {
     ) -> Result<MeltSaga<SetupComplete>, Error> {
     ) -> Result<MeltSaga<SetupComplete>, Error> {
         let Verification {
         let Verification {
             amount: input_amount,
             amount: input_amount,
-            unit: input_unit,
         } = input_verification;
         } = input_verification;
+        let input_unit = Some(input_amount.unit().clone());
 
 
         let mut tx = self.db.begin_transaction().await?;
         let mut tx = self.db.begin_transaction().await?;
 
 
@@ -220,10 +219,10 @@ impl MeltSaga<Initial> {
         let operation = Operation::new(
         let operation = Operation::new(
             self.state_data.operation_id,
             self.state_data.operation_id,
             cdk_common::mint::OperationKind::Melt,
             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
             Some(payment_method), // payment_method
         );
         );
 
 
@@ -297,23 +296,28 @@ impl MeltSaga<Initial> {
             .melt_quote_status(&*quote, None, None, MeltQuoteState::Pending);
             .melt_quote_status(&*quote, None, None, MeltQuoteState::Pending);
 
 
         let inputs_fee_breakdown = self.mint.get_proofs_fee(melt_request.inputs()).await?;
         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!(
             tracing::info!(
                 "Melt request unbalanced: inputs {}, amount {}, fee_reserve {}, input_fee {}, required {}",
                 "Melt request unbalanced: inputs {}, amount {}, fee_reserve {}, input_fee {}, required {}",
                 input_amount,
                 input_amount,
-                quote.amount,
-                quote.fee_reserve,
-                inputs_fee_breakdown.total,
+                quote.amount(),
+                fee_reserve,
+                inputs_fee,
                 required_total
                 required_total
             );
             );
             tx.rollback().await?;
             tx.rollback().await?;
             return Err(Error::TransactionUnbalanced(
             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?;
                     tx.rollback().await?;
                     return Err(Error::UnitMismatch);
                     return Err(Error::UnitMismatch);
                 }
                 }
@@ -338,8 +342,8 @@ impl MeltSaga<Initial> {
         // Add melt request tracking record
         // Add melt request tracking record
         tx.add_melt_request(
         tx.add_melt_request(
             melt_request.quote_id(),
             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?;
         .await?;
 
 
@@ -479,13 +483,16 @@ impl MeltSaga<SetupComplete> {
             return Err(Error::RequestAlreadyPaid);
             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!(
                 tracing::debug!(
                     "Not enough inputs provided: {} needed {}",
                     "Not enough inputs provided: {} needed {}",
                     inputs_amount_quote_unit,
                     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!(
         tracing::info!(
             "Mint quote {} paid {} from internal payment.",
             "Mint quote {} paid {} from internal payment.",
@@ -513,7 +520,7 @@ impl MeltSaga<SetupComplete> {
         )
         )
         .await?;
         .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?;
         tx.update_mint_quote(&mut mint_quote).await?;
 
 
         self.pubsub
         self.pubsub
@@ -590,7 +597,6 @@ impl MeltSaga<SetupComplete> {
                 MakePaymentResponse {
                 MakePaymentResponse {
                     status: MeltQuoteState::Paid,
                     status: MeltQuoteState::Paid,
                     total_spent: amount,
                     total_spent: amount,
-                    unit: self.state_data.quote.unit.clone(),
                     payment_proof: None,
                     payment_proof: None,
                     payment_lookup_id: self
                     payment_lookup_id: self
                         .state_data
                         .state_data
@@ -650,7 +656,7 @@ impl MeltSaga<SetupComplete> {
                             "Got {} status when paying melt quote {} for {} {}. Verifying with backend...",
                             "Got {} status when paying melt quote {} for {} {}. Verifying with backend...",
                             pay.status,
                             pay.status,
                             self.state_data.quote.id,
                             self.state_data.quote.id,
-                            self.state_data.quote.amount,
+                            self.state_data.quote.amount(),
                             self.state_data.quote.unit
                             self.state_data.quote.unit
                         );
                         );
 
 
@@ -825,15 +831,17 @@ impl MeltSaga<PaymentConfirmed> {
     /// - `UnitMismatch`: Failed to convert payment amount to quote unit
     /// - `UnitMismatch`: Failed to convert payment amount to quote unit
     #[instrument(skip_all)]
     #[instrument(skip_all)]
     pub async fn finalize(self) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
     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_preimage = self.state_data.payment_result.payment_proof.clone();
         let payment_lookup_id = &self.state_data.payment_result.payment_lookup_id;
         let payment_lookup_id = &self.state_data.payment_result.payment_lookup_id;
@@ -860,9 +868,9 @@ impl MeltSaga<PaymentConfirmed> {
             &self.pubsub,
             &self.pubsub,
             &mut quote,
             &mut quote,
             &self.state_data.input_ys,
             &self.state_data.input_ys,
-            inputs_amount,
-            inputs_fee,
-            total_spent,
+            inputs_amount.clone(),
+            inputs_fee.clone(),
+            total_spent.clone(),
             payment_preimage.clone(),
             payment_preimage.clone(),
             payment_lookup_id,
             payment_lookup_id,
         )
         )
@@ -895,8 +903,8 @@ impl MeltSaga<PaymentConfirmed> {
                 &self.mint,
                 &self.mint,
                 &self.db,
                 &self.db,
                 &self.state_data.quote.id,
                 &self.state_data.quote.id,
-                inputs_amount,
-                total_spent,
+                inputs_amount.clone(),
+                total_spent.clone(),
                 inputs_fee,
                 inputs_fee,
                 change_outputs,
                 change_outputs,
             )
             )
@@ -922,10 +930,9 @@ impl MeltSaga<PaymentConfirmed> {
         // Set payment details for melt operation
         // Set payment details for melt operation
         // payment_amount = the Lightning invoice amount
         // payment_amount = the Lightning invoice amount
         // payment_fee = actual fee paid (total_spent - 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)
         tx.add_completed_operation(&operation, &self.state_data.fee_breakdown.per_keyset)
             .await?;
             .await?;
@@ -956,11 +963,11 @@ impl MeltSaga<PaymentConfirmed> {
         }
         }
 
 
         let response = MeltQuoteBolt11Response {
         let response = MeltQuoteBolt11Response {
-            amount: self.state_data.quote.amount,
+            amount: self.state_data.quote.amount().into(),
             payment_preimage,
             payment_preimage,
             change,
             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,
             state: MeltQuoteState::Paid,
             expiry: self.state_data.quote.expiry,
             expiry: self.state_data.quote.expiry,
             request: Some(self.state_data.quote.request.to_string()),
             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::mint::Operation;
-use cdk_common::nuts::BlindedMessage;
+use cdk_common::nuts::{BlindedMessage, CurrencyUnit};
 use cdk_common::{Amount, PublicKey};
 use cdk_common::{Amount, PublicKey};
 use uuid::Uuid;
 use uuid::Uuid;
 
 
@@ -49,7 +49,7 @@ pub struct PaymentConfirmed {
 pub enum SettlementDecision {
 pub enum SettlementDecision {
     /// Payment was settled internally (melt-to-mint on the same mint).
     /// Payment was settled internally (melt-to-mint on the same mint).
     /// Contains the amount that was settled.
     /// Contains the amount that was settled.
-    Internal { amount: Amount },
+    Internal { amount: Amount<CurrencyUnit> },
     /// Payment requires external Lightning Network settlement.
     /// Payment requires external Lightning Network settlement.
     RequiresExternalPayment,
     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 } => {
         crate::mint::melt::melt_saga::state::SettlementDecision::Internal { amount } => {
             assert_eq!(
             assert_eq!(
                 amount,
                 amount,
-                Amount::from(4_000),
+                Amount::from(4_000).with_unit(cdk_common::CurrencyUnit::Sat),
                 "Internal settlement amount should match"
                 "Internal settlement amount should match"
             );
             );
         }
         }
@@ -1460,8 +1460,7 @@ async fn test_get_incomplete_sagas_filters_by_kind() {
     // STEP 3: Create a swap saga
     // STEP 3: Create a swap saga
     let swap_proofs = mint_test_proofs(&mint, Amount::from(5_000)).await.unwrap();
     let swap_proofs = mint_test_proofs(&mint, Amount::from(5_000)).await.unwrap();
     let swap_verification = crate::mint::Verification {
     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))
     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 std::str::FromStr;
 
 
-use cdk_common::amount::amount_for_offer;
 use cdk_common::melt::MeltQuoteRequest;
 use cdk_common::melt::MeltQuoteRequest;
 use cdk_common::mint::MeltPaymentRequest;
 use cdk_common::mint::MeltPaymentRequest;
 use cdk_common::nut00::KnownMethod;
 use cdk_common::nut00::KnownMethod;
@@ -22,7 +21,6 @@ use super::{
     CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, Mint,
     CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, Mint,
     PaymentMethod,
     PaymentMethod,
 };
 };
-use crate::amount::to_unit;
 use crate::nuts::MeltQuoteState;
 use crate::nuts::MeltQuoteState;
 use crate::types::PaymentProcessorKey;
 use crate::types::PaymentProcessorKey;
 use crate::util::unix_time;
 use crate::util::unix_time;
@@ -40,12 +38,12 @@ impl Mint {
     #[instrument(skip_all)]
     #[instrument(skip_all)]
     async fn check_melt_request_acceptable(
     async fn check_melt_request_acceptable(
         &self,
         &self,
-        amount: Amount,
-        unit: CurrencyUnit,
+        amount: Amount<CurrencyUnit>,
         method: PaymentMethod,
         method: PaymentMethod,
         request: String,
         request: String,
         options: Option<MeltOptions>,
         options: Option<MeltOptions>,
     ) -> Result<(), Error> {
     ) -> Result<(), Error> {
+        let unit = amount.unit().clone();
         let mint_info = self.mint_info().await?;
         let mint_info = self.mint_info().await?;
         let nut05 = mint_info.nuts.nut05;
         let nut05 = mint_info.nuts.nut05;
 
 
@@ -55,7 +53,7 @@ impl Mint {
             .get_settings(&unit, &method)
             .get_settings(&unit, &method)
             .ok_or(Error::UnsupportedUnit)?;
             .ok_or(Error::UnsupportedUnit)?;
 
 
-        let amount = match options {
+        match options {
             Some(MeltOptions::Mpp { mpp: _ }) => {
             Some(MeltOptions::Mpp { mpp: _ }) => {
                 let nut15 = mint_info.nuts.nut15;
                 let nut15 = mint_info.nuts.nut15;
                 // Verify there is no corresponding mint quote.
                 // Verify there is no corresponding mint quote.
@@ -72,9 +70,6 @@ impl Mint {
                 {
                 {
                     return Err(Error::MppUnitMethodNotSupported(unit, method));
                     return Err(Error::MppUnitMethodNotSupported(unit, method));
                 }
                 }
-                // Assign `amount`
-                // because should have already been converted to the partial amount
-                amount
             }
             }
             Some(MeltOptions::Amountless { amountless: _ }) => {
             Some(MeltOptions::Amountless { amountless: _ }) => {
                 if method.is_bolt11()
                 if method.is_bolt11()
@@ -85,14 +80,14 @@ impl Mint {
                 {
                 {
                     return Err(Error::AmountlessInvoiceNotSupported(unit, method));
                     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 {
         match is_above_max || is_below_min {
             true => {
             true => {
                 tracing::error!(
                 tracing::error!(
@@ -104,7 +99,7 @@ impl Mint {
                 Err(Error::AmountOutofLimitRange(
                 Err(Error::AmountOutofLimitRange(
                     settings.min_amount.unwrap_or_default(),
                     settings.min_amount.unwrap_or_default(),
                     settings.max_amount.unwrap_or_default(),
                     settings.max_amount.unwrap_or_default(),
-                    amount,
+                    amount.into(),
                 ))
                 ))
             }
             }
             false => Ok(()),
             false => Ok(()),
@@ -187,20 +182,23 @@ impl Mint {
                 err
                 err
             })?;
             })?;
 
 
-        if &payment_quote.unit != unit {
+        if payment_quote.unit() != unit {
             return Err(Error::UnitMismatch);
             return Err(Error::UnitMismatch);
         }
         }
 
 
         // Validate using processor quote amount for currency conversion
         // Validate using processor quote amount for currency conversion
         self.check_melt_request_acceptable(
         self.check_melt_request_acceptable(
-            payment_quote.amount,
-            unit.clone(),
+            payment_quote.amount.clone(),
             PaymentMethod::Known(KnownMethod::Bolt11),
             PaymentMethod::Known(KnownMethod::Bolt11),
             request.to_string(),
             request.to_string(),
             *options,
             *options,
         )
         )
         .await?;
         .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 melt_ttl = self.quote_ttl().await?.melt_ttl;
 
 
         let quote = MeltQuote::new(
         let quote = MeltQuote::new(
@@ -208,8 +206,8 @@ impl Mint {
                 bolt11: request.clone(),
                 bolt11: request.clone(),
             },
             },
             unit.clone(),
             unit.clone(),
-            payment_quote.amount,
-            payment_quote.fee,
+            quote_amount.clone(),
+            quote_fee,
             unix_time() + melt_ttl,
             unix_time() + melt_ttl,
             payment_quote.request_lookup_id.clone(),
             payment_quote.request_lookup_id.clone(),
             *options,
             *options,
@@ -220,7 +218,7 @@ impl Mint {
             "New {} melt quote {} for {} {} with request id {:?}",
             "New {} melt quote {} for {} {} with request id {:?}",
             quote.payment_method,
             quote.payment_method,
             quote.id,
             quote.id,
-            payment_quote.amount,
+            quote_amount,
             unit,
             unit,
             payment_quote.request_lookup_id
             payment_quote.request_lookup_id
         );
         );
@@ -244,18 +242,6 @@ impl Mint {
             options,
             options,
         } = melt_request;
         } = 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
         let ln = self
             .payment_processors
             .payment_processors
             .get(&PaymentProcessorKey::new(
             .get(&PaymentProcessorKey::new(
@@ -293,20 +279,23 @@ impl Mint {
                 err
                 err
             })?;
             })?;
 
 
-        if &payment_quote.unit != unit {
+        if payment_quote.unit() != unit {
             return Err(Error::UnitMismatch);
             return Err(Error::UnitMismatch);
         }
         }
 
 
         // Validate using processor quote amount for currency conversion
         // Validate using processor quote amount for currency conversion
         self.check_melt_request_acceptable(
         self.check_melt_request_acceptable(
-            payment_quote.amount,
-            unit.clone(),
+            payment_quote.amount.clone(),
             PaymentMethod::Known(KnownMethod::Bolt12),
             PaymentMethod::Known(KnownMethod::Bolt12),
             request.clone(),
             request.clone(),
             *options,
             *options,
         )
         )
         .await?;
         .await?;
 
 
+        // Extract values for quote creation
+        let quote_amount = payment_quote.amount;
+        let quote_fee = payment_quote.fee;
+
         let payment_request = MeltPaymentRequest::Bolt12 {
         let payment_request = MeltPaymentRequest::Bolt12 {
             offer: Box::new(offer),
             offer: Box::new(offer),
         };
         };
@@ -314,8 +303,8 @@ impl Mint {
         let quote = MeltQuote::new(
         let quote = MeltQuote::new(
             payment_request,
             payment_request,
             unit.clone(),
             unit.clone(),
-            payment_quote.amount,
-            payment_quote.fee,
+            quote_amount.clone(),
+            quote_fee,
             unix_time() + self.quote_ttl().await?.melt_ttl,
             unix_time() + self.quote_ttl().await?.melt_ttl,
             payment_quote.request_lookup_id.clone(),
             payment_quote.request_lookup_id.clone(),
             *options,
             *options,
@@ -326,7 +315,7 @@ impl Mint {
             "New {} melt quote {} for {} {} with request id {:?}",
             "New {} melt quote {} for {} {} with request id {:?}",
             quote.payment_method,
             quote.payment_method,
             quote.id,
             quote.id,
-            amount,
+            quote_amount,
             unit,
             unit,
             payment_quote.request_lookup_id
             payment_quote.request_lookup_id
         );
         );
@@ -408,15 +397,14 @@ impl Mint {
                 Error::UnsupportedUnit
                 Error::UnsupportedUnit
             })?;
             })?;
 
 
-        if &payment_quote.unit != unit {
+        if payment_quote.unit() != unit {
             return Err(Error::UnitMismatch);
             return Err(Error::UnitMismatch);
         }
         }
 
 
         // For custom methods, we don't validate amount limits upfront since
         // For custom methods, we don't validate amount limits upfront since
         // the payment processor handles method-specific validation
         // the payment processor handles method-specific validation
         self.check_melt_request_acceptable(
         self.check_melt_request_acceptable(
-            payment_quote.amount,
-            unit.clone(),
+            payment_quote.amount.clone(),
             PaymentMethod::from(method.as_str()),
             PaymentMethod::from(method.as_str()),
             request.clone(),
             request.clone(),
             None, // Custom methods don't use options
             None, // Custom methods don't use options
@@ -425,14 +413,18 @@ impl Mint {
 
 
         let melt_ttl = self.quote_ttl().await?.melt_ttl;
         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(
         let quote = MeltQuote::new(
             MeltPaymentRequest::Custom {
             MeltPaymentRequest::Custom {
                 method: method.to_string(),
                 method: method.to_string(),
                 request: request.clone(),
                 request: request.clone(),
             },
             },
             unit.clone(),
             unit.clone(),
-            payment_quote.amount,
-            payment_quote.fee,
+            quote_amount.clone(),
+            quote_fee,
             unix_time() + melt_ttl,
             unix_time() + melt_ttl,
             payment_quote.request_lookup_id.clone(),
             payment_quote.request_lookup_id.clone(),
             None, // Custom methods don't use options
             None, // Custom methods don't use options
@@ -443,7 +435,7 @@ impl Mint {
             "New {} melt quote {} for {} {} with request id {:?}",
             "New {} melt quote {} for {} {} with request id {:?}",
             method,
             method,
             quote.id,
             quote.id,
-            payment_quote.amount,
+            quote_amount,
             unit,
             unit,
             payment_quote.request_lookup_id
             payment_quote.request_lookup_id
         );
         );
@@ -511,11 +503,11 @@ impl Mint {
         let change = (!blind_signatures.is_empty()).then_some(blind_signatures);
         let change = (!blind_signatures.is_empty()).then_some(blind_signatures);
 
 
         let response = MeltQuoteBolt11Response {
         let response = MeltQuoteBolt11Response {
-            quote: quote.id,
+            quote: quote.id.clone(),
             state: quote.state,
             state: quote.state,
             expiry: quote.expiry,
             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,
             payment_preimage: quote.payment_preimage,
             change,
             change,
             request: Some(quote.request.to_string()),
             request: Some(quote.request.to_string()),
@@ -672,8 +664,8 @@ impl Mint {
         // Return immediately with the quote in PENDING state
         // Return immediately with the quote in PENDING state
         Ok(MeltQuoteBolt11Response {
         Ok(MeltQuoteBolt11Response {
             quote: quote_id,
             quote: quote_id,
-            amount: quote.amount,
-            fee_reserve: quote.fee_reserve,
+            amount: quote.amount().into(),
+            fee_reserve: quote.fee_reserve().into(),
             state: quote.state,
             state: quote.state,
             expiry: quote.expiry,
             expiry: quote.expiry,
             payment_preimage: None,
             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::database::{self, Acquired, DynMintDatabase};
 use cdk_common::nuts::{BlindSignature, BlindedMessage, MeltQuoteState, State};
 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 cdk_signatory::signatory::SignatoryKeySet;
 
 
 use crate::mint::subscription::PubSubManager;
 use crate::mint::subscription::PubSubManager;
@@ -183,9 +183,9 @@ pub async fn process_melt_change(
     mint: &super::super::Mint,
     mint: &super::super::Mint,
     db: &DynMintDatabase,
     db: &DynMintDatabase,
     quote_id: &QuoteId,
     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>,
     change_outputs: Vec<BlindedMessage>,
 ) -> Result<
 ) -> Result<
     (
     (
@@ -203,13 +203,16 @@ pub async fn process_melt_change(
         return Ok((None, tx));
         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
     // Get keyset configuration
     let fee_and_amounts = get_keyset_fee_and_amounts(&mint.keysets, &change_outputs);
     let fee_and_amounts = get_keyset_fee_and_amounts(&mint.keysets, &change_outputs);
 
 
     // Split change into denominations
     // 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() {
     if change_outputs.len() < amounts.len() {
         tracing::debug!(
         tracing::debug!(
@@ -363,27 +366,58 @@ pub async fn finalize_melt_core(
     pubsub: &PubSubManager,
     pubsub: &PubSubManager,
     quote: &mut Acquired<MeltQuote>,
     quote: &mut Acquired<MeltQuote>,
     input_ys: &[PublicKey],
     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_preimage: Option<String>,
     payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
     payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
 ) -> Result<(), Error> {
 ) -> Result<(), Error> {
     // Validate quote amount vs payment amount
     // Validate quote amount vs payment amount
-    if quote.amount > total_spent {
+    if quote.amount() > total_spent {
         tracing::error!(
         tracing::error!(
             "Payment amount {} is less than quote amount {} for quote {}",
             "Payment amount {} is less than quote amount {} for quote {}",
             total_spent,
             total_spent,
-            quote.amount,
+            quote.amount(),
             quote.id
             quote.id
         );
         );
         return Err(Error::IncorrectQuoteAmount);
         return Err(Error::IncorrectQuoteAmount);
     }
     }
 
 
     // Validate inputs amount
     // 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
     // Update quote state to Paid
@@ -442,17 +476,12 @@ pub async fn finalize_melt_quote(
     db: &DynMintDatabase,
     db: &DynMintDatabase,
     pubsub: &PubSubManager,
     pubsub: &PubSubManager,
     quote: &MeltQuote,
     quote: &MeltQuote,
-    total_spent: Amount,
+    total_spent: Amount<CurrencyUnit>,
     payment_preimage: Option<String>,
     payment_preimage: Option<String>,
     payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
     payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
 ) -> Result<Option<Vec<BlindSignature>>, Error> {
 ) -> Result<Option<Vec<BlindSignature>>, Error> {
-    use cdk_common::amount::to_unit;
-
     tracing::info!("Finalizing melt quote {}", quote.id);
     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?;
     let mut tx = db.begin_transaction().await?;
 
 
     // Acquire lock on the quote for safe state update
     // Acquire lock on the quote for safe state update
@@ -490,9 +519,9 @@ pub async fn finalize_melt_quote(
         pubsub,
         pubsub,
         &mut locked_quote,
         &mut locked_quote,
         &input_ys,
         &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_preimage.clone(),
         payment_lookup_id,
         payment_lookup_id,
     )
     )

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

@@ -5,7 +5,6 @@ use std::sync::Arc;
 use std::time::Duration;
 use std::time::Duration;
 
 
 use arc_swap::ArcSwap;
 use arc_swap::ArcSwap;
-use cdk_common::amount::to_unit;
 use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
 #[cfg(feature = "auth")]
 #[cfg(feature = "auth")]
 use cdk_common::database::DynMintAuthDatabase;
 use cdk_common::database::DynMintAuthDatabase;
@@ -707,7 +706,7 @@ impl Mint {
         pubsub_manager: &Arc<PubSubManager>,
         pubsub_manager: &Arc<PubSubManager>,
         wait_payment_response: WaitPaymentResponse,
         wait_payment_response: WaitPaymentResponse,
     ) -> Result<(), Error> {
     ) -> Result<(), Error> {
-        if wait_payment_response.payment_amount == Amount::ZERO {
+        if wait_payment_response.payment_amount.value() == 0 {
             tracing::warn!(
             tracing::warn!(
                 "Received payment response with 0 amount with payment id {}.",
                 "Received payment response with 0 amount with payment id {}.",
                 wait_payment_response.payment_id
                 wait_payment_response.payment_id
@@ -748,9 +747,9 @@ impl Mint {
         pubsub_manager: &Arc<PubSubManager>,
         pubsub_manager: &Arc<PubSubManager>,
     ) -> Result<(), Error> {
     ) -> Result<(), Error> {
         tracing::debug!(
         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.payment_amount,
-            wait_payment_response.unit,
+            wait_payment_response.unit(),
             mint_quote.id,
             mint_quote.id,
             wait_payment_response.payment_id.to_string()
             wait_payment_response.payment_id.to_string()
         );
         );
@@ -765,13 +764,11 @@ impl Mint {
             {
             {
                 tracing::info!("Received payment notification for already issued quote.");
                 tracing::info!("Received payment notification for already issued quote.");
             } else {
             } 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.");
                     tracing::error!("Zero amount payments should not be recorded.");
                     return Err(Error::AmountUndefined);
                     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(
     async fn finalize_paid_melt_quote(
         &self,
         &self,
         quote: &MeltQuote,
         quote: &MeltQuote,
-        total_spent: cdk_common::Amount,
+        total_spent: cdk_common::Amount<cdk_common::CurrencyUnit>,
         payment_preimage: Option<String>,
         payment_preimage: Option<String>,
         payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
         payment_lookup_id: &cdk_common::payment::PaymentIdentifier,
     ) -> Result<(), Error> {
     ) -> Result<(), Error> {
@@ -381,7 +381,7 @@ impl Mint {
                                 );
                                 );
 
 
                                 // Get payment info for finalization
                                 // Get payment info for finalization
-                                let total_spent = quote.amount;
+                                let total_spent = quote.amount();
                                 let payment_lookup_id =
                                 let payment_lookup_id =
                                     quote.request_lookup_id.clone().unwrap_or_else(|| {
                                     quote.request_lookup_id.clone().unwrap_or_else(|| {
                                         cdk_common::payment::PaymentIdentifier::CustomId(
                                         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::pub_sub::{Pubsub, Spec, Subscriber};
 use cdk_common::subscription::SubId;
 use cdk_common::subscription::SubId;
 use cdk_common::{
 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;
 use super::Mint;
@@ -193,7 +194,7 @@ impl PubSubManager {
     }
     }
 
 
     /// Helper function to publish even of a mint quote being paid
     /// 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 {
         match mint_quote.payment_method {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Issued);
                 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) => {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.mint_quote_bolt12_status(
                 self.mint_quote_bolt12_status(
                     mint_quote.clone(),
                     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
     /// 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 {
         match mint_quote.payment_method {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
                 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) => {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt12) => {
                 self.mint_quote_bolt12_status(
                 self.mint_quote_bolt12_status(
                     mint_quote.clone(),
                     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::database::DynMintDatabase;
 use cdk_common::mint::{Operation, Saga, SwapSagaState};
 use cdk_common::mint::{Operation, Saga, SwapSagaState};
 use cdk_common::nuts::BlindedMessage;
 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 tokio::sync::Mutex;
 use tracing::instrument;
 use tracing::instrument;
 
 
@@ -150,27 +150,37 @@ impl<'a> SwapSaga<'a, Initial> {
     ) -> Result<SwapSaga<'a, SetupComplete>, Error> {
     ) -> Result<SwapSaga<'a, SetupComplete>, Error> {
         let mut tx = self.db.begin_transaction().await?;
         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
         // Verify balance within the transaction
         self.mint
         self.mint
             .verify_transaction_balanced(
             .verify_transaction_balanced(
-                &mut tx,
                 input_verification.clone(),
                 input_verification.clone(),
+                output_verification.clone(),
                 input_proofs,
                 input_proofs,
-                blinded_messages,
             )
             )
             .await?;
             .await?;
 
 
         // Calculate amounts to create Operation
         // Calculate amounts to create Operation
         let total_redeemed = input_verification.amount;
         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?;
         let fee_breakdown = self.mint.get_proofs_fee(input_proofs).await?;
 
 
         // Create Operation with actual amounts now that we know them
         // Create Operation with actual amounts now that we know them
+        // Convert typed amounts to untyped for Operation::new
         let operation = Operation::new(
         let operation = Operation::new(
             self.state_data.operation_id,
             self.state_data.operation_id,
             cdk_common::mint::OperationKind::Swap,
             cdk_common::mint::OperationKind::Swap,
-            total_issued,
-            total_redeemed,
+            total_issued.clone().into(),
+            total_redeemed.clone().into(),
             fee_breakdown.total,
             fee_breakdown.total,
             None, // complete_at
             None, // complete_at
             None, // payment_method (not applicable for swap)
             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
 /// Helper to create a verification result for testing
 fn create_verification(amount: Amount) -> Verification {
 fn create_verification(amount: Amount) -> Verification {
     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 mint = test_mint.mint();
 
 
     let (alice_secret, alice_pubkey) = create_test_keypair();
     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)
     // Set locktime in the past (already expired)
     let locktime = unix_time() - 3600;
     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();
     let (_carol_secret, carol_pubkey) = create_test_keypair();
 
 
     // After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
     // 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 (_eve_secret, eve_pubkey) = create_test_keypair();
 
 
     let locktime = unix_time() - 100; // Already expired
     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 mint = test_mint.mint();
 
 
     let (alice_secret, alice_pubkey) = create_test_keypair();
     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)
     // Set locktime in the past (already expired)
     let locktime = unix_time() - 3600;
     let locktime = unix_time() - 3600;
@@ -544,7 +544,7 @@ async fn test_p2pk_multisig_locktime() {
     let (_carol_secret, carol_pubkey) = create_test_keypair();
     let (_carol_secret, carol_pubkey) = create_test_keypair();
 
 
     // After locktime: Need 1-of-2 from (Dave, Eve) as refund keys
     // 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 (_eve_secret, eve_pubkey) = create_test_keypair();
 
 
     let locktime = unix_time() - 100; // Already expired
     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 super::{Error, Mint};
 use crate::cdk_database;
 use crate::cdk_database;
 
 
-/// Verification result
+/// Verification result with typed amount
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct Verification {
 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 {
 impl Mint {
@@ -169,7 +167,10 @@ impl Mint {
     }
     }
 
 
     /// Verifies outputs
     /// 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)]
     #[instrument(skip_all)]
     pub async fn verify_outputs(
     pub async fn verify_outputs(
         &self,
         &self,
@@ -177,10 +178,8 @@ impl Mint {
         outputs: &[BlindedMessage],
         outputs: &[BlindedMessage],
     ) -> Result<Verification, Error> {
     ) -> Result<Verification, Error> {
         if outputs.is_empty() {
         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)?;
         Mint::check_outputs_unique(outputs)?;
@@ -188,81 +187,56 @@ impl Mint {
 
 
         let unit = self.verify_outputs_keyset(outputs)?;
         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
     /// 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
     /// **NOTE: This does not check if inputs have been spent
     #[instrument(skip_all)]
     #[instrument(skip_all)]
     pub async fn verify_inputs(&self, inputs: &Proofs) -> Result<Verification, Error> {
     pub async fn verify_inputs(&self, inputs: &Proofs) -> Result<Verification, Error> {
         Mint::check_inputs_unique(inputs)?;
         Mint::check_inputs_unique(inputs)?;
         let unit = self.verify_inputs_keyset(inputs).await?;
         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?;
         self.verify_proofs(inputs.clone()).await?;
 
 
-        Ok(Verification {
-            amount,
-            unit: Some(unit),
-        })
+        Ok(Verification { amount })
     }
     }
 
 
     /// Verify that inputs and outputs are valid and balanced
     /// Verify that inputs and outputs are valid and balanced
     #[instrument(skip_all)]
     #[instrument(skip_all)]
     pub async fn verify_transaction_balanced(
     pub async fn verify_transaction_balanced(
         &self,
         &self,
-        tx: &mut Box<dyn cdk_database::MintTransaction<cdk_database::Error> + Send + Sync>,
         input_verification: Verification,
         input_verification: Verification,
+        output_verification: Verification,
         inputs: &Proofs,
         inputs: &Proofs,
-        outputs: &[BlindedMessage],
     ) -> Result<(), Error> {
     ) -> 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?;
         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!(
             tracing::debug!(
                 "Output unit {:?} does not match input unit {:?}",
                 "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);
             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(
             return Err(Error::TransactionUnbalanced(
-                input_verification.amount.into(),
-                output_verification.amount.into(),
+                input_verification.amount.value(),
+                output_verification.amount.value(),
                 fee_breakdown.total.into(),
                 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 lightning_invoice::Bolt11Invoice;
 use tracing::instrument;
 use tracing::instrument;
 
 
-use crate::amount::to_unit;
 use crate::dhke::construct_proofs;
 use crate::dhke::construct_proofs;
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::{
 use crate::nuts::{
@@ -68,7 +67,9 @@ impl Wallet {
                 .or_else(|| invoice.amount_milli_satoshis())
                 .or_else(|| invoice.amount_milli_satoshis())
                 .ok_or(Error::InvoiceAmountUndefined)?;
                 .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 {
             if quote_res.amount != amount_quote_unit {
                 tracing::warn!(
                 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 lightning::offers::offer::Offer;
 use tracing::instrument;
 use tracing::instrument;
 
 
-use crate::amount::to_unit;
 use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request};
 use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request};
-use crate::{Error, Wallet};
+use crate::{Amount, Error, Wallet};
 
 
 impl Wallet {
 impl Wallet {
     /// Melt Quote for BOLT12 offer
     /// Melt Quote for BOLT12 offer
@@ -38,7 +37,9 @@ impl Wallet {
                 .map(|opt| opt.amount_msat())
                 .map(|opt| opt.amount_msat())
                 .or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok())
                 .or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok())
                 .ok_or(Error::AmountUndefined)?;
                 .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 {
             if quote_res.amount != amount_quote_unit {
                 tracing::warn!(
                 tracing::warn!(