Browse Source

refactor: ln backends within mint

thesimplekid 6 months ago
parent
commit
008c913583

+ 2 - 20
crates/cdk-axum/src/lib.rs

@@ -3,34 +3,19 @@
 #![warn(missing_docs)]
 #![warn(rustdoc::bare_urls)]
 
-use std::collections::HashMap;
-use std::str::FromStr;
 use std::sync::Arc;
 
 use anyhow::Result;
 use axum::routing::{get, post};
 use axum::Router;
-use cdk::cdk_lightning::{self, MintLightning};
 use cdk::mint::Mint;
-use cdk::mint_url::MintUrl;
-use cdk::types::LnKey;
 use router_handlers::*;
 
 mod router_handlers;
 
 /// Create mint [`Router`] with required endpoints for cashu mint
-pub async fn create_mint_router(
-    mint_url: &str,
-    mint: Arc<Mint>,
-    ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
-    quote_ttl: u64,
-) -> Result<Router> {
-    let state = MintState {
-        ln,
-        mint,
-        mint_url: MintUrl::from_str(mint_url)?,
-        quote_ttl,
-    };
+pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
+    let state = MintState { mint };
 
     let v1_router = Router::new()
         .route("/keys", get(get_keys))
@@ -61,8 +46,5 @@ pub async fn create_mint_router(
 /// CDK Mint State
 #[derive(Clone)]
 pub struct MintState {
-    ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
     mint: Arc<Mint>,
-    mint_url: MintUrl,
-    quote_ttl: u64,
 }

+ 16 - 280
crates/cdk-axum/src/router_handlers.rs

@@ -1,21 +1,18 @@
-use anyhow::{bail, Result};
+use anyhow::Result;
 use axum::extract::{Json, Path, State};
 use axum::http::StatusCode;
 use axum::response::{IntoResponse, Response};
-use cdk::cdk_lightning::{to_unit, MintLightning, PayInvoiceResponse};
-use cdk::error::{Error, ErrorResponse};
-use cdk::mint::MeltQuote;
+use cdk::error::ErrorResponse;
 use cdk::nuts::nut05::MeltBolt11Response;
 use cdk::nuts::{
-    CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
-    MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteState,
-    MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest,
-    SwapResponse,
+    CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
+    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
+    MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
+    SwapRequest, SwapResponse,
 };
 use cdk::util::unix_time;
 
-use crate::{LnKey, MintState};
+use crate::MintState;
 
 pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysResponse>, Response> {
     let pubkeys = state.mint.pubkeys().await.map_err(|err| {
@@ -51,52 +48,13 @@ pub async fn get_mint_bolt11_quote(
     State(state): State<MintState>,
     Json(payload): Json<MintQuoteBolt11Request>,
 ) -> Result<Json<MintQuoteBolt11Response>, Response> {
-    let ln = state
-        .ln
-        .get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
-        .ok_or_else(|| {
-            tracing::info!("Bolt11 mint request for unsupported unit");
-
-            into_response(Error::UnitUnsupported)
-        })?;
-
-    let quote_expiry = unix_time() + state.quote_ttl;
-
-    if payload.description.is_some() && !ln.get_settings().invoice_description {
-        tracing::error!("Backend does not support invoice description");
-        return Err(into_response(Error::InvoiceDescriptionUnsupported));
-    }
-
-    let create_invoice_response = ln
-        .create_invoice(
-            payload.amount,
-            &payload.unit,
-            payload.description.unwrap_or("".to_string()),
-            quote_expiry,
-        )
-        .await
-        .map_err(|err| {
-            tracing::error!("Could not create invoice: {}", err);
-            into_response(Error::InvalidPaymentRequest)
-        })?;
-
     let quote = state
         .mint
-        .new_mint_quote(
-            state.mint_url,
-            create_invoice_response.request.to_string(),
-            payload.unit,
-            payload.amount,
-            create_invoice_response.expiry.unwrap_or(0),
-            create_invoice_response.request_lookup_id,
-        )
+        .get_mint_bolt11_quote(payload)
         .await
-        .map_err(|err| {
-            tracing::error!("Could not create new mint quote: {}", err);
-            into_response(err)
-        })?;
+        .map_err(into_response)?;
 
-    Ok(Json(quote.into()))
+    Ok(Json(quote))
 }
 
 pub async fn get_check_mint_bolt11_quote(
@@ -135,42 +93,13 @@ pub async fn get_melt_bolt11_quote(
     State(state): State<MintState>,
     Json(payload): Json<MeltQuoteBolt11Request>,
 ) -> Result<Json<MeltQuoteBolt11Response>, Response> {
-    let ln = state
-        .ln
-        .get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
-        .ok_or_else(|| {
-            tracing::info!("Could not get ln backend for {}, bolt11 ", payload.unit);
-
-            into_response(Error::UnitUnsupported)
-        })?;
-
-    let payment_quote = ln.get_payment_quote(&payload).await.map_err(|err| {
-        tracing::error!(
-            "Could not get payment quote for mint quote, {} bolt11, {}",
-            payload.unit,
-            err
-        );
-
-        into_response(Error::UnitUnsupported)
-    })?;
-
     let quote = state
         .mint
-        .new_melt_quote(
-            payload.request.to_string(),
-            payload.unit,
-            payment_quote.amount,
-            payment_quote.fee,
-            unix_time() + state.quote_ttl,
-            payment_quote.request_lookup_id,
-        )
+        .get_melt_bolt11_quote(&payload)
         .await
-        .map_err(|err| {
-            tracing::error!("Could not create melt quote: {}", err);
-            into_response(err)
-        })?;
+        .map_err(into_response)?;
 
-    Ok(Json(quote.into()))
+    Ok(Json(quote))
 }
 
 pub async fn get_check_melt_bolt11_quote(
@@ -193,206 +122,13 @@ pub async fn post_melt_bolt11(
     State(state): State<MintState>,
     Json(payload): Json<MeltBolt11Request>,
 ) -> Result<Json<MeltBolt11Response>, Response> {
-    use std::sync::Arc;
-    async fn check_payment_state(
-        ln: Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Send + Sync>,
-        melt_quote: &MeltQuote,
-    ) -> Result<PayInvoiceResponse> {
-        match ln
-            .check_outgoing_payment(&melt_quote.request_lookup_id)
-            .await
-        {
-            Ok(response) => Ok(response),
-            Err(check_err) => {
-                // If we cannot check the status of the payment we keep the proofs stuck as pending.
-                tracing::error!(
-                    "Could not check the status of payment for {},. Proofs stuck as pending",
-                    melt_quote.id
-                );
-                tracing::error!("Checking payment error: {}", check_err);
-                bail!("Could not check payment status")
-            }
-        }
-    }
-
-    let quote = match state.mint.verify_melt_request(&payload).await {
-        Ok(quote) => quote,
-        Err(err) => {
-            tracing::debug!("Error attempting to verify melt quote: {}", err);
-
-            if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                tracing::error!(
-                    "Could not reset melt quote {} state: {}",
-                    payload.quote,
-                    err
-                );
-            }
-            return Err(into_response(err));
-        }
-    };
-
-    let settled_internally_amount =
-        match state.mint.handle_internal_melt_mint(&quote, &payload).await {
-            Ok(amount) => amount,
-            Err(err) => {
-                tracing::error!("Attempting to settle internally failed");
-                if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                    tracing::error!(
-                        "Could not reset melt quote {} state: {}",
-                        payload.quote,
-                        err
-                    );
-                }
-                return Err(into_response(err));
-            }
-        };
-
-    let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
-        Some(amount_spent) => (None, amount_spent),
-        None => {
-            // If the quote unit is SAT or MSAT we can check that the expected fees are
-            // provided. We also check if the quote is less then the invoice
-            // amount in the case that it is a mmp However, if the quote is not
-            // of a bitcoin unit we cannot do these checks as the mint
-            // is unaware of a conversion rate. In this case it is assumed that the quote is
-            // correct and the mint should pay the full invoice amount if inputs
-            // > `then quote.amount` are included. This is checked in the
-            // `verify_melt` method.
-            let partial_amount = match quote.unit {
-                CurrencyUnit::Sat | CurrencyUnit::Msat => {
-                    match state
-                        .mint
-                        .check_melt_expected_ln_fees(&quote, &payload)
-                        .await
-                    {
-                        Ok(amount) => amount,
-                        Err(err) => {
-                            tracing::error!("Fee is not expected: {}", err);
-                            if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                                tracing::error!("Could not reset melt quote state: {}", err);
-                            }
-                            return Err(into_response(Error::Internal));
-                        }
-                    }
-                }
-                _ => None,
-            };
-
-            let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
-                Some(ln) => ln,
-                None => {
-                    tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
-                    if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                        tracing::error!("Could not reset melt quote state: {}", err);
-                    }
-
-                    return Err(into_response(Error::UnitUnsupported));
-                }
-            };
-
-            let pre = match ln
-                .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
-                .await
-            {
-                Ok(pay)
-                    if pay.status == MeltQuoteState::Unknown
-                        || pay.status == MeltQuoteState::Failed =>
-                {
-                    let check_response = check_payment_state(Arc::clone(ln), &quote)
-                        .await
-                        .map_err(|_| into_response(Error::Internal))?;
-
-                    if check_response.status == MeltQuoteState::Paid {
-                        tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
-
-                        return Err(into_response(Error::Internal));
-                    }
-
-                    check_response
-                }
-                Ok(pay) => pay,
-                Err(err) => {
-                    // If the error is that the invoice was already paid we do not want to hold
-                    // hold the proofs as pending to we reset them  and return an error.
-                    if matches!(err, cdk::cdk_lightning::Error::InvoiceAlreadyPaid) {
-                        tracing::debug!("Invoice already paid, resetting melt quote");
-                        if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                            tracing::error!("Could not reset melt quote state: {}", err);
-                        }
-                        return Err(into_response(Error::RequestAlreadyPaid));
-                    }
-
-                    tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
-
-                    let check_response = check_payment_state(Arc::clone(ln), &quote)
-                        .await
-                        .map_err(|_| into_response(Error::Internal))?;
-                    // If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
-                    if check_response.status == MeltQuoteState::Paid {
-                        tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
-
-                        return Err(into_response(Error::Internal));
-                    }
-                    check_response
-                }
-            };
-
-            match pre.status {
-                MeltQuoteState::Paid => (),
-                MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
-                    tracing::info!("Lightning payment for quote {} failed.", payload.quote);
-                    if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                        tracing::error!("Could not reset melt quote state: {}", err);
-                    }
-                    return Err(into_response(Error::PaymentFailed));
-                }
-                MeltQuoteState::Pending => {
-                    tracing::warn!(
-                        "LN payment pending, proofs are stuck as pending for quote: {}",
-                        payload.quote
-                    );
-                    return Err(into_response(Error::PendingQuote));
-                }
-            }
-
-            // Convert from unit of backend to quote unit
-            // Note: this should never fail since these conversions happen earlier and would fail there.
-            // Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
-            let amount_spent = to_unit(pre.total_spent, &pre.unit, &quote.unit).unwrap_or_default();
-
-            let payment_lookup_id = pre.payment_lookup_id;
-
-            if payment_lookup_id != quote.request_lookup_id {
-                tracing::info!(
-                    "Payment lookup id changed post payment from {} to {}",
-                    quote.request_lookup_id,
-                    payment_lookup_id
-                );
-
-                let mut melt_quote = quote;
-                melt_quote.request_lookup_id = payment_lookup_id;
-
-                if let Err(err) = state.mint.localstore.add_melt_quote(melt_quote).await {
-                    tracing::warn!("Could not update payment lookup id: {}", err);
-                }
-            }
-
-            (pre.payment_preimage, amount_spent)
-        }
-    };
-
-    // If we made it here the payment has been made.
-    // We process the melt burning the inputs and returning change
     let res = state
         .mint
-        .process_melt_request(&payload, preimage, amount_spent_quote_unit)
+        .melt_bolt11(&payload)
         .await
-        .map_err(|err| {
-            tracing::error!("Could not process melt request: {}", err);
-            into_response(err)
-        })?;
+        .map_err(into_response)?;
 
-    Ok(Json(res.into()))
+    Ok(Json(res))
 }
 
 pub async fn post_check(

+ 17 - 39
crates/cdk-integration-tests/src/init_fake_wallet.rs

@@ -13,11 +13,11 @@ use cdk::{
     types::LnKey,
 };
 use cdk_fake_wallet::FakeWallet;
-use futures::StreamExt;
+use tokio::sync::Notify;
 use tower_http::cors::CorsLayer;
 use tracing_subscriber::EnvFilter;
 
-use crate::{handle_paid_invoice, init_regtest::create_mint};
+use crate::init_regtest::create_mint;
 
 pub async fn start_fake_mint<D>(addr: &str, port: u16, database: D) -> Result<()>
 where
@@ -36,7 +36,10 @@ where
     // Parse input
     tracing_subscriber::fmt().with_env_filter(env_filter).init();
 
-    let mint = create_mint(database).await?;
+    let mut ln_backends: HashMap<
+        LnKey,
+        Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
+    > = HashMap::new();
 
     let fee_reserve = FeeReserve {
         min_fee_reserve: 1.into(),
@@ -52,28 +55,18 @@ where
         0,
     );
 
-    let mut ln_backends: HashMap<
-        LnKey,
-        Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
-    > = HashMap::new();
-
     ln_backends.insert(
         LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11),
         Arc::new(fake_wallet),
     );
 
-    let quote_ttl = 100000;
+    let mint = create_mint(database, ln_backends.clone()).await?;
 
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(
-        &format!("http://{}:{}", addr, port),
-        Arc::clone(&mint_arc),
-        ln_backends.clone(),
-        quote_ttl,
-    )
-    .await
-    .unwrap();
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
+        .await
+        .unwrap();
 
     let mint_service = Router::new()
         .merge(v1_service)
@@ -81,28 +74,13 @@ where
 
     let mint = Arc::clone(&mint_arc);
 
-    for wallet in ln_backends.values() {
-        let wallet_clone = Arc::clone(wallet);
-        let mint = Arc::clone(&mint);
-        tokio::spawn(async move {
-            match wallet_clone.wait_any_invoice().await {
-                Ok(mut stream) => {
-                    while let Some(request_lookup_id) = stream.next().await {
-                        if let Err(err) =
-                            handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
-                        {
-                            // nosemgrep: direct-panic
-                            panic!("{:?}", err);
-                        }
-                    }
-                }
-                Err(err) => {
-                    // nosemgrep: direct-panic
-                    panic!("Could not get invoice stream: {}", err);
-                }
-            }
-        });
-    }
+    let shutdown = Arc::new(Notify::new());
+
+    tokio::spawn({
+        let shutdown = Arc::clone(&shutdown);
+        async move { mint.wait_for_paid_invoices(shutdown).await }
+    });
+
     println!("Staring Axum server");
     axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
         .serve(mint_service.into_make_service())

+ 24 - 54
crates/cdk-integration-tests/src/init_regtest.rs

@@ -8,14 +8,14 @@ use cdk::{
     cdk_lightning::MintLightning,
     mint::{FeeReserve, Mint},
     nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings},
-    types::LnKey,
+    types::{LnKey, QuoteTTL},
 };
 use cdk_cln::Cln as CdkCln;
-use futures::StreamExt;
 use ln_regtest_rs::{
     bitcoin_client::BitcoinClient, bitcoind::Bitcoind, cln::Clnd, cln_client::ClnClient, lnd::Lnd,
     lnd_client::LndClient,
 };
+use tokio::sync::Notify;
 use tower_http::cors::CorsLayer;
 use tracing_subscriber::EnvFilter;
 
@@ -140,7 +140,13 @@ pub async fn create_cln_backend(cln_client: &ClnClient) -> Result<CdkCln> {
     .await?)
 }
 
-pub async fn create_mint<D>(database: D) -> Result<Mint>
+pub async fn create_mint<D>(
+    database: D,
+    ln_backends: HashMap<
+        LnKey,
+        Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
+    >,
+) -> Result<Mint>
 where
     D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
 {
@@ -160,11 +166,15 @@ where
     let mut supported_units: HashMap<CurrencyUnit, (u64, u8)> = HashMap::new();
     supported_units.insert(CurrencyUnit::Sat, (0, 32));
 
+    let quote_ttl = QuoteTTL::new(10000, 10000);
+
     let mint = Mint::new(
         &get_mint_url(),
         &mnemonic.to_seed_normalized(""),
         mint_info,
+        quote_ttl,
         Arc::new(database),
+        ln_backends,
         supported_units,
     )
     .await?;
@@ -189,7 +199,6 @@ where
     // Parse input
     tracing_subscriber::fmt().with_env_filter(env_filter).init();
 
-    let mint = create_mint(database).await?;
     let cln_client = init_cln_client().await?;
 
     let cln_backend = create_cln_backend(&cln_client).await?;
@@ -204,18 +213,13 @@ where
         Arc::new(cln_backend),
     );
 
-    let quote_ttl = 100000;
+    let mint = create_mint(database, ln_backends.clone()).await?;
 
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(
-        &get_mint_url(),
-        Arc::clone(&mint_arc),
-        ln_backends.clone(),
-        quote_ttl,
-    )
-    .await
-    .unwrap();
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc))
+        .await
+        .unwrap();
 
     let mint_service = Router::new()
         .merge(v1_service)
@@ -223,28 +227,13 @@ where
 
     let mint = Arc::clone(&mint_arc);
 
-    for wallet in ln_backends.values() {
-        let wallet_clone = Arc::clone(wallet);
-        let mint = Arc::clone(&mint);
-        tokio::spawn(async move {
-            match wallet_clone.wait_any_invoice().await {
-                Ok(mut stream) => {
-                    while let Some(request_lookup_id) = stream.next().await {
-                        if let Err(err) =
-                            handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
-                        {
-                            // nosemgrep: direct-panic
-                            panic!("{:?}", err);
-                        }
-                    }
-                }
-                Err(err) => {
-                    // nosemgrep: direct-panic
-                    panic!("Could not get invoice stream: {}", err);
-                }
-            }
-        });
-    }
+    let shutdown = Arc::new(Notify::new());
+
+    tokio::spawn({
+        let shutdown = Arc::clone(&shutdown);
+        async move { mint.wait_for_paid_invoices(shutdown).await }
+    });
+
     println!("Staring Axum server");
     axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
         .serve(mint_service.into_make_service())
@@ -253,25 +242,6 @@ where
     Ok(())
 }
 
-/// Update mint quote when called for a paid invoice
-async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
-    println!("Invoice with lookup id paid: {}", request_lookup_id);
-    if let Ok(Some(mint_quote)) = mint
-        .localstore
-        .get_mint_quote_by_request_lookup_id(request_lookup_id)
-        .await
-    {
-        println!(
-            "Quote {} paid by lookup id {}",
-            mint_quote.id, request_lookup_id
-        );
-        mint.localstore
-            .update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
-            .await?;
-    }
-    Ok(())
-}
-
 pub async fn fund_ln(
     bitcoin_client: &BitcoinClient,
     cln_client: &ClnClient,

+ 13 - 52
crates/cdk-integration-tests/src/lib.rs

@@ -14,12 +14,12 @@ use cdk::nuts::{
     CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState,
     Nuts, PaymentMethod, PreMintSecrets, Proofs, State,
 };
-use cdk::types::LnKey;
+use cdk::types::{LnKey, QuoteTTL};
 use cdk::wallet::client::HttpClient;
 use cdk::{Mint, Wallet};
 use cdk_fake_wallet::FakeWallet;
-use futures::StreamExt;
 use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
+use tokio::sync::Notify;
 use tokio::time::sleep;
 use tower_http::cors::CorsLayer;
 
@@ -72,26 +72,22 @@ pub async fn start_mint(
 
     let mnemonic = Mnemonic::generate(12)?;
 
+    let quote_ttl = QuoteTTL::new(10000, 10000);
+
     let mint = Mint::new(
         &get_mint_url(),
         &mnemonic.to_seed_normalized(""),
         mint_info,
+        quote_ttl,
         Arc::new(MintMemoryDatabase::default()),
+        ln_backends.clone(),
         supported_units,
     )
     .await?;
 
-    let quote_ttl = 100000;
-
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(
-        &get_mint_url(),
-        Arc::clone(&mint_arc),
-        ln_backends.clone(),
-        quote_ttl,
-    )
-    .await?;
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc)).await?;
 
     let mint_service = Router::new()
         .merge(v1_service)
@@ -99,28 +95,12 @@ pub async fn start_mint(
 
     let mint = Arc::clone(&mint_arc);
 
-    for wallet in ln_backends.values() {
-        let wallet_clone = Arc::clone(wallet);
-        let mint = Arc::clone(&mint);
-        tokio::spawn(async move {
-            match wallet_clone.wait_any_invoice().await {
-                Ok(mut stream) => {
-                    while let Some(request_lookup_id) = stream.next().await {
-                        if let Err(err) =
-                            handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
-                        {
-                            // nosemgrep: direct-panic
-                            panic!("{:?}", err);
-                        }
-                    }
-                }
-                Err(err) => {
-                    // nosemgrep: direct-panic
-                    panic!("Could not get invoice stream: {}", err);
-                }
-            }
-        });
-    }
+    let shutdown = Arc::new(Notify::new());
+
+    tokio::spawn({
+        let shutdown = Arc::clone(&shutdown);
+        async move { mint.wait_for_paid_invoices(shutdown).await }
+    });
 
     axum::Server::bind(
         &format!("{}:{}", get_mint_addr(), get_mint_port())
@@ -133,25 +113,6 @@ pub async fn start_mint(
     Ok(())
 }
 
-/// Update mint quote when called for a paid invoice
-async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
-    println!("Invoice with lookup id paid: {}", request_lookup_id);
-    if let Ok(Some(mint_quote)) = mint
-        .localstore
-        .get_mint_quote_by_request_lookup_id(request_lookup_id)
-        .await
-    {
-        println!(
-            "Quote {} paid by lookup id {}",
-            mint_quote.id, request_lookup_id
-        );
-        mint.localstore
-            .update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
-            .await?;
-    }
-    Ok(())
-}
-
 pub async fn wallet_mint(
     wallet: Arc<Wallet>,
     amount: Amount,

+ 17 - 11
crates/cdk-integration-tests/tests/mint.rs

@@ -5,10 +5,12 @@ use bip39::Mnemonic;
 use cdk::amount::{Amount, SplitTarget};
 use cdk::cdk_database::mint_memory::MintMemoryDatabase;
 use cdk::dhke::construct_proofs;
+use cdk::mint::MintQuote;
 use cdk::nuts::{
     CurrencyUnit, Id, MintBolt11Request, MintInfo, Nuts, PreMintSecrets, Proofs, SecretKey,
     SpendingConditions, SwapRequest,
 };
+use cdk::types::QuoteTTL;
 use cdk::util::unix_time;
 use cdk::Mint;
 use std::collections::HashMap;
@@ -36,11 +38,15 @@ async fn new_mint(fee: u64) -> Mint {
 
     let mnemonic = Mnemonic::generate(12).unwrap();
 
+    let quote_ttl = QuoteTTL::new(10000, 10000);
+
     Mint::new(
         MINT_URL,
         &mnemonic.to_seed_normalized(""),
         mint_info,
+        quote_ttl,
         Arc::new(MintMemoryDatabase::default()),
+        HashMap::new(),
         supported_units,
     )
     .await
@@ -59,16 +65,16 @@ async fn mint_proofs(
 ) -> Result<Proofs> {
     let request_lookup = uuid::Uuid::new_v4().to_string();
 
-    let mint_quote = mint
-        .new_mint_quote(
-            MINT_URL.parse()?,
-            "".to_string(),
-            CurrencyUnit::Sat,
-            amount,
-            unix_time() + 36000,
-            request_lookup.to_string(),
-        )
-        .await?;
+    let quote = MintQuote::new(
+        mint.mint_url.clone(),
+        "".to_string(),
+        CurrencyUnit::Sat,
+        amount,
+        unix_time() + 36000,
+        request_lookup.to_string(),
+    );
+
+    mint.localstore.add_mint_quote(quote.clone()).await?;
 
     mint.pay_mint_quote_for_request_id(&request_lookup).await?;
     let keyset_id = Id::from(&keys);
@@ -76,7 +82,7 @@ async fn mint_proofs(
     let premint = PreMintSecrets::random(keyset_id, amount, split_target)?;
 
     let mint_request = MintBolt11Request {
-        quote: mint_quote.id,
+        quote: quote.id,
         outputs: premint.blinded_messages(),
     };
 

+ 29 - 39
crates/cdk-mintd/src/main.rs

@@ -20,7 +20,7 @@ use cdk::nuts::{
     nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MeltQuoteState, MintInfo,
     MintMethodSettings, MintVersion, MppMethodSettings, Nuts, PaymentMethod,
 };
-use cdk::types::LnKey;
+use cdk::types::{LnKey, QuoteTTL};
 use cdk_cln::Cln;
 use cdk_fake_wallet::FakeWallet;
 use cdk_lnbits::LNbits;
@@ -32,8 +32,7 @@ use cdk_strike::Strike;
 use clap::Parser;
 use cli::CLIArgs;
 use config::{DatabaseEngine, LnBackend};
-use futures::StreamExt;
-use tokio::sync::Mutex;
+use tokio::sync::{Mutex, Notify};
 use tower_http::cors::CorsLayer;
 use tracing_subscriber::EnvFilter;
 use url::Url;
@@ -425,11 +424,15 @@ async fn main() -> anyhow::Result<()> {
 
     let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
 
+    let quote_ttl = QuoteTTL::new(10000, 10000);
+
     let mint = Mint::new(
         &settings.info.url,
         &mnemonic.to_seed_normalized(""),
         mint_info,
+        quote_ttl,
         localstore,
+        ln_backends.clone(),
         supported_units,
     )
     .await?;
@@ -449,17 +452,14 @@ async fn main() -> anyhow::Result<()> {
     // Pending melt quotes where the paynment has **failed** inputs are reset to unspent
     check_pending_melt_quotes(Arc::clone(&mint), &ln_backends).await?;
 
-    let mint_url = settings.info.url;
     let listen_addr = settings.info.listen_host;
     let listen_port = settings.info.listen_port;
-    let quote_ttl = settings
+    let _quote_ttl = settings
         .info
         .seconds_quote_is_valid_for
         .unwrap_or(DEFAULT_QUOTE_TTL_SECS);
 
-    let v1_service =
-        cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), ln_backends.clone(), quote_ttl)
-            .await?;
+    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint)).await?;
 
     let mut mint_service = Router::new()
         .merge(v1_service)
@@ -469,45 +469,35 @@ async fn main() -> anyhow::Result<()> {
         mint_service = mint_service.merge(router);
     }
 
-    // Spawn task to wait for invoces to be paid and update mint quotes
-    for (_, ln) in ln_backends {
-        let mint = Arc::clone(&mint);
-        tokio::spawn(async move {
-            loop {
-                match ln.wait_any_invoice().await {
-                    Ok(mut stream) => {
-                        while let Some(request_lookup_id) = stream.next().await {
-                            if let Err(err) =
-                                handle_paid_invoice(mint.clone(), &request_lookup_id).await
-                            {
-                                tracing::warn!("{:?}", err);
-                            }
-                        }
-                    }
-                    Err(err) => {
-                        tracing::warn!("Could not get invoice stream: {}", err);
-                    }
-                }
-            }
-        });
-    }
+    let shutdown = Arc::new(Notify::new());
+
+    tokio::spawn({
+        let shutdown = Arc::clone(&shutdown);
+        async move { mint.wait_for_paid_invoices(shutdown).await }
+    });
 
-    axum::Server::bind(
+    let axum_result = axum::Server::bind(
         &format!("{}:{}", listen_addr, listen_port)
             .as_str()
             .parse()?,
     )
     .serve(mint_service.into_make_service())
-    .await?;
+    .await;
 
-    Ok(())
-}
+    shutdown.notify_waiters();
+
+    match axum_result {
+        Ok(_) => {
+            tracing::info!("Axum server stopped with okay status");
+        }
+        Err(err) => {
+            tracing::warn!("Axum server stopped with error");
+            tracing::error!("{}", err);
+
+            bail!("Axum exited with error")
+        }
+    }
 
-/// Update mint quote when called for a paid invoice
-async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
-    tracing::debug!("Invoice with lookup id paid: {}", request_lookup_id);
-    mint.pay_mint_quote_for_request_id(request_lookup_id)
-        .await?;
     Ok(())
 }
 

+ 2 - 2
crates/cdk/Cargo.toml

@@ -46,7 +46,7 @@ sync_wrapper = "0.1.2"
 bech32 = "0.9.1"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tokio = { version = "1", features = [
+tokio = { version = "1.21", features = [
     "rt-multi-thread",
     "time",
     "macros",
@@ -54,7 +54,7 @@ tokio = { version = "1", features = [
 ] }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-tokio = { version = "1", features = ["rt", "macros", "sync", "time"] }
+tokio = { version = "1.21", features = ["rt", "macros", "sync", "time"] }
 getrandom = { version = "0.2", features = ["js"] }
 instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
 

+ 398 - 32
crates/cdk/src/mint/mod.rs

@@ -4,23 +4,27 @@ use std::collections::{HashMap, HashSet};
 use std::str::FromStr;
 use std::sync::Arc;
 
+use anyhow::bail;
 use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
 use bitcoin::secp256k1::{self, Secp256k1};
+use futures::StreamExt;
 use lightning_invoice::Bolt11Invoice;
 use serde::{Deserialize, Serialize};
-use tokio::sync::RwLock;
+use tokio::sync::{Notify, RwLock};
+use tokio::task::JoinSet;
 use tracing::instrument;
 
-use self::nut05::QuoteState;
+use self::nut05::{MeltBolt11Response, QuoteState};
 use self::nut11::EnforceSigFlag;
 use crate::cdk_database::{self, MintDatabase};
-use crate::cdk_lightning::to_unit;
+use crate::cdk_lightning::{self, to_unit, MintLightning, PayInvoiceResponse};
 use crate::dhke::{hash_to_curve, sign_message, verify_message};
 use crate::error::Error;
 use crate::fees::calculate_fee;
 use crate::mint_url::MintUrl;
 use crate::nuts::nut11::enforce_sig_flag;
 use crate::nuts::*;
+use crate::types::{LnKey, QuoteTTL};
 use crate::util::unix_time;
 use crate::Amount;
 
@@ -35,8 +39,12 @@ pub struct Mint {
     pub mint_url: MintUrl,
     /// Mint Info
     pub mint_info: MintInfo,
+    /// Quotes ttl
+    pub quote_ttl: QuoteTTL,
     /// Mint Storage backend
     pub localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
+    /// Ln backends for mint
+    pub ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
     /// Active Mint Keysets
     keysets: Arc<RwLock<HashMap<Id, MintKeySet>>>,
     secp_ctx: Secp256k1<secp256k1::All>,
@@ -49,7 +57,9 @@ impl Mint {
         mint_url: &str,
         seed: &[u8],
         mint_info: MintInfo,
+        quote_ttl: QuoteTTL,
         localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
+        ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
         // Hashmap where the key is the unit and value is (input fee ppk, max_order)
         supported_units: HashMap<CurrencyUnit, (u64, u8)>,
     ) -> Result<Self, Error> {
@@ -160,9 +170,11 @@ impl Mint {
             mint_url: MintUrl::from_str(mint_url)?,
             keysets: Arc::new(RwLock::new(active_keysets)),
             secp_ctx,
+            quote_ttl,
             xpriv,
             localstore,
             mint_info,
+            ln,
         })
     }
 
@@ -190,17 +202,12 @@ impl Mint {
         &self.mint_info
     }
 
-    /// New mint quote
-    #[instrument(skip_all)]
-    pub async fn new_mint_quote(
+    /// Checks that minting is enabled, request is supported unit and within range
+    fn check_mint_request_acceptable(
         &self,
-        mint_url: MintUrl,
-        request: String,
-        unit: CurrencyUnit,
         amount: Amount,
-        expiry: u64,
-        ln_lookup: String,
-    ) -> Result<MintQuote, Error> {
+        unit: CurrencyUnit,
+    ) -> Result<(), Error> {
         let nut04 = &self.mint_info.nuts.nut04;
 
         if nut04.disabled {
@@ -236,18 +243,72 @@ impl Mint {
             }
         }
 
-        let quote = MintQuote::new(mint_url, request, unit, amount, expiry, ln_lookup.clone());
+        Ok(())
+    }
+
+    /// Create new mint bolt11 quote
+    #[instrument(skip_all)]
+    pub async fn get_mint_bolt11_quote(
+        &self,
+        mint_quote_request: MintQuoteBolt11Request,
+    ) -> Result<MintQuoteBolt11Response, Error> {
+        let MintQuoteBolt11Request {
+            amount,
+            unit,
+            description,
+        } = mint_quote_request;
+
+        self.check_mint_request_acceptable(amount, unit)?;
+
+        let ln = self
+            .ln
+            .get(&LnKey::new(unit, PaymentMethod::Bolt11))
+            .ok_or_else(|| {
+                tracing::info!("Bolt11 mint request for unsupported unit");
+
+                Error::UnitUnsupported
+            })?;
+
+        let quote_expiry = unix_time() + self.quote_ttl.mint_ttl;
+
+        if description.is_some() && !ln.get_settings().invoice_description {
+            tracing::error!("Backend does not support invoice description");
+            return Err(Error::InvoiceDescriptionUnsupported);
+        }
+
+        let create_invoice_response = ln
+            .create_invoice(
+                amount,
+                &unit,
+                description.unwrap_or("".to_string()),
+                quote_expiry,
+            )
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not create invoice: {}", err);
+                Error::InvalidPaymentRequest
+            })?;
+
+        let quote = MintQuote::new(
+            self.mint_url.clone(),
+            create_invoice_response.request.to_string(),
+            unit,
+            amount,
+            create_invoice_response.expiry.unwrap_or(0),
+            create_invoice_response.request_lookup_id.clone(),
+        );
+
         tracing::debug!(
             "New mint quote {} for {} {} with request id {}",
             quote.id,
             amount,
             unit,
-            &ln_lookup
+            create_invoice_response.request_lookup_id,
         );
 
         self.localstore.add_mint_quote(quote.clone()).await?;
 
-        Ok(quote)
+        Ok(quote.into())
     }
 
     /// Check mint quote
@@ -345,24 +406,72 @@ impl Mint {
         Ok(())
     }
 
-    /// New melt quote
-    #[instrument(skip_all)]
-    pub async fn new_melt_quote(
+    /// Wait for any invoice to be paid
+    /// For each backend starts a task that waits for any invoice to be paid
+    /// Once invoice is paid mint quote status is updated
+    #[allow(clippy::incompatible_msrv)]
+    // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0)
+    pub async fn wait_for_paid_invoices(&self, shutdown: Arc<Notify>) -> Result<(), Error> {
+        let mint_arc = Arc::new(self.clone());
+
+        let mut join_set = JoinSet::new();
+
+        for (key, ln) in self.ln.iter() {
+            let mint = Arc::clone(&mint_arc);
+            let ln = Arc::clone(ln);
+            let shutdown = Arc::clone(&shutdown);
+            let key = *key;
+            join_set.spawn(async move {
+            loop {
+                tokio::select! {
+                    _ = shutdown.notified() => {
+                        tracing::info!("Shutdown signal received, stopping task for {:?}", key);
+                        break;
+                    }
+                    result = ln.wait_any_invoice() => {
+                        match result {
+                            Ok(mut stream) => {
+                                while let Some(request_lookup_id) = stream.next().await {
+                                    if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id).await {
+                                        tracing::warn!("{:?}", err);
+                                    }
+                                }
+                            }
+                            Err(err) => {
+                                tracing::warn!("Could not get invoice stream for {:?}: {}",key, err);
+                                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+                            }
+                        }
+                    }
+                }
+            }
+        });
+        }
+
+        // Spawn a task to manage the JoinSet
+        while let Some(result) = join_set.join_next().await {
+            match result {
+                Ok(_) => tracing::info!("A task completed successfully."),
+                Err(err) => tracing::warn!("A task failed: {:?}", err),
+            }
+        }
+
+        Ok(())
+    }
+
+    fn check_melt_request_acceptable(
         &self,
-        request: String,
-        unit: CurrencyUnit,
         amount: Amount,
-        fee_reserve: Amount,
-        expiry: u64,
-        request_lookup_id: String,
-    ) -> Result<MeltQuote, Error> {
+        unit: CurrencyUnit,
+        method: PaymentMethod,
+    ) -> Result<(), Error> {
         let nut05 = &self.mint_info.nuts.nut05;
 
         if nut05.disabled {
             return Err(Error::MeltingDisabled);
         }
 
-        match nut05.get_settings(&unit, &PaymentMethod::Bolt11) {
+        match nut05.get_settings(&unit, &method) {
             Some(settings) => {
                 if settings
                     .max_amount
@@ -391,13 +500,61 @@ impl Mint {
             }
         }
 
-        let quote = MeltQuote::new(
+        Ok(())
+    }
+
+    /// Get melt bolt11 quote
+    #[instrument(skip_all)]
+    pub async fn get_melt_bolt11_quote(
+        &self,
+        melt_request: &MeltQuoteBolt11Request,
+    ) -> Result<MeltQuoteBolt11Response, Error> {
+        let MeltQuoteBolt11Request {
             request,
             unit,
-            amount,
-            fee_reserve,
-            expiry,
-            request_lookup_id.clone(),
+            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)?;
+
+                to_unit(amount_msat, &CurrencyUnit::Msat, unit)
+                    .map_err(|_err| Error::UnsupportedUnit)?
+            }
+        };
+
+        self.check_melt_request_acceptable(amount, *unit, PaymentMethod::Bolt11)?;
+
+        let ln = self
+            .ln
+            .get(&LnKey::new(*unit, PaymentMethod::Bolt11))
+            .ok_or_else(|| {
+                tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
+
+                Error::UnitUnsupported
+            })?;
+
+        let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| {
+            tracing::error!(
+                "Could not get payment quote for mint quote, {} bolt11, {}",
+                unit,
+                err
+            );
+
+            Error::UnitUnsupported
+        })?;
+
+        let quote = MeltQuote::new(
+            request.to_string(),
+            *unit,
+            payment_quote.amount,
+            payment_quote.fee,
+            unix_time() + self.quote_ttl.melt_ttl,
+            payment_quote.request_lookup_id.clone(),
         );
 
         tracing::debug!(
@@ -405,12 +562,12 @@ impl Mint {
             quote.id,
             amount,
             unit,
-            request_lookup_id
+            payment_quote.request_lookup_id
         );
 
         self.localstore.add_melt_quote(quote.clone()).await?;
 
-        Ok(quote)
+        Ok(quote.into())
     }
 
     /// Fee required for proof set
@@ -1257,6 +1414,212 @@ impl Mint {
         Ok(())
     }
 
+    /// Melt Bolt11
+    #[instrument(skip_all)]
+    pub async fn melt_bolt11(
+        &self,
+        melt_request: &MeltBolt11Request,
+    ) -> Result<MeltBolt11Response, Error> {
+        use std::sync::Arc;
+        async fn check_payment_state(
+            ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+            melt_quote: &MeltQuote,
+        ) -> anyhow::Result<PayInvoiceResponse> {
+            match ln
+                .check_outgoing_payment(&melt_quote.request_lookup_id)
+                .await
+            {
+                Ok(response) => Ok(response),
+                Err(check_err) => {
+                    // If we cannot check the status of the payment we keep the proofs stuck as pending.
+                    tracing::error!(
+                        "Could not check the status of payment for {},. Proofs stuck as pending",
+                        melt_quote.id
+                    );
+                    tracing::error!("Checking payment error: {}", check_err);
+                    bail!("Could not check payment status")
+                }
+            }
+        }
+
+        let quote = match self.verify_melt_request(melt_request).await {
+            Ok(quote) => quote,
+            Err(err) => {
+                tracing::debug!("Error attempting to verify melt quote: {}", err);
+
+                if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                    tracing::error!(
+                        "Could not reset melt quote {} state: {}",
+                        melt_request.quote,
+                        err
+                    );
+                }
+                return Err(err);
+            }
+        };
+
+        let settled_internally_amount =
+            match self.handle_internal_melt_mint(&quote, melt_request).await {
+                Ok(amount) => amount,
+                Err(err) => {
+                    tracing::error!("Attempting to settle internally failed");
+                    if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                        tracing::error!(
+                            "Could not reset melt quote {} state: {}",
+                            melt_request.quote,
+                            err
+                        );
+                    }
+                    return Err(err);
+                }
+            };
+
+        let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
+            Some(amount_spent) => (None, amount_spent),
+            None => {
+                // If the quote unit is SAT or MSAT we can check that the expected fees are
+                // provided. We also check if the quote is less then the invoice
+                // amount in the case that it is a mmp However, if the quote is not
+                // of a bitcoin unit we cannot do these checks as the mint
+                // is unaware of a conversion rate. In this case it is assumed that the quote is
+                // correct and the mint should pay the full invoice amount if inputs
+                // > `then quote.amount` are included. This is checked in the
+                // `verify_melt` method.
+                let partial_amount = match quote.unit {
+                    CurrencyUnit::Sat | CurrencyUnit::Msat => {
+                        match self.check_melt_expected_ln_fees(&quote, melt_request).await {
+                            Ok(amount) => amount,
+                            Err(err) => {
+                                tracing::error!("Fee is not expected: {}", err);
+                                if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                                    tracing::error!("Could not reset melt quote state: {}", err);
+                                }
+                                return Err(Error::Internal);
+                            }
+                        }
+                    }
+                    _ => None,
+                };
+                let ln = match self.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
+                    Some(ln) => ln,
+                    None => {
+                        tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
+                        if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                            tracing::error!("Could not reset melt quote state: {}", err);
+                        }
+
+                        return Err(Error::UnitUnsupported);
+                    }
+                };
+
+                let pre = match ln
+                    .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
+                    .await
+                {
+                    Ok(pay)
+                        if pay.status == MeltQuoteState::Unknown
+                            || pay.status == MeltQuoteState::Failed =>
+                    {
+                        let check_response = check_payment_state(Arc::clone(ln), &quote)
+                            .await
+                            .map_err(|_| Error::Internal)?;
+
+                        if check_response.status == MeltQuoteState::Paid {
+                            tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
+
+                            return Err(Error::Internal);
+                        }
+
+                        check_response
+                    }
+                    Ok(pay) => pay,
+                    Err(err) => {
+                        // If the error is that the invoice was already paid we do not want to hold
+                        // hold the proofs as pending to we reset them  and return an error.
+                        if matches!(err, cdk_lightning::Error::InvoiceAlreadyPaid) {
+                            tracing::debug!("Invoice already paid, resetting melt quote");
+                            if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                                tracing::error!("Could not reset melt quote state: {}", err);
+                            }
+                            return Err(Error::RequestAlreadyPaid);
+                        }
+
+                        tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
+
+                        let check_response = check_payment_state(Arc::clone(ln), &quote)
+                            .await
+                            .map_err(|_| Error::Internal)?;
+                        // If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
+                        if check_response.status == MeltQuoteState::Paid {
+                            tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
+
+                            return Err(Error::Internal);
+                        }
+                        check_response
+                    }
+                };
+
+                match pre.status {
+                    MeltQuoteState::Paid => (),
+                    MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
+                        tracing::info!(
+                            "Lightning payment for quote {} failed.",
+                            melt_request.quote
+                        );
+                        if let Err(err) = self.process_unpaid_melt(melt_request).await {
+                            tracing::error!("Could not reset melt quote state: {}", err);
+                        }
+                        return Err(Error::PaymentFailed);
+                    }
+                    MeltQuoteState::Pending => {
+                        tracing::warn!(
+                            "LN payment pending, proofs are stuck as pending for quote: {}",
+                            melt_request.quote
+                        );
+                        return Err(Error::PendingQuote);
+                    }
+                }
+
+                // Convert from unit of backend to quote unit
+                // Note: this should never fail since these conversions happen earlier and would fail there.
+                // Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
+                let amount_spent =
+                    to_unit(pre.total_spent, &pre.unit, &quote.unit).unwrap_or_default();
+
+                let payment_lookup_id = pre.payment_lookup_id;
+
+                if payment_lookup_id != quote.request_lookup_id {
+                    tracing::info!(
+                        "Payment lookup id changed post payment from {} to {}",
+                        quote.request_lookup_id,
+                        payment_lookup_id
+                    );
+
+                    let mut melt_quote = quote;
+                    melt_quote.request_lookup_id = payment_lookup_id;
+
+                    if let Err(err) = self.localstore.add_melt_quote(melt_quote).await {
+                        tracing::warn!("Could not update payment lookup id: {}", err);
+                    }
+                }
+
+                (pre.payment_preimage, amount_spent)
+            }
+        };
+
+        // If we made it here the payment has been made.
+        // We process the melt burning the inputs and returning change
+        let res = self
+            .process_melt_request(melt_request, preimage, amount_spent_quote_unit)
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not process melt request: {}", err);
+                err
+            })?;
+
+        Ok(res.into())
+    }
+
     /// Process melt request marking [`Proofs`] as spent
     /// The melt request must be verifyed using [`Self::verify_melt_request`]
     /// before calling [`Self::process_melt_request`]
@@ -1680,6 +2043,7 @@ mod tests {
         mint_info: MintInfo,
         supported_units: HashMap<CurrencyUnit, (u64, u8)>,
         melt_requests: Vec<(MeltBolt11Request, LnKey)>,
+        quote_ttl: QuoteTTL,
     }
 
     async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> {
@@ -1703,7 +2067,9 @@ mod tests {
             config.mint_url,
             config.seed,
             config.mint_info,
+            config.quote_ttl,
             localstore,
+            HashMap::new(),
             config.supported_units,
         )
         .await

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

@@ -155,6 +155,22 @@ impl LnKey {
     }
 }
 
+/// Secs wuotes are valid
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
+pub struct QuoteTTL {
+    /// Seconds mint quote is valid
+    pub mint_ttl: u64,
+    /// Seconds melt quote is valid
+    pub melt_ttl: u64,
+}
+
+impl QuoteTTL {
+    /// Create new [`QuoteTTL`]
+    pub fn new(mint_ttl: u64, melt_ttl: u64) -> QuoteTTL {
+        Self { mint_ttl, melt_ttl }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::str::FromStr;