thesimplekid 2 months ago
parent
commit
6a8a5a7941

+ 27 - 4
crates/cdk-cli/src/sub_commands/melt.rs

@@ -3,7 +3,8 @@ use std::io::Write;
 use std::str::FromStr;
 
 use anyhow::{bail, Result};
-use cdk::nuts::CurrencyUnit;
+use cdk::amount::MSAT_IN_SAT;
+use cdk::nuts::{CurrencyUnit, MeltOptions};
 use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey};
 use cdk::Bolt11Invoice;
 use clap::Args;
@@ -15,6 +16,9 @@ pub struct MeltSubCommand {
     /// Currency unit e.g. sat
     #[arg(default_value = "sat")]
     unit: String,
+    /// Mpp
+    #[arg(short, long)]
+    mpp: bool,
 }
 
 pub async fn pay(
@@ -52,14 +56,33 @@ pub async fn pay(
     stdin.read_line(&mut user_input)?;
     let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
 
-    if bolt11
+    let mut options: Option<MeltOptions> = None;
+
+    if sub_command_args.mpp {
+        println!("Enter the amount you would like to pay in sats, for a mpp payment.");
+        let mut user_input = String::new();
+        let stdin = io::stdin();
+        io::stdout().flush().unwrap();
+        stdin.read_line(&mut user_input)?;
+
+        let user_amount = user_input.trim_end().parse::<u64>()?;
+
+        if user_amount
+            .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
+        {
+            bail!("Not enough funds");
+        }
+
+        options = Some(MeltOptions::new_mpp(user_amount * MSAT_IN_SAT));
+    } else if bolt11
         .amount_milli_satoshis()
         .unwrap()
-        .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * 1000_u64))
+        .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
     {
         bail!("Not enough funds");
     }
-    let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
+
+    let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
 
     println!("{:?}", quote);
 

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

@@ -11,7 +11,7 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use async_trait::async_trait;
-use cdk::amount::{to_unit, Amount};
+use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk::cdk_lightning::{
     self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
 };
@@ -196,16 +196,9 @@ impl MintLightning for Cln {
         &self,
         melt_quote_request: &MeltQuoteBolt11Request,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let invoice_amount_msat = melt_quote_request
-            .request
-            .amount_milli_satoshis()
-            .ok_or(Error::UnknownInvoiceAmount)?;
+        let amount = melt_quote_request.amount_msat()?;
 
-        let amount = to_unit(
-            invoice_amount_msat,
-            &CurrencyUnit::Msat,
-            &melt_quote_request.unit,
-        )?;
+        let amount = amount / MSAT_IN_SAT.into();
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -248,11 +241,15 @@ impl MintLightning for Cln {
             }
         }
 
+        let amount_msat = melt_quote
+            .msat_to_pay
+            .map(|a| CLN_Amount::from_msat(a.into()));
+
         let mut cln_client = self.cln_client.lock().await;
         let cln_response = cln_client
             .call(Request::Pay(PayRequest {
                 bolt11: melt_quote.request.to_string(),
-                amount_msat: None,
+                amount_msat,
                 label: None,
                 riskfactor: None,
                 maxfeepercent: None,
@@ -264,9 +261,7 @@ impl MintLightning for Cln {
                 maxfee: max_fee
                     .map(|a| {
                         let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
-                        Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
-                            msat.into(),
-                        ))
+                        Ok::<CLN_Amount, Self::Err>(CLN_Amount::from_msat(msat.into()))
                     })
                     .transpose()?,
                 description: None,
@@ -289,6 +284,7 @@ impl MintLightning for Cln {
                     PayStatus::PENDING => MeltQuoteState::Pending,
                     PayStatus::FAILED => MeltQuoteState::Failed,
                 };
+
                 PayInvoiceResponse {
                     payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
                     payment_lookup_id: pay_response.payment_hash.to_string(),
@@ -301,6 +297,10 @@ impl MintLightning for Cln {
                     unit: melt_quote.unit,
                 }
             }
+            Err(err) => {
+                tracing::error!("Could not pay invoice: {}", err);
+                return Err(Error::ClnRpc(err).into());
+            }
             _ => {
                 tracing::error!(
                     "Error attempting to pay invoice: {}",

+ 4 - 11
crates/cdk-fake-wallet/src/lib.rs

@@ -14,7 +14,7 @@ use std::sync::Arc;
 use async_trait::async_trait;
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::secp256k1::{Secp256k1, SecretKey};
-use cdk::amount::{to_unit, Amount};
+use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk::cdk_lightning::{
     self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
 };
@@ -127,16 +127,9 @@ impl MintLightning for FakeWallet {
         &self,
         melt_quote_request: &MeltQuoteBolt11Request,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let invoice_amount_msat = melt_quote_request
-            .request
-            .amount_milli_satoshis()
-            .ok_or(Error::UnknownInvoiceAmount)?;
-
-        let amount = to_unit(
-            invoice_amount_msat,
-            &CurrencyUnit::Msat,
-            &melt_quote_request.unit,
-        )?;
+        let amount = melt_quote_request.amount_msat()?;
+
+        let amount = amount / MSAT_IN_SAT.into();
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;

+ 0 - 14
crates/cdk-integration-tests/src/init_regtest.rs

@@ -19,7 +19,6 @@ use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
 use ln_regtest_rs::lnd::Lnd;
 use tokio::sync::Notify;
 use tower_http::cors::CorsLayer;
-use tracing_subscriber::EnvFilter;
 
 const BITCOIND_ADDR: &str = "127.0.0.1:18443";
 const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332";
@@ -193,19 +192,6 @@ pub async fn start_cln_mint<D>(addr: &str, port: u16, database: D) -> Result<()>
 where
     D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
 {
-    let default_filter = "debug";
-
-    let sqlx_filter = "sqlx=warn";
-    let hyper_filter = "hyper=warn";
-
-    let env_filter = EnvFilter::new(format!(
-        "{},{},{}",
-        default_filter, sqlx_filter, hyper_filter
-    ));
-
-    // Parse input
-    tracing_subscriber::fmt().with_env_filter(env_filter).init();
-
     let cln_client = init_cln_client().await?;
 
     let cln_backend = create_cln_backend(&cln_client).await?;

+ 15 - 0
crates/cdk-integration-tests/src/main.rs

@@ -8,9 +8,24 @@ use cdk_integration_tests::init_regtest::{
 };
 use cdk_redb::MintRedbDatabase;
 use cdk_sqlite::MintSqliteDatabase;
+use tracing_subscriber::EnvFilter;
 
 #[tokio::main]
 async fn main() -> Result<()> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn";
+    let hyper_filter = "hyper=warn";
+    let h2_filter = "h2=warn";
+
+    let env_filter = EnvFilter::new(format!(
+        "{},{},{},{}",
+        default_filter, sqlx_filter, hyper_filter, h2_filter
+    ));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
     let mut bitcoind = init_bitcoind();
     bitcoind.start_bitcoind()?;
 

+ 1 - 1
crates/cdk-integration-tests/tests/regtest.rs

@@ -77,7 +77,7 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> {
 
     let mint_quote = wallet.mint_quote(100.into(), None).await?;
 
-    lnd_client.pay_invoice(mint_quote.request).await?;
+    lnd_client.pay_invoice(mint_quote.request).await.unwrap();
 
     let proofs = wallet
         .mint(&mint_quote.id, SplitTarget::default(), None)

+ 3 - 10
crates/cdk-lnbits/src/lib.rs

@@ -152,16 +152,9 @@ impl MintLightning for LNbits {
             return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
         }
 
-        let invoice_amount_msat = melt_quote_request
-            .request
-            .amount_milli_satoshis()
-            .ok_or(Error::UnknownInvoiceAmount)?;
-
-        let amount = to_unit(
-            invoice_amount_msat,
-            &CurrencyUnit::Msat,
-            &melt_quote_request.unit,
-        )?;
+        let amount = melt_quote_request.amount_msat()?;
+
+        let amount = amount / MSAT_IN_SAT.into();
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;

+ 15 - 18
crates/cdk-lnd/src/lib.rs

@@ -77,7 +77,7 @@ impl MintLightning for Lnd {
 
     fn get_settings(&self) -> Settings {
         Settings {
-            mpp: true,
+            mpp: false,
             unit: CurrencyUnit::Msat,
             invoice_description: true,
         }
@@ -164,16 +164,9 @@ impl MintLightning for Lnd {
         &self,
         melt_quote_request: &MeltQuoteBolt11Request,
     ) -> Result<PaymentQuoteResponse, Self::Err> {
-        let invoice_amount_msat = melt_quote_request
-            .request
-            .amount_milli_satoshis()
-            .ok_or(Error::UnknownInvoiceAmount)?;
+        let amount = melt_quote_request.amount_msat()?;
 
-        let amount = to_unit(
-            invoice_amount_msat,
-            &CurrencyUnit::Msat,
-            &melt_quote_request.unit,
-        )?;
+        let amount = amount / MSAT_IN_SAT.into();
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -196,11 +189,21 @@ impl MintLightning for Lnd {
     async fn pay_invoice(
         &self,
         melt_quote: mint::MeltQuote,
-        partial_amount: Option<Amount>,
+        _partial_amount: Option<Amount>,
         max_fee: Option<Amount>,
     ) -> Result<PayInvoiceResponse, Self::Err> {
         let payment_request = melt_quote.request;
 
+        let amount_msat: u64 = match melt_quote.msat_to_pay {
+            Some(amount_msat) => amount_msat.into(),
+            None => {
+                let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
+                bolt11
+                    .amount_milli_satoshis()
+                    .ok_or(Error::UnknownInvoiceAmount)?
+            }
+        };
+
         let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
             payment_request,
             fee_limit: max_fee.map(|f| {
@@ -208,13 +211,7 @@ impl MintLightning for Lnd {
 
                 FeeLimit { limit: Some(limit) }
             }),
-            amt_msat: partial_amount
-                .map(|a| {
-                    let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat).unwrap();
-
-                    u64::from(msat) as i64
-                })
-                .unwrap_or_default(),
+            amt_msat: amount_msat as i64,
             ..Default::default()
         };
 

+ 8 - 12
crates/cdk-phoenixd/src/lib.rs

@@ -81,7 +81,6 @@ impl MintLightning for Phoenixd {
             invoice_description: true,
         }
     }
-
     fn is_wait_invoice_active(&self) -> bool {
         self.wait_invoice_is_active.load(Ordering::SeqCst)
     }
@@ -162,16 +161,9 @@ impl MintLightning for Phoenixd {
             return Err(Error::UnsupportedUnit.into());
         }
 
-        let invoice_amount_msat = melt_quote_request
-            .request
-            .amount_milli_satoshis()
-            .ok_or(Error::UnknownInvoiceAmount)?;
+        let amount = melt_quote_request.amount_msat()?;
 
-        let amount = to_unit(
-            invoice_amount_msat,
-            &CurrencyUnit::Msat,
-            &melt_quote_request.unit,
-        )?;
+        let amount = amount / MSAT_IN_SAT.into();
 
         let relative_fee_reserve =
             (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -197,12 +189,16 @@ impl MintLightning for Phoenixd {
     async fn pay_invoice(
         &self,
         melt_quote: mint::MeltQuote,
-        partial_amount: Option<Amount>,
+        _partial_amount: Option<Amount>,
         _max_fee_msats: Option<Amount>,
     ) -> Result<PayInvoiceResponse, Self::Err> {
+        let msat_to_pay: Option<u64> = melt_quote
+            .msat_to_pay
+            .map(|a| <cdk::Amount as Into<u64>>::into(a) / MSAT_IN_SAT);
+
         let pay_response = self
             .phoenixd_api
-            .pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into()))
+            .pay_bolt11_invoice(&melt_quote.request, msat_to_pay)
             .await?;
 
         // The pay invoice response does not give the needed fee info so we have to check.

+ 1 - 0
crates/cdk-sqlite/src/mint/migrations/20250103201327_amount_to_pay_msats.sql

@@ -0,0 +1 @@
+ALTER TABLE melt_quote ADD COLUMN msat_to_pay INTEGER;

+ 8 - 12
crates/cdk-sqlite/src/mint/mod.rs

@@ -468,8 +468,8 @@ WHERE id=?
         let res = sqlx::query(
             r#"
 INSERT OR REPLACE INTO melt_quote
-(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id)
-VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
+(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id, msat_to_pay)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
         "#,
         )
         .bind(quote.id.to_string())
@@ -481,6 +481,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
         .bind(quote.expiry as i64)
         .bind(quote.payment_preimage)
         .bind(quote.request_lookup_id)
+        .bind(quote.msat_to_pay.map(|a| u64::from(a) as i64))
         .execute(&mut transaction)
         .await;
 
@@ -804,11 +805,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
             .map(|row| {
                 PublicKey::from_slice(row.get("y"))
                     .map_err(Error::from)
-                    .and_then(|y| {
-                        sqlite_row_to_proof(row)
-                            .map_err(Error::from)
-                            .map(|proof| (y, proof))
-                    })
+                    .and_then(|y| sqlite_row_to_proof(row).map(|proof| (y, proof)))
             })
             .collect::<Result<HashMap<_, _>, _>>()?;
 
@@ -1060,11 +1057,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
             .map(|row| {
                 PublicKey::from_slice(row.get("y"))
                     .map_err(Error::from)
-                    .and_then(|y| {
-                        sqlite_row_to_blind_signature(row)
-                            .map_err(Error::from)
-                            .map(|blinded| (y, blinded))
-                    })
+                    .and_then(|y| sqlite_row_to_blind_signature(row).map(|blinded| (y, blinded)))
             })
             .collect::<Result<HashMap<_, _>, _>>()?;
 
@@ -1307,6 +1300,8 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<mint::MeltQuote, Error> {
 
     let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone());
 
+    let row_msat_to_pay: Option<i64> = row.try_get("msat_to_pay").map_err(Error::from)?;
+
     Ok(mint::MeltQuote {
         id: row_id.into_uuid(),
         amount: Amount::from(row_amount as u64),
@@ -1317,6 +1312,7 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<mint::MeltQuote, Error> {
         expiry: row_expiry as u64,
         payment_preimage: row_preimage,
         request_lookup_id,
+        msat_to_pay: row_msat_to_pay.map(|a| Amount::from(a as u64)),
     })
 }
 

+ 2 - 0
crates/cdk-strike/Cargo.toml

@@ -22,3 +22,5 @@ tracing = { version = "0.1", default-features = false, features = ["attributes",
 thiserror = "1"
 uuid = { version = "1", features = ["v4"] }
 strike-rs = "0.4.0"
+# strike-rs = { path = "../../../../strike-rs" }
+# strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "577ad9591" }

+ 3 - 1
crates/cdk-strike/src/lib.rs

@@ -167,9 +167,11 @@ impl MintLightning for Strike {
 
         let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?;
 
+        let amount = from_strike_amount(quote.amount, &melt_quote_request.unit)?.into();
+
         Ok(PaymentQuoteResponse {
             request_lookup_id: quote.payment_quote_id,
-            amount: from_strike_amount(quote.amount, &melt_quote_request.unit)?.into(),
+            amount,
             fee: fee.into(),
             state: MeltQuoteState::Unpaid,
         })

+ 0 - 2
crates/cdk/src/cdk_database/mint_memory.rs

@@ -367,8 +367,6 @@ impl MintDatabase for MintMemoryDatabase {
         if let Some(quote_id) = quote_id {
             let mut current_quote_signatures = self.quote_signatures.write().await;
             current_quote_signatures.insert(quote_id, blind_signatures.to_vec());
-            let t = current_quote_signatures.get(&quote_id);
-            println!("after insert: {:?}", t);
         }
 
         Ok(())

+ 1 - 2
crates/cdk/src/cdk_database/wallet_memory.rs

@@ -96,8 +96,7 @@ impl WalletDatabase for WalletMemoryDatabase {
     ) -> Result<(), Self::Err> {
         let proofs = self
             .get_proofs(Some(old_mint_url), None, None, None)
-            .await
-            .map_err(Error::from)?;
+            .await?;
 
         // Update proofs
         {

+ 3 - 0
crates/cdk/src/cdk_lightning/mod.rs

@@ -41,6 +41,9 @@ pub enum Error {
     /// Amount Error
     #[error(transparent)]
     Amount(#[from] crate::amount::Error),
+    /// NUT05 Error
+    #[error(transparent)]
+    NUT05(#[from] crate::nuts::nut05::Error),
 }
 
 /// MintLighting Trait

+ 4 - 1
crates/cdk/src/error.rs

@@ -48,6 +48,9 @@ pub enum Error {
     /// Witness missing or invalid
     #[error("Signature missing or invalid")]
     SignatureMissingOrInvalid,
+    /// Amountless Invoice Not supported
+    #[error("Amount Less Invoice is not allowed")]
+    AmountLessNotAllowed,
 
     // Mint Errors
     /// Minting is disabled
@@ -60,7 +63,7 @@ pub enum Error {
     #[error("Expired quote: Expired: `{0}`, Time: `{1}`")]
     ExpiredQuote(u64, u64),
     /// Amount is outside of allowed range
-    #[error("Amount but be between `{0}` and `{1}` is `{2}`")]
+    #[error("Amount must be between `{0}` and `{1}` is `{2}`")]
     AmountOutofLimitRange(Amount, Amount, Amount),
     /// Quote is not paiud
     #[error("Quote not paid")]

+ 21 - 17
crates/cdk/src/mint/melt.rs

@@ -60,21 +60,14 @@ impl Mint {
             request,
             unit,
             options: _,
+            ..
         } = melt_request;
 
-        let amount = match melt_request.options {
-            Some(mpp_amount) => mpp_amount.amount,
-            None => {
-                let amount_msat = request
-                    .amount_milli_satoshis()
-                    .ok_or(Error::InvoiceAmountUndefined)?;
+        let amount_msats = melt_request.amount_msat()?;
 
-                to_unit(amount_msat, &CurrencyUnit::Msat, unit)
-                    .map_err(|_err| Error::UnsupportedUnit)?
-            }
-        };
+        let amount_quote_unit = to_unit(amount_msats, &CurrencyUnit::Msat, unit)?;
 
-        self.check_melt_request_acceptable(amount, unit.clone(), PaymentMethod::Bolt11)?;
+        self.check_melt_request_acceptable(amount_quote_unit, unit.clone(), PaymentMethod::Bolt11)?;
 
         let ln = self
             .ln
@@ -95,6 +88,14 @@ impl Mint {
             Error::UnitUnsupported
         })?;
 
+        // We only want to set the msats_to_pay of the melt quote if the invoice is amountless
+        // or we want to ignore the amount and do an mpp payment
+        let msats_to_pay = if request.amount_milli_satoshis().is_some() {
+            None
+        } else {
+            Some(amount_msats)
+        };
+
         let quote = MeltQuote::new(
             request.to_string(),
             unit.clone(),
@@ -102,12 +103,13 @@ impl Mint {
             payment_quote.fee,
             unix_time() + self.config.quote_ttl().melt_ttl,
             payment_quote.request_lookup_id.clone(),
+            msats_to_pay,
         );
 
         tracing::debug!(
             "New melt quote {} for {} {} with request id {}",
             quote.id,
-            amount,
+            amount_quote_unit,
             unit,
             payment_quote.request_lookup_id
         );
@@ -182,10 +184,13 @@ impl Mint {
         let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
             .expect("Quote unit is checked above that it can convert to msat");
 
-        let invoice_amount_msats: Amount = invoice
-            .amount_milli_satoshis()
-            .ok_or(Error::InvoiceAmountUndefined)?
-            .into();
+        let invoice_amount_msats: Amount = match melt_quote.msat_to_pay {
+            Some(amount) => amount,
+            None => invoice
+                .amount_milli_satoshis()
+                .ok_or(Error::InvoiceAmountUndefined)?
+                .into(),
+        };
 
         let partial_amount = match invoice_amount_msats > quote_msats {
             true => {
@@ -582,7 +587,6 @@ impl Mint {
 
         Ok(res)
     }
-
     /// Process melt request marking [`Proofs`] as spent
     /// The melt request must be verifyed using [`Self::verify_melt_request`]
     /// before calling [`Self::process_melt_request`]

+ 1 - 4
crates/cdk/src/mint/mod.rs

@@ -591,10 +591,7 @@ fn create_new_keyset<C: secp256k1::Signing>(
 }
 
 fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option<DerivationPath> {
-    let unit_index = match unit.derivation_index() {
-        Some(index) => index,
-        None => return None,
-    };
+    let unit_index = unit.derivation_index()?;
 
     Some(DerivationPath::from(vec![
         ChildNumber::from_hardened_idx(0).expect("0 is a valid index"),

+ 6 - 0
crates/cdk/src/mint/types.rs

@@ -79,6 +79,10 @@ pub struct MeltQuote {
     pub payment_preimage: Option<String>,
     /// Value used by ln backend to look up state of request
     pub request_lookup_id: String,
+    /// Msat to pay
+    ///
+    /// Used for an amountless invoice
+    pub msat_to_pay: Option<Amount>,
 }
 
 impl MeltQuote {
@@ -90,6 +94,7 @@ impl MeltQuote {
         fee_reserve: Amount,
         expiry: u64,
         request_lookup_id: String,
+        msat_to_pay: Option<Amount>,
     ) -> Self {
         let id = Uuid::new_v4();
 
@@ -103,6 +108,7 @@ impl MeltQuote {
             expiry,
             payment_preimage: None,
             request_lookup_id,
+            msat_to_pay,
         }
     }
 }

+ 3 - 3
crates/cdk/src/nuts/mod.rs

@@ -25,7 +25,7 @@ pub mod nut20;
 
 pub use nut00::{
     BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
-    Proofs, Token, TokenV3, TokenV4, Witness,
+    Proofs, ProofsMethods, Token, TokenV3, TokenV4, Witness,
 };
 pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey};
 #[cfg(feature = "mint")]
@@ -39,8 +39,8 @@ pub use nut04::{
     MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
 };
 pub use nut05::{
-    MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
-    QuoteState as MeltQuoteState, Settings as NUT05Settings,
+    MeltBolt11Request, MeltMethodSettings, MeltOptions, MeltQuoteBolt11Request,
+    MeltQuoteBolt11Response, QuoteState as MeltQuoteState, Settings as NUT05Settings,
 };
 pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
 pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};

+ 0 - 1
crates/cdk/src/nuts/nut00/mod.rs

@@ -47,7 +47,6 @@ impl ProofsMethods for Proofs {
         self.iter()
             .map(|p| p.y())
             .collect::<Result<Vec<PublicKey>, _>>()
-            .map_err(Into::into)
     }
 }
 

+ 63 - 1
crates/cdk/src/nuts/nut05.rs

@@ -28,6 +28,12 @@ pub enum Error {
     /// Amount overflow
     #[error("Amount Overflow")]
     AmountOverflow,
+    /// Invalid Amount
+    #[error("Invalid Request")]
+    InvalidAmountRequest,
+    /// Unsupported unit
+    #[error("Unsupported unit")]
+    UnsupportedUnit,
 }
 
 /// Melt quote request [NUT-05]
@@ -40,7 +46,63 @@ pub struct MeltQuoteBolt11Request {
     /// Unit wallet would like to pay with
     pub unit: CurrencyUnit,
     /// Payment Options
-    pub options: Option<Mpp>,
+    pub options: Option<MeltOptions>,
+}
+
+/// Melt Options
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(untagged)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum MeltOptions {
+    /// Mpp Options
+    Mpp {
+        /// MPP
+        mpp: Mpp,
+    },
+}
+
+impl MeltOptions {
+    /// Create new [`Options::Mpp`]
+    pub fn new_mpp<A>(amount: A) -> Self
+    where
+        A: Into<Amount>,
+    {
+        Self::Mpp {
+            mpp: Mpp {
+                amount: amount.into(),
+            },
+        }
+    }
+
+    /// Payment amount
+    pub fn amount_msat(&self) -> Amount {
+        match self {
+            Self::Mpp { mpp } => mpp.amount,
+        }
+    }
+}
+
+impl MeltQuoteBolt11Request {
+    /// Amount from [`MeltQuoteBolt11Request`]
+    ///
+    /// Amount can either be defined in the bolt11 invoice,
+    /// in the request for an amountless bolt11 or in MPP option.
+    pub fn amount_msat(&self) -> Result<Amount, Error> {
+        let MeltQuoteBolt11Request {
+            request,
+            unit: _,
+            options,
+            ..
+        } = self;
+
+        match options {
+            None => Ok(request
+                .amount_milli_satoshis()
+                .ok_or(Error::InvalidAmountRequest)?
+                .into()),
+            Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
+        }
+    }
 }
 
 /// Possible states of a quote

+ 1 - 0
crates/cdk/src/types.rs

@@ -40,6 +40,7 @@ impl Melted {
             Some(change_proofs) => change_proofs.total_amount()?,
             None => Amount::ZERO,
         };
+
         let fee_paid = proofs_amount
             .checked_sub(amount + change_amount)
             .ok_or(Error::AmountOverflow)?;

+ 1 - 1
crates/cdk/src/util/hex.rs

@@ -85,7 +85,7 @@ where
     for i in (0..len).step_by(2) {
         let high = val(hex[i], i)?;
         let low = val(hex[i + 1], i + 1)?;
-        bytes.push(high << 4 | low);
+        bytes.push((high << 4) | low);
     }
 
     Ok(bytes)

+ 16 - 16
crates/cdk/src/wallet/melt.rs

@@ -4,15 +4,15 @@ use lightning_invoice::Bolt11Invoice;
 use tracing::instrument;
 
 use super::MeltQuote;
+use crate::amount::to_unit;
 use crate::dhke::construct_proofs;
-use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::{
-    CurrencyUnit, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, Mpp,
-    PreMintSecrets, Proofs, State,
+    CurrencyUnit, MeltBolt11Request, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    PreMintSecrets, Proofs, ProofsMethods, State,
 };
 use crate::types::{Melted, ProofInfo};
 use crate::util::unix_time;
-use crate::{Amount, Error, Wallet};
+use crate::{Error, Wallet};
 
 impl Wallet {
     /// Melt Quote
@@ -43,21 +43,16 @@ impl Wallet {
     pub async fn melt_quote(
         &self,
         request: String,
-        mpp: Option<Amount>,
+        options: Option<MeltOptions>,
     ) -> Result<MeltQuote, Error> {
         let invoice = Bolt11Invoice::from_str(&request)?;
 
-        let request_amount = invoice
-            .amount_milli_satoshis()
+        let amount_msat = options
+            .map(|opt| opt.amount_msat().into())
+            .or_else(|| invoice.amount_milli_satoshis())
             .ok_or(Error::InvoiceAmountUndefined)?;
 
-        let amount = match self.unit {
-            CurrencyUnit::Sat => Amount::from(request_amount / 1000),
-            CurrencyUnit::Msat => Amount::from(request_amount),
-            _ => return Err(Error::UnitUnsupported),
-        };
-
-        let options = mpp.map(|amount| Mpp { amount });
+        let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit).unwrap();
 
         let quote_request = MeltQuoteBolt11Request {
             request: Bolt11Invoice::from_str(&request)?,
@@ -67,13 +62,18 @@ impl Wallet {
 
         let quote_res = self.client.post_melt_quote(quote_request).await?;
 
-        if quote_res.amount != amount {
+        if quote_res.amount != amount_quote_unit {
+            tracing::warn!(
+                "Mint returned incorrect quote amount. Expected {}, got {}",
+                amount_quote_unit,
+                quote_res.amount
+            );
             return Err(Error::IncorrectQuoteAmount);
         }
 
         let quote = MeltQuote {
             id: quote_res.quote,
-            amount,
+            amount: amount_quote_unit,
             request,
             unit: self.unit.clone(),
             fee_reserve: quote_res.fee_reserve,

+ 3 - 2
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -16,7 +16,7 @@ use super::types::SendKind;
 use super::Error;
 use crate::amount::SplitTarget;
 use crate::mint_url::MintUrl;
-use crate::nuts::{CurrencyUnit, Proof, Proofs, SecretKey, SpendingConditions, Token};
+use crate::nuts::{CurrencyUnit, MeltOptions, Proof, Proofs, SecretKey, SpendingConditions, Token};
 use crate::types::Melted;
 use crate::wallet::types::MintQuote;
 use crate::{Amount, Wallet};
@@ -281,6 +281,7 @@ impl MultiMintWallet {
     pub async fn pay_invoice_for_wallet(
         &self,
         bolt11: &str,
+        options: Option<MeltOptions>,
         wallet_key: &WalletKey,
         max_fee: Option<Amount>,
     ) -> Result<Melted, Error> {
@@ -289,7 +290,7 @@ impl MultiMintWallet {
             .await
             .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
 
-        let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
+        let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
         if let Some(max_fee) = max_fee {
             if quote.fee_reserve > max_fee {
                 return Err(Error::MaxFeeExceeded);