From 169f5f1533fb05fb43fa1a64b3510a20399a1d4d Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 26 Jul 2024 08:21:41 -0400 Subject: [PATCH] feat: strike multi unit fix: mint create new keysets for units fix: use amount from melt quote fix: melt quote correct payment unit --- CHANGELOG.md | 2 + crates/cdk-axum/src/router_handlers.rs | 15 +-- crates/cdk-mintd/example.config.toml | 9 +- crates/cdk-mintd/src/config.rs | 24 ++-- crates/cdk-mintd/src/main.rs | 174 ++++++++++++++----------- crates/cdk-strike/Cargo.toml | 2 +- crates/cdk-strike/src/lib.rs | 14 +- crates/cdk/src/cdk_lightning/mod.rs | 1 + crates/cdk/src/mint/mod.rs | 23 ++++ crates/cdk/src/nuts/nut00/token.rs | 4 +- 10 files changed, 156 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9df998..60e44b20 100644 --- a/CHANGELOG.md +++ b/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]). diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index dc0ee1cf..aff3b764 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/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, diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 7d595420..e4ed3fed 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/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=[""] diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index c59a9486..b7df7705 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/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, - pub strike_api_key: Option, - pub greenlight_invite_code: Option, pub invoice_description: Option, 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>, +} + +#[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, + pub strike: Option, 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) diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 0a4fe115..b97dac24 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -119,81 +119,97 @@ 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 + Send + Sync>, - Option, - ) = 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 = 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"))?; + let cln = Arc::new( + Cln::new( + cln_socket, + fee_reserve, + MintMeltSettings::default(), + MintMeltSettings::default(), + ) + .await?, + ); - ( - Arc::new( - Cln::new( - cln_socket, - fee_reserve, - MintMeltSettings::default(), - MintMeltSettings::default(), - ) - .await?, - ), - None, - ) + 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"); + let strike_settings = settings.strike.expect("Checked on config load"); + let api_key = strike_settings.api_key; - // Channel used for strike web hook - let (sender, receiver) = tokio::sync::mpsc::channel(8); + let units = strike_settings + .supported_units + .unwrap_or(vec![CurrencyUnit::Sat]); - let webhook_endpoint = "/webhook/invoice"; + let mut routers = vec![]; - let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint); + 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 strike = Strike::new( - api_key, - MintMeltSettings::default(), - MintMeltSettings::default(), - CurrencyUnit::Sat, - Arc::new(Mutex::new(Some(receiver))), - webhook_url, - ) - .await?; + let webhook_url = format!("{}{}", settings.info.url, webhook_endpoint); - let router = strike - .create_invoice_webhook(webhook_endpoint, sender) + let strike = Strike::new( + api_key.clone(), + MintMeltSettings::default(), + MintMeltSettings::default(), + unit, + Arc::new(Mutex::new(Some(receiver))), + webhook_url, + ) .await?; - (Arc::new(strike), Some(router)) + let router = strike + .create_invoice_webhook(&webhook_endpoint, sender) + .await?; + routers.push(router); + + let ln_key = LnKey::new(unit, PaymentMethod::Bolt11); + + ln_backends.insert(ln_key, Arc::new(strike)); + + supported_units.insert(unit, (input_fee_ppk, 64)); + } + + 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, - ), + )); + + ln_backends.insert(ln_key, wallet); + supported_units.insert(CurrencyUnit::Sat, (input_fee_ppk, 64)); + + vec![] + } }; - let mut ln_backends = HashMap::new(); - - ln_backends.insert( - LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11), - Arc::clone(&ln), - ); - let (nut04_settings, nut05_settings, mpp_settings): ( nut04::Settings, nut05::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?; diff --git a/crates/cdk-strike/Cargo.toml b/crates/cdk-strike/Cargo.toml index 6b2286d8..fa05b543 100644 --- a/crates/cdk-strike/Cargo.toml +++ b/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" diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index 91a4fe2f..bf2d7a7c 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/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"); } } } diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 058e16ec..c299c77f 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/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), } } diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index ac3372eb..a2bd24a2 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/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; @@ -121,6 +122,28 @@ impl Mint { *input_fee_ppk, ); + let id = keyset_info.id; + 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?; diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index 1e860db5..9dc3864c 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/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);