ソースを参照

feat: check outgoing payment status flow

thesimplekid 5 ヶ月 前
コミット
5139c47dac
38 ファイル変更1676 行追加338 行削除
  1. 30 1
      .github/workflows/ci.yml
  2. 1 18
      crates/cdk-axum/src/lib.rs
  3. 130 148
      crates/cdk-axum/src/router_handlers.rs
  4. 3 0
      crates/cdk-cln/src/error.rs
  5. 75 53
      crates/cdk-cln/src/lib.rs
  6. 2 0
      crates/cdk-fake-wallet/Cargo.toml
  7. 125 35
      crates/cdk-fake-wallet/src/lib.rs
  8. 2 0
      crates/cdk-integration-tests/Cargo.toml
  9. 32 0
      crates/cdk-integration-tests/src/bin/fake_wallet.rs
  10. 112 0
      crates/cdk-integration-tests/src/init_fake_wallet.rs
  11. 1 1
      crates/cdk-integration-tests/src/init_regtest.rs
  12. 45 4
      crates/cdk-integration-tests/src/lib.rs
  13. 313 0
      crates/cdk-integration-tests/tests/fake_wallet.rs
  14. 6 7
      crates/cdk-integration-tests/tests/mint.rs
  15. 3 1
      crates/cdk-lnbits/Cargo.toml
  16. 42 5
      crates/cdk-lnbits/src/lib.rs
  17. 6 0
      crates/cdk-lnd/src/error.rs
  18. 67 2
      crates/cdk-lnd/src/lib.rs
  19. 104 10
      crates/cdk-mintd/src/main.rs
  20. 2 1
      crates/cdk-phoenixd/Cargo.toml
  21. 3 0
      crates/cdk-phoenixd/src/error.rs
  22. 44 35
      crates/cdk-phoenixd/src/lib.rs
  23. 46 2
      crates/cdk-redb/src/mint/mod.rs
  24. 3 0
      crates/cdk-sqlite/src/mint/error.rs
  25. 8 0
      crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql
  26. 104 2
      crates/cdk-sqlite/src/mint/mod.rs
  27. 3 1
      crates/cdk-strike/Cargo.toml
  28. 3 0
      crates/cdk-strike/src/error.rs
  29. 47 6
      crates/cdk-strike/src/lib.rs
  30. 32 2
      crates/cdk/src/cdk_database/mint_memory.rs
  31. 16 0
      crates/cdk/src/cdk_database/mod.rs
  32. 11 2
      crates/cdk/src/cdk_lightning/mod.rs
  33. 16 0
      crates/cdk/src/error.rs
  34. 121 1
      crates/cdk/src/mint/mod.rs
  35. 8 0
      crates/cdk/src/nuts/nut05.rs
  36. 19 1
      crates/cdk/src/types.rs
  37. 86 0
      misc/fake_itests.sh
  38. 5 0
      misc/test.just

+ 30 - 1
.github/workflows/ci.yml

@@ -75,7 +75,7 @@ jobs:
         run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
 
   itest:
-    name: "Integration tests"
+    name: "Integration regtest tests"
     runs-on: ubuntu-latest
     strategy:
       matrix:
@@ -102,6 +102,35 @@ jobs:
         run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
       - name: Test
         run: nix develop -i -L .#stable --command just itest ${{ matrix.database }}
+          
+  fake-wallet-itest:
+    name: "Integration fake wallet tests"
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        build-args:
+          [
+            -p cdk-integration-tests,
+          ]
+        database: 
+          [
+          REDB,
+          SQLITE,
+          MEMORY
+          ]
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Clippy
+        run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
+      - name: Test fake mint
+        run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }}
 
   msrv-build:
     name: "MSRV build"

+ 1 - 18
crates/cdk-axum/src/lib.rs

@@ -13,7 +13,7 @@ use axum::Router;
 use cdk::cdk_lightning::{self, MintLightning};
 use cdk::mint::Mint;
 use cdk::mint_url::MintUrl;
-use cdk::nuts::{CurrencyUnit, PaymentMethod};
+use cdk::types::LnKey;
 use router_handlers::*;
 
 mod router_handlers;
@@ -66,20 +66,3 @@ pub struct MintState {
     mint_url: MintUrl,
     quote_ttl: u64,
 }
-
-/// Key used in hashmap of ln backends to identify what unit and payment method
-/// it is for
-#[derive(Debug, Clone, Hash, PartialEq, Eq)]
-pub struct LnKey {
-    /// Unit of Payment backend
-    pub unit: CurrencyUnit,
-    /// Method of payment backend
-    pub method: PaymentMethod,
-}
-
-impl LnKey {
-    /// Create new [`LnKey`]
-    pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
-        Self { unit, method }
-    }
-}

+ 130 - 148
crates/cdk-axum/src/router_handlers.rs

@@ -1,22 +1,19 @@
-use std::str::FromStr;
-
-use anyhow::Result;
+use anyhow::{bail, Result};
 use axum::extract::{Json, Path, State};
 use axum::http::StatusCode;
 use axum::response::{IntoResponse, Response};
-use cdk::amount::Amount;
-use cdk::cdk_lightning::to_unit;
+use cdk::cdk_lightning::{to_unit, MintLightning, PayInvoiceResponse};
 use cdk::error::{Error, ErrorResponse};
+use cdk::mint::MeltQuote;
 use cdk::nuts::nut05::MeltBolt11Response;
 use cdk::nuts::{
     CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
     MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteState,
     MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, MintQuoteState, PaymentMethod, RestoreRequest, RestoreResponse,
-    SwapRequest, SwapResponse,
+    MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest,
+    SwapResponse,
 };
 use cdk::util::unix_time;
-use cdk::Bolt11Invoice;
 
 use crate::{LnKey, MintState};
 
@@ -196,6 +193,28 @@ 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) => {
@@ -212,138 +231,52 @@ pub async fn post_melt_bolt11(
         }
     };
 
-    // Check to see if there is a corresponding mint quote for a melt.
-    // In this case the mint can settle the payment internally and no ln payment is
-    // needed
-    let mint_quote = match state
-        .mint
-        .localstore
-        .get_mint_quote_by_request(&quote.request)
-        .await
-    {
-        Ok(mint_quote) => mint_quote,
-        Err(err) => {
-            tracing::debug!("Error attempting to get mint quote: {}", 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));
-        }
-    };
-
-    let inputs_amount_quote_unit = payload.proofs_amount().map_err(|_| {
-        tracing::error!("Proof inputs in melt quote overflowed");
-        into_response(Error::AmountOverflow)
-    })?;
-
-    let (preimage, amount_spent_quote_unit) = match mint_quote {
-        Some(mint_quote) => {
-            if mint_quote.state == MintQuoteState::Issued
-                || mint_quote.state == MintQuoteState::Paid
-            {
-                return Err(into_response(Error::RequestAlreadyPaid));
-            }
-
-            let mut mint_quote = mint_quote;
-
-            if mint_quote.amount > inputs_amount_quote_unit {
-                tracing::debug!(
-                    "Not enough inuts provided: {} needed {}",
-                    inputs_amount_quote_unit,
-                    mint_quote.amount
-                );
-                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::InsufficientFunds));
-            }
-
-            mint_quote.state = MintQuoteState::Paid;
-
-            let amount = quote.amount;
-
-            if let Err(_err) = state.mint.update_mint_quote(mint_quote).await {
+    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: {}", err);
+                    tracing::error!(
+                        "Could not reset melt quote {} state: {}",
+                        payload.quote,
+                        err
+                    );
                 }
-                return Err(into_response(Error::Internal));
+                return Err(into_response(err));
             }
+        };
 
-            (None, amount)
-        }
+    let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
+        Some(amount_spent) => (None, amount_spent),
         None => {
-            let invoice = match Bolt11Invoice::from_str(&quote.request) {
-                Ok(bolt11) => bolt11,
-                Err(_) => {
-                    tracing::error!("Melt quote has invalid payment request");
-                    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::InvalidPaymentRequest));
-                }
-            };
-
-            let mut partial_amount = 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 id not
+            // 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.
-            if quote.unit == CurrencyUnit::Msat || quote.unit == CurrencyUnit::Sat {
-                let quote_msats = to_unit(quote.amount, &quote.unit, &CurrencyUnit::Msat)
-                    .expect("Quote unit is checked above that it can convert to msat");
-
-                let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
-                    Some(amount) => amount.into(),
-                    None => {
-                        if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                            tracing::error!("Could not reset melt quote state: {}", err);
+            // > `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));
                         }
-                        return Err(into_response(Error::InvoiceAmountUndefined));
-                    }
-                };
-
-                partial_amount = match invoice_amount_msats > quote_msats {
-                    true => {
-                        let partial_msats = invoice_amount_msats - quote_msats;
-
-                        Some(
-                            to_unit(partial_msats, &CurrencyUnit::Msat, &quote.unit)
-                                .map_err(|_| into_response(Error::UnitUnsupported))?,
-                        )
                     }
-                    false => None,
-                };
-
-                let amount_to_pay = match partial_amount {
-                    Some(amount_to_pay) => amount_to_pay,
-                    None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &quote.unit)
-                        .map_err(|_| into_response(Error::UnitUnsupported))?,
-                };
-
-                if amount_to_pay + quote.fee_reserve > inputs_amount_quote_unit {
-                    tracing::debug!(
-                        "Not enough inuts provided: {} msats needed {} msats",
-                        inputs_amount_quote_unit,
-                        amount_to_pay
-                    );
-
-                    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::TransactionUnbalanced(
-                        inputs_amount_quote_unit.into(),
-                        amount_to_pay.into(),
-                        quote.fee_reserve.into(),
-                    )));
                 }
-            }
+                _ => None,
+            };
 
             let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
                 Some(ln) => ln,
@@ -361,46 +294,95 @@ pub async fn post_melt_bolt11(
                 .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) => {
-                    tracing::error!("Could not pay invoice: {}", err);
-                    if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                        tracing::error!("Could not reset melt quote state: {}", 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));
                     }
 
-                    let err = match err {
-                        cdk::cdk_lightning::Error::InvoiceAlreadyPaid => Error::RequestAlreadyPaid,
-                        _ => Error::PaymentFailed,
-                    };
+                    tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
 
-                    return Err(into_response(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
                 }
             };
 
-            // Check that melt quote status paid by in ln backend
-            if pre.status != MeltQuoteState::Paid {
-                if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
-                    tracing::error!("Could not reset melt quote state: {}", err);
+            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));
                 }
-
-                return Err(into_response(Error::PaymentFailed));
             }
 
             // Convert from unit of backend to quote unit
-            let amount_spent = to_unit(pre.total_spent, &pre.unit, &quote.unit).map_err(|_| {
-                tracing::error!(
-                    "Could not convert from {} to {} in melt.",
-                    pre.unit,
-                    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
                 );
 
-                into_response(Error::UnitUnsupported)
-            })?;
+                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)

+ 3 - 0
crates/cdk-cln/src/error.rs

@@ -14,6 +14,9 @@ pub enum Error {
     /// Unknown invoice
     #[error("Unknown invoice")]
     UnknownInvoice,
+    /// Invalid payment hash
+    #[error("Invalid hash")]
+    InvalidHash,
     /// Cln Error
     #[error(transparent)]
     Cln(#[from] cln_rpc::Error),

+ 75 - 53
crates/cdk-cln/src/lib.rs

@@ -113,7 +113,7 @@ impl MintLightning for Cln {
 
                     last_pay_idx = invoice.pay_index;
 
-                    break Some((invoice.label, (cln_client, last_pay_idx)));
+                    break Some((invoice.payment_hash.to_string(), (cln_client, last_pay_idx)));
                 }
             },
         )
@@ -159,12 +159,13 @@ impl MintLightning for Cln {
         partial_amount: Option<Amount>,
         max_fee: Option<Amount>,
     ) -> Result<PayInvoiceResponse, Self::Err> {
-        let mut cln_client = self.cln_client.lock().await;
-
-        let pay_state =
-            check_pay_invoice_status(&mut cln_client, melt_quote.request.to_string()).await?;
+        let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
+        let pay_state = self
+            .check_outgoing_payment(&bolt11.payment_hash().to_string())
+            .await?;
 
-        match pay_state {
+        match pay_state.status {
+            MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
             MeltQuoteState::Paid => {
                 tracing::debug!("Melt attempted on invoice already paid");
                 return Err(Self::Err::InvoiceAlreadyPaid);
@@ -173,9 +174,9 @@ impl MintLightning for Cln {
                 tracing::debug!("Melt attempted on invoice already pending");
                 return Err(Self::Err::InvoicePaymentPending);
             }
-            MeltQuoteState::Unpaid => (),
         }
 
+        let mut cln_client = self.cln_client.lock().await;
         let cln_response = cln_client
             .call(Request::Pay(PayRequest {
                 bolt11: melt_quote.request.to_string(),
@@ -207,19 +208,18 @@ impl MintLightning for Cln {
                     })
                     .transpose()?,
             }))
-            .await
-            .map_err(Error::from)?;
+            .await;
 
         let response = match cln_response {
-            cln_rpc::Response::Pay(pay_response) => {
+            Ok(cln_rpc::Response::Pay(pay_response)) => {
                 let status = match pay_response.status {
                     PayStatus::COMPLETE => MeltQuoteState::Paid,
                     PayStatus::PENDING => MeltQuoteState::Pending,
-                    PayStatus::FAILED => MeltQuoteState::Unpaid,
+                    PayStatus::FAILED => MeltQuoteState::Failed,
                 };
                 PayInvoiceResponse {
                     payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
-                    payment_hash: pay_response.payment_hash.to_string(),
+                    payment_lookup_id: pay_response.payment_hash.to_string(),
                     status,
                     total_spent: to_unit(
                         pay_response.amount_sent_msat.msat(),
@@ -230,8 +230,11 @@ impl MintLightning for Cln {
                 }
             }
             _ => {
-                tracing::warn!("CLN returned wrong response kind");
-                return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
+                tracing::error!(
+                    "Error attempting to pay invoice: {}",
+                    bolt11.payment_hash().to_string()
+                );
+                return Err(Error::WrongClnResponse.into());
             }
         };
 
@@ -274,9 +277,10 @@ impl MintLightning for Cln {
             cln_rpc::Response::Invoice(invoice_res) => {
                 let request = Bolt11Invoice::from_str(&invoice_res.bolt11)?;
                 let expiry = request.expires_at().map(|t| t.as_secs());
+                let payment_hash = request.payment_hash();
 
                 Ok(CreateInvoiceResponse {
-                    request_lookup_id: label,
+                    request_lookup_id: payment_hash.to_string(),
                     request,
                     expiry,
                 })
@@ -288,16 +292,16 @@ impl MintLightning for Cln {
         }
     }
 
-    async fn check_invoice_status(
+    async fn check_incoming_invoice_status(
         &self,
-        request_lookup_id: &str,
+        payment_hash: &str,
     ) -> Result<MintQuoteState, Self::Err> {
         let mut cln_client = self.cln_client.lock().await;
 
         let cln_response = cln_client
             .call(Request::ListInvoices(ListinvoicesRequest {
-                payment_hash: None,
-                label: Some(request_lookup_id.to_string()),
+                payment_hash: Some(payment_hash.to_string()),
+                label: None,
                 invstring: None,
                 offer_id: None,
                 index: None,
@@ -316,7 +320,7 @@ impl MintLightning for Cln {
                     None => {
                         tracing::info!(
                             "Check invoice called on unknown look up id: {}",
-                            request_lookup_id
+                            payment_hash
                         );
                         return Err(Error::WrongClnResponse.into());
                     }
@@ -330,6 +334,51 @@ impl MintLightning for Cln {
 
         Ok(status)
     }
+
+    async fn check_outgoing_payment(
+        &self,
+        payment_hash: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        let mut cln_client = self.cln_client.lock().await;
+
+        let cln_response = cln_client
+            .call(Request::ListPays(ListpaysRequest {
+                payment_hash: Some(payment_hash.parse().map_err(|_| Error::InvalidHash)?),
+                bolt11: None,
+                status: None,
+            }))
+            .await
+            .map_err(Error::from)?;
+
+        match cln_response {
+            cln_rpc::Response::ListPays(pays_response) => match pays_response.pays.first() {
+                Some(pays_response) => {
+                    let status = cln_pays_status_to_mint_state(pays_response.status);
+
+                    Ok(PayInvoiceResponse {
+                        payment_lookup_id: pays_response.payment_hash.to_string(),
+                        payment_preimage: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
+                        status,
+                        total_spent: pays_response
+                            .amount_sent_msat
+                            .map_or(Amount::ZERO, |a| a.msat().into()),
+                        unit: CurrencyUnit::Msat,
+                    })
+                }
+                None => Ok(PayInvoiceResponse {
+                    payment_lookup_id: payment_hash.to_string(),
+                    payment_preimage: None,
+                    status: MeltQuoteState::Unknown,
+                    total_spent: Amount::ZERO,
+                    unit: CurrencyUnit::Msat,
+                }),
+            },
+            _ => {
+                tracing::warn!("CLN returned wrong response kind");
+                Err(Error::WrongClnResponse.into())
+            }
+        }
+    }
 }
 
 impl Cln {
@@ -370,37 +419,10 @@ fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQ
     }
 }
 
-async fn check_pay_invoice_status(
-    cln_client: &mut cln_rpc::ClnRpc,
-    bolt11: String,
-) -> Result<MeltQuoteState, cdk_lightning::Error> {
-    let cln_response = cln_client
-        .call(Request::ListPays(ListpaysRequest {
-            bolt11: Some(bolt11),
-            payment_hash: None,
-            status: None,
-        }))
-        .await
-        .map_err(Error::from)?;
-
-    let state = match cln_response {
-        cln_rpc::Response::ListPays(pay_response) => {
-            let pay = pay_response.pays.first();
-
-            match pay {
-                Some(pay) => match pay.status {
-                    ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
-                    ListpaysPaysStatus::PENDING => MeltQuoteState::Pending,
-                    ListpaysPaysStatus::FAILED => MeltQuoteState::Unpaid,
-                },
-                None => MeltQuoteState::Unpaid,
-            }
-        }
-        _ => {
-            tracing::warn!("CLN returned wrong response kind. When checking pay status");
-            return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
-        }
-    };
-
-    Ok(state)
+fn cln_pays_status_to_mint_state(status: ListpaysPaysStatus) -> MeltQuoteState {
+    match status {
+        ListpaysPaysStatus::PENDING => MeltQuoteState::Pending,
+        ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
+        ListpaysPaysStatus::FAILED => MeltQuoteState::Failed,
+    }
 }

+ 2 - 0
crates/cdk-fake-wallet/Cargo.toml

@@ -17,6 +17,8 @@ futures = { version = "0.3.28", default-features = false }
 tokio = { version = "1", default-features = false }
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
 thiserror = "1"
+serde = "1"
+serde_json = "1"
 uuid = { version = "1", features = ["v4"] }
 lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
 tokio-stream = "0.1.15"

+ 125 - 35
crates/cdk-fake-wallet/src/lib.rs

@@ -5,7 +5,9 @@
 #![warn(missing_docs)]
 #![warn(rustdoc::bare_urls)]
 
+use std::collections::{HashMap, HashSet};
 use std::pin::Pin;
+use std::str::FromStr;
 use std::sync::Arc;
 
 use async_trait::async_trait;
@@ -26,11 +28,12 @@ use cdk::util::unix_time;
 use error::Error;
 use futures::stream::StreamExt;
 use futures::Stream;
-use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
+use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
+use rand::Rng;
+use serde::{Deserialize, Serialize};
 use tokio::sync::Mutex;
 use tokio::time;
 use tokio_stream::wrappers::ReceiverStream;
-use uuid::Uuid;
 
 pub mod error;
 
@@ -42,6 +45,9 @@ pub struct FakeWallet {
     receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
     mint_settings: MintMethodSettings,
     melt_settings: MeltMethodSettings,
+    payment_states: Arc<Mutex<HashMap<String, MeltQuoteState>>>,
+    failed_payment_check: Arc<Mutex<HashSet<String>>>,
+    payment_delay: u64,
 }
 
 impl FakeWallet {
@@ -50,6 +56,9 @@ impl FakeWallet {
         fee_reserve: FeeReserve,
         mint_settings: MintMethodSettings,
         melt_settings: MeltMethodSettings,
+        payment_states: HashMap<String, MeltQuoteState>,
+        fail_payment_check: HashSet<String>,
+        payment_delay: u64,
     ) -> Self {
         let (sender, receiver) = tokio::sync::mpsc::channel(8);
 
@@ -59,10 +68,26 @@ impl FakeWallet {
             receiver: Arc::new(Mutex::new(Some(receiver))),
             mint_settings,
             melt_settings,
+            payment_states: Arc::new(Mutex::new(payment_states)),
+            failed_payment_check: Arc::new(Mutex::new(fail_payment_check)),
+            payment_delay,
         }
     }
 }
 
+/// Struct for signaling what methods should respond via invoice description
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct FakeInvoiceDescription {
+    /// State to be returned from pay invoice state
+    pub pay_invoice_state: MeltQuoteState,
+    /// State to be returned by check payment state
+    pub check_payment_state: MeltQuoteState,
+    /// Should pay invoice error
+    pub pay_err: bool,
+    /// Should check failure
+    pub check_err: bool,
+}
+
 #[async_trait]
 impl MintLightning for FakeWallet {
     type Err = cdk_lightning::Error;
@@ -124,10 +149,42 @@ impl MintLightning for FakeWallet {
         _partial_msats: Option<Amount>,
         _max_fee_msats: Option<Amount>,
     ) -> Result<PayInvoiceResponse, Self::Err> {
+        let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
+
+        let payment_hash = bolt11.payment_hash().to_string();
+
+        let description = bolt11.description().to_string();
+
+        let status: Option<FakeInvoiceDescription> = serde_json::from_str(&description).ok();
+
+        let mut payment_states = self.payment_states.lock().await;
+        let payment_status = status
+            .clone()
+            .map(|s| s.pay_invoice_state)
+            .unwrap_or(MeltQuoteState::Paid);
+
+        let checkout_going_status = status
+            .clone()
+            .map(|s| s.check_payment_state)
+            .unwrap_or(MeltQuoteState::Paid);
+
+        payment_states.insert(payment_hash.clone(), checkout_going_status);
+
+        if let Some(description) = status {
+            if description.check_err {
+                let mut fail = self.failed_payment_check.lock().await;
+                fail.insert(payment_hash.clone());
+            }
+
+            if description.pay_err {
+                return Err(Error::UnknownInvoice.into());
+            }
+        }
+
         Ok(PayInvoiceResponse {
             payment_preimage: Some("".to_string()),
-            payment_hash: "".to_string(),
-            status: MeltQuoteState::Paid,
+            payment_lookup_id: payment_hash,
+            status: payment_status,
             total_spent: melt_quote.amount,
             unit: melt_quote.unit,
         })
@@ -143,62 +200,95 @@ impl MintLightning for FakeWallet {
         let time_now = unix_time();
         assert!(unix_expiry > time_now);
 
-        let label = Uuid::new_v4().to_string();
-
-        let private_key = SecretKey::from_slice(
-            &[
-                0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2,
-                0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca,
-                0x3b, 0x2d, 0xb7, 0x34,
-            ][..],
-        )
-        .unwrap();
+        let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
 
-        let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
-        let payment_secret = PaymentSecret([42u8; 32]);
+        let invoice = create_fake_invoice(amount_msat.into(), description);
 
-        let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
+        let sender = self.sender.clone();
 
-        let invoice = InvoiceBuilder::new(Currency::Bitcoin)
-            .description(description)
-            .payment_hash(payment_hash)
-            .payment_secret(payment_secret)
-            .amount_milli_satoshis(amount.into())
-            .current_timestamp()
-            .min_final_cltv_expiry_delta(144)
-            .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
-            .unwrap();
+        let payment_hash = invoice.payment_hash();
 
-        // Create a random delay between 3 and 6 seconds
-        let duration = time::Duration::from_secs(3)
-            + time::Duration::from_millis(rand::random::<u64>() % 3001);
+        let payment_hash_clone = payment_hash.to_string();
 
-        let sender = self.sender.clone();
-        let label_clone = label.clone();
+        let duration = time::Duration::from_secs(self.payment_delay);
 
         tokio::spawn(async move {
             // Wait for the random delay to elapse
             time::sleep(duration).await;
 
             // Send the message after waiting for the specified duration
-            if sender.send(label_clone.clone()).await.is_err() {
-                tracing::error!("Failed to send label: {}", label_clone);
+            if sender.send(payment_hash_clone.clone()).await.is_err() {
+                tracing::error!("Failed to send label: {}", payment_hash_clone);
             }
         });
 
         let expiry = invoice.expires_at().map(|t| t.as_secs());
 
         Ok(CreateInvoiceResponse {
-            request_lookup_id: label,
+            request_lookup_id: payment_hash.to_string(),
             request: invoice,
             expiry,
         })
     }
 
-    async fn check_invoice_status(
+    async fn check_incoming_invoice_status(
         &self,
         _request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err> {
         Ok(MintQuoteState::Paid)
     }
+
+    async fn check_outgoing_payment(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        // For fake wallet if the state is not explicitly set default to paid
+        let states = self.payment_states.lock().await;
+        let status = states.get(request_lookup_id).cloned();
+
+        let status = status.unwrap_or(MeltQuoteState::Paid);
+
+        let fail_payments = self.failed_payment_check.lock().await;
+
+        if fail_payments.contains(request_lookup_id) {
+            return Err(cdk_lightning::Error::InvoicePaymentPending);
+        }
+
+        Ok(PayInvoiceResponse {
+            payment_preimage: Some("".to_string()),
+            payment_lookup_id: request_lookup_id.to_string(),
+            status,
+            total_spent: Amount::ZERO,
+            unit: self.get_settings().unit,
+        })
+    }
+}
+
+/// Create fake invoice
+pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
+    let private_key = SecretKey::from_slice(
+        &[
+            0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2,
+            0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca,
+            0x3b, 0x2d, 0xb7, 0x34,
+        ][..],
+    )
+    .unwrap();
+
+    let mut rng = rand::thread_rng();
+    let mut random_bytes = [0u8; 32];
+    rng.fill(&mut random_bytes);
+
+    let payment_hash = sha256::Hash::from_slice(&random_bytes).unwrap();
+    let payment_secret = PaymentSecret([42u8; 32]);
+
+    InvoiceBuilder::new(Currency::Bitcoin)
+        .description(description)
+        .payment_hash(payment_hash)
+        .payment_secret(payment_secret)
+        .amount_milli_satoshis(amount_msat)
+        .current_timestamp()
+        .min_final_cltv_expiry_delta(144)
+        .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
+        .unwrap()
 }

+ 2 - 0
crates/cdk-integration-tests/Cargo.toml

@@ -28,6 +28,8 @@ tower-http = { version = "0.4.4", features = ["cors"] }
 futures = { version = "0.3.28", default-features = false, features = ["executor"] }
 once_cell = "1.19.0"
 uuid = { version = "1", features = ["v4"] }
+serde = "1"
+serde_json = "1"
 # ln-regtest-rs = { path = "../../../../ln-regtest-rs" }
 ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "1d88d3d0b" }
 lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }

+ 32 - 0
crates/cdk-integration-tests/src/bin/fake_wallet.rs

@@ -0,0 +1,32 @@
+use std::env;
+
+use anyhow::Result;
+use cdk::cdk_database::mint_memory::MintMemoryDatabase;
+use cdk_integration_tests::{init_fake_wallet::start_fake_mint, init_regtest::get_temp_dir};
+use cdk_redb::MintRedbDatabase;
+use cdk_sqlite::MintSqliteDatabase;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let addr = "127.0.0.1";
+    let port = 8086;
+
+    let mint_db_kind = env::var("MINT_DATABASE")?;
+
+    match mint_db_kind.as_str() {
+        "MEMORY" => {
+            start_fake_mint(addr, port, MintMemoryDatabase::default()).await?;
+        }
+        "SQLITE" => {
+            let sqlite_db = MintSqliteDatabase::new(&get_temp_dir().join("mint")).await?;
+            sqlite_db.migrate().await;
+            start_fake_mint(addr, port, sqlite_db).await?;
+        }
+        "REDB" => {
+            let redb_db = MintRedbDatabase::new(&get_temp_dir().join("mint")).unwrap();
+            start_fake_mint(addr, port, redb_db).await?;
+        }
+        _ => panic!("Unknown mint db type: {}", mint_db_kind),
+    };
+    Ok(())
+}

+ 112 - 0
crates/cdk-integration-tests/src/init_fake_wallet.rs

@@ -0,0 +1,112 @@
+use std::{
+    collections::{HashMap, HashSet},
+    sync::Arc,
+};
+
+use anyhow::Result;
+use axum::Router;
+use cdk::{
+    cdk_database::{self, MintDatabase},
+    cdk_lightning::MintLightning,
+    mint::FeeReserve,
+    nuts::{CurrencyUnit, MeltMethodSettings, MintMethodSettings},
+    types::LnKey,
+};
+use cdk_fake_wallet::FakeWallet;
+use futures::StreamExt;
+use tower_http::cors::CorsLayer;
+use tracing_subscriber::EnvFilter;
+
+use crate::{handle_paid_invoice, init_regtest::create_mint};
+
+pub async fn start_fake_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 mint = create_mint(database).await?;
+
+    let fee_reserve = FeeReserve {
+        min_fee_reserve: 1.into(),
+        percent_fee_reserve: 1.0,
+    };
+
+    let fake_wallet = FakeWallet::new(
+        fee_reserve,
+        MintMethodSettings::default(),
+        MeltMethodSettings::default(),
+        HashMap::default(),
+        HashSet::default(),
+        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_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 mint_service = Router::new()
+        .merge(v1_service)
+        .layer(CorsLayer::permissive());
+
+    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);
+                }
+            }
+        });
+    }
+    println!("Staring Axum server");
+    axum::Server::bind(&format!("{}:{}", addr, port).as_str().parse().unwrap())
+        .serve(mint_service.into_make_service())
+        .await?;
+
+    Ok(())
+}

+ 1 - 1
crates/cdk-integration-tests/src/init_regtest.rs

@@ -8,8 +8,8 @@ use cdk::{
     cdk_lightning::MintLightning,
     mint::{FeeReserve, Mint},
     nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings},
+    types::LnKey,
 };
-use cdk_axum::LnKey;
 use cdk_cln::Cln as CdkCln;
 use futures::StreamExt;
 use ln_regtest_rs::{

+ 45 - 4
crates/cdk-integration-tests/src/lib.rs

@@ -1,8 +1,8 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::sync::Arc;
 use std::time::Duration;
 
-use anyhow::Result;
+use anyhow::{bail, Result};
 use axum::Router;
 use bip39::Mnemonic;
 use cdk::amount::{Amount, SplitTarget};
@@ -12,17 +12,18 @@ use cdk::dhke::construct_proofs;
 use cdk::mint::FeeReserve;
 use cdk::nuts::{
     CurrencyUnit, Id, KeySet, MeltMethodSettings, MintInfo, MintMethodSettings, MintQuoteState,
-    Nuts, PaymentMethod, PreMintSecrets, Proofs,
+    Nuts, PaymentMethod, PreMintSecrets, Proofs, State,
 };
+use cdk::types::LnKey;
 use cdk::wallet::client::HttpClient;
 use cdk::{Mint, Wallet};
-use cdk_axum::LnKey;
 use cdk_fake_wallet::FakeWallet;
 use futures::StreamExt;
 use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
 use tokio::time::sleep;
 use tower_http::cors::CorsLayer;
 
+pub mod init_fake_wallet;
 pub mod init_regtest;
 
 pub fn create_backends_fake_wallet(
@@ -41,6 +42,9 @@ pub fn create_backends_fake_wallet(
         fee_reserve.clone(),
         MintMethodSettings::default(),
         MeltMethodSettings::default(),
+        HashMap::default(),
+        HashSet::default(),
+        0,
     ));
 
     ln_backends.insert(ln_key, wallet.clone());
@@ -224,3 +228,40 @@ pub async fn mint_proofs(
 
     Ok(pre_swap_proofs)
 }
+
+// Get all pending from wallet and attempt to swap
+// Will panic if there are no pending
+// Will return Ok if swap fails as expected
+pub async fn attempt_to_swap_pending(wallet: &Wallet) -> Result<()> {
+    let pending = wallet
+        .localstore
+        .get_proofs(None, None, Some(vec![State::Pending]), None)
+        .await?;
+
+    assert!(!pending.is_empty());
+
+    let swap = wallet
+        .swap(
+            None,
+            SplitTarget::None,
+            pending.into_iter().map(|p| p.proof).collect(),
+            None,
+            false,
+        )
+        .await;
+
+    match swap {
+        Ok(_swap) => {
+            bail!("These proofs should be pending")
+        }
+        Err(err) => match err {
+            cdk::error::Error::TokenPending => (),
+            _ => {
+                println!("{:?}", err);
+                bail!("Wrong error")
+            }
+        },
+    }
+
+    Ok(())
+}

+ 313 - 0
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -0,0 +1,313 @@
+use std::{sync::Arc, time::Duration};
+
+use anyhow::Result;
+use bip39::Mnemonic;
+use cdk::{
+    amount::SplitTarget,
+    cdk_database::WalletMemoryDatabase,
+    nuts::{CurrencyUnit, MeltQuoteState, State},
+    wallet::Wallet,
+};
+use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
+use cdk_integration_tests::attempt_to_swap_pending;
+use tokio::time::sleep;
+
+const MINT_URL: &str = "http://127.0.0.1:8086";
+
+// If both pay and check return pending input proofs should remain pending
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_tokens_pending() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    sleep(Duration::from_secs(5)).await;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Pending,
+        check_payment_state: MeltQuoteState::Pending,
+        pay_err: false,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    let melt = wallet.melt(&melt_quote.id).await;
+
+    assert!(melt.is_err());
+
+    attempt_to_swap_pending(&wallet).await?;
+
+    Ok(())
+}
+
+// If the pay error fails and the check returns unknown or failed
+// The inputs proofs should be unset as spending
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_melt_payment_fail() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Unknown,
+        check_payment_state: MeltQuoteState::Unknown,
+        pay_err: true,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Failed,
+        check_payment_state: MeltQuoteState::Failed,
+        pay_err: true,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    // The mint should have unset proofs from pending since payment failed
+    let all_proof = wallet.get_proofs().await?;
+    let states = wallet.check_proofs_spent(all_proof).await?;
+    for state in states {
+        assert!(state.state == State::Unspent);
+    }
+
+    let wallet_bal = wallet.total_balance().await?;
+    assert!(wallet_bal == 100.into());
+
+    Ok(())
+}
+
+// When both the pay_invoice and check_invoice both fail
+// the proofs should remain as pending
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_melt_payment_fail_and_check() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Unknown,
+        check_payment_state: MeltQuoteState::Unknown,
+        pay_err: true,
+        check_err: true,
+    };
+
+    let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    let pending = wallet
+        .localstore
+        .get_proofs(None, None, Some(vec![State::Pending]), None)
+        .await?;
+
+    assert!(!pending.is_empty());
+
+    Ok(())
+}
+
+// In the case that the ln backend returns a failed status but does not error
+// The mint should do a second check, then remove proofs from pending
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_melt_payment_return_fail_status() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Failed,
+        check_payment_state: MeltQuoteState::Failed,
+        pay_err: false,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Unknown,
+        check_payment_state: MeltQuoteState::Unknown,
+        pay_err: false,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    let pending = wallet
+        .localstore
+        .get_proofs(None, None, Some(vec![State::Pending]), None)
+        .await?;
+
+    assert!(pending.is_empty());
+
+    Ok(())
+}
+
+// In the case that the ln backend returns a failed status but does not error
+// The mint should do a second check, then remove proofs from pending
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_melt_payment_error_unknown() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Failed,
+        check_payment_state: MeltQuoteState::Unknown,
+        pay_err: true,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Unknown,
+        check_payment_state: MeltQuoteState::Unknown,
+        pay_err: true,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    let pending = wallet
+        .localstore
+        .get_proofs(None, None, Some(vec![State::Pending]), None)
+        .await?;
+
+    assert!(pending.is_empty());
+
+    Ok(())
+}
+
+// In the case that the ln backend returns an err
+// The mint should do a second check, that returns paid
+// Proofs should remain pending
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_fake_melt_payment_err_paid() -> Result<()> {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    let fake_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Failed,
+        check_payment_state: MeltQuoteState::Paid,
+        pay_err: true,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
+
+    // The melt should error at the payment invoice command
+    let melt = wallet.melt(&melt_quote.id).await;
+    assert!(melt.is_err());
+
+    attempt_to_swap_pending(&wallet).await?;
+
+    Ok(())
+}

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

@@ -1,20 +1,19 @@
 //! Mint tests
 
-use cdk::amount::{Amount, SplitTarget};
-use cdk::dhke::construct_proofs;
-use cdk::util::unix_time;
-use std::collections::HashMap;
-use std::sync::Arc;
-use tokio::sync::OnceCell;
-
 use anyhow::{bail, Result};
 use bip39::Mnemonic;
+use cdk::amount::{Amount, SplitTarget};
 use cdk::cdk_database::mint_memory::MintMemoryDatabase;
+use cdk::dhke::construct_proofs;
 use cdk::nuts::{
     CurrencyUnit, Id, MintBolt11Request, MintInfo, Nuts, PreMintSecrets, Proofs, SecretKey,
     SpendingConditions, SwapRequest,
 };
+use cdk::util::unix_time;
 use cdk::Mint;
+use std::collections::HashMap;
+use std::sync::Arc;
+use tokio::sync::OnceCell;
 
 pub const MINT_URL: &str = "http://127.0.0.1:8088";
 

+ 3 - 1
crates/cdk-lnbits/Cargo.toml

@@ -19,4 +19,6 @@ futures = { version = "0.3.28", default-features = false }
 tokio = { version = "1", default-features = false }
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
 thiserror = "1"
-lnbits-rs = "0.2.0"
+# lnbits-rs = "0.2.0"
+# lnbits-rs = { path = "../../../../lnbits-rs" }
+lnbits-rs = { git = "https://github.com/thesimplekid/lnbits-rs.git", rev = "9fff4d" }

+ 42 - 5
crates/cdk-lnbits/src/lib.rs

@@ -12,7 +12,7 @@ use axum::Router;
 use cdk::amount::Amount;
 use cdk::cdk_lightning::{
     self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
-    Settings,
+    Settings, MSAT_IN_SAT,
 };
 use cdk::mint::FeeReserve;
 use cdk::nuts::{
@@ -189,7 +189,7 @@ impl MintLightning for LNbits {
         let total_spent = Amount::from((invoice_info.amount + invoice_info.fee).unsigned_abs());
 
         Ok(PayInvoiceResponse {
-            payment_hash: pay_response.payment_hash,
+            payment_lookup_id: pay_response.payment_hash,
             payment_preimage: Some(invoice_info.payment_hash),
             status,
             total_spent,
@@ -243,13 +243,13 @@ impl MintLightning for LNbits {
         })
     }
 
-    async fn check_invoice_status(
+    async fn check_incoming_invoice_status(
         &self,
-        request_lookup_id: &str,
+        payment_hash: &str,
     ) -> Result<MintQuoteState, Self::Err> {
         let paid = self
             .lnbits_api
-            .is_invoice_paid(request_lookup_id)
+            .is_invoice_paid(payment_hash)
             .await
             .map_err(|err| {
                 tracing::error!("Could not check invoice status");
@@ -264,6 +264,43 @@ impl MintLightning for LNbits {
 
         Ok(state)
     }
+
+    async fn check_outgoing_payment(
+        &self,
+        payment_hash: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        let payment = self
+            .lnbits_api
+            .get_payment_info(payment_hash)
+            .await
+            .map_err(|err| {
+                tracing::error!("Could not check invoice status");
+                tracing::error!("{}", err.to_string());
+                Self::Err::Anyhow(anyhow!("Could not check invoice status"))
+            })?;
+
+        let pay_response = PayInvoiceResponse {
+            payment_lookup_id: payment.details.payment_hash,
+            payment_preimage: Some(payment.preimage),
+            status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
+            total_spent: Amount::from(
+                payment.details.amount.unsigned_abs()
+                    + payment.details.fee.unsigned_abs() / MSAT_IN_SAT,
+            ),
+            unit: self.get_settings().unit,
+        };
+
+        Ok(pay_response)
+    }
+}
+
+fn lnbits_to_melt_status(status: &str, pending: bool) -> MeltQuoteState {
+    match (status, pending) {
+        ("success", false) => MeltQuoteState::Paid,
+        ("failed", false) => MeltQuoteState::Unpaid,
+        (_, false) => MeltQuoteState::Unknown,
+        (_, true) => MeltQuoteState::Pending,
+    }
 }
 
 impl LNbits {

+ 6 - 0
crates/cdk-lnd/src/error.rs

@@ -14,9 +14,15 @@ pub enum Error {
     /// Connection error
     #[error("LND connection error")]
     Connection,
+    /// Invalid hash
+    #[error("Invalid hash")]
+    InvalidHash,
     /// Payment failed
     #[error("LND payment failed")]
     PaymentFailed,
+    /// Unknown payment status
+    #[error("LND unknown payment status")]
+    UnknownPaymentStatus,
 }
 
 impl From<Error> for cdk::cdk_lightning::Error {

+ 67 - 2
crates/cdk-lnd/src/lib.rs

@@ -26,6 +26,7 @@ use cdk::util::{hex, unix_time};
 use cdk::{mint, Bolt11Invoice};
 use error::Error;
 use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
+use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus;
 use fedimint_tonic_lnd::lnrpc::FeeLimit;
 use fedimint_tonic_lnd::Client;
 use futures::{Stream, StreamExt};
@@ -206,7 +207,7 @@ impl MintLightning for Lnd {
         };
 
         Ok(PayInvoiceResponse {
-            payment_hash: hex::encode(payment_response.payment_hash),
+            payment_lookup_id: hex::encode(payment_response.payment_hash),
             payment_preimage,
             status,
             total_spent: total_amount.into(),
@@ -251,7 +252,7 @@ impl MintLightning for Lnd {
         })
     }
 
-    async fn check_invoice_status(
+    async fn check_incoming_invoice_status(
         &self,
         request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err> {
@@ -282,4 +283,68 @@ impl MintLightning for Lnd {
             _ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))),
         }
     }
+
+    async fn check_outgoing_payment(
+        &self,
+        payment_hash: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest {
+            payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
+            no_inflight_updates: true,
+        };
+        let mut payment_stream = self
+            .client
+            .lock()
+            .await
+            .router()
+            .track_payment_v2(track_request)
+            .await
+            .unwrap()
+            .into_inner();
+
+        while let Some(update_result) = payment_stream.next().await {
+            match update_result {
+                Ok(update) => {
+                    let status = update.status();
+
+                    let response = match status {
+                        PaymentStatus::Unknown => PayInvoiceResponse {
+                            payment_lookup_id: payment_hash.to_string(),
+                            payment_preimage: Some(update.payment_preimage),
+                            status: MeltQuoteState::Unknown,
+                            total_spent: Amount::ZERO,
+                            unit: self.get_settings().unit,
+                        },
+                        PaymentStatus::InFlight => {
+                            // Continue waiting for the next update
+                            continue;
+                        }
+                        PaymentStatus::Succeeded => PayInvoiceResponse {
+                            payment_lookup_id: payment_hash.to_string(),
+                            payment_preimage: Some(update.payment_preimage),
+                            status: MeltQuoteState::Paid,
+                            total_spent: Amount::from((update.value_sat + update.fee_sat) as u64),
+                            unit: CurrencyUnit::Sat,
+                        },
+                        PaymentStatus::Failed => PayInvoiceResponse {
+                            payment_lookup_id: payment_hash.to_string(),
+                            payment_preimage: Some(update.payment_preimage),
+                            status: MeltQuoteState::Failed,
+                            total_spent: Amount::ZERO,
+                            unit: self.get_settings().unit,
+                        },
+                    };
+
+                    return Ok(response);
+                }
+                Err(_) => {
+                    // Handle the case where the update itself is an error (e.g., stream failure)
+                    return Err(Error::UnknownPaymentStatus.into());
+                }
+            }
+        }
+
+        // If the stream is exhausted without a final status
+        Err(Error::UnknownPaymentStatus.into())
+    }
 }

+ 104 - 10
crates/cdk-mintd/src/main.rs

@@ -3,7 +3,7 @@
 #![warn(missing_docs)]
 #![warn(rustdoc::bare_urls)]
 
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::path::PathBuf;
 use std::str::FromStr;
 use std::sync::Arc;
@@ -14,13 +14,13 @@ use bip39::Mnemonic;
 use cdk::cdk_database::{self, MintDatabase};
 use cdk::cdk_lightning;
 use cdk::cdk_lightning::MintLightning;
-use cdk::mint::{FeeReserve, Mint};
+use cdk::mint::{FeeReserve, MeltQuote, Mint};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::{
-    nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings,
-    MintVersion, MppMethodSettings, Nuts, PaymentMethod,
+    nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MeltQuoteState, MintInfo,
+    MintMethodSettings, MintVersion, MppMethodSettings, Nuts, PaymentMethod,
 };
-use cdk_axum::LnKey;
+use cdk::types::LnKey;
 use cdk_cln::Cln;
 use cdk_fake_wallet::FakeWallet;
 use cdk_lnbits::LNbits;
@@ -329,6 +329,9 @@ async fn main() -> anyhow::Result<()> {
                     fee_reserve.clone(),
                     MintMethodSettings::default(),
                     MeltMethodSettings::default(),
+                    HashMap::default(),
+                    HashSet::default(),
+                    0,
                 ));
 
                 ln_backends.insert(ln_key, wallet);
@@ -438,9 +441,14 @@ async fn main() -> anyhow::Result<()> {
     // it is possible that a mint quote was paid but the mint has not been updated
     // this will check and update the mint state of those quotes
     for ln in ln_backends.values() {
-        check_pending_quotes(Arc::clone(&mint), Arc::clone(ln)).await?;
+        check_pending_mint_quotes(Arc::clone(&mint), Arc::clone(ln)).await?;
     }
 
+    // Checks the status of all pending melt quotes
+    // Pending melt quotes where the payment has gone through inputs are burnt
+    // 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;
@@ -462,7 +470,6 @@ async fn main() -> anyhow::Result<()> {
     }
 
     // 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 {
@@ -505,7 +512,7 @@ async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result
 }
 
 /// Used on mint start up to check status of all pending mint quotes
-async fn check_pending_quotes(
+async fn check_pending_mint_quotes(
     mint: Arc<Mint>,
     ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
 ) -> Result<()> {
@@ -519,10 +526,10 @@ async fn check_pending_quotes(
     for quote in unpaid_quotes {
         tracing::trace!("Checking status of mint quote: {}", quote.id);
         let lookup_id = quote.request_lookup_id;
-        match ln.check_invoice_status(&lookup_id).await {
+        match ln.check_incoming_invoice_status(&lookup_id).await {
             Ok(state) => {
                 if state != quote.state {
-                    tracing::trace!("Mintquote status changed: {}", quote.id);
+                    tracing::trace!("Mint quote status changed: {}", quote.id);
                     mint.localstore
                         .update_mint_quote_state(&quote.id, state)
                         .await?;
@@ -539,6 +546,93 @@ async fn check_pending_quotes(
     Ok(())
 }
 
+async fn check_pending_melt_quotes(
+    mint: Arc<Mint>,
+    ln_backends: &HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>,
+) -> Result<()> {
+    let melt_quotes = mint.localstore.get_melt_quotes().await?;
+    let pending_quotes: Vec<MeltQuote> = melt_quotes
+        .into_iter()
+        .filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown)
+        .collect();
+
+    for pending_quote in pending_quotes {
+        let melt_request_ln_key = mint.localstore.get_melt_request(&pending_quote.id).await?;
+
+        let (melt_request, ln_key) = match melt_request_ln_key {
+            None => (
+                None,
+                LnKey {
+                    unit: pending_quote.unit,
+                    method: PaymentMethod::Bolt11,
+                },
+            ),
+            Some((melt_request, ln_key)) => (Some(melt_request), ln_key),
+        };
+
+        let ln_backend = match ln_backends.get(&ln_key) {
+            Some(ln_backend) => ln_backend,
+            None => {
+                tracing::warn!("No backend for ln key: {:?}", ln_key);
+                continue;
+            }
+        };
+
+        let pay_invoice_response = ln_backend
+            .check_outgoing_payment(&pending_quote.request_lookup_id)
+            .await?;
+
+        match melt_request {
+            Some(melt_request) => {
+                match pay_invoice_response.status {
+                    MeltQuoteState::Paid => {
+                        if let Err(err) = mint
+                            .process_melt_request(
+                                &melt_request,
+                                pay_invoice_response.payment_preimage,
+                                pay_invoice_response.total_spent,
+                            )
+                            .await
+                        {
+                            tracing::error!(
+                                "Could not process melt request for pending quote: {}",
+                                melt_request.quote
+                            );
+                            tracing::error!("{}", err);
+                        }
+                    }
+                    MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
+                        // Payment has not been made we want to unset
+                        tracing::info!("Lightning payment for quote {} failed.", pending_quote.id);
+                        if let Err(err) = mint.process_unpaid_melt(&melt_request).await {
+                            tracing::error!("Could not reset melt quote state: {}", err);
+                        }
+                    }
+                    MeltQuoteState::Pending => {
+                        tracing::warn!(
+                            "LN payment pending, proofs are stuck as pending for quote: {}",
+                            melt_request.quote
+                        );
+                        // Quote is still pending we do not want to do anything
+                        // continue to check next quote
+                    }
+                }
+            }
+            None => {
+                tracing::warn!(
+                    "There is no stored melt request for pending melt quote: {}",
+                    pending_quote.id
+                );
+
+                mint.localstore
+                    .update_melt_quote_state(&pending_quote.id, pay_invoice_response.status)
+                    .await?;
+            }
+        };
+    }
+    Ok(())
+}
+
 fn expand_path(path: &str) -> Option<PathBuf> {
     if path.starts_with('~') {
         if let Some(home_dir) = home::home_dir().as_mut() {

+ 2 - 1
crates/cdk-phoenixd/Cargo.toml

@@ -19,5 +19,6 @@ futures = { version = "0.3.28", default-features = false }
 tokio = { version = "1", default-features = false }
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
 thiserror = "1"
-phoenixd-rs = "0.3.0"
+# phoenixd-rs = "0.3.0"
+phoenixd-rs = { git = "https://github.com/thesimplekid/phoenixd-rs", rev = "22a44f0"}
 uuid = { version = "1", features = ["v4"] }

+ 3 - 0
crates/cdk-phoenixd/src/error.rs

@@ -14,6 +14,9 @@ pub enum Error {
     /// Unsupported unit
     #[error("Unit Unsupported")]
     UnsupportedUnit,
+    /// phd error
+    #[error(transparent)]
+    Phd(#[from] phoenixd_rs::Error),
     /// Anyhow error
     #[error(transparent)]
     Anyhow(#[from] anyhow::Error),

+ 44 - 35
crates/cdk-phoenixd/src/lib.rs

@@ -175,24 +175,18 @@ impl MintLightning for Phoenixd {
             .pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into()))
             .await?;
 
-        // The pay response does not include the fee paided to Aciq so we check it here
+        // The pay invoice response does not give the needed fee info so we have to check.
         let check_outgoing_response = self
-            .check_outgoing_invoice(&pay_response.payment_id)
+            .check_outgoing_payment(&pay_response.payment_id)
             .await?;
 
-        if check_outgoing_response.state != MeltQuoteState::Paid {
-            return Err(anyhow!("Invoice is not paid").into());
-        }
-
-        let total_spent_sats = check_outgoing_response.fee + check_outgoing_response.amount;
-
         let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
 
         Ok(PayInvoiceResponse {
-            payment_hash: bolt11.payment_hash().to_string(),
+            payment_lookup_id: bolt11.payment_hash().to_string(),
             payment_preimage: Some(pay_response.payment_preimage),
             status: MeltQuoteState::Paid,
-            total_spent: total_spent_sats,
+            total_spent: check_outgoing_response.total_spent,
             unit: CurrencyUnit::Sat,
         })
     }
@@ -226,7 +220,10 @@ impl MintLightning for Phoenixd {
         })
     }
 
-    async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err> {
+    async fn check_incoming_invoice_status(
+        &self,
+        payment_hash: &str,
+    ) -> Result<MintQuoteState, Self::Err> {
         let invoice = self.phoenixd_api.get_incoming_invoice(payment_hash).await?;
 
         let state = match invoice.is_paid {
@@ -236,33 +233,45 @@ impl MintLightning for Phoenixd {
 
         Ok(state)
     }
-}
 
-impl Phoenixd {
-    /// Check the status of an outgooing invoice
-    // TODO: This should likely bee added to the trait. Both CLN and PhD use a form
-    // of it
-    async fn check_outgoing_invoice(
+    /// Check the status of an outgoing invoice
+    async fn check_outgoing_payment(
         &self,
-        payment_hash: &str,
-    ) -> Result<PaymentQuoteResponse, Error> {
-        let res = self.phoenixd_api.get_outgoing_invoice(payment_hash).await?;
-
-        // Phenixd gives fees in msats so we need to round up to the nearst sat
-        let fee_sats = (res.fees + 999) / MSAT_IN_SAT;
-
-        let state = match res.is_paid {
-            true => MeltQuoteState::Paid,
-            false => MeltQuoteState::Unpaid,
-        };
-
-        let quote_response = PaymentQuoteResponse {
-            request_lookup_id: res.payment_hash,
-            amount: res.sent.into(),
-            fee: fee_sats.into(),
-            state,
+        payment_id: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        let res = self.phoenixd_api.get_outgoing_invoice(payment_id).await;
+
+        let state = match res {
+            Ok(res) => {
+                let status = match res.is_paid {
+                    true => MeltQuoteState::Paid,
+                    false => MeltQuoteState::Unpaid,
+                };
+
+                let total_spent = res.sent + (res.fees + 999) / MSAT_IN_SAT;
+
+                PayInvoiceResponse {
+                    payment_lookup_id: res.payment_hash,
+                    payment_preimage: Some(res.preimage),
+                    status,
+                    total_spent: total_spent.into(),
+                    unit: CurrencyUnit::Sat,
+                }
+            }
+            Err(err) => match err {
+                phoenixd_rs::Error::NotFound => PayInvoiceResponse {
+                    payment_lookup_id: payment_id.to_string(),
+                    payment_preimage: None,
+                    status: MeltQuoteState::Unknown,
+                    total_spent: Amount::ZERO,
+                    unit: CurrencyUnit::Sat,
+                },
+                _ => {
+                    return Err(Error::from(err).into());
+                }
+            },
         };
 
-        Ok(quote_response)
+        Ok(state)
     }
 }

+ 46 - 2
crates/cdk-redb/src/mint/mod.rs

@@ -11,9 +11,10 @@ use cdk::cdk_database::MintDatabase;
 use cdk::dhke::hash_to_curve;
 use cdk::mint::{MintKeySetInfo, MintQuote};
 use cdk::nuts::{
-    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
-    State,
+    BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
+    Proofs, PublicKey, State,
 };
+use cdk::types::LnKey;
 use cdk::{cdk_database, mint};
 use migrations::migrate_01_to_02;
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
@@ -39,6 +40,8 @@ const QUOTE_PROOFS_TABLE: MultimapTableDefinition<&str, [u8; 33]> =
 const QUOTE_SIGNATURES_TABLE: MultimapTableDefinition<&str, &str> =
     MultimapTableDefinition::new("quote_signatures");
 
+const MELT_REQUESTS: TableDefinition<&str, (&str, &str)> = TableDefinition::new("melt_requests");
+
 const DATABASE_VERSION: u32 = 4;
 
 /// Mint Redbdatabase
@@ -735,4 +738,45 @@ impl MintDatabase for MintRedbDatabase {
             })
             .collect())
     }
+
+    /// Add melt request
+    async fn add_melt_request(
+        &self,
+        melt_request: MeltBolt11Request,
+        ln_key: LnKey,
+    ) -> Result<(), Self::Err> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        let mut table = write_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
+
+        table
+            .insert(
+                melt_request.quote.as_str(),
+                (
+                    serde_json::to_string(&melt_request)?.as_str(),
+                    serde_json::to_string(&ln_key)?.as_str(),
+                ),
+            )
+            .map_err(Error::from)?;
+
+        Ok(())
+    }
+    /// Get melt request
+    async fn get_melt_request(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err> {
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
+        let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
+
+        match table.get(quote_id).map_err(Error::from)? {
+            Some(melt_request) => {
+                let (melt_request_str, ln_key_str) = melt_request.value();
+                let melt_request = serde_json::from_str(melt_request_str)?;
+                let ln_key = serde_json::from_str(ln_key_str)?;
+
+                Ok(Some((melt_request, ln_key)))
+            }
+            None => Ok(None),
+        }
+    }
 }

+ 3 - 0
crates/cdk-sqlite/src/mint/error.rs

@@ -41,6 +41,9 @@ pub enum Error {
     /// Invalid Database Path
     #[error("Invalid database path")]
     InvalidDbPath,
+    /// Serde Error
+    #[error(transparent)]
+    Serde(#[from] serde_json::Error),
 }
 
 impl From<Error> for cdk::cdk_database::Error {

+ 8 - 0
crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql

@@ -0,0 +1,8 @@
+-- Melt Request Table
+CREATE TABLE IF NOT EXISTS melt_request (
+id TEXT PRIMARY KEY,
+inputs TEXT NOT NULL,
+outputs TEXT,
+method TEXT NOT NULL,
+unit TEXT NOT NULL
+);

+ 104 - 2
crates/cdk-sqlite/src/mint/mod.rs

@@ -12,10 +12,11 @@ use cdk::mint::{MintKeySetInfo, MintQuote};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut05::QuoteState;
 use cdk::nuts::{
-    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
-    State,
+    BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState,
+    PaymentMethod, Proof, Proofs, PublicKey, State,
 };
 use cdk::secret::Secret;
+use cdk::types::LnKey;
 use cdk::{mint, Amount};
 use error::Error;
 use lightning_invoice::Bolt11Invoice;
@@ -1121,6 +1122,86 @@ WHERE keyset_id=?;
             }
         }
     }
+
+    async fn add_melt_request(
+        &self,
+        melt_request: MeltBolt11Request,
+        ln_key: LnKey,
+    ) -> Result<(), Self::Err> {
+        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
+
+        let res = sqlx::query(
+            r#"
+INSERT OR REPLACE INTO melt_request
+(id, inputs, outputs, method, unit)
+VALUES (?, ?, ?, ?, ?);
+        "#,
+        )
+        .bind(melt_request.quote)
+        .bind(serde_json::to_string(&melt_request.inputs)?)
+        .bind(serde_json::to_string(&melt_request.outputs)?)
+        .bind(ln_key.method.to_string())
+        .bind(ln_key.unit.to_string())
+        .execute(&mut transaction)
+        .await;
+
+        match res {
+            Ok(_) => {
+                transaction.commit().await.map_err(Error::from)?;
+                Ok(())
+            }
+            Err(err) => {
+                tracing::error!("SQLite Could not update keyset");
+                if let Err(err) = transaction.rollback().await {
+                    tracing::error!("Could not rollback sql transaction: {}", err);
+                }
+
+                Err(Error::from(err).into())
+            }
+        }
+    }
+
+    async fn get_melt_request(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err> {
+        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
+
+        let rec = sqlx::query(
+            r#"
+SELECT *
+FROM melt_request
+WHERE id=?;
+        "#,
+        )
+        .bind(quote_id)
+        .fetch_one(&mut transaction)
+        .await;
+
+        match rec {
+            Ok(rec) => {
+                transaction.commit().await.map_err(Error::from)?;
+
+                let (request, key) = sqlite_row_to_melt_request(rec)?;
+
+                Ok(Some((request, key)))
+            }
+            Err(err) => match err {
+                sqlx::Error::RowNotFound => {
+                    transaction.commit().await.map_err(Error::from)?;
+                    return Ok(None);
+                }
+                _ => {
+                    return {
+                        if let Err(err) = transaction.rollback().await {
+                            tracing::error!("Could not rollback sql transaction: {}", err);
+                        }
+                        Err(Error::SQLX(err).into())
+                    }
+                }
+            },
+        }
+    }
 }
 
 fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
@@ -1259,3 +1340,24 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error
         dleq: None,
     })
 }
+
+fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request, LnKey), Error> {
+    let quote_id: String = row.try_get("id").map_err(Error::from)?;
+    let row_inputs: String = row.try_get("inputs").map_err(Error::from)?;
+    let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?;
+    let row_method: String = row.try_get("method").map_err(Error::from)?;
+    let row_unit: String = row.try_get("unit").map_err(Error::from)?;
+
+    let melt_request = MeltBolt11Request {
+        quote: quote_id,
+        inputs: serde_json::from_str(&row_inputs)?,
+        outputs: row_outputs.and_then(|o| serde_json::from_str(&o).ok()),
+    };
+
+    let ln_key = LnKey {
+        unit: CurrencyUnit::from_str(&row_unit)?,
+        method: PaymentMethod::from_str(&row_method)?,
+    };
+
+    Ok((melt_request, ln_key))
+}

+ 3 - 1
crates/cdk-strike/Cargo.toml

@@ -20,4 +20,6 @@ tokio = { version = "1", default-features = false }
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
 thiserror = "1"
 uuid = { version = "1", features = ["v4"] }
-strike-rs = "0.3.0"
+# strike-rs = "0.3.0"
+# strike-rs = { path = "../../../../strike-rs" }
+strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "c6167ce" }

+ 3 - 0
crates/cdk-strike/src/error.rs

@@ -11,6 +11,9 @@ pub enum Error {
     /// Unknown invoice
     #[error("Unknown invoice")]
     UnknownInvoice,
+    /// Strikers error
+    #[error(transparent)]
+    StrikeRs(#[from] strike_rs::Error),
     /// Anyhow error
     #[error(transparent)]
     Anyhow(#[from] anyhow::Error),

+ 47 - 6
crates/cdk-strike/src/lib.rs

@@ -99,7 +99,7 @@ impl MintLightning for Strike {
             |(mut receiver, strike_api)| async move {
                 match receiver.recv().await {
                     Some(msg) => {
-                        let check = strike_api.find_invoice(&msg).await;
+                        let check = strike_api.get_incoming_invoice(&msg).await;
 
                         match check {
                             Ok(state) => {
@@ -172,10 +172,8 @@ impl MintLightning for Strike {
 
         let total_spent = from_strike_amount(pay_response.total_amount, &melt_quote.unit)?.into();
 
-        let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
-
         Ok(PayInvoiceResponse {
-            payment_hash: bolt11.payment_hash().to_string(),
+            payment_lookup_id: pay_response.payment_id,
             payment_preimage: None,
             status: state,
             total_spent,
@@ -217,11 +215,14 @@ impl MintLightning for Strike {
         })
     }
 
-    async fn check_invoice_status(
+    async fn check_incoming_invoice_status(
         &self,
         request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err> {
-        let invoice = self.strike_api.find_invoice(request_lookup_id).await?;
+        let invoice = self
+            .strike_api
+            .get_incoming_invoice(request_lookup_id)
+            .await?;
 
         let state = match invoice.state {
             InvoiceState::Paid => MintQuoteState::Paid,
@@ -232,6 +233,46 @@ impl MintLightning for Strike {
 
         Ok(state)
     }
+
+    async fn check_outgoing_payment(
+        &self,
+        payment_id: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        let invoice = self.strike_api.get_outgoing_payment(payment_id).await;
+
+        let pay_invoice_response = match invoice {
+            Ok(invoice) => {
+                let state = match invoice.state {
+                    InvoiceState::Paid => MeltQuoteState::Paid,
+                    InvoiceState::Unpaid => MeltQuoteState::Unpaid,
+                    InvoiceState::Completed => MeltQuoteState::Paid,
+                    InvoiceState::Pending => MeltQuoteState::Pending,
+                };
+
+                PayInvoiceResponse {
+                    payment_lookup_id: invoice.payment_id,
+                    payment_preimage: None,
+                    status: state,
+                    total_spent: from_strike_amount(invoice.total_amount, &self.unit)?.into(),
+                    unit: self.unit,
+                }
+            }
+            Err(err) => match err {
+                strike_rs::Error::NotFound => PayInvoiceResponse {
+                    payment_lookup_id: payment_id.to_string(),
+                    payment_preimage: None,
+                    status: MeltQuoteState::Unknown,
+                    total_spent: Amount::ZERO,
+                    unit: self.unit,
+                },
+                _ => {
+                    return Err(Error::from(err).into());
+                }
+            },
+        };
+
+        Ok(pay_invoice_response)
+    }
 }
 
 impl Strike {

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

@@ -11,9 +11,10 @@ use crate::dhke::hash_to_curve;
 use crate::mint::{self, MintKeySetInfo, MintQuote};
 use crate::nuts::nut07::State;
 use crate::nuts::{
-    nut07, BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs,
-    PublicKey,
+    nut07, BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState,
+    Proof, Proofs, PublicKey,
 };
+use crate::types::LnKey;
 
 /// Mint Memory Database
 #[derive(Debug, Clone, Default)]
@@ -27,6 +28,7 @@ pub struct MintMemoryDatabase {
     quote_proofs: Arc<Mutex<HashMap<String, Vec<PublicKey>>>>,
     blinded_signatures: Arc<RwLock<HashMap<[u8; 33], BlindSignature>>>,
     quote_signatures: Arc<RwLock<HashMap<String, Vec<BlindSignature>>>>,
+    melt_requests: Arc<RwLock<HashMap<String, (MeltBolt11Request, LnKey)>>>,
 }
 
 impl MintMemoryDatabase {
@@ -42,6 +44,7 @@ impl MintMemoryDatabase {
         quote_proofs: HashMap<String, Vec<PublicKey>>,
         blinded_signatures: HashMap<[u8; 33], BlindSignature>,
         quote_signatures: HashMap<String, Vec<BlindSignature>>,
+        melt_request: Vec<(MeltBolt11Request, LnKey)>,
     ) -> Result<Self, Error> {
         let mut proofs = HashMap::new();
         let mut proof_states = HashMap::new();
@@ -58,6 +61,11 @@ impl MintMemoryDatabase {
             proof_states.insert(y, State::Spent);
         }
 
+        let melt_requests = melt_request
+            .into_iter()
+            .map(|(request, ln_key)| (request.quote.clone(), (request, ln_key)))
+            .collect();
+
         Ok(Self {
             active_keysets: Arc::new(RwLock::new(active_keysets)),
             keysets: Arc::new(RwLock::new(
@@ -74,6 +82,7 @@ impl MintMemoryDatabase {
             blinded_signatures: Arc::new(RwLock::new(blinded_signatures)),
             quote_proofs: Arc::new(Mutex::new(quote_proofs)),
             quote_signatures: Arc::new(RwLock::new(quote_signatures)),
+            melt_requests: Arc::new(RwLock::new(melt_requests)),
         })
     }
 }
@@ -225,6 +234,27 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(())
     }
 
+    async fn add_melt_request(
+        &self,
+        melt_request: MeltBolt11Request,
+        ln_key: LnKey,
+    ) -> Result<(), Self::Err> {
+        let mut melt_requests = self.melt_requests.write().await;
+        melt_requests.insert(melt_request.quote.clone(), (melt_request, ln_key));
+        Ok(())
+    }
+
+    async fn get_melt_request(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err> {
+        let melt_requests = self.melt_requests.read().await;
+
+        let melt_request = melt_requests.get(quote_id);
+
+        Ok(melt_request.cloned())
+    }
+
     async fn add_proofs(&self, proofs: Proofs, quote_id: Option<String>) -> Result<(), Self::Err> {
         let mut db_proofs = self.proofs.write().await;
 

+ 16 - 0
crates/cdk/src/cdk_database/mod.rs

@@ -17,11 +17,15 @@ use crate::mint::MintQuote as MintMintQuote;
 #[cfg(feature = "wallet")]
 use crate::mint_url::MintUrl;
 #[cfg(feature = "mint")]
+use crate::nuts::MeltBolt11Request;
+#[cfg(feature = "mint")]
 use crate::nuts::{BlindSignature, MeltQuoteState, MintQuoteState, Proof, Proofs};
 #[cfg(any(feature = "wallet", feature = "mint"))]
 use crate::nuts::{CurrencyUnit, Id, PublicKey, State};
 #[cfg(feature = "wallet")]
 use crate::nuts::{KeySetInfo, Keys, MintInfo, SpendingConditions};
+#[cfg(feature = "mint")]
+use crate::types::LnKey;
 #[cfg(feature = "wallet")]
 use crate::types::ProofInfo;
 #[cfg(feature = "wallet")]
@@ -220,6 +224,18 @@ pub trait MintDatabase {
     /// Remove [`mint::MeltQuote`]
     async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
+    /// Add melt request
+    async fn add_melt_request(
+        &self,
+        melt_request: MeltBolt11Request,
+        ln_key: LnKey,
+    ) -> Result<(), Self::Err>;
+    /// Get melt request
+    async fn get_melt_request(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<(MeltBolt11Request, LnKey)>, Self::Err>;
+
     /// Add [`MintKeySetInfo`]
     async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
     /// Get [`MintKeySetInfo`]

+ 11 - 2
crates/cdk/src/cdk_lightning/mod.rs

@@ -26,6 +26,9 @@ pub enum Error {
     /// Unsupported unit
     #[error("Unsupported unit")]
     UnsupportedUnit,
+    /// Payment state is unknown
+    #[error("Payment state is unknown")]
+    UnknownPaymentState,
     /// Lightning Error
     #[error(transparent)]
     Lightning(Box<dyn std::error::Error + Send + Sync>),
@@ -83,10 +86,16 @@ pub trait MintLightning {
     ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
 
     /// Check the status of an incoming payment
-    async fn check_invoice_status(
+    async fn check_incoming_invoice_status(
         &self,
         request_lookup_id: &str,
     ) -> Result<MintQuoteState, Self::Err>;
+
+    /// Check the status of an outgoing payment
+    async fn check_outgoing_payment(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<PayInvoiceResponse, Self::Err>;
 }
 
 /// Create invoice response
@@ -104,7 +113,7 @@ pub struct CreateInvoiceResponse {
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PayInvoiceResponse {
     /// Payment hash
-    pub payment_hash: String,
+    pub payment_lookup_id: String,
     /// Payment Preimage
     pub payment_preimage: Option<String>,
     /// Status

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

@@ -25,6 +25,9 @@ pub enum Error {
     /// Payment failed
     #[error("Payment failed")]
     PaymentFailed,
+    /// Payment pending
+    #[error("Payment pending")]
+    PaymentPending,
     /// Invoice already paid
     #[error("Request already paid")]
     RequestAlreadyPaid,
@@ -66,6 +69,9 @@ pub enum Error {
     /// Quote has already been paid
     #[error("Quote is already paid")]
     PaidQuote,
+    /// Payment state is unknown
+    #[error("Payment state is unknown")]
+    UnknownPaymentState,
     /// Melting is disabled
     #[error("Minting is disabled")]
     MeltingDisabled,
@@ -352,6 +358,11 @@ impl From<Error> for ErrorResponse {
                 error: Some(err.to_string()),
                 detail: None,
             },
+            Error::TokenPending => ErrorResponse {
+                code: ErrorCode::TokenPending,
+                error: Some(err.to_string()),
+                detail: None,
+            },
             _ => ErrorResponse {
                 code: ErrorCode::Unknown(9999),
                 error: Some(err.to_string()),
@@ -380,6 +391,7 @@ impl From<ErrorResponse> for Error {
             ErrorCode::AmountOutofLimitRange => {
                 Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
             }
+            ErrorCode::TokenPending => Self::TokenPending,
             _ => Self::UnknownErrorResponse(err.to_string()),
         }
     }
@@ -390,6 +402,8 @@ impl From<ErrorResponse> for Error {
 pub enum ErrorCode {
     /// Token is already spent
     TokenAlreadySpent,
+    /// Token Pending
+    TokenPending,
     /// Quote is not paid
     QuoteNotPaid,
     /// Quote is not expired
@@ -432,6 +446,7 @@ impl ErrorCode {
             11002 => Self::TransactionUnbalanced,
             11005 => Self::UnitUnsupported,
             11006 => Self::AmountOutofLimitRange,
+            11007 => Self::TokenPending,
             12001 => Self::KeysetNotFound,
             12002 => Self::KeysetInactive,
             20000 => Self::LightningError,
@@ -454,6 +469,7 @@ impl ErrorCode {
             Self::TransactionUnbalanced => 11002,
             Self::UnitUnsupported => 11005,
             Self::AmountOutofLimitRange => 11006,
+            Self::TokenPending => 11007,
             Self::KeysetNotFound => 12001,
             Self::KeysetInactive => 12002,
             Self::LightningError => 20000,

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

@@ -6,6 +6,7 @@ use std::sync::Arc;
 
 use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
 use bitcoin::secp256k1::{self, Secp256k1};
+use lightning_invoice::Bolt11Invoice;
 use serde::{Deserialize, Serialize};
 use tokio::sync::RwLock;
 use tracing::instrument;
@@ -13,6 +14,7 @@ use tracing::instrument;
 use self::nut05::QuoteState;
 use self::nut11::EnforceSigFlag;
 use crate::cdk_database::{self, MintDatabase};
+use crate::cdk_lightning::to_unit;
 use crate::dhke::{hash_to_curve, sign_message, verify_message};
 use crate::error::Error;
 use crate::fees::calculate_fee;
@@ -993,6 +995,117 @@ impl Mint {
         Ok(())
     }
 
+    /// Check melt has expected fees
+    #[instrument(skip_all)]
+    pub async fn check_melt_expected_ln_fees(
+        &self,
+        melt_quote: &MeltQuote,
+        melt_request: &MeltBolt11Request,
+    ) -> Result<Option<Amount>, Error> {
+        let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
+
+        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 partial_amount = match invoice_amount_msats > quote_msats {
+            true => {
+                let partial_msats = invoice_amount_msats - quote_msats;
+
+                Some(
+                    to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
+                        .map_err(|_| Error::UnitUnsupported)?,
+                )
+            }
+            false => None,
+        };
+
+        let amount_to_pay = match partial_amount {
+            Some(amount_to_pay) => amount_to_pay,
+            None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &melt_quote.unit)
+                .map_err(|_| Error::UnitUnsupported)?,
+        };
+
+        let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
+            tracing::error!("Proof inputs in melt quote overflowed");
+            Error::AmountOverflow
+        })?;
+
+        if amount_to_pay + melt_quote.fee_reserve > inputs_amount_quote_unit {
+            tracing::debug!(
+                "Not enough inputs provided: {} msats needed {} msats",
+                inputs_amount_quote_unit,
+                amount_to_pay
+            );
+
+            return Err(Error::TransactionUnbalanced(
+                inputs_amount_quote_unit.into(),
+                amount_to_pay.into(),
+                melt_quote.fee_reserve.into(),
+            ));
+        }
+
+        Ok(partial_amount)
+    }
+
+    /// Verify melt request is valid
+    /// Check to see if there is a corresponding mint quote for a melt.
+    /// In this case the mint can settle the payment internally and no ln payment is
+    /// needed
+    #[instrument(skip_all)]
+    pub async fn handle_internal_melt_mint(
+        &self,
+        melt_quote: &MeltQuote,
+        melt_request: &MeltBolt11Request,
+    ) -> Result<Option<Amount>, Error> {
+        let mint_quote = match self
+            .localstore
+            .get_mint_quote_by_request(&melt_quote.request)
+            .await
+        {
+            Ok(Some(mint_quote)) => mint_quote,
+            // Not an internal melt -> mint
+            Ok(None) => return Ok(None),
+            Err(err) => {
+                tracing::debug!("Error attempting to get mint quote: {}", err);
+                return Err(Error::Internal);
+            }
+        };
+
+        // Mint quote has already been settled, proofs should not be burned or held.
+        if mint_quote.state == MintQuoteState::Issued || mint_quote.state == MintQuoteState::Paid {
+            return Err(Error::RequestAlreadyPaid);
+        }
+
+        let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
+            tracing::error!("Proof inputs in melt quote overflowed");
+            Error::AmountOverflow
+        })?;
+
+        let mut mint_quote = mint_quote;
+
+        if mint_quote.amount > inputs_amount_quote_unit {
+            tracing::debug!(
+                "Not enough inuts provided: {} needed {}",
+                inputs_amount_quote_unit,
+                mint_quote.amount
+            );
+            return Err(Error::InsufficientFunds);
+        }
+
+        mint_quote.state = MintQuoteState::Paid;
+
+        let amount = melt_quote.amount;
+
+        self.update_mint_quote(mint_quote).await?;
+
+        Ok(Some(amount))
+    }
+
     /// Verify melt request is valid
     #[instrument(skip_all)]
     pub async fn verify_melt_request(
@@ -1005,13 +1118,16 @@ impl Mint {
             .await?;
 
         match state {
-            MeltQuoteState::Unpaid => (),
+            MeltQuoteState::Unpaid | MeltQuoteState::Failed => (),
             MeltQuoteState::Pending => {
                 return Err(Error::PendingQuote);
             }
             MeltQuoteState::Paid => {
                 return Err(Error::PaidQuote);
             }
+            MeltQuoteState::Unknown => {
+                return Err(Error::UnknownPaymentState);
+            }
         }
 
         let ys = melt_request
@@ -1456,6 +1572,8 @@ mod tests {
     use bitcoin::Network;
     use secp256k1::Secp256k1;
 
+    use crate::types::LnKey;
+
     use super::*;
 
     #[test]
@@ -1561,6 +1679,7 @@ mod tests {
         seed: &'a [u8],
         mint_info: MintInfo,
         supported_units: HashMap<CurrencyUnit, (u64, u8)>,
+        melt_requests: Vec<(MeltBolt11Request, LnKey)>,
     }
 
     async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> {
@@ -1575,6 +1694,7 @@ mod tests {
                 config.quote_proofs,
                 config.blinded_signatures,
                 config.quote_signatures,
+                config.melt_requests,
             )
             .unwrap(),
         );

+ 8 - 0
crates/cdk/src/nuts/nut05.rs

@@ -48,6 +48,10 @@ pub enum QuoteState {
     Paid,
     /// Paying quote is in progress
     Pending,
+    /// Unknown state
+    Unknown,
+    /// Failed
+    Failed,
 }
 
 impl fmt::Display for QuoteState {
@@ -56,6 +60,8 @@ impl fmt::Display for QuoteState {
             Self::Unpaid => write!(f, "UNPAID"),
             Self::Paid => write!(f, "PAID"),
             Self::Pending => write!(f, "PENDING"),
+            Self::Unknown => write!(f, "UNKNOWN"),
+            Self::Failed => write!(f, "FAILED"),
         }
     }
 }
@@ -68,6 +74,8 @@ impl FromStr for QuoteState {
             "PENDING" => Ok(Self::Pending),
             "PAID" => Ok(Self::Paid),
             "UNPAID" => Ok(Self::Unpaid),
+            "UNKNOWN" => Ok(Self::Unknown),
+            "FAILED" => Ok(Self::Failed),
             _ => Err(Error::UnknownState),
         }
     }

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

@@ -5,7 +5,8 @@ use serde::{Deserialize, Serialize};
 use crate::error::Error;
 use crate::mint_url::MintUrl;
 use crate::nuts::{
-    CurrencyUnit, MeltQuoteState, Proof, Proofs, PublicKey, SpendingConditions, State,
+    CurrencyUnit, MeltQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SpendingConditions,
+    State,
 };
 use crate::Amount;
 
@@ -137,6 +138,23 @@ impl ProofInfo {
     }
 }
 
+/// Key used in hashmap of ln backends to identify what unit and payment method
+/// it is for
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct LnKey {
+    /// Unit of Payment backend
+    pub unit: CurrencyUnit,
+    /// Method of payment backend
+    pub method: PaymentMethod,
+}
+
+impl LnKey {
+    /// Create new [`LnKey`]
+    pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
+        Self { unit, method }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::str::FromStr;

+ 86 - 0
misc/fake_itests.sh

@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+
+# Function to perform cleanup
+cleanup() {
+    echo "Cleaning up..."
+
+    # Kill the Rust binary process
+    echo "Killing the Rust binary with PID $RUST_BIN_PID"
+    kill $CDK_ITEST_MINT_BIN_PID
+
+    # Wait for the Rust binary to terminate
+    wait $CDK_ITEST_MINT_BIN_PID
+
+    echo "Mint binary terminated"
+    
+    # Remove the temporary directory
+    rm -rf "$cdk_itests"
+    echo "Temp directory removed: $cdk_itests"
+    unset cdk_itests
+    unset cdk_itests_mint_addr
+    unset cdk_itests_mint_port
+}
+
+# Set up trap to call cleanup on script exit
+trap cleanup EXIT
+
+# Create a temporary directory
+export cdk_itests=$(mktemp -d)
+export cdk_itests_mint_addr="127.0.0.1";
+export cdk_itests_mint_port=8086;
+
+URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port/v1/info"
+# Check if the temporary directory was created successfully
+if [[ ! -d "$cdk_itests" ]]; then
+    echo "Failed to create temp directory"
+    exit 1
+fi
+
+echo "Temp directory created: $cdk_itests"
+export MINT_DATABASE="$1";
+
+cargo build -p cdk-integration-tests 
+cargo build --bin fake_wallet 
+cargo run --bin fake_wallet &
+# Capture its PID
+CDK_ITEST_MINT_BIN_PID=$!
+
+TIMEOUT=100
+START_TIME=$(date +%s)
+# Loop until the endpoint returns a 200 OK status or timeout is reached
+while true; do
+    # Get the current time
+    CURRENT_TIME=$(date +%s)
+    
+    # Calculate the elapsed time
+    ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
+
+    # Check if the elapsed time exceeds the timeout
+    if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
+        echo "Timeout of $TIMEOUT seconds reached. Exiting..."
+        exit 1
+    fi
+
+    # Make a request to the endpoint and capture the HTTP status code
+    HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL)
+
+    # Check if the HTTP status is 200 OK
+    if [ "$HTTP_STATUS" -eq 200 ]; then
+        echo "Received 200 OK from $URL"
+        break
+    else
+        echo "Waiting for 200 OK response, current status: $HTTP_STATUS"
+        sleep 2  # Wait for 2 seconds before retrying
+    fi
+done
+
+
+# Run cargo test
+cargo test -p cdk-integration-tests --test fake_wallet
+cargo test -p cdk-integration-tests --test mint
+
+# Capture the exit status of cargo test
+test_status=$?
+
+# Exit with the status of the tests
+exit $test_status

+ 5 - 0
misc/test.just

@@ -2,3 +2,8 @@ itest db:
   #!/usr/bin/env bash
   ./misc/itests.sh "{{db}}"
 
+  
+fake-mint-itest db:
+  #!/usr/bin/env bash
+  ./misc/fake_itests.sh "{{db}}"
+