Browse Source

feat: strike multi unit

fix: mint create new keysets for units

fix: use amount from melt quote

fix: melt quote correct payment unit
thesimplekid 7 months ago
parent
commit
169f5f1533

+ 2 - 0
CHANGELOG.md

@@ -51,6 +51,8 @@
 ### Fixed
 - cdk(mint): `SIG_ALL` is not allowed in `melt` ([thesimplekid]).
 - cdk(mint): On `swap` verify correct number of sigs on outputs when `SigAll` ([thesimplekid]).
+- cdk(mint): Use amount in payment_quote response from ln backend ([thesimplekid]).
+- cdk(mint): Create new keysets for added supported units ([thesimplekid]).
 
 ### Removed
 - cdk(wallet): Remove unused argument `SplitTarget` on `melt` ([thesimplekid]).

+ 1 - 14
crates/cdk-axum/src/router_handlers.rs

@@ -141,19 +141,6 @@ pub async fn get_melt_bolt11_quote(
             into_response(Error::UnsupportedUnit)
         })?;
 
-    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 = ln.get_payment_quote(&payload).await.map_err(|err| {
         tracing::error!(
             "Could not get payment quote for mint quote, {} bolt11, {}",
@@ -169,7 +156,7 @@ pub async fn get_melt_bolt11_quote(
         .new_melt_quote(
             payload.request.to_string(),
             payload.unit,
-            amount.into(),
+            payment_quote.amount.into(),
             payment_quote.fee.into(),
             unix_time() + state.quote_ttl,
             payment_quote.request_lookup_id,

+ 5 - 4
crates/cdk-mintd/example.config.toml

@@ -28,10 +28,11 @@ mnemonic = ""
 # Required ln backend `cln`, `strike`, `fakewallet`
 ln_backend = "cln"
 
-# CLN
+# [cln]
 # Required if using cln backend path to rpc
 # cln_path = ""
 
-# Strike
-# Required if using strike backed
-# strike_api_key=""
+# [strike]
+# api_key=""
+# Optional default sats
+# supported_units=[""]

+ 16 - 8
crates/cdk-mintd/src/config.rs

@@ -1,6 +1,6 @@
 use std::path::PathBuf;
 
-use cdk::nuts::PublicKey;
+use cdk::nuts::{CurrencyUnit, PublicKey};
 use cdk::Amount;
 use config::{Config, ConfigError, File};
 use serde::{Deserialize, Serialize};
@@ -29,14 +29,22 @@ pub enum LnBackend {
 #[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,
     pub reserve_fee_min: Amount,
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Strike {
+    pub api_key: String,
+    pub supported_units: Option<Vec<CurrencyUnit>>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Cln {
+    pub rpc_path: PathBuf,
+}
+
 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
 #[serde(rename_all = "lowercase")]
 pub enum DatabaseEngine {
@@ -55,6 +63,8 @@ pub struct Settings {
     pub info: Info,
     pub mint_info: MintInfo,
     pub ln: Ln,
+    pub cln: Option<Cln>,
+    pub strike: Option<Strike>,
     pub database: Database,
 }
 
@@ -114,11 +124,9 @@ impl Settings {
         let settings: Settings = config.try_deserialize()?;
 
         match settings.ln.ln_backend {
-            LnBackend::Cln => assert!(settings.ln.cln_path.is_some()),
-            //LnBackend::Greenlight => (),
-            //LnBackend::Ldk => (),
+            LnBackend::Cln => assert!(settings.cln.is_some()),
             LnBackend::FakeWallet => (),
-            LnBackend::Strike => assert!(settings.ln.strike_api_key.is_some()),
+            LnBackend::Strike => assert!(settings.strike.is_some()),
         }
 
         Ok(settings)

+ 97 - 81
crates/cdk-mintd/src/main.rs

@@ -119,80 +119,96 @@ async fn main() -> anyhow::Result<()> {
         min_fee_reserve: absolute_ln_fee_reserve,
         percent_fee_reserve: relative_ln_fee,
     };
-    let (ln, ln_router): (
+
+    let mut ln_backends: HashMap<
+        LnKey,
         Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>,
-        Option<Router>,
-    ) = match settings.ln.ln_backend {
+    > = HashMap::new();
+
+    let mut supported_units = HashMap::new();
+    let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0);
+
+    let ln_routers: Vec<Router> = match settings.ln.ln_backend {
         LnBackend::Cln => {
             let cln_socket = expand_path(
                 settings
-                    .ln
-                    .cln_path
-                    .clone()
-                    .ok_or(anyhow!("cln socket not defined"))?
+                    .cln
+                    .expect("Config checked at load that cln is some")
+                    .rpc_path
                     .to_str()
                     .ok_or(anyhow!("cln socket not defined"))?,
             )
             .ok_or(anyhow!("cln socket not defined"))?;
-
-            (
-                Arc::new(
-                    Cln::new(
-                        cln_socket,
-                        fee_reserve,
-                        MintMeltSettings::default(),
-                        MintMeltSettings::default(),
-                    )
-                    .await?,
-                ),
-                None,
-            )
+            let cln = Arc::new(
+                Cln::new(
+                    cln_socket,
+                    fee_reserve,
+                    MintMeltSettings::default(),
+                    MintMeltSettings::default(),
+                )
+                .await?,
+            );
+
+            ln_backends.insert(LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11), cln);
+            supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
+            vec![]
         }
         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 strike_settings = settings.strike.expect("Checked on config load");
+            let api_key = strike_settings.api_key;
+
+            let units = strike_settings
+                .supported_units
+                .unwrap_or(vec![CurrencyUnit::Sat]);
+
+            let mut routers = vec![];
+
+            for unit in units {
+                // Channel used for strike web hook
+                let (sender, receiver) = tokio::sync::mpsc::channel(8);
+                let webhook_endpoint = format!("/webhook/{}/invoice", unit);
+
+                let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint);
+
+                let strike = Strike::new(
+                    api_key.clone(),
+                    MintMeltSettings::default(),
+                    MintMeltSettings::default(),
+                    unit,
+                    Arc::new(Mutex::new(Some(receiver))),
+                    webhook_url,
+                )
+                .await?;
 
-            let webhook_endpoint = "/webhook/invoice";
+                let router = strike
+                    .create_invoice_webhook(&webhook_endpoint, sender)
+                    .await?;
+                routers.push(router);
 
-            let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint);
+                let ln_key = LnKey::new(unit, PaymentMethod::Bolt11);
 
-            let strike = Strike::new(
-                api_key,
-                MintMeltSettings::default(),
-                MintMeltSettings::default(),
-                CurrencyUnit::Sat,
-                Arc::new(Mutex::new(Some(receiver))),
-                webhook_url,
-            )
-            .await?;
+                ln_backends.insert(ln_key, Arc::new(strike));
 
-            let router = strike
-                .create_invoice_webhook(webhook_endpoint, sender)
-                .await?;
+                supported_units.insert(unit, (input_fee_ppk, 64));
+            }
 
-            (Arc::new(strike), Some(router))
+            routers
         }
-        LnBackend::FakeWallet => (
-            Arc::new(FakeWallet::new(
+        LnBackend::FakeWallet => {
+            let ln_key = LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11);
+
+            let wallet = Arc::new(FakeWallet::new(
                 fee_reserve,
                 MintMeltSettings::default(),
                 MintMeltSettings::default(),
-            )),
-            None,
-        ),
-    };
+            ));
 
-    let mut ln_backends = HashMap::new();
+            ln_backends.insert(ln_key, wallet);
+            supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
 
-    ln_backends.insert(
-        LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11),
-        Arc::clone(&ln),
-    );
+            vec![]
+        }
+    };
 
     let (nut04_settings, nut05_settings, mpp_settings): (
         nut04::Settings,
@@ -271,12 +287,6 @@ async fn main() -> anyhow::Result<()> {
 
     let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
 
-    let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0);
-
-    let mut supported_units = HashMap::new();
-
-    supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64));
-
     let mint = Mint::new(
         &settings.info.url,
         &mnemonic.to_seed_normalized(""),
@@ -292,7 +302,9 @@ async fn main() -> anyhow::Result<()> {
     // 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?;
+    for ln in ln_backends.values() {
+        check_pending_quotes(Arc::clone(&mint), Arc::clone(ln)).await?;
+    }
 
     let mint_url = settings.info.url;
     let listen_addr = settings.info.listen_host;
@@ -303,36 +315,40 @@ async fn main() -> anyhow::Result<()> {
         .unwrap_or(DEFAULT_QUOTE_TTL_SECS);
 
     let v1_service =
-        cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), ln_backends, quote_ttl).await?;
+        cdk_axum::create_mint_router(&mint_url, Arc::clone(&mint), ln_backends.clone(), quote_ttl)
+            .await?;
 
-    let mint_service = Router::new()
-        .nest("/", v1_service)
+    let mut mint_service = Router::new()
+        .merge(v1_service)
         .layer(CorsLayer::permissive());
 
-    let mint_service = match ln_router {
-        Some(ln_router) => mint_service.nest("/", ln_router),
-        None => mint_service,
-    };
+    for router in ln_routers {
+        mint_service = mint_service.merge(router);
+    }
 
     // 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(request_lookup_id) = stream.next().await {
-                        if let Err(err) =
-                            handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
-                        {
-                            tracing::warn!("{:?}", err);
+
+    for (_, ln) in ln_backends {
+        let mint = Arc::clone(&mint);
+        tokio::spawn(async move {
+            loop {
+                match ln.wait_any_invoice().await {
+                    Ok(mut stream) => {
+                        while let Some(request_lookup_id) = stream.next().await {
+                            if let Err(err) =
+                                handle_paid_invoice(mint.clone(), &request_lookup_id).await
+                            {
+                                tracing::warn!("{:?}", err);
+                            }
                         }
                     }
-                }
-                Err(err) => {
-                    tracing::warn!("Could not get invoice stream: {}", err);
+                    Err(err) => {
+                        tracing::warn!("Could not get invoice stream: {}", err);
+                    }
                 }
             }
-        }
-    });
+        });
+    }
 
     let listener =
         tokio::net::TcpListener::bind(format!("{}:{}", listen_addr, listen_port)).await?;

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

@@ -20,4 +20,4 @@ tokio.workspace = true
 tracing.workspace = true
 thiserror.workspace = true
 uuid.workspace = true
-strike-rs = "0.2.2"
+strike-rs = "0.2.3"

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

@@ -123,10 +123,18 @@ impl MintLightning for Strike {
             return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
         }
 
+        let source_currency = match melt_quote_request.unit {
+            CurrencyUnit::Sat => StrikeCurrencyUnit::BTC,
+            CurrencyUnit::Msat => StrikeCurrencyUnit::BTC,
+            CurrencyUnit::Usd => StrikeCurrencyUnit::USD,
+            CurrencyUnit::Eur => StrikeCurrencyUnit::EUR,
+        };
+
         let payment_quote_request = PayInvoiceQuoteRequest {
             ln_invoice: melt_quote_request.request.to_string(),
-            source_currency: strike_rs::Currency::BTC,
+            source_currency,
         };
+
         let quote = self.strike_api.payment_quote(payment_quote_request).await?;
 
         let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?;
@@ -249,14 +257,14 @@ pub(crate) fn from_strike_amount(
             if strike_amount.currency == StrikeCurrencyUnit::USD {
                 Ok((strike_amount.amount * 100.0).round() as u64)
             } else {
-                bail!("Could not convert ");
+                bail!("Could not convert strike USD");
             }
         }
         CurrencyUnit::Eur => {
             if strike_amount.currency == StrikeCurrencyUnit::EUR {
                 Ok((strike_amount.amount * 100.0).round() as u64)
             } else {
-                bail!("Could not convert ");
+                bail!("Could not convert to EUR");
             }
         }
     }

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

@@ -167,6 +167,7 @@ where
         (CurrencyUnit::Sat, CurrencyUnit::Msat) => Ok(amount * MSAT_IN_SAT),
         (CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok(amount / MSAT_IN_SAT),
         (CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount),
+        (CurrencyUnit::Eur, CurrencyUnit::Eur) => Ok(amount),
         _ => Err(Error::CannotConvertUnits),
     }
 }

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

@@ -72,6 +72,7 @@ impl Mint {
                         acc.entry(ks.unit).or_default().push(ks.clone());
                         acc
                     });
+                let mut keyset_units = vec![];
 
                 for (unit, keysets) in keysets_by_unit {
                     let mut keysets = keysets;
@@ -125,6 +126,28 @@ impl Mint {
                         localstore.add_keyset_info(keyset_info).await?;
                         localstore.set_active_keyset(unit, id).await?;
                         active_keysets.insert(id, keyset);
+                        keyset_units.push(unit);
+                    }
+                }
+
+                for (unit, (fee, max_order)) in supported_units {
+                    if !keyset_units.contains(&unit) {
+                        let derivation_path = derivation_path_from_unit(unit, 0);
+
+                        let (keyset, keyset_info) = create_new_keyset(
+                            &secp_ctx,
+                            xpriv,
+                            derivation_path,
+                            Some(0),
+                            unit,
+                            max_order,
+                            fee,
+                        );
+
+                        let id = keyset_info.id;
+                        localstore.add_keyset_info(keyset_info).await?;
+                        localstore.set_active_keyset(unit, id).await?;
+                        active_keysets.insert(id, keyset);
                     }
                 }
             }

+ 1 - 3
crates/cdk/src/nuts/nut00/token.rs

@@ -452,8 +452,6 @@ mod tests {
             Id::from_str("00ad268c4d1f5826").unwrap()
         );
 
-        let token: TokenV4 = token.try_into().unwrap();
-
         let encoded = &token.to_string();
 
         let token_data = TokenV4::from_str(encoded).unwrap();
@@ -470,7 +468,7 @@ mod tests {
 
         assert_eq!(amount, Amount::from(4));
 
-        let unit = token.unit().clone().unwrap();
+        let unit = (*token.unit()).unwrap();
 
         assert_eq!(CurrencyUnit::Sat, unit);