Browse Source

feat: mintd axum server

feat: deafult NUT-04 and NUT-05 settings to enable bolt11 sats
thesimplekid 9 months ago
parent
commit
6a315fc3b9
49 changed files with 1774 additions and 248 deletions
  1. 2 0
      .github/workflows/ci.yml
  2. 1 0
      .gitignore
  3. 3 0
      Cargo.toml
  4. 3 1
      bindings/cdk-js/src/nuts/nut00/currency_unit.rs
  5. 2 1
      bindings/cdk-js/src/nuts/nut02/keyset.rs
  6. 1 1
      bindings/cdk-js/src/types/melt_quote.rs
  7. 1 1
      bindings/cdk-js/src/types/mint_quote.rs
  8. 19 0
      crates/cdk-axum/Cargo.toml
  9. 63 0
      crates/cdk-axum/src/lib.rs
  10. 402 0
      crates/cdk-axum/src/router_handlers.rs
  11. 1 1
      crates/cdk-cli/Cargo.toml
  12. 3 0
      crates/cdk-cln/src/error.rs
  13. 63 20
      crates/cdk-cln/src/lib.rs
  14. 30 0
      crates/cdk-mintd/Cargo.toml
  15. 40 0
      crates/cdk-mintd/example.config.toml
  16. 24 0
      crates/cdk-mintd/src/cli.rs
  17. 123 0
      crates/cdk-mintd/src/config.rs
  18. 253 0
      crates/cdk-mintd/src/main.rs
  19. 1 0
      crates/cdk-redb/Cargo.toml
  20. 42 10
      crates/cdk-redb/src/migrations.rs
  21. 99 0
      crates/cdk-redb/src/mint/migrations.rs
  22. 30 9
      crates/cdk-redb/src/mint/mod.rs
  23. 5 4
      crates/cdk-redb/src/wallet/mod.rs
  24. 2 1
      crates/cdk-rexie/src/wallet.rs
  25. 1 0
      crates/cdk-sqlite/Cargo.toml
  26. 3 0
      crates/cdk-sqlite/src/mint/error.rs
  27. 2 0
      crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql
  28. 2 0
      crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql
  29. 62 16
      crates/cdk-sqlite/src/mint/mod.rs
  30. 3 0
      crates/cdk-sqlite/src/wallet/error.rs
  31. 12 10
      crates/cdk-sqlite/src/wallet/mod.rs
  32. 1 1
      crates/cdk/Cargo.toml
  33. 21 7
      crates/cdk/src/cdk_database/mint_memory.rs
  34. 34 23
      crates/cdk/src/cdk_database/mod.rs
  35. 7 5
      crates/cdk/src/cdk_database/wallet_memory.rs
  36. 65 6
      crates/cdk/src/cdk_lightning/mod.rs
  37. 44 0
      crates/cdk/src/error.rs
  38. 3 7
      crates/cdk/src/mint/error.rs
  39. 41 5
      crates/cdk/src/mint/mod.rs
  40. 103 0
      crates/cdk/src/mint/types.rs
  41. 19 16
      crates/cdk/src/nuts/nut00/mod.rs
  42. 20 4
      crates/cdk/src/nuts/nut04.rs
  43. 22 4
      crates/cdk/src/nuts/nut05.rs
  44. 41 1
      crates/cdk/src/nuts/nut06.rs
  45. 1 92
      crates/cdk/src/types.rs
  46. 4 1
      crates/cdk/src/wallet/mod.rs
  47. 2 1
      crates/cdk/src/wallet/multi_mint_wallet.rs
  48. 46 0
      crates/cdk/src/wallet/types.rs
  49. 2 0
      misc/scripts/check-crates.sh

+ 2 - 0
.github/workflows/ci.yml

@@ -32,7 +32,9 @@ jobs:
             -p cdk --no-default-features --features mint,
             -p cdk-redb,
             -p cdk-sqlite,
+            -p cdk-axum,
             --bin cdk-cli,
+            --bin cdk-mintd,
             --examples
           ]
     steps:

+ 1 - 0
.gitignore

@@ -5,3 +5,4 @@
 .idea/
 *.redb
 *.sqlite*
+config.toml

+ 3 - 0
Cargo.toml

@@ -30,6 +30,7 @@ cdk-rexie = { version = "0.1", path = "./crates/cdk-rexie", default-features = f
 cdk-sqlite = { version = "0.1", path = "./crates/cdk-sqlite", default-features = false }
 cdk-redb = { version = "0.1", path = "./crates/cdk-redb", default-features = false }
 cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false }
+cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = false }
 tokio = { version = "1", default-features = false }
 thiserror = "1"
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
@@ -39,6 +40,8 @@ serde-wasm-bindgen = "0.6.5"
 futures = { version = "0.3.28", default-feature = false }
 web-sys =  { version = "0.3.69", default-features = false, features = ["console"] }
 uuid = { version = "1", features = ["v4"] }
+lightning-invoice = { version = "0.31", features = ["serde"] }
+home = "0.5.9"
 
 [profile]
 

+ 3 - 1
bindings/cdk-js/src/nuts/nut00/currency_unit.rs

@@ -8,6 +8,7 @@ pub enum JsCurrencyUnit {
     Sat,
     Msat,
     Usd,
+    Eur,
 }
 
 impl From<CurrencyUnit> for JsCurrencyUnit {
@@ -16,7 +17,7 @@ impl From<CurrencyUnit> for JsCurrencyUnit {
             CurrencyUnit::Sat => JsCurrencyUnit::Sat,
             CurrencyUnit::Msat => JsCurrencyUnit::Msat,
             CurrencyUnit::Usd => JsCurrencyUnit::Usd,
-            CurrencyUnit::Custom(_) => todo!(),
+            CurrencyUnit::Eur => JsCurrencyUnit::Eur,
         }
     }
 }
@@ -27,6 +28,7 @@ impl From<JsCurrencyUnit> for CurrencyUnit {
             JsCurrencyUnit::Sat => CurrencyUnit::Sat,
             JsCurrencyUnit::Msat => CurrencyUnit::Msat,
             JsCurrencyUnit::Usd => CurrencyUnit::Usd,
+            JsCurrencyUnit::Eur => CurrencyUnit::Eur,
         }
     }
 }

+ 2 - 1
bindings/cdk-js/src/nuts/nut02/keyset.rs

@@ -1,4 +1,5 @@
 use std::ops::Deref;
+use std::str::FromStr;
 
 use cdk::nuts::{CurrencyUnit, KeySet, KeysResponse, KeysetResponse};
 use wasm_bindgen::prelude::*;
@@ -33,7 +34,7 @@ impl JsKeySet {
         Self {
             inner: KeySet {
                 id: *id.deref(),
-                unit: CurrencyUnit::from(&unit),
+                unit: CurrencyUnit::from_str(&unit).unwrap(),
                 keys: keys.deref().clone(),
             },
         }

+ 1 - 1
bindings/cdk-js/src/types/melt_quote.rs

@@ -1,6 +1,6 @@
 use std::ops::Deref;
 
-use cdk::types::MeltQuote;
+use cdk::wallet::types::MeltQuote;
 use wasm_bindgen::prelude::*;
 
 use crate::nuts::JsCurrencyUnit;

+ 1 - 1
bindings/cdk-js/src/types/mint_quote.rs

@@ -1,6 +1,6 @@
 use std::ops::Deref;
 
-use cdk::types::MintQuote;
+use cdk::wallet::MintQuote;
 use wasm_bindgen::prelude::*;
 
 use crate::nuts::JsCurrencyUnit;

+ 19 - 0
crates/cdk-axum/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "cdk-axum"
+version = "0.1.0"
+edition = "2021"
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+anyhow = "1.0.75"
+async-trait.workspace = true
+axum = "0.7.5"
+axum-macros = "0.4.1"
+cdk = { workspace = true, default-features = false, features = ["mint"] }
+tokio.workspace = true
+tower-http = { version = "0.5.2", features = ["cors"] }
+tracing.workspace = true
+futures = "0.3.28"

+ 63 - 0
crates/cdk-axum/src/lib.rs

@@ -0,0 +1,63 @@
+//! Axum server for Mint
+
+#![warn(missing_docs)]
+#![warn(rustdoc::bare_urls)]
+
+use std::sync::Arc;
+
+use anyhow::Result;
+use axum::routing::{get, post};
+use axum::Router;
+use cdk::cdk_lightning::{self, MintLightning};
+use cdk::mint::Mint;
+use router_handlers::*;
+
+mod router_handlers;
+
+/// Create mint [`Router`] with required endpoints for cashu mint
+pub async fn create_mint_router(
+    mint_url: &str,
+    mint: Arc<Mint>,
+    ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+    quote_ttl: u64,
+) -> Result<Router> {
+    let state = MintState {
+        ln,
+        mint,
+        mint_url: mint_url.to_string(),
+        quote_ttl,
+    };
+
+    let v1_router = Router::new()
+        .route("/keys", get(get_keys))
+        .route("/keysets", get(get_keysets))
+        .route("/keys/:keyset_id", get(get_keyset_pubkeys))
+        .route("/swap", post(post_swap))
+        .route("/mint/quote/bolt11", post(get_mint_bolt11_quote))
+        .route(
+            "/mint/quote/bolt11/:quote_id",
+            get(get_check_mint_bolt11_quote),
+        )
+        .route("/mint/bolt11", post(post_mint_bolt11))
+        .route("/melt/quote/bolt11", post(get_melt_bolt11_quote))
+        .route(
+            "/melt/quote/bolt11/:quote_id",
+            get(get_check_melt_bolt11_quote),
+        )
+        .route("/melt/bolt11", post(post_melt_bolt11))
+        .route("/checkstate", post(post_check))
+        .route("/info", get(get_mint_info))
+        .route("/restore", post(post_restore));
+
+    let mint_router = Router::new().nest("/v1", v1_router).with_state(state);
+
+    Ok(mint_router)
+}
+
+#[derive(Clone)]
+struct MintState {
+    ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+    mint: Arc<Mint>,
+    mint_url: String,
+    quote_ttl: u64,
+}

+ 402 - 0
crates/cdk-axum/src/router_handlers.rs

@@ -0,0 +1,402 @@
+use std::str::FromStr;
+
+use anyhow::Result;
+use axum::extract::{Json, Path, State};
+use axum::http::StatusCode;
+use axum::response::{IntoResponse, Response};
+use cdk::cdk_lightning::to_unit;
+use cdk::error::{Error, ErrorResponse};
+use cdk::nuts::nut05::MeltBolt11Response;
+use cdk::nuts::{
+    CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
+    MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
+    MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
+    RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
+};
+use cdk::util::unix_time;
+use cdk::Bolt11Invoice;
+
+use crate::MintState;
+
+pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysResponse>, Response> {
+    let pubkeys = state.mint.pubkeys().await.map_err(|err| {
+        tracing::error!("Could not get keys: {}", err);
+        into_response(err)
+    })?;
+
+    Ok(Json(pubkeys))
+}
+
+pub async fn get_keyset_pubkeys(
+    State(state): State<MintState>,
+    Path(keyset_id): Path<Id>,
+) -> Result<Json<KeysResponse>, Response> {
+    let pubkeys = state.mint.keyset_pubkeys(&keyset_id).await.map_err(|err| {
+        tracing::error!("Could not get keyset pubkeys: {}", err);
+        into_response(err)
+    })?;
+
+    Ok(Json(pubkeys))
+}
+
+pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetResponse>, Response> {
+    let mint = state.mint.keysets().await.map_err(|err| {
+        tracing::error!("Could not get keyset: {}", err);
+        into_response(err)
+    })?;
+
+    Ok(Json(mint))
+}
+
+pub async fn get_mint_bolt11_quote(
+    State(state): State<MintState>,
+    Json(payload): Json<MintQuoteBolt11Request>,
+) -> Result<Json<MintQuoteBolt11Response>, Response> {
+    let amount =
+        to_unit(payload.amount, &payload.unit, &state.ln.get_base_unit()).map_err(|err| {
+            tracing::error!("Backed does not support unit: {}", err);
+            into_response(Error::UnsupportedUnit)
+        })?;
+
+    let quote_expiry = unix_time() + state.quote_ttl;
+
+    let create_invoice_response = state
+        .ln
+        .create_invoice(amount, "".to_string(), quote_expiry)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not create invoice: {}", err);
+            into_response(Error::InvalidPaymentRequest)
+        })?;
+
+    let quote = state
+        .mint
+        .new_mint_quote(
+            state.mint_url.into(),
+            create_invoice_response.request.to_string(),
+            payload.unit,
+            payload.amount,
+            quote_expiry,
+            create_invoice_response.request_lookup_id,
+        )
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not create new mint quote: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(quote.into()))
+}
+
+pub async fn get_check_mint_bolt11_quote(
+    State(state): State<MintState>,
+    Path(quote_id): Path<String>,
+) -> Result<Json<MintQuoteBolt11Response>, Response> {
+    let quote = state
+        .mint
+        .check_mint_quote(&quote_id)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not check mint quote {}: {}", quote_id, err);
+            into_response(err)
+        })?;
+
+    Ok(Json(quote))
+}
+
+pub async fn post_mint_bolt11(
+    State(state): State<MintState>,
+    Json(payload): Json<MintBolt11Request>,
+) -> Result<Json<MintBolt11Response>, Response> {
+    let res = state
+        .mint
+        .process_mint_request(payload)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not process mint: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(res))
+}
+
+pub async fn get_melt_bolt11_quote(
+    State(state): State<MintState>,
+    Json(payload): Json<MeltQuoteBolt11Request>,
+) -> Result<Json<MeltQuoteBolt11Response>, Response> {
+    let invoice_amount_msat = payload
+        .request
+        .amount_milli_satoshis()
+        .ok_or(Error::InvoiceAmountUndefined)
+        .map_err(into_response)?;
+
+    // Convert amount to quote unit
+    let amount =
+        to_unit(invoice_amount_msat, &CurrencyUnit::Msat, &payload.unit).map_err(|err| {
+            tracing::error!("Backed does not support unit: {}", err);
+            into_response(Error::UnsupportedUnit)
+        })?;
+
+    let payment_quote = state.ln.get_payment_quote(&payload).await.unwrap();
+
+    let quote = state
+        .mint
+        .new_melt_quote(
+            payload.request.to_string(),
+            payload.unit,
+            amount.into(),
+            payment_quote.fee.into(),
+            unix_time() + state.quote_ttl,
+            payment_quote.request_lookup_id,
+        )
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not create melt quote: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(quote.into()))
+}
+
+pub async fn get_check_melt_bolt11_quote(
+    State(state): State<MintState>,
+    Path(quote_id): Path<String>,
+) -> Result<Json<MeltQuoteBolt11Response>, Response> {
+    let quote = state
+        .mint
+        .check_melt_quote(&quote_id)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not check melt quote: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(quote))
+}
+
+pub async fn post_melt_bolt11(
+    State(state): State<MintState>,
+    Json(payload): Json<MeltBolt11Request>,
+) -> Result<Json<MeltBolt11Response>, Response> {
+    let quote = match state.mint.verify_melt_request(&payload).await {
+        Ok(quote) => quote,
+        Err(err) => {
+            tracing::debug!("Error attempting to verify melt quote: {}", err);
+
+            if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
+                tracing::error!("Could not reset melt quote state: {}", err);
+            }
+            return Err(into_response(Error::MeltRequestInvalid));
+        }
+    };
+
+    // 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::DatabaseError));
+        }
+    };
+
+    let inputs_amount_quote_unit = payload.proofs_amount();
+
+    let (preimage, amount_spent_quote_unit) = match mint_quote {
+        Some(mint_quote) => {
+            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::InsufficientInputProofs));
+            }
+
+            mint_quote.state = MintQuoteState::Paid;
+
+            let amount = quote.amount;
+
+            if let Err(_err) = state.mint.update_mint_quote(mint_quote).await {
+                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::DatabaseError));
+            }
+
+            (None, amount)
+        }
+        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_msats = None;
+            let mut max_fee_msats = 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 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 = match invoice.amount_milli_satoshis() {
+                    Some(amount) => amount,
+                    None => {
+                        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::InvoiceAmountUndefined));
+                    }
+                };
+
+                partial_msats = match invoice_amount_msats > quote_msats {
+                    true => Some(invoice_amount_msats - quote_msats),
+                    false => None,
+                };
+
+                let max_fee = to_unit(quote.fee_reserve, &quote.unit, &CurrencyUnit::Msat)
+                    .expect("Quote unit is checked above that it can convert to msat");
+
+                max_fee_msats = Some(max_fee);
+
+                let amount_to_pay_msats = match partial_msats {
+                    Some(amount_to_pay) => amount_to_pay,
+                    None => invoice_amount_msats,
+                };
+
+                let input_amount_msats =
+                    to_unit(inputs_amount_quote_unit, &quote.unit, &CurrencyUnit::Msat)
+                        .expect("Quote unit is checked above that it can convert to msat");
+
+                if amount_to_pay_msats + max_fee > input_amount_msats {
+                    tracing::debug!(
+                        "Not enough inuts provided: {} msats needed {} msats",
+                        input_amount_msats,
+                        amount_to_pay_msats
+                    );
+
+                    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::InsufficientInputProofs));
+                }
+            }
+
+            let pre = match state
+                .ln
+                .pay_invoice(quote.clone(), partial_msats, max_fee_msats)
+                .await
+            {
+                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);
+                    }
+
+                    return Err(into_response(Error::PaymentFailed));
+                }
+            };
+
+            let amount_spent = to_unit(
+                pre.total_spent_msats,
+                &state.ln.get_base_unit(),
+                &quote.unit,
+            )
+            .map_err(|_| into_response(Error::UnsupportedUnit))?;
+
+            (pre.payment_preimage, amount_spent.into())
+        }
+    };
+
+    let res = state
+        .mint
+        .process_melt_request(&payload, preimage, amount_spent_quote_unit)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not process melt request: {}", err);
+            into_response(err)
+        })?;
+
+    Ok(Json(res.into()))
+}
+
+pub async fn post_check(
+    State(state): State<MintState>,
+    Json(payload): Json<CheckStateRequest>,
+) -> Result<Json<CheckStateResponse>, Response> {
+    let state = state.mint.check_state(&payload).await.map_err(|err| {
+        tracing::error!("Could not check state of proofs");
+        into_response(err)
+    })?;
+
+    Ok(Json(state))
+}
+
+pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintInfo>, Response> {
+    Ok(Json(state.mint.mint_info().clone()))
+}
+
+pub async fn post_swap(
+    State(state): State<MintState>,
+    Json(payload): Json<SwapRequest>,
+) -> Result<Json<SwapResponse>, Response> {
+    let swap_response = state
+        .mint
+        .process_swap_request(payload)
+        .await
+        .map_err(|err| {
+            tracing::error!("Could not process swap request: {}", err);
+            into_response(err)
+        })?;
+    Ok(Json(swap_response))
+}
+
+pub async fn post_restore(
+    State(state): State<MintState>,
+    Json(payload): Json<RestoreRequest>,
+) -> Result<Json<RestoreResponse>, Response> {
+    let restore_response = state.mint.restore(payload).await.map_err(|err| {
+        tracing::error!("Could not process restore: {}", err);
+        into_response(err)
+    })?;
+
+    Ok(Json(restore_response))
+}
+
+pub fn into_response<T>(error: T) -> Response
+where
+    T: Into<ErrorResponse>,
+{
+    (
+        StatusCode::INTERNAL_SERVER_ERROR,
+        Json::<ErrorResponse>(error.into()),
+    )
+        .into_response()
+}

+ 1 - 1
crates/cdk-cli/Cargo.toml

@@ -24,7 +24,7 @@ tokio.workspace = true
 tracing.workspace = true
 tracing-subscriber = "0.3.18"
 rand = "0.8.5"
-home = "0.5.9"
+home.workspace = true
 nostr-sdk = { version = "0.32.0", default-features = false, features = [
     "nip04",
     "nip44"

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

@@ -8,6 +8,9 @@ pub enum Error {
     /// Unknown invoice
     #[error("Unknown invoice")]
     UnknownInvoice,
+    /// Invoice amount not defined
+    #[error("Unknown invoice amount")]
+    UnknownInvoiceAmount,
     /// Cln Error
     #[error(transparent)]
     Cln(#[from] cln_rpc::Error),

+ 63 - 20
crates/cdk-cln/src/lib.rs

@@ -7,10 +7,13 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use async_trait::async_trait;
-use cdk::cdk_lightning::{self, MintLightning, PayInvoiceResponse};
-use cdk::nuts::{MeltQuoteState, MintQuoteState};
+use cdk::cdk_lightning::{
+    self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
+};
+use cdk::mint::FeeReserve;
+use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
 use cdk::util::{hex, unix_time};
-use cdk::Bolt11Invoice;
+use cdk::{mint, Bolt11Invoice};
 use cln_rpc::model::requests::{
     InvoiceRequest, ListinvoicesRequest, PayRequest, WaitanyinvoiceRequest,
 };
@@ -28,15 +31,17 @@ pub mod error;
 pub struct Cln {
     rpc_socket: PathBuf,
     cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
+    fee_reserve: FeeReserve,
 }
 
 impl Cln {
-    pub async fn new(rpc_socket: PathBuf) -> Result<Self, Error> {
+    pub async fn new(rpc_socket: PathBuf, fee_reserve: FeeReserve) -> Result<Self, Error> {
         let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
 
         Ok(Self {
             rpc_socket,
             cln_client: Arc::new(Mutex::new(cln_client)),
+            fee_reserve,
         })
     }
 }
@@ -45,6 +50,10 @@ impl Cln {
 impl MintLightning for Cln {
     type Err = cdk_lightning::Error;
 
+    fn get_base_unit(&self) -> CurrencyUnit {
+        CurrencyUnit::Msat
+    }
+
     async fn wait_any_invoice(
         &self,
     ) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err> {
@@ -88,16 +97,47 @@ impl MintLightning for Cln {
         .boxed())
     }
 
+    async fn get_payment_quote(
+        &self,
+        melt_quote_request: &MeltQuoteBolt11Request,
+    ) -> Result<PaymentQuoteResponse, Self::Err> {
+        let invoice_amount_msat = melt_quote_request
+            .request
+            .amount_milli_satoshis()
+            .ok_or(Error::UnknownInvoiceAmount)?;
+
+        let amount = to_unit(
+            invoice_amount_msat,
+            &CurrencyUnit::Msat,
+            &melt_quote_request.unit,
+        )?;
+
+        let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * amount as f32) as u64;
+
+        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
+
+        let fee = match relative_fee_reserve > absolute_fee_reserve {
+            true => relative_fee_reserve,
+            false => absolute_fee_reserve,
+        };
+
+        Ok(PaymentQuoteResponse {
+            request_lookup_id: melt_quote_request.request.to_string(),
+            amount,
+            fee,
+        })
+    }
+
     async fn pay_invoice(
         &self,
-        bolt11: Bolt11Invoice,
+        melt_quote: mint::MeltQuote,
         partial_msats: Option<u64>,
         max_fee_msats: Option<u64>,
     ) -> Result<PayInvoiceResponse, Self::Err> {
         let mut cln_client = self.cln_client.lock().await;
         let cln_response = cln_client
             .call(Request::Pay(PayRequest {
-                bolt11: bolt11.to_string(),
+                bolt11: melt_quote.request.to_string(),
                 amount_msat: None,
                 label: None,
                 riskfactor: None,
@@ -142,12 +182,13 @@ impl MintLightning for Cln {
         amount_msats: u64,
         description: String,
         unix_expiry: u64,
-    ) -> Result<Bolt11Invoice, Self::Err> {
+    ) -> Result<CreateInvoiceResponse, Self::Err> {
         let time_now = unix_time();
         assert!(unix_expiry > time_now);
 
         let mut cln_client = self.cln_client.lock().await;
 
+        let label = Uuid::new_v4().to_string();
         let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount_msats));
         let cln_response = cln_client
             .call(cln_rpc::Request::Invoice(InvoiceRequest {
@@ -164,26 +205,28 @@ impl MintLightning for Cln {
             .await
             .map_err(Error::from)?;
 
-        let invoice = match cln_response {
-            cln_rpc::Response::Invoice(invoice_res) => {
-                Bolt11Invoice::from_str(&invoice_res.bolt11)?
-            }
+        match cln_response {
+            cln_rpc::Response::Invoice(invoice_res) => Ok(CreateInvoiceResponse {
+                request_lookup_id: label,
+                request: Bolt11Invoice::from_str(&invoice_res.bolt11)?,
+            }),
             _ => {
                 tracing::warn!("CLN returned wrong response kind");
-                return Err(Error::WrongClnResponse.into());
+                Err(Error::WrongClnResponse.into())
             }
-        };
-
-        Ok(invoice)
+        }
     }
 
-    async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err> {
+    async fn check_invoice_status(
+        &self,
+        request_lookup_id: &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: Some(payment_hash.to_string()),
-                label: None,
+                payment_hash: None,
+                label: Some(request_lookup_id.to_string()),
                 invstring: None,
                 offer_id: None,
                 index: None,
@@ -201,8 +244,8 @@ impl MintLightning for Cln {
                     }
                     None => {
                         tracing::info!(
-                            "Check invoice called on unknown payment_hash: {}",
-                            payment_hash
+                            "Check invoice called on unknown look up id: {}",
+                            request_lookup_id
                         );
                         return Err(Error::WrongClnResponse.into());
                     }

+ 30 - 0
crates/cdk-mintd/Cargo.toml

@@ -0,0 +1,30 @@
+[package]
+name = "cdk-mintd"
+version = "0.1.0"
+edition = "2021"
+authors = ["CDK Developers"]
+homepage.workspace = true
+repository.workspace = true
+rust-version.workspace = true # MSRV
+license.workspace = true
+
+[dependencies]
+anyhow = "1.0.75"
+axum = "0.7.5"
+axum-macros = "0.4.1"
+cdk = { workspace = true, default-features = false, features = ["mint"] }
+cdk-redb = { workspace = true, default-features = false, features = ["mint"] }
+cdk-sqlite = { workspace = true, default-features = false, features = ["mint"] }
+cdk-cln = { workspace = true, default-features = false }
+cdk-axum = { workspace = true, default-features = false }
+config = { version = "0.13.3", features = ["toml"] }
+clap = { version = "4.4.8", features = ["derive", "env", "default"] }
+tokio.workspace = true
+tracing.workspace = true
+tracing-subscriber = "0.3.18"
+futures = "0.3.28"
+serde.workspace = true
+bip39.workspace = true
+tower-http = { version = "0.5.2", features = ["cors"] }
+lightning-invoice.workspace = true
+home.workspace = true

+ 40 - 0
crates/cdk-mintd/example.config.toml

@@ -0,0 +1,40 @@
+[info]
+url = "https://mint.thesimplekid.dev/"
+listen_host = "127.0.0.1"
+listen_port = 8085
+mnemonic = ""
+
+
+
+[mint_info]
+# name = "cdk-mintd mutiney net mint"
+# Hex publey of mint
+# pubkey = ""
+# description = "These are not real sats for testing only"
+# description_long = "A longer mint for testing"
+# motd = "Hello world"
+# contact_email = "hello@cashu.me"
+# Nostr pubkey of mint (Hex)
+# contact_nostr_public_key = ""
+
+
+[database]
+# Database engine (sqlite/redb) defaults to sqlite
+# engine = "sqlite"
+
+[ln]
+
+# Required ln backend `cln`
+ln_backend = "cln"
+
+# CLN
+# Required if using cln backend path to rpc
+cln_path = ""
+
+# Required to start greenlight for the first time
+# greenlight_invite_code = ""
+
+# Fee reserve for melting as a percent of payment amount
+fee_percent = 1.0
+# Fee reserve for melting as an absolute value
+reserve_fee_min = 1000

+ 24 - 0
crates/cdk-mintd/src/cli.rs

@@ -0,0 +1,24 @@
+use std::path::PathBuf;
+
+use clap::Parser;
+
+#[derive(Parser)]
+#[command(about = "A cashu mint written in rust", author = env!("CARGO_PKG_AUTHORS"), version = env!("CARGO_PKG_VERSION"))]
+pub struct CLIArgs {
+    #[arg(
+        short,
+        long,
+        help = "Use the <directory> as the location of the database",
+        required = false
+    )]
+    pub work_dir: Option<PathBuf>,
+    #[arg(
+        short,
+        long,
+        help = "Use the <file name> as the location of the config file",
+        required = false
+    )]
+    pub config: Option<PathBuf>,
+    #[arg(short, long, help = "Recover Greenlight from seed", required = false)]
+    pub recover: Option<String>,
+}

+ 123 - 0
crates/cdk-mintd/src/config.rs

@@ -0,0 +1,123 @@
+use std::path::PathBuf;
+
+use cdk::nuts::PublicKey;
+use cdk::Amount;
+use config::{Config, ConfigError, File};
+use serde::{Deserialize, Serialize};
+use tracing::{debug, warn};
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Info {
+    pub url: String,
+    pub listen_host: String,
+    pub listen_port: u16,
+    pub mnemonic: String,
+    pub seconds_quote_is_valid_for: Option<u64>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
+#[serde(rename_all = "lowercase")]
+pub enum LnBackend {
+    #[default]
+    Cln,
+    //  Greenlight,
+    //  Ldk,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Ln {
+    pub ln_backend: LnBackend,
+    pub cln_path: Option<PathBuf>,
+    pub greenlight_invite_code: Option<String>,
+    pub invoice_description: Option<String>,
+    pub fee_percent: f32,
+    pub reserve_fee_min: Amount,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
+#[serde(rename_all = "lowercase")]
+pub enum DatabaseEngine {
+    #[default]
+    Sqlite,
+    Redb,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Database {
+    pub engine: DatabaseEngine,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Settings {
+    pub info: Info,
+    pub mint_info: MintInfo,
+    pub ln: Ln,
+    pub database: Database,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct MintInfo {
+    /// name of the mint and should be recognizable
+    pub name: Option<String>,
+    /// hex pubkey of the mint
+    pub pubkey: Option<PublicKey>,
+    /// short description of the mint
+    pub description: Option<String>,
+    /// long description
+    pub description_long: Option<String>,
+    /// message of the day that the wallet must display to the user
+    pub motd: Option<String>,
+    /// Nostr publickey
+    pub contact_nostr_public_key: Option<String>,
+    /// Contact email
+    pub contact_email: Option<String>,
+}
+
+impl Settings {
+    #[must_use]
+    pub fn new(config_file_name: &Option<PathBuf>) -> Self {
+        let default_settings = Self::default();
+        // attempt to construct settings with file
+        let from_file = Self::new_from_default(&default_settings, config_file_name);
+        match from_file {
+            Ok(f) => f,
+            Err(e) => {
+                warn!("Error reading config file ({:?})", e);
+                default_settings
+            }
+        }
+    }
+
+    fn new_from_default(
+        default: &Settings,
+        config_file_name: &Option<PathBuf>,
+    ) -> Result<Self, ConfigError> {
+        let mut default_config_file_name = home::home_dir()
+            .ok_or(ConfigError::NotFound("Config Path".to_string()))?
+            .join("cashu-rs-mint");
+
+        default_config_file_name.push("config.toml");
+        let config: String = match config_file_name {
+            Some(value) => value.clone().to_string_lossy().to_string(),
+            None => default_config_file_name.to_string_lossy().to_string(),
+        };
+        let builder = Config::builder();
+        let config: Config = builder
+            // use defaults
+            .add_source(Config::try_from(default)?)
+            // override with file contents
+            .add_source(File::with_name(&config))
+            .build()?;
+        let settings: Settings = config.try_deserialize()?;
+
+        debug!("{settings:?}");
+
+        match settings.ln.ln_backend {
+            LnBackend::Cln => assert!(settings.ln.cln_path.is_some()),
+            //LnBackend::Greenlight => (),
+            //LnBackend::Ldk => (),
+        }
+
+        Ok(settings)
+    }
+}

+ 253 - 0
crates/cdk-mintd/src/main.rs

@@ -0,0 +1,253 @@
+//! CDK Mint Server
+
+#![warn(missing_docs)]
+#![warn(rustdoc::bare_urls)]
+
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use anyhow::{anyhow, Result};
+use axum::Router;
+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::nuts::{ContactInfo, MintInfo, MintVersion, Nuts};
+use cdk_cln::Cln;
+use cdk_redb::MintRedbDatabase;
+use cdk_sqlite::MintSqliteDatabase;
+use clap::Parser;
+use cli::CLIArgs;
+use config::{DatabaseEngine, LnBackend};
+use futures::StreamExt;
+use tower_http::cors::CorsLayer;
+
+mod cli;
+mod config;
+
+const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
+const DEFAULT_QUOTE_TTL_SECS: u64 = 1800;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    tracing_subscriber::fmt()
+        .with_max_level(tracing::Level::DEBUG)
+        .init();
+
+    let args = CLIArgs::parse();
+
+    let work_dir = match args.work_dir {
+        Some(w) => w,
+        None => work_dir()?,
+    };
+
+    // get config file name from args
+    let config_file_arg = match args.config {
+        Some(c) => c,
+        None => work_dir.join("config.toml"),
+    };
+
+    let settings = config::Settings::new(&Some(config_file_arg));
+
+    let localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync> =
+        match settings.database.engine {
+            DatabaseEngine::Sqlite => {
+                let sql_db_path = work_dir.join("cdk-mintd.sqlite");
+                let sqlite_db = MintSqliteDatabase::new(&sql_db_path).await?;
+
+                sqlite_db.migrate().await;
+
+                Arc::new(sqlite_db)
+            }
+            DatabaseEngine::Redb => {
+                let redb_path = work_dir.join("cdk-mintd.redb");
+                Arc::new(MintRedbDatabase::new(&redb_path)?)
+            }
+        };
+
+    let mut contact_info: Option<Vec<ContactInfo>> = None;
+
+    if let Some(nostr_contact) = settings.mint_info.contact_nostr_public_key {
+        let nostr_contact = ContactInfo::new("nostr".to_string(), nostr_contact);
+
+        contact_info = match contact_info {
+            Some(mut vec) => {
+                vec.push(nostr_contact);
+                Some(vec)
+            }
+            None => Some(vec![nostr_contact]),
+        };
+    }
+
+    if let Some(email_contact) = settings.mint_info.contact_email {
+        let email_contact = ContactInfo::new("email".to_string(), email_contact);
+
+        contact_info = match contact_info {
+            Some(mut vec) => {
+                vec.push(email_contact);
+                Some(vec)
+            }
+            None => Some(vec![email_contact]),
+        };
+    }
+
+    let mint_version = MintVersion::new(
+        "cdk-mintd".to_string(),
+        CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(),
+    );
+
+    let mint_info = MintInfo::new(
+        settings.mint_info.name,
+        settings.mint_info.pubkey,
+        Some(mint_version),
+        settings.mint_info.description,
+        settings.mint_info.description_long,
+        contact_info,
+        Nuts::default(),
+        settings.mint_info.motd,
+    );
+
+    let relative_ln_fee = settings.ln.fee_percent;
+
+    let absolute_ln_fee_reserve = settings.ln.reserve_fee_min;
+
+    let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
+
+    let fee_reserve = FeeReserve {
+        min_fee_reserve: absolute_ln_fee_reserve,
+        percent_fee_reserve: relative_ln_fee,
+    };
+
+    let mint = Mint::new(
+        &settings.info.url,
+        &mnemonic.to_seed_normalized(""),
+        mint_info,
+        localstore,
+        absolute_ln_fee_reserve,
+        relative_ln_fee,
+    )
+    .await?;
+
+    let ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync> =
+        match settings.ln.ln_backend {
+            LnBackend::Cln => {
+                let cln_socket = expand_path(
+                    settings
+                        .ln
+                        .cln_path
+                        .clone()
+                        .ok_or(anyhow!("cln socket not defined"))?
+                        .to_str()
+                        .ok_or(anyhow!("cln socket not defined"))?,
+                )
+                .ok_or(anyhow!("cln socket not defined"))?;
+
+                Arc::new(Cln::new(cln_socket, fee_reserve).await?)
+            }
+        };
+
+    let mint = Arc::new(mint);
+
+    // Check the status of any mint quotes that are pending
+    // In the event that the mint server is down but the ln node is not
+    // 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
+    check_pending_quotes(Arc::clone(&mint), Arc::clone(&ln)).await?;
+
+    let mint_url = settings.info.url;
+    let listen_addr = settings.info.listen_host;
+    let listen_port = settings.info.listen_port;
+    let quote_ttl = settings
+        .info
+        .seconds_quote_is_valid_for
+        .unwrap_or(DEFAULT_QUOTE_TTL_SECS);
+
+    let v1_service =
+        cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), Arc::clone(&ln), quote_ttl)
+            .await?;
+
+    let mint_service = Router::new()
+        .nest("/", v1_service)
+        .layer(CorsLayer::permissive());
+
+    // Spawn task to wait for invoces to be paid and update mint quotes
+    tokio::spawn(async move {
+        loop {
+            match ln.wait_any_invoice().await {
+                Ok(mut stream) => {
+                    while let Some(invoice) = stream.next().await {
+                        if let Err(err) =
+                            handle_paid_invoice(Arc::clone(&mint), &invoice.to_string()).await
+                        {
+                            tracing::warn!("{:?}", err);
+                        }
+                    }
+                }
+                Err(err) => {
+                    tracing::warn!("Could not get invoice stream: {}", err);
+                }
+            }
+        }
+    });
+
+    let listener =
+        tokio::net::TcpListener::bind(format!("{}:{}", listen_addr, listen_port)).await?;
+
+    axum::serve(listener, mint_service).await?;
+
+    Ok(())
+}
+
+/// Update mint quote when called for a paid invoice
+async fn handle_paid_invoice(mint: Arc<Mint>, request: &str) -> Result<()> {
+    if let Ok(Some(mint_quote)) = mint.localstore.get_mint_quote_by_request(request).await {
+        mint.localstore
+            .update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
+            .await?;
+    }
+    Ok(())
+}
+
+/// Used on mint start up to check status of all pending mint quotes
+async fn check_pending_quotes(
+    mint: Arc<Mint>,
+    ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+) -> Result<()> {
+    let pending_quotes = mint.get_pending_mint_quotes().await?;
+
+    for quote in pending_quotes {
+        let lookup_id = quote.request_lookup_id;
+        let state = ln.check_invoice_status(&lookup_id).await?;
+
+        if state != quote.state {
+            mint.localstore
+                .update_mint_quote_state(&quote.id, state)
+                .await?;
+        }
+    }
+
+    Ok(())
+}
+
+fn expand_path(path: &str) -> Option<PathBuf> {
+    if path.starts_with('~') {
+        if let Some(home_dir) = home::home_dir().as_mut() {
+            let remainder = &path[2..];
+            home_dir.push(remainder);
+            let expanded_path = home_dir;
+            Some(expanded_path.clone())
+        } else {
+            None
+        }
+    } else {
+        Some(PathBuf::from(path))
+    }
+}
+
+fn work_dir() -> Result<PathBuf> {
+    let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
+
+    Ok(home_dir.join(".cdk-mintd"))
+}

+ 1 - 0
crates/cdk-redb/Cargo.toml

@@ -24,3 +24,4 @@ thiserror.workspace = true
 tracing.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+lightning-invoice.workspace = true

+ 42 - 10
crates/cdk-redb/src/migrations.rs

@@ -2,7 +2,6 @@ use std::collections::HashMap;
 use std::sync::Arc;
 
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
-use cdk::types::{MeltQuote, MintQuote};
 use cdk::{Amount, UncheckedUrl};
 use redb::{Database, ReadableTable, TableDefinition};
 use serde::{Deserialize, Serialize};
@@ -24,18 +23,51 @@ pub struct V0MintQuote {
     pub expiry: u64,
 }
 
-impl From<V0MintQuote> for MintQuote {
-    fn from(quote: V0MintQuote) -> MintQuote {
+/// Mint Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct V1MintQuote {
+    pub id: String,
+    pub mint_url: UncheckedUrl,
+    pub amount: Amount,
+    pub unit: CurrencyUnit,
+    pub request: String,
+    pub state: MintQuoteState,
+    pub expiry: u64,
+}
+
+/// Melt Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct V1MeltQuote {
+    /// Quote id
+    pub id: String,
+    /// Quote unit
+    pub unit: CurrencyUnit,
+    /// Quote amount
+    pub amount: Amount,
+    /// Quote Payment request e.g. bolt11
+    pub request: String,
+    /// Quote fee reserve
+    pub fee_reserve: Amount,
+    /// Quote state
+    pub state: MeltQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Payment preimage
+    pub payment_preimage: Option<String>,
+}
+
+impl From<V0MintQuote> for V1MintQuote {
+    fn from(quote: V0MintQuote) -> V1MintQuote {
         let state = match quote.paid {
             true => MintQuoteState::Paid,
             false => MintQuoteState::Unpaid,
         };
-        MintQuote {
+        V1MintQuote {
             id: quote.id,
             mint_url: quote.mint_url,
             amount: quote.amount,
             unit: quote.unit,
-            request: quote.request,
+            request: quote.request.clone(),
             state,
             expiry: quote.expiry,
         }
@@ -54,13 +86,13 @@ pub struct V0MeltQuote {
     pub expiry: u64,
 }
 
-impl From<V0MeltQuote> for MeltQuote {
-    fn from(quote: V0MeltQuote) -> MeltQuote {
+impl From<V0MeltQuote> for V1MeltQuote {
+    fn from(quote: V0MeltQuote) -> V1MeltQuote {
         let state = match quote.paid {
             true => MeltQuoteState::Paid,
             false => MeltQuoteState::Unpaid,
         };
-        MeltQuote {
+        V1MeltQuote {
             id: quote.id,
             amount: quote.amount,
             unit: quote.unit,
@@ -94,7 +126,7 @@ fn migrate_mint_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
             .collect();
     }
 
-    let migrated_mint_quotes: HashMap<String, Option<MintQuote>> = mint_quotes
+    let migrated_mint_quotes: HashMap<String, Option<V1MintQuote>> = mint_quotes
         .into_iter()
         .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
         .collect();
@@ -147,7 +179,7 @@ fn migrate_melt_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
             .collect();
     }
 
-    let migrated_melt_quotes: HashMap<String, Option<MeltQuote>> = melt_quotes
+    let migrated_melt_quotes: HashMap<String, Option<V1MeltQuote>> = melt_quotes
         .into_iter()
         .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
         .collect();

+ 99 - 0
crates/cdk-redb/src/mint/migrations.rs

@@ -0,0 +1,99 @@
+use std::collections::HashMap;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use cdk::mint::MintQuote;
+use cdk::nuts::{CurrencyUnit, MintQuoteState};
+use cdk::{Amount, UncheckedUrl};
+use lightning_invoice::Bolt11Invoice;
+use redb::{Database, ReadableTable, TableDefinition};
+use serde::{Deserialize, Serialize};
+
+use super::Error;
+
+const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
+
+pub fn migrate_01_to_02(db: Arc<Database>) -> Result<u32, Error> {
+    migrate_mint_quotes_01_to_02(db)?;
+
+    Ok(2)
+}
+/// Mint Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+struct V1MintQuote {
+    pub id: String,
+    pub mint_url: UncheckedUrl,
+    pub amount: Amount,
+    pub unit: CurrencyUnit,
+    pub request: String,
+    pub state: MintQuoteState,
+    pub expiry: u64,
+}
+
+impl From<V1MintQuote> for MintQuote {
+    fn from(quote: V1MintQuote) -> MintQuote {
+        MintQuote {
+            id: quote.id,
+            mint_url: quote.mint_url,
+            amount: quote.amount,
+            unit: quote.unit,
+            request: quote.request.clone(),
+            state: quote.state,
+            expiry: quote.expiry,
+            request_lookup_id: Bolt11Invoice::from_str(&quote.request).unwrap().to_string(),
+        }
+    }
+}
+
+fn migrate_mint_quotes_01_to_02(db: Arc<Database>) -> Result<(), Error> {
+    let read_txn = db.begin_read().map_err(Error::from)?;
+    let table = read_txn
+        .open_table(MINT_QUOTES_TABLE)
+        .map_err(Error::from)?;
+
+    let mint_quotes: HashMap<String, Option<V1MintQuote>>;
+    {
+        mint_quotes = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .map(|(quote_id, mint_quote)| {
+                (
+                    quote_id.value().to_string(),
+                    serde_json::from_str(mint_quote.value()).ok(),
+                )
+            })
+            .collect();
+    }
+
+    let migrated_mint_quotes: HashMap<String, Option<MintQuote>> = mint_quotes
+        .into_iter()
+        .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
+        .collect();
+
+    {
+        let write_txn = db.begin_write()?;
+
+        {
+            let mut table = write_txn
+                .open_table(MINT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+            for (quote_id, quote) in migrated_mint_quotes {
+                match quote {
+                    Some(quote) => {
+                        let quote_str = serde_json::to_string(&quote)?;
+
+                        table.insert(quote_id.as_str(), quote_str.as_str())?;
+                    }
+                    None => {
+                        table.remove(quote_id.as_str())?;
+                    }
+                }
+            }
+        }
+
+        write_txn.commit()?;
+    }
+
+    Ok(())
+}

+ 30 - 9
crates/cdk-redb/src/mint/mod.rs

@@ -7,21 +7,23 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use async_trait::async_trait;
-use cdk::cdk_database;
 use cdk::cdk_database::MintDatabase;
 use cdk::dhke::hash_to_curve;
-use cdk::mint::MintKeySetInfo;
+use cdk::mint::{MintKeySetInfo, MintQuote};
 use cdk::nuts::{
     BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey,
 };
 use cdk::secret::Secret;
-use cdk::types::{MeltQuote, MintQuote};
+use cdk::{cdk_database, mint};
+use migrations::migrate_01_to_02;
 use redb::{Database, ReadableTable, TableDefinition};
 use tokio::sync::Mutex;
 
 use super::error::Error;
 use crate::migrations::migrate_00_to_01;
 
+mod migrations;
+
 const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets");
 const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets");
 const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
@@ -34,7 +36,7 @@ const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config")
 const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> =
     TableDefinition::new("blinded_signatures");
 
-const DATABASE_VERSION: u32 = 1;
+const DATABASE_VERSION: u32 = 2;
 
 /// Mint Redbdatabase
 #[derive(Debug, Clone)]
@@ -72,6 +74,10 @@ impl MintRedbDatabase {
                                 current_file_version = migrate_00_to_01(Arc::clone(&db))?;
                             }
 
+                            if current_file_version == 1 {
+                                current_file_version = migrate_01_to_02(Arc::clone(&db))?;
+                            }
+
                             if current_file_version != DATABASE_VERSION {
                                 tracing::warn!(
                                     "Database upgrade did not complete at {} current is {}",
@@ -169,7 +175,7 @@ impl MintDatabase for MintRedbDatabase {
         let mut active_keysets = HashMap::new();
 
         for (unit, id) in (table.iter().map_err(Error::from)?).flatten() {
-            let unit = CurrencyUnit::from(unit.value());
+            let unit = CurrencyUnit::from_str(unit.value())?;
             let id = Id::from_str(id.value()).map_err(Error::from)?;
 
             active_keysets.insert(unit, id);
@@ -309,6 +315,21 @@ impl MintDatabase for MintRedbDatabase {
 
         Ok(current_state)
     }
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let quotes = self.get_mint_quotes().await?;
+
+        let quote = quotes
+            .into_iter()
+            .filter(|q| q.request.eq(request))
+            .collect::<Vec<MintQuote>>()
+            .first()
+            .cloned();
+
+        Ok(quote)
+    }
 
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
         let db = self.db.lock().await;
@@ -344,7 +365,7 @@ impl MintDatabase for MintRedbDatabase {
         Ok(())
     }
 
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
+    async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
 
         let write_txn = db.begin_write().map_err(Error::from)?;
@@ -365,7 +386,7 @@ impl MintDatabase for MintRedbDatabase {
         Ok(())
     }
 
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
         let table = read_txn
@@ -383,7 +404,7 @@ impl MintDatabase for MintRedbDatabase {
         state: MeltQuoteState,
     ) -> Result<MeltQuoteState, Self::Err> {
         let db = self.db.lock().await;
-        let mut melt_quote: MeltQuote;
+        let mut melt_quote: mint::MeltQuote;
         {
             let read_txn = db.begin_read().map_err(Error::from)?;
             let table = read_txn
@@ -423,7 +444,7 @@ impl MintDatabase for MintRedbDatabase {
         Ok(current_state)
     }
 
-    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
         let table = read_txn

+ 5 - 4
crates/cdk-redb/src/wallet/mod.rs

@@ -7,14 +7,15 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use async_trait::async_trait;
-use cdk::cdk_database;
 use cdk::cdk_database::WalletDatabase;
 use cdk::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
 };
-use cdk::types::{MeltQuote, MintQuote, ProofInfo};
+use cdk::types::ProofInfo;
 use cdk::url::UncheckedUrl;
 use cdk::util::unix_time;
+use cdk::wallet::MintQuote;
+use cdk::{cdk_database, wallet};
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
 use tokio::sync::Mutex;
 use tracing::instrument;
@@ -432,7 +433,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip_all)]
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
         let write_txn = db.begin_write().map_err(Error::from)?;
 
@@ -454,7 +455,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip_all)]
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
         let table = read_txn

+ 2 - 1
crates/cdk-rexie/src/wallet.rs

@@ -9,9 +9,10 @@ use cdk::cdk_database::WalletDatabase;
 use cdk::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
 };
-use cdk::types::{MeltQuote, MintQuote, ProofInfo};
+use cdk::types::ProofInfo;
 use cdk::url::UncheckedUrl;
 use cdk::util::unix_time;
+use cdk::wallet::{MeltQuote, MintQuote};
 use rexie::*;
 use thiserror::Error;
 use tokio::sync::Mutex;

+ 1 - 0
crates/cdk-sqlite/Cargo.toml

@@ -28,3 +28,4 @@ tokio = { workspace = true, features = [
 ] }
 tracing.workspace = true
 serde_json.workspace = true
+lightning-invoice.workspace = true

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

@@ -8,6 +8,9 @@ pub enum Error {
     /// SQLX Error
     #[error(transparent)]
     SQLX(#[from] sqlx::Error),
+    /// NUT00 Error
+    #[error(transparent)]
+    CDKNUT00(#[from] cdk::nuts::nut00::Error),
     /// NUT01 Error
     #[error(transparent)]
     CDKNUT01(#[from] cdk::nuts::nut01::Error),

+ 2 - 0
crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql

@@ -42,6 +42,7 @@ CREATE TABLE IF NOT EXISTS mint_quote (
 
 CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid);
 CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request);
+CREATE INDEX IF NOT EXISTS expiry_index ON mint_quote(expiry);
 
 CREATE TABLE IF NOT EXISTS melt_quote (
     id TEXT PRIMARY KEY,
@@ -55,6 +56,7 @@ CREATE TABLE IF NOT EXISTS melt_quote (
 
 CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid);
 CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request);
+CREATE INDEX IF NOT EXISTS expiry_index ON melt_quote(expiry);
 
 CREATE TABLE IF NOT EXISTS blind_signature (
     y BLOB PRIMARY KEY,

+ 2 - 0
crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql

@@ -0,0 +1,2 @@
+ALTER TABLE mint_quote ADD request_lookup_id TEXT;
+ALTER TABLE melt_quote ADD request_lookup_id TEXT;

+ 62 - 16
crates/cdk-sqlite/src/mint/mod.rs

@@ -7,15 +7,15 @@ use std::str::FromStr;
 use async_trait::async_trait;
 use bitcoin::bip32::DerivationPath;
 use cdk::cdk_database::{self, MintDatabase};
-use cdk::mint::MintKeySetInfo;
+use cdk::mint::{MintKeySetInfo, MintQuote};
 use cdk::nuts::nut05::QuoteState;
 use cdk::nuts::{
     BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
 };
 use cdk::secret::Secret;
-use cdk::types::{MeltQuote, MintQuote};
-use cdk::Amount;
+use cdk::{mint, Amount};
 use error::Error;
+use lightning_invoice::Bolt11Invoice;
 use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
 use sqlx::{ConnectOptions, Row};
 
@@ -116,7 +116,10 @@ WHERE active = 1
         let keysets = recs
             .iter()
             .filter_map(|r| match Id::from_str(r.get("id")) {
-                Ok(id) => Some((CurrencyUnit::from(r.get::<'_, &str, &str>("unit")), id)),
+                Ok(id) => Some((
+                    CurrencyUnit::from_str(r.get::<'_, &str, &str>("unit")).unwrap(),
+                    id,
+                )),
                 Err(_) => None,
             })
             .collect();
@@ -128,8 +131,8 @@ WHERE active = 1
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO mint_quote
-(id, mint_url, amount, unit, request, state, expiry)
-VALUES (?, ?, ?, ?, ?, ?, ?);
+(id, mint_url, amount, unit, request, state, expiry, request_lookup_id)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?);
         "#,
         )
         .bind(quote.id.to_string())
@@ -139,6 +142,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
         .bind(quote.request)
         .bind(quote.state.to_string())
         .bind(quote.expiry as i64)
+        .bind(quote.request_lookup_id)
         .execute(&self.pool)
         .await
         // TODO: should check if error is not found and return none
@@ -169,6 +173,31 @@ WHERE id=?;
         Ok(Some(sqlite_row_to_mint_quote(rec)?))
     }
 
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let rec = sqlx::query(
+            r#"
+SELECT *
+FROM mint_quote
+WHERE request=?;
+        "#,
+        )
+        .bind(request)
+        .fetch_one(&self.pool)
+        .await;
+
+        let rec = match rec {
+            Ok(rec) => rec,
+            Err(err) => match err {
+                sqlx::Error::RowNotFound => return Ok(None),
+                _ => return Err(Error::SQLX(err).into()),
+            },
+        };
+
+        Ok(Some(sqlite_row_to_mint_quote(rec)?))
+    }
     async fn update_mint_quote_state(
         &self,
         quote_id: &str,
@@ -236,12 +265,12 @@ WHERE id=?
         Ok(())
     }
 
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
+    async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO melt_quote
-(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage)
-VALUES (?, ?, ?, ?, ?, ?, ?, ?);
+(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
         "#,
         )
         .bind(quote.id.to_string())
@@ -252,13 +281,14 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
         .bind(quote.state.to_string())
         .bind(quote.expiry as i64)
         .bind(quote.payment_preimage)
+        .bind(quote.request_lookup_id)
         .execute(&self.pool)
         .await
         .map_err(Error::from)?;
 
         Ok(())
     }
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err> {
         let rec = sqlx::query(
             r#"
 SELECT *
@@ -280,7 +310,7 @@ WHERE id=?;
 
         Ok(Some(sqlite_row_to_melt_quote(rec)?))
     }
-    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
         let rec = sqlx::query(
             r#"
 SELECT *
@@ -661,7 +691,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
 
     Ok(MintKeySetInfo {
         id: Id::from_str(&row_id).map_err(Error::from)?,
-        unit: CurrencyUnit::from(&row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         active: row_active,
         valid_from: row_valid_from as u64,
         valid_to: row_valid_to.map(|v| v as u64),
@@ -678,19 +708,30 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
     let row_request: String = row.try_get("request").map_err(Error::from)?;
     let row_state: String = row.try_get("state").map_err(Error::from)?;
     let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
+    let row_request_lookup_id: Option<String> =
+        row.try_get("request_lookup_id").map_err(Error::from)?;
+
+    let request_lookup_id = match row_request_lookup_id {
+        Some(id) => id,
+        None => match Bolt11Invoice::from_str(&row_request) {
+            Ok(invoice) => invoice.payment_hash().to_string(),
+            Err(_) => row_request.clone(),
+        },
+    };
 
     Ok(MintQuote {
         id: row_id,
         mint_url: row_mint_url.into(),
         amount: Amount::from(row_amount as u64),
-        unit: CurrencyUnit::from(row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         request: row_request,
         state: MintQuoteState::from_str(&row_state).map_err(Error::from)?,
         expiry: row_expiry as u64,
+        request_lookup_id,
     })
 }
 
-fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
+fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<mint::MeltQuote, Error> {
     let row_id: String = row.try_get("id").map_err(Error::from)?;
     let row_unit: String = row.try_get("unit").map_err(Error::from)?;
     let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
@@ -699,16 +740,21 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
     let row_state: String = row.try_get("state").map_err(Error::from)?;
     let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
     let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
+    let row_request_lookup: Option<String> =
+        row.try_get("request_lookup_id").map_err(Error::from)?;
+
+    let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone());
 
-    Ok(MeltQuote {
+    Ok(mint::MeltQuote {
         id: row_id,
         amount: Amount::from(row_amount as u64),
-        unit: CurrencyUnit::from(row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         request: row_request,
         fee_reserve: Amount::from(row_fee_reserve as u64),
         state: QuoteState::from_str(&row_state)?,
         expiry: row_expiry as u64,
         payment_preimage: row_preimage,
+        request_lookup_id,
     })
 }
 

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

@@ -14,6 +14,9 @@ pub enum Error {
     /// Wallet Error
     #[error(transparent)]
     CDKWallet(#[from] cdk::wallet::error::Error),
+    /// NUT00 Error
+    #[error(transparent)]
+    CDKNUT00(#[from] cdk::nuts::nut00::Error),
     /// NUT01 Error
     #[error(transparent)]
     CDKNUT01(#[from] cdk::nuts::nut01::Error),

+ 12 - 10
crates/cdk-sqlite/src/wallet/mod.rs

@@ -5,15 +5,17 @@ use std::path::Path;
 use std::str::FromStr;
 
 use async_trait::async_trait;
+use cdk::amount::Amount;
 use cdk::cdk_database::{self, WalletDatabase};
 use cdk::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs,
     PublicKey, SpendingConditions, State,
 };
 use cdk::secret::Secret;
-use cdk::types::{MeltQuote, MintQuote, ProofInfo};
+use cdk::types::ProofInfo;
 use cdk::url::UncheckedUrl;
-use cdk::Amount;
+use cdk::wallet;
+use cdk::wallet::MintQuote;
 use error::Error;
 use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow};
 use sqlx::{ConnectOptions, Row};
@@ -350,7 +352,7 @@ WHERE id=?
         Ok(())
     }
 
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO melt_quote
@@ -371,7 +373,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
 
         Ok(())
     }
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
         let rec = sqlx::query(
             r#"
 SELECT *
@@ -709,7 +711,7 @@ fn sqlite_row_to_keyset(row: &SqliteRow) -> Result<KeySetInfo, Error> {
 
     Ok(KeySetInfo {
         id: Id::from_str(&row_id)?,
-        unit: CurrencyUnit::from(row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         active,
     })
 }
@@ -729,14 +731,14 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
         id: row_id,
         mint_url: row_mint_url.into(),
         amount: Amount::from(row_amount as u64),
-        unit: CurrencyUnit::from(row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         request: row_request,
         state,
         expiry: row_expiry as u64,
     })
 }
 
-fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<MeltQuote, Error> {
+fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<wallet::MeltQuote, Error> {
     let row_id: String = row.try_get("id").map_err(Error::from)?;
     let row_unit: String = row.try_get("unit").map_err(Error::from)?;
     let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
@@ -747,10 +749,10 @@ fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<MeltQuote, Error> {
     let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
 
     let state = MeltQuoteState::from_str(&row_state)?;
-    Ok(MeltQuote {
+    Ok(wallet::MeltQuote {
         id: row_id,
         amount: Amount::from(row_amount as u64),
-        unit: CurrencyUnit::from(row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         request: row_request,
         fee_reserve: Amount::from(row_fee_reserve as u64),
         state,
@@ -788,6 +790,6 @@ fn sqlite_row_to_proof_info(row: &SqliteRow) -> Result<ProofInfo, Error> {
         mint_url: row_mint_url.into(),
         state: State::from_str(&row_state)?,
         spending_condition: row_spending_condition.and_then(|r| serde_json::from_str(&r).ok()),
-        unit: CurrencyUnit::from(row_unit),
+        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
     })
 }

+ 1 - 1
crates/cdk/Cargo.toml

@@ -27,7 +27,7 @@ bitcoin = { workspace = true, features = [
 ] } # lightning-invoice uses v0.30
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
 http = "1.0"
-lightning-invoice = { version = "0.31", features = ["serde"] }
+lightning-invoice.workspace = true
 once_cell = "1.19"
 reqwest = { version = "0.12", default-features = false, features = [
     "json",

+ 21 - 7
crates/cdk/src/cdk_database/mint_memory.rs

@@ -8,12 +8,11 @@ use tokio::sync::RwLock;
 
 use super::{Error, MintDatabase};
 use crate::dhke::hash_to_curve;
-use crate::mint::MintKeySetInfo;
+use crate::mint::{self, MintKeySetInfo, MintQuote};
 use crate::nuts::{
     BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
 };
 use crate::secret::Secret;
-use crate::types::{MeltQuote, MintQuote};
 
 /// Mint Memory Database
 #[derive(Debug, Clone)]
@@ -21,7 +20,7 @@ pub struct MintMemoryDatabase {
     active_keysets: Arc<RwLock<HashMap<CurrencyUnit, Id>>>,
     keysets: Arc<RwLock<HashMap<Id, MintKeySetInfo>>>,
     mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
-    melt_quotes: Arc<RwLock<HashMap<String, MeltQuote>>>,
+    melt_quotes: Arc<RwLock<HashMap<String, mint::MeltQuote>>>,
     pending_proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>,
     spent_proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>,
     blinded_signatures: Arc<RwLock<HashMap<[u8; 33], BlindSignature>>>,
@@ -34,7 +33,7 @@ impl MintMemoryDatabase {
         active_keysets: HashMap<CurrencyUnit, Id>,
         keysets: Vec<MintKeySetInfo>,
         mint_quotes: Vec<MintQuote>,
-        melt_quotes: Vec<MeltQuote>,
+        melt_quotes: Vec<mint::MeltQuote>,
         pending_proofs: Proofs,
         spent_proofs: Proofs,
         blinded_signatures: HashMap<[u8; 33], BlindSignature>,
@@ -129,6 +128,21 @@ impl MintDatabase for MintMemoryDatabase {
 
         Ok(current_state)
     }
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let quotes = self.get_mint_quotes().await?;
+
+        let quote = quotes
+            .into_iter()
+            .filter(|q| q.request.eq(request))
+            .collect::<Vec<MintQuote>>()
+            .first()
+            .cloned();
+
+        Ok(quote)
+    }
 
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
         Ok(self.mint_quotes.read().await.values().cloned().collect())
@@ -140,7 +154,7 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(())
     }
 
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
+    async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
         self.melt_quotes
             .write()
             .await
@@ -148,7 +162,7 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(())
     }
 
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err> {
         Ok(self.melt_quotes.read().await.get(quote_id).cloned())
     }
 
@@ -173,7 +187,7 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(current_state)
     }
 
-    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
         Ok(self.melt_quotes.read().await.values().cloned().collect())
     }
 

+ 34 - 23
crates/cdk/src/cdk_database/mod.rs

@@ -9,7 +9,11 @@ use async_trait::async_trait;
 use thiserror::Error;
 
 #[cfg(feature = "mint")]
+use crate::mint;
+#[cfg(feature = "mint")]
 use crate::mint::MintKeySetInfo;
+#[cfg(feature = "mint")]
+use crate::mint::MintQuote as MintMintQuote;
 #[cfg(feature = "wallet")]
 use crate::nuts::State;
 #[cfg(feature = "mint")]
@@ -22,10 +26,12 @@ use crate::nuts::{KeySetInfo, Keys, MintInfo, SpendingConditions};
 use crate::secret::Secret;
 #[cfg(feature = "wallet")]
 use crate::types::ProofInfo;
-#[cfg(any(feature = "wallet", feature = "mint"))]
-use crate::types::{MeltQuote, MintQuote};
 #[cfg(feature = "wallet")]
 use crate::url::UncheckedUrl;
+#[cfg(feature = "wallet")]
+use crate::wallet;
+#[cfg(feature = "wallet")]
+use crate::wallet::MintQuote as WalletMintQuote;
 
 #[cfg(feature = "mint")]
 pub mod mint_memory;
@@ -94,18 +100,18 @@ pub trait WalletDatabase: Debug {
     async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err>;
 
     /// Add mint quote to storage
-    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err>;
+    async fn add_mint_quote(&self, quote: WalletMintQuote) -> Result<(), Self::Err>;
     /// Get mint quote from storage
-    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err>;
+    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<WalletMintQuote>, Self::Err>;
     /// Get mint quotes from storage
-    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err>;
+    async fn get_mint_quotes(&self) -> Result<Vec<WalletMintQuote>, Self::Err>;
     /// Remove mint quote from storage
     async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
     /// Add melt quote to storage
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err>;
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err>;
     /// Get melt quote from storage
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err>;
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err>;
     /// Remove melt quote from storage
     async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
@@ -164,34 +170,39 @@ pub trait MintDatabase {
     /// Get all Active Keyset
     async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err>;
 
-    /// Add [`MintQuote`]
-    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err>;
-    /// Get [`MintQuote`]
-    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err>;
-    /// Update state of [`MintQuote`]
+    /// Add [`MintMintQuote`]
+    async fn add_mint_quote(&self, quote: MintMintQuote) -> Result<(), Self::Err>;
+    /// Get [`MintMintQuote`]
+    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintMintQuote>, Self::Err>;
+    /// Update state of [`MintMintQuote`]
     async fn update_mint_quote_state(
         &self,
         quote_id: &str,
         state: MintQuoteState,
     ) -> Result<MintQuoteState, Self::Err>;
-    /// Get all [`MintQuote`]s
-    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err>;
-    /// Remove [`MintQuote`]
+    /// Get all [`MintMintQuote`]s
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintMintQuote>, Self::Err>;
+    /// Get Mint Quotes
+    async fn get_mint_quotes(&self) -> Result<Vec<MintMintQuote>, Self::Err>;
+    /// Remove [`MintMintQuote`]
     async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
-    /// Add [`MeltQuote`]
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err>;
-    /// Get [`MeltQuote`]
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err>;
-    /// Update [`MeltQuote`] state
+    /// Add [`mint::MeltQuote`]
+    async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err>;
+    /// Get [`mint::MeltQuote`]
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<mint::MeltQuote>, Self::Err>;
+    /// Update [`mint::MeltQuote`] state
     async fn update_melt_quote_state(
         &self,
         quote_id: &str,
         state: MeltQuoteState,
     ) -> Result<MeltQuoteState, Self::Err>;
-    /// Get all [`MeltQuote`]s
-    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err>;
-    /// Remove [`MeltQuote`]
+    /// Get all [`mint::MeltQuote`]s
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err>;
+    /// Remove [`mint::MeltQuote`]
     async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
     /// Add [`MintKeySetInfo`]

+ 7 - 5
crates/cdk/src/cdk_database/wallet_memory.rs

@@ -11,9 +11,11 @@ use crate::cdk_database::Error;
 use crate::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State,
 };
-use crate::types::{MeltQuote, MintQuote, ProofInfo};
+use crate::types::ProofInfo;
 use crate::url::UncheckedUrl;
 use crate::util::unix_time;
+use crate::wallet;
+use crate::wallet::types::MintQuote;
 
 /// Wallet in Memory Database
 #[derive(Default, Debug, Clone)]
@@ -22,7 +24,7 @@ pub struct WalletMemoryDatabase {
     mint_keysets: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Id>>>>,
     keysets: Arc<RwLock<HashMap<Id, KeySetInfo>>>,
     mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
-    melt_quotes: Arc<RwLock<HashMap<String, MeltQuote>>>,
+    melt_quotes: Arc<RwLock<HashMap<String, wallet::MeltQuote>>>,
     mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
     proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
     keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
@@ -33,7 +35,7 @@ impl WalletMemoryDatabase {
     /// Create new [`WalletMemoryDatabase`]
     pub fn new(
         mint_quotes: Vec<MintQuote>,
-        melt_quotes: Vec<MeltQuote>,
+        melt_quotes: Vec<wallet::MeltQuote>,
         mint_keys: Vec<Keys>,
         keyset_counter: HashMap<Id, u32>,
         nostr_last_checked: HashMap<PublicKey, u32>,
@@ -207,7 +209,7 @@ impl WalletDatabase for WalletMemoryDatabase {
         Ok(())
     }
 
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Error> {
         self.melt_quotes
             .write()
             .await
@@ -215,7 +217,7 @@ impl WalletDatabase for WalletMemoryDatabase {
         Ok(())
     }
 
-    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Error> {
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Error> {
         Ok(self.melt_quotes.read().await.get(quote_id).cloned())
     }
 

+ 65 - 6
crates/cdk/src/cdk_lightning/mod.rs

@@ -8,7 +8,8 @@ use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
-use crate::nuts::{MeltQuoteState, MintQuoteState};
+use crate::mint;
+use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
 
 /// CDK Lightning Error
 #[derive(Debug, Error)]
@@ -25,6 +26,9 @@ pub enum Error {
     /// Parse Error
     #[error(transparent)]
     Parse(#[from] ParseOrSemanticError),
+    /// Cannot convert units
+    #[error("Cannot convert units")]
+    CannotConvertUnits,
 }
 
 /// MintLighting Trait
@@ -33,18 +37,28 @@ pub trait MintLightning {
     /// Mint Lightning Error
     type Err: Into<Error> + From<Error>;
 
+    /// Base Unit
+    fn get_base_unit(&self) -> CurrencyUnit;
+
     /// Create a new invoice
     async fn create_invoice(
         &self,
-        msats: u64,
+        amount: u64,
         description: String,
         unix_expiry: u64,
-    ) -> Result<Bolt11Invoice, Self::Err>;
+    ) -> Result<CreateInvoiceResponse, Self::Err>;
+
+    /// Get payment quote
+    /// Used to get fee and amount required for a payment request
+    async fn get_payment_quote(
+        &self,
+        melt_quote_request: &MeltQuoteBolt11Request,
+    ) -> Result<PaymentQuoteResponse, Self::Err>;
 
     /// Pay bolt11 invoice
     async fn pay_invoice(
         &self,
-        bolt11: Bolt11Invoice,
+        melt_quote: mint::MeltQuote,
         partial_msats: Option<u64>,
         max_fee_msats: Option<u64>,
     ) -> Result<PayInvoiceResponse, Self::Err>;
@@ -55,11 +69,23 @@ pub trait MintLightning {
     ) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err>;
 
     /// Check the status of an incoming payment
-    async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err>;
+    async fn check_invoice_status(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<MintQuoteState, Self::Err>;
+}
+
+/// Create invoice response
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CreateInvoiceResponse {
+    /// Id that is used to look up the invoice from the ln backend
+    pub request_lookup_id: String,
+    /// Bolt11 payment request
+    pub request: Bolt11Invoice,
 }
 
 /// Pay invoice response
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PayInvoiceResponse {
     /// Payment hash
     pub payment_hash: String,
@@ -70,3 +96,36 @@ pub struct PayInvoiceResponse {
     /// Totoal Amount Spent in msats
     pub total_spent_msats: u64,
 }
+
+/// Payment quote response
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PaymentQuoteResponse {
+    /// Request look up id
+    pub request_lookup_id: String,
+    /// Amount
+    pub amount: u64,
+    /// Fee required for melt
+    pub fee: u64,
+}
+
+const MSAT_IN_SAT: u64 = 1000;
+
+/// Helper function to convert units
+pub fn to_unit<T>(
+    amount: T,
+    current_unit: &CurrencyUnit,
+    target_unit: &CurrencyUnit,
+) -> Result<u64, Error>
+where
+    T: Into<u64>,
+{
+    let amount = amount.into();
+    match (current_unit, target_unit) {
+        (CurrencyUnit::Sat, CurrencyUnit::Sat) => Ok(amount),
+        (CurrencyUnit::Msat, CurrencyUnit::Msat) => Ok(amount),
+        (CurrencyUnit::Sat, CurrencyUnit::Msat) => Ok(amount * MSAT_IN_SAT),
+        (CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok(amount / MSAT_IN_SAT),
+        (CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount),
+        _ => Err(Error::CannotConvertUnits),
+    }
+}

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

@@ -15,6 +15,21 @@ pub enum Error {
     /// Mint does not have a key for amount
     #[error("No Key for Amount")]
     AmountKey,
+    /// Not enough input proofs provided
+    #[error("Not enough input proofs spent")]
+    InsufficientInputProofs,
+    /// Database update failed
+    #[error("Database error")]
+    DatabaseError,
+    /// Unsupported unit
+    #[error("Unit unsupported")]
+    UnsupportedUnit,
+    /// Payment failed
+    #[error("Payment failed")]
+    PaymentFailed,
+    /// Melt Request is not valid
+    #[error("Melt request is not valid")]
+    MeltRequestInvalid,
     /// Amount is not what expected
     #[error("Amount miss match")]
     Amount,
@@ -24,6 +39,9 @@ pub enum Error {
     /// Token could not be validated
     #[error("Token not verified")]
     TokenNotVerified,
+    /// Invalid payment request
+    #[error("Invalid payment request")]
+    InvalidPaymentRequest,
     /// Bolt11 invoice does not have amount
     #[error("Invoice Amount undefined")]
     InvoiceAmountUndefined,
@@ -104,6 +122,15 @@ impl fmt::Display for ErrorResponse {
 }
 
 impl ErrorResponse {
+    /// Create new [`ErrorResponse`]
+    pub fn new(code: ErrorCode, error: Option<String>, detail: Option<String>) -> Self {
+        Self {
+            code,
+            error,
+            detail,
+        }
+    }
+
     /// Error response from json
     pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
         let value: Value = serde_json::from_str(json)?;
@@ -124,6 +151,23 @@ impl ErrorResponse {
     }
 }
 
+impl From<Error> for ErrorResponse {
+    fn from(err: Error) -> ErrorResponse {
+        match err {
+            Error::TokenSpent => ErrorResponse {
+                code: ErrorCode::TokenAlreadySpent,
+                error: Some(err.to_string()),
+                detail: None,
+            },
+            _ => ErrorResponse {
+                code: ErrorCode::Unknown(9999),
+                error: Some(err.to_string()),
+                detail: None,
+            },
+        }
+    }
+}
+
 /// Possible Error Codes
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
 pub enum ErrorCode {

+ 3 - 7
crates/cdk/src/mint/error.rs

@@ -1,6 +1,5 @@
 //! Mint Errors
 
-use http::StatusCode;
 use thiserror::Error;
 
 use crate::cdk_database;
@@ -51,6 +50,9 @@ pub enum Error {
     /// Multiple units provided
     #[error("Cannot have multiple units")]
     MultipleUnits,
+    /// Unit not supported
+    #[error("Unit not supported")]
+    UnsupportedUnit,
     /// BlindMessage is already signed
     #[error("Blinded Message is already signed")]
     BlindedMessageAlreadySigned,
@@ -103,12 +105,6 @@ impl From<Error> for ErrorResponse {
     }
 }
 
-impl From<Error> for (StatusCode, ErrorResponse) {
-    fn from(err: Error) -> (StatusCode, ErrorResponse) {
-        (StatusCode::NOT_FOUND, err.into())
-    }
-}
-
 #[cfg(test)]
 mod tests {
 

+ 41 - 5
crates/cdk/src/mint/mod.rs

@@ -14,12 +14,14 @@ use crate::cdk_database::{self, MintDatabase};
 use crate::dhke::{hash_to_curve, sign_message, verify_message};
 use crate::nuts::nut11::enforce_sig_flag;
 use crate::nuts::*;
-use crate::types::{MeltQuote, MintQuote};
 use crate::url::UncheckedUrl;
 use crate::util::unix_time;
 use crate::Amount;
 
 pub mod error;
+pub mod types;
+
+pub use types::{MeltQuote, MintQuote};
 
 /// Cashu Mint
 #[derive(Clone)]
@@ -120,8 +122,9 @@ impl Mint {
         unit: CurrencyUnit,
         amount: Amount,
         expiry: u64,
+        ln_lookup: String,
     ) -> Result<MintQuote, Error> {
-        let quote = MintQuote::new(mint_url, request, unit, amount, expiry);
+        let quote = MintQuote::new(mint_url, request, unit, amount, expiry, ln_lookup);
 
         self.localstore.add_mint_quote(quote.clone()).await?;
 
@@ -159,6 +162,16 @@ impl Mint {
         Ok(quotes)
     }
 
+    /// Get pending mint quotes
+    pub async fn get_pending_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let mint_quotes = self.localstore.get_mint_quotes().await?;
+
+        Ok(mint_quotes
+            .into_iter()
+            .filter(|p| p.state == MintQuoteState::Pending)
+            .collect())
+    }
+
     /// Remove mint quote
     pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
         self.localstore.remove_mint_quote(quote_id).await?;
@@ -174,8 +187,16 @@ impl Mint {
         amount: Amount,
         fee_reserve: Amount,
         expiry: u64,
+        request_lookup_id: String,
     ) -> Result<MeltQuote, Error> {
-        let quote = MeltQuote::new(request, unit, amount, fee_reserve, expiry);
+        let quote = MeltQuote::new(
+            request,
+            unit,
+            amount,
+            fee_reserve,
+            expiry,
+            request_lookup_id,
+        );
 
         self.localstore.add_melt_quote(quote.clone()).await?;
 
@@ -699,12 +720,27 @@ impl Mint {
         Ok(quote)
     }
 
+    /// Process unpaid melt request
+    /// In the event that a melt request fails and the lighthing payment is not made
+    /// The [`Proofs`] should be returned to an unspent state and the quote should be unpaid
+    pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> {
+        self.localstore
+            .remove_pending_proofs(melt_request.inputs.iter().map(|p| &p.secret).collect())
+            .await?;
+
+        self.localstore
+            .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Unpaid)
+            .await?;
+
+        Ok(())
+    }
+
     /// Process melt request marking [`Proofs`] as spent
     /// The melt request must be verifyed using [`Self::verify_melt_request`] before calling [`Self::process_melt_request`]
     pub async fn process_melt_request(
         &self,
         melt_request: &MeltBolt11Request,
-        preimage: &str,
+        payment_preimage: Option<String>,
         total_spent: Amount,
     ) -> Result<MeltQuoteBolt11Response, Error> {
         tracing::debug!("Processing melt quote: {}", melt_request.quote);
@@ -788,7 +824,7 @@ impl Mint {
         Ok(MeltQuoteBolt11Response {
             amount: quote.amount,
             paid: Some(true),
-            payment_preimage: Some(preimage.to_string()),
+            payment_preimage,
             change,
             quote: quote.id,
             fee_reserve: quote.fee_reserve,

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

@@ -0,0 +1,103 @@
+//! Mint Types
+
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use super::CurrencyUnit;
+use crate::nuts::{MeltQuoteState, MintQuoteState};
+use crate::{Amount, UncheckedUrl};
+
+/// Mint Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintQuote {
+    /// Quote id
+    pub id: String,
+    /// Mint Url
+    pub mint_url: UncheckedUrl,
+    /// Amount of quote
+    pub amount: Amount,
+    /// Unit of quote
+    pub unit: CurrencyUnit,
+    /// Quote payment request e.g. bolt11
+    pub request: String,
+    /// Quote state
+    pub state: MintQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Value used by ln backend to look up state of request
+    pub request_lookup_id: String,
+}
+
+impl MintQuote {
+    /// Create new [`MintQuote`]
+    pub fn new(
+        mint_url: UncheckedUrl,
+        request: String,
+        unit: CurrencyUnit,
+        amount: Amount,
+        expiry: u64,
+        request_lookup_id: String,
+    ) -> Self {
+        let id = Uuid::new_v4();
+
+        Self {
+            mint_url,
+            id: id.to_string(),
+            amount,
+            unit,
+            request,
+            state: MintQuoteState::Unpaid,
+            expiry,
+            request_lookup_id,
+        }
+    }
+}
+
+/// Melt Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MeltQuote {
+    /// Quote id
+    pub id: String,
+    /// Quote unit
+    pub unit: CurrencyUnit,
+    /// Quote amount
+    pub amount: Amount,
+    /// Quote Payment request e.g. bolt11
+    pub request: String,
+    /// Quote fee reserve
+    pub fee_reserve: Amount,
+    /// Quote state
+    pub state: MeltQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Payment preimage
+    pub payment_preimage: Option<String>,
+    /// Value used by ln backend to look up state of request
+    pub request_lookup_id: String,
+}
+
+impl MeltQuote {
+    /// Create new [`MeltQuote`]
+    pub fn new(
+        request: String,
+        unit: CurrencyUnit,
+        amount: Amount,
+        fee_reserve: Amount,
+        expiry: u64,
+        request_lookup_id: String,
+    ) -> Self {
+        let id = Uuid::new_v4();
+
+        Self {
+            id: id.to_string(),
+            amount,
+            unit,
+            request,
+            fee_reserve,
+            state: MeltQuoteState::Unpaid,
+            expiry,
+            payment_preimage: None,
+            request_lookup_id,
+        }
+    }
+}

+ 19 - 16
crates/cdk/src/nuts/nut00/mod.rs

@@ -5,6 +5,7 @@
 use std::cmp::Ordering;
 use std::fmt;
 use std::hash::{Hash, Hasher};
+use std::str::FromStr;
 use std::string::FromUtf8Error;
 
 use serde::{Deserialize, Deserializer, Serialize};
@@ -37,6 +38,9 @@ pub enum Error {
     /// Unsupported token
     #[error("Unsupported token")]
     UnsupportedToken,
+    /// Unsupported token
+    #[error("Unsupported unit")]
+    UnsupportedUnit,
     /// Invalid Url
     #[error("Invalid Url")]
     InvalidUrl,
@@ -317,20 +321,19 @@ pub enum CurrencyUnit {
     Msat,
     /// Usd
     Usd,
-    /// Custom unit
-    Custom(String),
-}
-
-impl<S> From<S> for CurrencyUnit
-where
-    S: AsRef<str>,
-{
-    fn from(currency: S) -> Self {
-        match currency.as_ref() {
-            "sat" => Self::Sat,
-            "usd" => Self::Usd,
-            "msat" => Self::Msat,
-            o => Self::Custom(o.to_string()),
+    /// Euro
+    Eur,
+}
+
+impl FromStr for CurrencyUnit {
+    type Err = Error;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        match value {
+            "sat" => Ok(Self::Sat),
+            "msat" => Ok(Self::Msat),
+            "usd" => Ok(Self::Usd),
+            "eur" => Ok(Self::Eur),
+            _ => Err(Error::UnsupportedUnit),
         }
     }
 }
@@ -341,7 +344,7 @@ impl fmt::Display for CurrencyUnit {
             CurrencyUnit::Sat => write!(f, "sat"),
             CurrencyUnit::Msat => write!(f, "msat"),
             CurrencyUnit::Usd => write!(f, "usd"),
-            CurrencyUnit::Custom(unit) => write!(f, "{unit}"),
+            CurrencyUnit::Eur => write!(f, "eur"),
         }
     }
 }
@@ -361,7 +364,7 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
         D: Deserializer<'de>,
     {
         let currency: String = String::deserialize(deserializer)?;
-        Ok(Self::from(currency))
+        Self::from_str(&currency).map_err(|_| serde::de::Error::custom("Unsupported unit"))
     }
 }
 

+ 20 - 4
crates/cdk/src/nuts/nut04.rs

@@ -11,7 +11,6 @@ use thiserror::Error;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
 use super::MintQuoteState;
-use crate::types::MintQuote;
 use crate::Amount;
 
 /// NUT04 Error
@@ -153,8 +152,9 @@ impl<'de> Deserialize<'de> for MintQuoteBolt11Response {
     }
 }
 
-impl From<MintQuote> for MintQuoteBolt11Response {
-    fn from(mint_quote: MintQuote) -> MintQuoteBolt11Response {
+#[cfg(feature = "mint")]
+impl From<crate::mint::MintQuote> for MintQuoteBolt11Response {
+    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response {
         let paid = mint_quote.state == QuoteState::Paid;
         MintQuoteBolt11Response {
             quote: mint_quote.id,
@@ -208,8 +208,24 @@ pub struct MintMethodSettings {
 }
 
 /// Mint Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Settings {
     methods: Vec<MintMethodSettings>,
     disabled: bool,
 }
+
+impl Default for Settings {
+    fn default() -> Self {
+        let bolt11_mint = MintMethodSettings {
+            method: PaymentMethod::Bolt11,
+            unit: CurrencyUnit::Sat,
+            min_amount: Some(Amount::from(1)),
+            max_amount: Some(Amount::from(1000000)),
+        };
+
+        Settings {
+            methods: vec![bolt11_mint],
+            disabled: false,
+        }
+    }
+}

+ 22 - 4
crates/cdk/src/nuts/nut05.rs

@@ -11,7 +11,8 @@ use thiserror::Error;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
 use super::nut15::Mpp;
-use crate::types::MeltQuote;
+#[cfg(feature = "mint")]
+use crate::mint;
 use crate::{Amount, Bolt11Invoice};
 
 /// NUT05 Error
@@ -178,8 +179,9 @@ impl<'de> Deserialize<'de> for MeltQuoteBolt11Response {
     }
 }
 
-impl From<MeltQuote> for MeltQuoteBolt11Response {
-    fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response {
+#[cfg(feature = "mint")]
+impl From<mint::MeltQuote> for MeltQuoteBolt11Response {
+    fn from(melt_quote: mint::MeltQuote) -> MeltQuoteBolt11Response {
         let paid = melt_quote.state == QuoteState::Paid;
         MeltQuoteBolt11Response {
             quote: melt_quote.id,
@@ -251,8 +253,24 @@ pub struct MeltMethodSettings {
 }
 
 /// Melt Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Settings {
     methods: Vec<MeltMethodSettings>,
     disabled: bool,
 }
+
+impl Default for Settings {
+    fn default() -> Self {
+        let bolt11_mint = MeltMethodSettings {
+            method: PaymentMethod::Bolt11,
+            unit: CurrencyUnit::Sat,
+            min_amount: Some(Amount::from(1)),
+            max_amount: Some(Amount::from(1000000)),
+        };
+
+        Settings {
+            methods: vec![bolt11_mint],
+            disabled: false,
+        }
+    }
+}

+ 41 - 1
crates/cdk/src/nuts/nut06.rs

@@ -19,6 +19,13 @@ pub struct MintVersion {
     pub version: String,
 }
 
+impl MintVersion {
+    /// Create new [`MintVersion`]
+    pub fn new(name: String, version: String) -> Self {
+        Self { name, version }
+    }
+}
+
 impl Serialize for MintVersion {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
@@ -46,7 +53,7 @@ impl<'de> Deserialize<'de> for MintVersion {
     }
 }
 
-/// Mint Info [NIP-09]
+/// Mint Info [NIP-06]
 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct MintInfo {
     /// name of the mint and should be recognizable
@@ -75,6 +82,32 @@ pub struct MintInfo {
     pub motd: Option<String>,
 }
 
+impl MintInfo {
+    #![allow(clippy::too_many_arguments)]
+    /// Create new [`MintInfo`]
+    pub fn new(
+        name: Option<String>,
+        pubkey: Option<PublicKey>,
+        version: Option<MintVersion>,
+        description: Option<String>,
+        description_long: Option<String>,
+        contact: Option<Vec<ContactInfo>>,
+        nuts: Nuts,
+        motd: Option<String>,
+    ) -> Self {
+        Self {
+            name,
+            pubkey,
+            version,
+            description,
+            description_long,
+            contact,
+            nuts,
+            motd,
+        }
+    }
+}
+
 /// Supported nuts and settings
 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Nuts {
@@ -145,6 +178,13 @@ pub struct ContactInfo {
     pub info: String,
 }
 
+impl ContactInfo {
+    /// Create new [`ContactInfo`]
+    pub fn new(method: String, info: String) -> Self {
+        Self { method, info }
+    }
+}
+
 fn deserialize_contact_info<'de, D>(deserializer: D) -> Result<Option<Vec<ContactInfo>>, D::Error>
 where
     D: Deserializer<'de>,

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

@@ -1,15 +1,12 @@
 //! Types
 
 use serde::{Deserialize, Serialize};
-use uuid::Uuid;
 
 use crate::error::Error;
 use crate::nuts::{
-    CurrencyUnit, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, SpendingConditions,
-    State,
+    CurrencyUnit, MeltQuoteState, Proof, Proofs, PublicKey, SpendingConditions, State,
 };
 use crate::url::UncheckedUrl;
-use crate::Amount;
 
 /// Melt response with proofs
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
@@ -22,94 +19,6 @@ pub struct Melted {
     pub change: Option<Proofs>,
 }
 
-/// Mint Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct MintQuote {
-    /// Quote id
-    pub id: String,
-    /// Mint Url
-    pub mint_url: UncheckedUrl,
-    /// Amount of quote
-    pub amount: Amount,
-    /// Unit of quote
-    pub unit: CurrencyUnit,
-    /// Quote payment request e.g. bolt11
-    pub request: String,
-    /// Quote state
-    pub state: MintQuoteState,
-    /// Expiration time of quote
-    pub expiry: u64,
-}
-
-impl MintQuote {
-    /// Create new [`MintQuote`]
-    pub fn new(
-        mint_url: UncheckedUrl,
-        request: String,
-        unit: CurrencyUnit,
-        amount: Amount,
-        expiry: u64,
-    ) -> Self {
-        let id = Uuid::new_v4();
-
-        Self {
-            mint_url,
-            id: id.to_string(),
-            amount,
-            unit,
-            request,
-            state: MintQuoteState::Unpaid,
-            expiry,
-        }
-    }
-}
-
-/// Melt Quote Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct MeltQuote {
-    /// Quote id
-    pub id: String,
-    /// Quote unit
-    pub unit: CurrencyUnit,
-    /// Quote amount
-    pub amount: Amount,
-    /// Quote Payment request e.g. bolt11
-    pub request: String,
-    /// Quote fee reserve
-    pub fee_reserve: Amount,
-    /// Quote state
-    pub state: MeltQuoteState,
-    /// Expiration time of quote
-    pub expiry: u64,
-    /// Payment preimage
-    pub payment_preimage: Option<String>,
-}
-
-#[cfg(feature = "mint")]
-impl MeltQuote {
-    /// Create new [`MeltQuote`]
-    pub fn new(
-        request: String,
-        unit: CurrencyUnit,
-        amount: Amount,
-        fee_reserve: Amount,
-        expiry: u64,
-    ) -> Self {
-        let id = Uuid::new_v4();
-
-        Self {
-            id: id.to_string(),
-            amount,
-            unit,
-            request,
-            fee_reserve,
-            state: MeltQuoteState::Unpaid,
-            expiry,
-            payment_preimage: None,
-        }
-    }
-}
-
 /// Prooinfo
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct ProofInfo {

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

@@ -25,7 +25,7 @@ use crate::nuts::{
     PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey,
     SigFlag, SpendingConditions, State, SwapRequest,
 };
-use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo};
+use crate::types::{Melted, ProofInfo};
 use crate::url::UncheckedUrl;
 use crate::util::{hex, unix_time};
 use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
@@ -33,8 +33,11 @@ use crate::{Amount, Bolt11Invoice, HttpClient, SECP256K1};
 pub mod client;
 pub mod error;
 pub mod multi_mint_wallet;
+pub mod types;
 pub mod util;
 
+pub use types::{MeltQuote, MintQuote};
+
 /// CDK Wallet
 #[derive(Debug, Clone)]
 pub struct Wallet {

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

@@ -14,7 +14,8 @@ use tracing::instrument;
 use super::Error;
 use crate::amount::SplitTarget;
 use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
-use crate::types::{Melted, MintQuote};
+use crate::types::Melted;
+use crate::wallet::types::MintQuote;
 use crate::{Amount, UncheckedUrl, Wallet};
 
 /// Multi Mint Wallet

+ 46 - 0
crates/cdk/src/wallet/types.rs

@@ -0,0 +1,46 @@
+//! Wallet Types
+
+use serde::{Deserialize, Serialize};
+
+use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
+use crate::{Amount, UncheckedUrl};
+
+/// Mint Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintQuote {
+    /// Quote id
+    pub id: String,
+    /// Mint Url
+    pub mint_url: UncheckedUrl,
+    /// Amount of quote
+    pub amount: Amount,
+    /// Unit of quote
+    pub unit: CurrencyUnit,
+    /// Quote payment request e.g. bolt11
+    pub request: String,
+    /// Quote state
+    pub state: MintQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+}
+
+/// Melt Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MeltQuote {
+    /// Quote id
+    pub id: String,
+    /// Quote unit
+    pub unit: CurrencyUnit,
+    /// Quote amount
+    pub amount: Amount,
+    /// Quote Payment request e.g. bolt11
+    pub request: String,
+    /// Quote fee reserve
+    pub fee_reserve: Amount,
+    /// Quote state
+    pub state: MeltQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Payment preimage
+    pub payment_preimage: Option<String>,
+}

+ 2 - 0
misc/scripts/check-crates.sh

@@ -33,7 +33,9 @@ buildargs=(
     "-p cdk-sqlite --no-default-features --features mint"
     "-p cdk-sqlite --no-default-features --features wallet"
     "-p cdk-cln"
+    "-p cdk-axum"
     "--bin cdk-cli"
+    "--bin cdk-mintd"
     "--examples"
 )