Parcourir la source

feat: strike api for mint backend

feat: Use mint melt settings
thesimplekid il y a 7 mois
Parent
commit
bc9fad9e0e

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

@@ -35,6 +35,7 @@ jobs:
             -p cdk-axum,
             -p cdk-cln,
             -p cdk-fake-wallet,
+            -p cdk-strike,
             --bin cdk-cli,
             --bin cdk-mintd,
             --examples

+ 2 - 0
Cargo.toml

@@ -23,6 +23,7 @@ keywords = ["bitcoin", "e-cash", "cashu"]
 [workspace.dependencies]
 async-trait = "0.1.74"
 anyhow = "1"
+axum = "0.7.5"
 bitcoin = { version = "0.30", default-features = false } # lightning-invoice uses v0.30
 bip39 = "2.0"
 cdk = { version = "0.2", path = "./crates/cdk", default-features = false }
@@ -32,6 +33,7 @@ cdk-redb = { version = "0.2", path = "./crates/cdk-redb", default-features = fal
 cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false }
 cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = false }
 cdk-fake-wallet = { version = "0.1", path = "./crates/cdk-fake-wallet", default-features = false }
+cdk-strike = { version = "0.1", path = "./crates/cdk-strike", default-features = false }
 tokio = { version = "1", default-features = false }
 thiserror = "1"
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }

+ 1 - 0
README.md

@@ -20,6 +20,7 @@ The project is split up into several crates in the `crates/` directory:
     * [**cdk-rexie**](./crates/cdk-rexie/): Rexie Storage backend for browsers
     * [**cdk-axum**](./crates/cdk-axum/): Axum webserver for mint.
     * [**cdk-cln**](./crates/cdk-cln/): CLN Lightning backend for mint.
+    * [**cdk-strike**](./crates/cdk-strike/): Strike Lightning backend for mint.
     * [**cdk-fake-wallet**](./crates/cdk-fake-wallet/): Fake Lightning backend for mint. To be used only for testing, quotes are automatically filled.
 * Binaries:
     * [**cdk-cli**](./crates/cdk-cli/): Cashu wallet CLI.

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

@@ -11,7 +11,7 @@ description = "Cashu CDK axum webserver"
 [dependencies]
 anyhow = "1.0.75"
 async-trait.workspace = true
-axum = "0.7.5"
+axum.workspace = true
 axum-macros = "0.4.1"
 cdk = { workspace = true, default-features = false, features = ["mint"] }
 tokio.workspace = true

+ 8 - 8
crates/cdk-axum/src/router_handlers.rs

@@ -55,7 +55,7 @@ pub async fn get_mint_bolt11_quote(
     let ln = state
         .ln
         .get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
-        .ok_or({
+        .ok_or_else(|| {
             tracing::info!("Bolt11 mint request for unsupported unit");
 
             into_response(Error::UnsupportedUnit)
@@ -135,7 +135,7 @@ pub async fn get_melt_bolt11_quote(
     let ln = state
         .ln
         .get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
-        .ok_or({
+        .ok_or_else(|| {
             tracing::info!("Could not get ln backend for {}, bolt11 ", payload.unit);
 
             into_response(Error::UnsupportedUnit)
@@ -339,17 +339,17 @@ pub async fn post_melt_bolt11(
                 }
             }
 
-            let ln = state
-                .ln
-                .get(&LnKey::new(quote.unit, PaymentMethod::Bolt11))
-                .ok_or({
+            let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
+                Some(ln) => ln,
+                None => {
                     tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
                     if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
                         tracing::error!("Could not reset melt quote state: {}", err);
                     }
 
-                    into_response(Error::UnsupportedUnit)
-                })?;
+                    return Err(into_response(Error::UnsupportedUnit));
+                }
+            };
 
             let pre = match ln
                 .pay_invoice(quote.clone(), partial_msats, max_fee_msats)

+ 10 - 24
crates/cdk-cln/src/lib.rs

@@ -8,8 +8,8 @@ use std::time::Duration;
 
 use async_trait::async_trait;
 use cdk::cdk_lightning::{
-    self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
-    Settings,
+    self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
+    PaymentQuoteResponse, Settings,
 };
 use cdk::mint::FeeReserve;
 use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
@@ -35,22 +35,16 @@ pub struct Cln {
     rpc_socket: PathBuf,
     cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
     fee_reserve: FeeReserve,
-    min_melt_amount: u64,
-    max_melt_amount: u64,
-    min_mint_amount: u64,
-    max_mint_amount: u64,
-    mint_enabled: bool,
-    melt_enabled: bool,
+    mint_settings: MintMeltSettings,
+    melt_settings: MintMeltSettings,
 }
 
 impl Cln {
     pub async fn new(
         rpc_socket: PathBuf,
         fee_reserve: FeeReserve,
-        min_melt_amount: u64,
-        max_melt_amount: u64,
-        min_mint_amount: u64,
-        max_mint_amount: u64,
+        mint_settings: MintMeltSettings,
+        melt_settings: MintMeltSettings,
     ) -> Result<Self, Error> {
         let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
 
@@ -58,12 +52,8 @@ impl Cln {
             rpc_socket,
             cln_client: Arc::new(Mutex::new(cln_client)),
             fee_reserve,
-            min_mint_amount,
-            max_mint_amount,
-            min_melt_amount,
-            max_melt_amount,
-            mint_enabled: true,
-            melt_enabled: true,
+            mint_settings,
+            melt_settings,
         })
     }
 }
@@ -75,13 +65,9 @@ impl MintLightning for Cln {
     fn get_settings(&self) -> Settings {
         Settings {
             mpp: true,
-            min_mint_amount: self.min_mint_amount,
-            max_mint_amount: self.max_mint_amount,
-            min_melt_amount: self.min_melt_amount,
-            max_melt_amount: self.max_melt_amount,
             unit: CurrencyUnit::Msat,
-            mint_enabled: self.mint_enabled,
-            melt_enabled: self.melt_enabled,
+            mint_settings: self.mint_settings,
+            melt_settings: self.melt_settings,
         }
     }
 

+ 10 - 24
crates/cdk-fake-wallet/src/lib.rs

@@ -7,8 +7,8 @@ use async_trait::async_trait;
 use bitcoin::hashes::{sha256, Hash};
 use bitcoin::secp256k1::{Secp256k1, SecretKey};
 use cdk::cdk_lightning::{
-    self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
-    Settings,
+    self, to_unit, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
+    PaymentQuoteResponse, Settings,
 };
 use cdk::mint;
 use cdk::mint::FeeReserve;
@@ -29,36 +29,26 @@ pub mod error;
 #[derive(Clone)]
 pub struct FakeWallet {
     fee_reserve: FeeReserve,
-    min_melt_amount: u64,
-    max_melt_amount: u64,
-    min_mint_amount: u64,
-    max_mint_amount: u64,
-    mint_enabled: bool,
-    melt_enabled: bool,
     sender: tokio::sync::mpsc::Sender<String>,
     receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
+    mint_settings: MintMeltSettings,
+    melt_settings: MintMeltSettings,
 }
 
 impl FakeWallet {
     pub fn new(
         fee_reserve: FeeReserve,
-        min_melt_amount: u64,
-        max_melt_amount: u64,
-        min_mint_amount: u64,
-        max_mint_amount: u64,
+        mint_settings: MintMeltSettings,
+        melt_settings: MintMeltSettings,
     ) -> Self {
         let (sender, receiver) = tokio::sync::mpsc::channel(8);
 
         Self {
             fee_reserve,
-            min_mint_amount,
-            max_mint_amount,
-            min_melt_amount,
-            max_melt_amount,
-            mint_enabled: true,
-            melt_enabled: true,
             sender,
             receiver: Arc::new(Mutex::new(Some(receiver))),
+            mint_settings,
+            melt_settings,
         }
     }
 }
@@ -70,13 +60,9 @@ impl MintLightning for FakeWallet {
     fn get_settings(&self) -> Settings {
         Settings {
             mpp: true,
-            min_mint_amount: self.min_mint_amount,
-            max_mint_amount: self.max_mint_amount,
-            min_melt_amount: self.min_melt_amount,
-            max_melt_amount: self.max_melt_amount,
             unit: CurrencyUnit::Msat,
-            mint_enabled: self.mint_enabled,
-            melt_enabled: self.melt_enabled,
+            melt_settings: self.melt_settings,
+            mint_settings: self.mint_settings,
         }
     }
 

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

@@ -18,6 +18,7 @@ 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-fake-wallet = { workspace = true, default-features = false }
+cdk-strike.workspace = true
 cdk-axum = { workspace = true, default-features = false }
 config = { version = "0.13.3", features = ["toml"] }
 clap = { version = "4.4.8", features = ["derive", "env", "default"] }

+ 6 - 2
crates/cdk-mintd/example.config.toml

@@ -25,9 +25,13 @@ mnemonic = ""
 
 [ln]
 
-# Required ln backend `cln`
+# Required ln backend `cln`, `strike`, `fakewallet`
 ln_backend = "cln"
 
 # CLN
 # Required if using cln backend path to rpc
-cln_path = ""
+# cln_path = ""
+
+# Strike
+# Required if using strike backed
+# strike_api_key=""

+ 6 - 2
crates/cdk-mintd/src/config.rs

@@ -20,14 +20,17 @@ pub struct Info {
 pub enum LnBackend {
     #[default]
     Cln,
-    FakeWallet, //  Greenlight,
-                //  Ldk,
+    Strike,
+    FakeWallet,
+    //  Greenlight,
+    //  Ldk,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
 pub struct Ln {
     pub ln_backend: LnBackend,
     pub cln_path: Option<PathBuf>,
+    pub strike_api_key: Option<String>,
     pub greenlight_invite_code: Option<String>,
     pub invoice_description: Option<String>,
     pub fee_percent: f32,
@@ -115,6 +118,7 @@ impl Settings {
             //LnBackend::Greenlight => (),
             //LnBackend::Ldk => (),
             LnBackend::FakeWallet => (),
+            LnBackend::Strike => assert!(settings.ln.strike_api_key.is_some()),
         }
 
         Ok(settings)

+ 75 - 14
crates/cdk-mintd/src/main.rs

@@ -12,22 +12,24 @@ use anyhow::{anyhow, Result};
 use axum::Router;
 use bip39::Mnemonic;
 use cdk::cdk_database::{self, MintDatabase};
-use cdk::cdk_lightning::MintLightning;
+use cdk::cdk_lightning;
+use cdk::cdk_lightning::{MintLightning, MintMeltSettings};
 use cdk::mint::{FeeReserve, Mint};
 use cdk::nuts::{
     nut04, nut05, ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings,
     MintVersion, MppMethodSettings, Nuts, PaymentMethod,
 };
-use cdk::{cdk_lightning, Amount};
 use cdk_axum::LnKey;
 use cdk_cln::Cln;
 use cdk_fake_wallet::FakeWallet;
 use cdk_redb::MintRedbDatabase;
 use cdk_sqlite::MintSqliteDatabase;
+use cdk_strike::Strike;
 use clap::Parser;
 use cli::CLIArgs;
 use config::{DatabaseEngine, LnBackend};
 use futures::StreamExt;
+use tokio::sync::Mutex;
 use tower_http::cors::CorsLayer;
 use tracing_subscriber::EnvFilter;
 
@@ -117,10 +119,10 @@ async fn main() -> anyhow::Result<()> {
         min_fee_reserve: absolute_ln_fee_reserve,
         percent_fee_reserve: relative_ln_fee,
     };
-    let ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync> = match settings
-        .ln
-        .ln_backend
-    {
+    let (ln, ln_router): (
+        Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
+        Option<Router>,
+    ) = match settings.ln.ln_backend {
         LnBackend::Cln => {
             let cln_socket = expand_path(
                 settings
@@ -133,9 +135,56 @@ async fn main() -> anyhow::Result<()> {
             )
             .ok_or(anyhow!("cln socket not defined"))?;
 
-            Arc::new(Cln::new(cln_socket, fee_reserve, 1000, 1000000, 1000, 100000).await?)
+            (
+                Arc::new(
+                    Cln::new(
+                        cln_socket,
+                        fee_reserve,
+                        MintMeltSettings::default(),
+                        MintMeltSettings::default(),
+                    )
+                    .await?,
+                ),
+                None,
+            )
+        }
+        LnBackend::Strike => {
+            let api_key = settings
+                .ln
+                .strike_api_key
+                .expect("Checked when validaing config");
+
+            // Channel used for strike web hook
+            let (sender, receiver) = tokio::sync::mpsc::channel(8);
+
+            let webhook_endpoint = "/webhook/invoice";
+
+            let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint);
+
+            let strike = Strike::new(
+                api_key,
+                MintMeltSettings::default(),
+                MintMeltSettings::default(),
+                CurrencyUnit::Sat,
+                Arc::new(Mutex::new(Some(receiver))),
+                webhook_url,
+            )
+            .await?;
+
+            let router = strike
+                .create_invoice_webhook(webhook_endpoint, sender)
+                .await?;
+
+            (Arc::new(strike), Some(router))
         }
-        LnBackend::FakeWallet => Arc::new(FakeWallet::new(fee_reserve, 1000, 1000000, 1000, 10000)),
+        LnBackend::FakeWallet => (
+            Arc::new(FakeWallet::new(
+                fee_reserve,
+                MintMeltSettings::default(),
+                MintMeltSettings::default(),
+            )),
+            None,
+        ),
     };
 
     let mut ln_backends = HashMap::new();
@@ -167,15 +216,15 @@ async fn main() -> anyhow::Result<()> {
             let n4 = MintMethodSettings {
                 method: key.method.clone(),
                 unit: key.unit,
-                min_amount: Some(Amount::from(settings.min_mint_amount)),
-                max_amount: Some(Amount::from(settings.max_mint_amount)),
+                min_amount: Some(settings.mint_settings.min_amount),
+                max_amount: Some(settings.mint_settings.max_amount),
             };
 
             let n5 = MeltMethodSettings {
                 method: key.method.clone(),
                 unit: key.unit,
-                min_amount: Some(Amount::from(settings.min_melt_amount)),
-                max_amount: Some(Amount::from(settings.max_melt_amount)),
+                min_amount: Some(settings.melt_settings.min_amount),
+                max_amount: Some(settings.melt_settings.max_amount),
             };
 
             nut_04.methods.push(n4);
@@ -260,6 +309,11 @@ async fn main() -> anyhow::Result<()> {
         .nest("/", v1_service)
         .layer(CorsLayer::permissive());
 
+    let mint_service = match ln_router {
+        Some(ln_router) => mint_service.nest("/", ln_router),
+        None => mint_service,
+    };
+
     // Spawn task to wait for invoces to be paid and update mint quotes
     tokio::spawn(async move {
         loop {
@@ -313,13 +367,20 @@ 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?;
+    let mut pending_quotes = mint.get_pending_mint_quotes().await?;
+    tracing::trace!("There are {} pending mint quotes.", pending_quotes.len());
+    let mut unpaid_quotes = mint.get_unpaid_mint_quotes().await?;
+    tracing::trace!("There are {} unpaid mint quotes.", unpaid_quotes.len());
+
+    unpaid_quotes.append(&mut pending_quotes);
 
-    for quote in pending_quotes {
+    for quote in unpaid_quotes {
+        tracing::trace!("Checking status of mint quote: {}", quote.id);
         let lookup_id = quote.request_lookup_id;
         let state = ln.check_invoice_status(&lookup_id).await?;
 
         if state != quote.state {
+            tracing::trace!("Mintquote status changed: {}", quote.id);
             mint.localstore
                 .update_mint_quote_state(&quote.id, state)
                 .await?;

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

@@ -0,0 +1,23 @@
+[package]
+name = "cdk-strike"
+version = "0.1.0"
+edition = "2021"
+authors = ["CDK Developers"]
+homepage.workspace = true
+repository.workspace = true
+rust-version.workspace = true # MSRV
+license.workspace = true
+description = "CDK ln backend for Strike api"
+
+[dependencies]
+async-trait.workspace = true
+anyhow.workspace = true
+axum.workspace = true
+bitcoin.workspace = true
+cdk = { workspace = true, default-features = false, features = ["mint"] }
+futures.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+thiserror.workspace = true
+uuid.workspace = true
+strike-rs = "0.1.0"

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

@@ -0,0 +1,23 @@
+//! Error for Strike ln backend
+
+use thiserror::Error;
+
+/// Strike Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Invoice amount not defined
+    #[error("Unknown invoice amount")]
+    UnknownInvoiceAmount,
+    /// Unknown invoice
+    #[error("Unknown invoice")]
+    UnknownInvoice,
+    /// Anyhow error
+    #[error(transparent)]
+    Anyhow(#[from] anyhow::Error),
+}
+
+impl From<Error> for cdk::cdk_lightning::Error {
+    fn from(e: Error) -> Self {
+        Self::Lightning(Box::new(e))
+    }
+}

+ 279 - 0
crates/cdk-strike/src/lib.rs

@@ -0,0 +1,279 @@
+//! CDK lightning backend for Strike
+
+#![warn(missing_docs)]
+#![warn(rustdoc::bare_urls)]
+
+use std::pin::Pin;
+use std::sync::Arc;
+
+use anyhow::{anyhow, bail};
+use async_trait::async_trait;
+use axum::Router;
+use cdk::cdk_lightning::{
+    self, CreateInvoiceResponse, MintLightning, MintMeltSettings, PayInvoiceResponse,
+    PaymentQuoteResponse, Settings,
+};
+use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use cdk::util::unix_time;
+use cdk::{mint, Bolt11Invoice};
+use error::Error;
+use futures::stream::StreamExt;
+use futures::Stream;
+use strike_rs::{
+    Amount as StrikeAmount, Currency as StrikeCurrencyUnit, InvoiceRequest, InvoiceState,
+    PayInvoiceQuoteRequest, Strike as StrikeApi,
+};
+use tokio::sync::Mutex;
+use uuid::Uuid;
+
+pub mod error;
+
+/// Strike
+#[derive(Clone)]
+pub struct Strike {
+    strike_api: StrikeApi,
+    mint_settings: MintMeltSettings,
+    melt_settings: MintMeltSettings,
+    unit: CurrencyUnit,
+    receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
+    webhook_url: String,
+}
+
+impl Strike {
+    /// Create new [`Strike`] wallet
+    pub async fn new(
+        api_key: String,
+        mint_settings: MintMeltSettings,
+        melt_settings: MintMeltSettings,
+        unit: CurrencyUnit,
+        receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
+        webhook_url: String,
+    ) -> Result<Self, Error> {
+        let strike = StrikeApi::new(&api_key, None)?;
+        Ok(Self {
+            strike_api: strike,
+            mint_settings,
+            melt_settings,
+            receiver,
+            unit,
+            webhook_url,
+        })
+    }
+}
+
+#[async_trait]
+impl MintLightning for Strike {
+    type Err = cdk_lightning::Error;
+
+    fn get_settings(&self) -> Settings {
+        Settings {
+            mpp: false,
+            unit: self.unit,
+            mint_settings: self.mint_settings,
+            melt_settings: self.melt_settings,
+        }
+    }
+
+    async fn wait_any_invoice(
+        &self,
+    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
+        self.strike_api
+            .subscribe_to_invoice_webhook(self.webhook_url.clone())
+            .await?;
+
+        let receiver = self
+            .receiver
+            .lock()
+            .await
+            .take()
+            .ok_or(anyhow!("No receiver"))?;
+
+        let strike_api = self.strike_api.clone();
+
+        Ok(futures::stream::unfold(
+            (receiver, strike_api),
+            |(mut receiver, strike_api)| async move {
+                match receiver.recv().await {
+                    Some(msg) => {
+                        let check = strike_api.find_invoice(&msg).await;
+
+                        match check {
+                            Ok(state) => {
+                                if state.state == InvoiceState::Paid {
+                                    Some((msg, (receiver, strike_api)))
+                                } else {
+                                    None
+                                }
+                            }
+                            _ => None,
+                        }
+                    }
+                    None => None,
+                }
+            },
+        )
+        .boxed())
+    }
+
+    async fn get_payment_quote(
+        &self,
+        melt_quote_request: &MeltQuoteBolt11Request,
+    ) -> Result<PaymentQuoteResponse, Self::Err> {
+        if melt_quote_request.unit != self.unit {
+            return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
+        }
+
+        let payment_quote_request = PayInvoiceQuoteRequest {
+            ln_invoice: melt_quote_request.request.to_string(),
+            source_currency: strike_rs::Currency::BTC,
+        };
+        let quote = self.strike_api.payment_quote(payment_quote_request).await?;
+
+        let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?;
+
+        Ok(PaymentQuoteResponse {
+            request_lookup_id: quote.payment_quote_id,
+            amount: from_strike_amount(quote.amount, &melt_quote_request.unit)?,
+            fee,
+        })
+    }
+
+    async fn pay_invoice(
+        &self,
+        melt_quote: mint::MeltQuote,
+        _partial_msats: Option<u64>,
+        _max_fee_msats: Option<u64>,
+    ) -> Result<PayInvoiceResponse, Self::Err> {
+        let pay_response = self
+            .strike_api
+            .pay_quote(&melt_quote.request_lookup_id)
+            .await?;
+
+        let state = match pay_response.state {
+            InvoiceState::Paid => MeltQuoteState::Paid,
+            InvoiceState::Unpaid => MeltQuoteState::Unpaid,
+            InvoiceState::Completed => MeltQuoteState::Paid,
+            InvoiceState::Pending => MeltQuoteState::Pending,
+        };
+
+        let total_spent_msats = from_strike_amount(pay_response.total_amount, &melt_quote.unit)?;
+
+        let bolt11: Bolt11Invoice = melt_quote.request.parse()?;
+
+        Ok(PayInvoiceResponse {
+            payment_hash: bolt11.payment_hash().to_string(),
+            payment_preimage: None,
+            status: state,
+            total_spent_msats,
+        })
+    }
+
+    async fn create_invoice(
+        &self,
+        amount: u64,
+        description: String,
+        unix_expiry: u64,
+    ) -> Result<CreateInvoiceResponse, Self::Err> {
+        let time_now = unix_time();
+        assert!(unix_expiry > time_now);
+        let request_lookup_id = Uuid::new_v4();
+
+        let invoice_request = InvoiceRequest {
+            correlation_id: Some(request_lookup_id.to_string()),
+            amount: to_strike_unit(amount, &self.unit),
+            description: Some(description),
+        };
+
+        let create_invoice_response = self.strike_api.create_invoice(invoice_request).await?;
+
+        let quote = self
+            .strike_api
+            .invoice_quote(&create_invoice_response.invoice_id)
+            .await?;
+
+        Ok(CreateInvoiceResponse {
+            request_lookup_id: create_invoice_response.invoice_id,
+            request: quote.ln_invoice.parse()?,
+        })
+    }
+
+    async fn check_invoice_status(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<MintQuoteState, Self::Err> {
+        let invoice = self.strike_api.find_invoice(request_lookup_id).await?;
+
+        let state = match invoice.state {
+            InvoiceState::Paid => MintQuoteState::Paid,
+            InvoiceState::Unpaid => MintQuoteState::Unpaid,
+            InvoiceState::Completed => MintQuoteState::Paid,
+            InvoiceState::Pending => MintQuoteState::Pending,
+        };
+
+        Ok(state)
+    }
+}
+
+impl Strike {
+    /// Create invoice webhook
+    pub async fn create_invoice_webhook(
+        &self,
+        webhook_endpoint: &str,
+        sender: tokio::sync::mpsc::Sender<String>,
+    ) -> anyhow::Result<Router> {
+        self.strike_api
+            .create_invoice_webhook_router(webhook_endpoint, sender)
+            .await
+    }
+}
+
+pub(crate) fn from_strike_amount(
+    strike_amount: StrikeAmount,
+    target_unit: &CurrencyUnit,
+) -> anyhow::Result<u64> {
+    match target_unit {
+        CurrencyUnit::Sat => strike_amount.to_sats(),
+        CurrencyUnit::Msat => Ok(strike_amount.to_sats()? * 1000),
+        CurrencyUnit::Usd => {
+            if strike_amount.currency == StrikeCurrencyUnit::USD {
+                Ok((strike_amount.amount * 100.0).round() as u64)
+            } else {
+                bail!("Could not convert ");
+            }
+        }
+        CurrencyUnit::Eur => {
+            if strike_amount.currency == StrikeCurrencyUnit::EUR {
+                Ok((strike_amount.amount * 100.0).round() as u64)
+            } else {
+                bail!("Could not convert ");
+            }
+        }
+    }
+}
+
+pub(crate) fn to_strike_unit<T>(amount: T, current_unit: &CurrencyUnit) -> StrikeAmount
+where
+    T: Into<u64>,
+{
+    let amount = amount.into();
+    match current_unit {
+        CurrencyUnit::Sat => StrikeAmount::from_sats(amount),
+        CurrencyUnit::Msat => StrikeAmount::from_sats(amount / 1000),
+        CurrencyUnit::Usd => {
+            let dollars = (amount as f64 / 100_f64) * 100.0;
+
+            StrikeAmount {
+                currency: StrikeCurrencyUnit::USD,
+                amount: dollars.round() / 100.0,
+            }
+        }
+        CurrencyUnit::Eur => {
+            let euro = (amount as f64 / 100_f64) * 100.0;
+
+            StrikeAmount {
+                currency: StrikeCurrencyUnit::EUR,
+                amount: euro.round() / 100.0,
+            }
+        }
+    }
+}

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

@@ -8,8 +8,8 @@ use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
-use crate::mint;
 use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
+use crate::{mint, Amount};
 
 /// CDK Lightning Error
 #[derive(Debug, Error)]
@@ -121,19 +121,32 @@ pub struct Settings {
     /// MPP supported
     pub mpp: bool,
     /// Min amount to mint
-    pub min_mint_amount: u64,
+    pub mint_settings: MintMeltSettings,
     /// Max amount to mint
-    pub max_mint_amount: u64,
-    /// Min amount to melt
-    pub min_melt_amount: u64,
-    /// Max amount to melt
-    pub max_melt_amount: u64,
+    pub melt_settings: MintMeltSettings,
     /// Base unit of backend
     pub unit: CurrencyUnit,
-    /// Minting enabled
-    pub mint_enabled: bool,
-    /// Melting enabled
-    pub melt_enabled: bool,
+}
+
+/// Mint or melt settings
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintMeltSettings {
+    /// Min Amount
+    pub min_amount: Amount,
+    /// Max Amount
+    pub max_amount: Amount,
+    /// Enabled
+    pub enabled: bool,
+}
+
+impl Default for MintMeltSettings {
+    fn default() -> Self {
+        Self {
+            min_amount: Amount::from(1),
+            max_amount: Amount::from(500000),
+            enabled: true,
+        }
+    }
 }
 
 const MSAT_IN_SAT: u64 = 1000;

+ 11 - 0
crates/cdk/src/mint/mod.rs

@@ -262,6 +262,17 @@ impl Mint {
             .collect())
     }
 
+    /// Get pending mint quotes
+    #[instrument(skip_all)]
+    pub async fn get_unpaid_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::Unpaid)
+            .collect())
+    }
+
     /// Remove mint quote
     #[instrument(skip_all)]
     pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {

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

@@ -35,6 +35,7 @@ buildargs=(
     "-p cdk-cln"
     "-p cdk-axum"
     "-p cdk-fake-wallet"
+    "-p cdk-strike"
     "--bin cdk-cli"
     "--bin cdk-mintd"
     "--examples"