diff --git a/moksha-mint/src/bin/moksha-mint.rs b/moksha-mint/src/bin/moksha-mint.rs index 2c5c2436..3e694290 100644 --- a/moksha-mint/src/bin/moksha-mint.rs +++ b/moksha-mint/src/bin/moksha-mint.rs @@ -2,6 +2,7 @@ use mokshamint::{ info::MintInfoSettings, lightning::{ AlbyLightningSettings, LightningType, LnbitsLightningSettings, LndLightningSettings, + StrikeLightningSettings, }, MintBuilder, }; @@ -52,8 +53,14 @@ pub async fn main() -> anyhow::Result<()> { .expect("Please provide alby info"); LightningType::Alby(alby_settings) } + "Strike" => { + let strike_settings = envy::prefixed("STRIKE_") + .from_env::() + .expect("Please provide strike info"); + LightningType::Strike(strike_settings) + } _ => panic!( - "env MINT_LIGHTNING_BACKEND not found or invalid values. Valid values are Lnbits and Lnd" + "env MINT_LIGHTNING_BACKEND not found or invalid values. Valid values are Lnbits, Lnd, Alby, and Strike" ), }; diff --git a/moksha-mint/src/error.rs b/moksha-mint/src/error.rs index 8306db02..ad4c71e9 100644 --- a/moksha-mint/src/error.rs +++ b/moksha-mint/src/error.rs @@ -11,7 +11,7 @@ use thiserror::Error; use tonic_lnd::ConnectError; use tracing::{event, Level}; -use crate::{alby::AlbyError, lnbits::LNBitsError, strike::StrikeError}; +use crate::lightning::error::LightningError; #[derive(Error, Debug)] pub enum MokshaMintError { @@ -22,13 +22,7 @@ pub enum MokshaMintError { DecodeInvoice(String, ParseOrSemanticError), #[error("Failed to pay invoice {0} - Error {1}")] - PayInvoice(String, LNBitsError), - - #[error("Failed to pay invoice {0} - Error {1}")] - PayInvoiceAlby(String, AlbyError), - - #[error("Failed to pay invoice {0} - Error {1}")] - PayInvoiceStrike(String, StrikeError), + PayInvoice(String, LightningError), #[error("DB Error {0}")] Db(#[from] rocksdb::Error), @@ -64,13 +58,7 @@ pub enum MokshaMintError { InvalidAmount, #[error("Lightning Error {0}")] - LightningLNBits(#[from] LNBitsError), - - #[error("Lightning Error {0}")] - LightningAlby(#[from] AlbyError), - - #[error("Lightning Error {0}")] - LightningStrike(#[from] StrikeError), + Lightning(#[from] LightningError), } impl IntoResponse for MokshaMintError { diff --git a/moksha-mint/src/lib.rs b/moksha-mint/src/lib.rs index 3b967b0c..ceb52aad 100644 --- a/moksha-mint/src/lib.rs +++ b/moksha-mint/src/lib.rs @@ -33,15 +33,12 @@ use tracing::{event, info, Level}; use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -mod alby; mod database; mod error; pub mod info; pub mod lightning; -mod lnbits; pub mod mint; mod model; -mod strike; #[derive(Debug, Default)] pub struct MintBuilder { diff --git a/moksha-mint/src/alby.rs b/moksha-mint/src/lightning/alby.rs similarity index 78% rename from moksha-mint/src/alby.rs rename to moksha-mint/src/lightning/alby.rs index 9c5c8106..d19fe7b4 100644 --- a/moksha-mint/src/alby.rs +++ b/moksha-mint/src/lightning/alby.rs @@ -1,25 +1,12 @@ use hyper::{header::CONTENT_TYPE, http::HeaderValue}; use url::Url; -use crate::model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}; +use crate::{ + info, + model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}, +}; -#[derive(Debug, thiserror::Error)] -pub enum AlbyError { - #[error("reqwest error: {0}")] - ReqwestError(#[from] reqwest::Error), - - #[error("url error: {0}")] - UrlError(#[from] url::ParseError), - - #[error("serde error: {0}")] - SerdeError(#[from] serde_json::Error), - - #[error("Not found")] - NotFound, - - #[error("Unauthorized")] - Unauthorized, -} +use super::error::LightningError; #[derive(Clone)] pub struct AlbyClient { @@ -29,7 +16,7 @@ pub struct AlbyClient { } impl AlbyClient { - pub fn new(api_key: &str) -> Result { + pub fn new(api_key: &str) -> Result { let alby_url = Url::parse("https://api.getalby.com")?; let reqwest_client = reqwest::Client::builder().build()?; @@ -43,7 +30,7 @@ impl AlbyClient { } impl AlbyClient { - pub async fn make_get(&self, endpoint: &str) -> Result { + pub async fn make_get(&self, endpoint: &str) -> Result { let url = self.alby_url.join(endpoint)?; let response = self .reqwest_client @@ -52,14 +39,15 @@ impl AlbyClient { .send() .await?; - if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(AlbyError::NotFound); - } + // Alby API returns a 404 for invoices that aren't settled yet + // if response.status() == reqwest::StatusCode::NOT_FOUND { + // return Err(LightningError::NotFound); + // } Ok(response.text().await?) } - pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { + pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { let url = self.alby_url.join(endpoint)?; let response = self .reqwest_client @@ -74,11 +62,11 @@ impl AlbyClient { .await?; if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(AlbyError::NotFound); + return Err(LightningError::NotFound); } if response.status() == reqwest::StatusCode::UNAUTHORIZED { - return Err(AlbyError::Unauthorized); + return Err(LightningError::Unauthorized); } Ok(response.text().await?) @@ -89,7 +77,7 @@ impl AlbyClient { pub async fn create_invoice( &self, params: &CreateInvoiceParams, - ) -> Result { + ) -> Result { let params = serde_json::json!({ "amount": params.amount, "description": params.memo, @@ -115,7 +103,7 @@ impl AlbyClient { }) } - pub async fn pay_invoice(&self, bolt11: &str) -> Result { + pub async fn pay_invoice(&self, bolt11: &str) -> Result { let body = self .make_post( "payments/bolt11", @@ -133,9 +121,10 @@ impl AlbyClient { }) } - pub async fn is_invoice_paid(&self, payment_hash: &str) -> Result { + pub async fn is_invoice_paid(&self, payment_hash: &str) -> Result { + info!("KODY checking if invoice is paid: {}", payment_hash); let body = self.make_get(&format!("invoices/{payment_hash}")).await?; - + info!("KODY body: {}", body); Ok(serde_json::from_str::(&body)?["settled"] .as_bool() .unwrap_or(false)) diff --git a/moksha-mint/src/lightning/error.rs b/moksha-mint/src/lightning/error.rs new file mode 100644 index 00000000..3b79a958 --- /dev/null +++ b/moksha-mint/src/lightning/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug, thiserror::Error)] +pub enum LightningError { + #[error("reqwest error: {0}")] + ReqwestError(#[from] reqwest::Error), + + #[error("url error: {0}")] + UrlError(#[from] url::ParseError), + + #[error("serde error: {0}")] + SerdeError(#[from] serde_json::Error), + + #[error("Not found")] + NotFound, + + #[error("Unauthorized")] + Unauthorized, + + #[error("Payment failed")] + PaymentFailed, +} diff --git a/moksha-mint/src/lnbits.rs b/moksha-mint/src/lightning/lnbits.rs similarity index 83% rename from moksha-mint/src/lnbits.rs rename to moksha-mint/src/lightning/lnbits.rs index c48408e5..25c16aba 100644 --- a/moksha-mint/src/lnbits.rs +++ b/moksha-mint/src/lightning/lnbits.rs @@ -3,23 +3,7 @@ use url::Url; use crate::model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}; -#[derive(Debug, thiserror::Error)] -pub enum LNBitsError { - #[error("reqwest error: {0}")] - ReqwestError(#[from] reqwest::Error), - - #[error("url error: {0}")] - UrlError(#[from] url::ParseError), - - #[error("serde error: {0}")] - SerdeError(#[from] serde_json::Error), - - #[error("Not found")] - NotFound, - - #[error("Unauthorized")] - Unauthorized, -} +use super::error::LightningError; #[derive(Clone)] pub struct LNBitsClient { @@ -33,7 +17,7 @@ impl LNBitsClient { admin_key: &str, lnbits_url: &str, tor_socket: Option<&str>, - ) -> Result { + ) -> Result { let lnbits_url = Url::parse(lnbits_url)?; let reqwest_client = { @@ -54,7 +38,7 @@ impl LNBitsClient { } impl LNBitsClient { - pub async fn make_get(&self, endpoint: &str) -> Result { + pub async fn make_get(&self, endpoint: &str) -> Result { let url = self.lnbits_url.join(endpoint)?; let response = self .reqwest_client @@ -64,13 +48,13 @@ impl LNBitsClient { .await?; if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(LNBitsError::NotFound); + return Err(LightningError::NotFound); } Ok(response.text().await?) } - pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { + pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { let url = self.lnbits_url.join(endpoint)?; let response = self .reqwest_client @@ -85,11 +69,11 @@ impl LNBitsClient { .await?; if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(LNBitsError::NotFound); + return Err(LightningError::NotFound); } if response.status() == reqwest::StatusCode::UNAUTHORIZED { - return Err(LNBitsError::Unauthorized); + return Err(LightningError::Unauthorized); } Ok(response.text().await?) @@ -100,7 +84,7 @@ impl LNBitsClient { pub async fn create_invoice( &self, params: &CreateInvoiceParams, - ) -> Result { + ) -> Result { // Add out: true to the params let params = serde_json::json!({ "out": false, @@ -132,7 +116,7 @@ impl LNBitsClient { }) } - pub async fn pay_invoice(&self, bolt11: &str) -> Result { + pub async fn pay_invoice(&self, bolt11: &str) -> Result { let body = self .make_post( "api/v1/payments", @@ -143,7 +127,7 @@ impl LNBitsClient { Ok(serde_json::from_str(&body)?) } - pub async fn is_invoice_paid(&self, payment_hash: &str) -> Result { + pub async fn is_invoice_paid(&self, payment_hash: &str) -> Result { let body = self .make_get(&format!("api/v1/payments/{payment_hash}")) .await?; diff --git a/moksha-mint/src/lightning.rs b/moksha-mint/src/lightning/mod.rs similarity index 97% rename from moksha-mint/src/lightning.rs rename to moksha-mint/src/lightning/mod.rs index 55088978..4f64816c 100644 --- a/moksha-mint/src/lightning.rs +++ b/moksha-mint/src/lightning/mod.rs @@ -2,24 +2,29 @@ use async_trait::async_trait; use std::fmt::{self, Formatter}; use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard}; use tonic_lnd::Client; + use url::Url; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{ - alby::AlbyClient, error::MokshaMintError, - lnbits::LNBitsClient, model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}, - strike::{StrikeClient, StrikeError}, }; use lightning_invoice::{Bolt11Invoice as LNInvoice, SignedRawBolt11Invoice}; +mod alby; +pub mod error; +mod lnbits; +mod strike; + #[cfg(test)] use mockall::automock; use std::{path::PathBuf, str::FromStr, sync::Arc}; +use self::{alby::AlbyClient, error::LightningError, lnbits::LNBitsClient, strike::StrikeClient}; + #[derive(Debug, Clone)] pub enum LightningType { Lnbits(LnbitsLightningSettings), @@ -55,26 +60,21 @@ pub trait Lightning: Send + Sync { } } -impl LnbitsLightning { - pub fn new(admin_key: String, url: String) -> Self { - Self { - client: LNBitsClient::new(&admin_key, &url, None) - .expect("Can not create Lnbits client"), - } - } -} - -#[derive(Clone)] -pub struct LnbitsLightning { - pub client: LNBitsClient, -} - #[derive(Deserialize, Serialize, Debug, Clone, Default)] pub struct LnbitsLightningSettings { pub admin_key: Option, pub url: Option, // FIXME use Url type instead } +impl LnbitsLightningSettings { + pub fn new(admin_key: &str, url: &str) -> Self { + Self { + admin_key: Some(admin_key.to_owned()), + url: Some(url.to_owned()), + } + } +} + impl fmt::Display for LnbitsLightningSettings { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( @@ -86,11 +86,16 @@ impl fmt::Display for LnbitsLightningSettings { } } -impl LnbitsLightningSettings { - pub fn new(admin_key: &str, url: &str) -> Self { +#[derive(Clone)] +pub struct LnbitsLightning { + pub client: LNBitsClient, +} + +impl LnbitsLightning { + pub fn new(admin_key: String, url: String) -> Self { Self { - admin_key: Some(admin_key.to_owned()), - url: Some(url.to_owned()), + client: LNBitsClient::new(&admin_key, &url, None) + .expect("Can not create Lnbits client"), } } } @@ -130,11 +135,6 @@ impl Lightning for LnbitsLightning { } } -#[derive(Clone)] -pub struct AlbyLightning { - pub client: AlbyClient, -} - #[derive(Deserialize, Serialize, Debug, Clone, Default)] pub struct AlbyLightningSettings { pub api_key: Option, @@ -154,6 +154,11 @@ impl AlbyLightningSettings { } } +#[derive(Clone)] +pub struct AlbyLightning { + pub client: AlbyClient, +} + impl AlbyLightning { pub fn new(api_key: String) -> Self { Self { @@ -161,7 +166,6 @@ impl AlbyLightning { } } } - #[async_trait] impl Lightning for AlbyLightning { async fn is_invoice_paid(&self, invoice: String) -> Result { @@ -193,15 +197,10 @@ impl Lightning for AlbyLightning { self.client .pay_invoice(&payment_request) .await - .map_err(|err| MokshaMintError::PayInvoiceAlby(payment_request, err)) + .map_err(|err| MokshaMintError::PayInvoice(payment_request, err)) } } -#[derive(Clone)] -pub struct StrikeLightning { - pub client: StrikeClient, -} - #[derive(Deserialize, Serialize, Debug, Clone, Default)] pub struct StrikeLightningSettings { pub api_key: Option, @@ -221,6 +220,11 @@ impl StrikeLightningSettings { } } +#[derive(Clone)] +pub struct StrikeLightning { + pub client: StrikeClient, +} + impl StrikeLightning { pub fn new(api_key: String) -> Self { Self { @@ -239,8 +243,9 @@ impl Lightning for StrikeLightning { .unwrap() .0; - // invoiceId is the first 32 bytes of the description hash - let invoice_id = format_as_uuid_string(&description_hash[..16]); + // invoiceId is the last 16 bytes of the description hash + let invoice_id = format_as_uuid_string(&description_hash[16..]); + Ok(self.client.is_invoice_paid(&invoice_id).await?) } @@ -258,7 +263,6 @@ impl Lightning for StrikeLightning { .await?; let payment_request = self.client.create_strike_quote(&strike_invoice_id).await?; - // strike doesn't return the payment_hash so we have to read the invoice into a Bolt11 and extract it let invoice = LNInvoice::from_signed(payment_request.parse::().unwrap()) @@ -290,9 +294,9 @@ impl Lightning for StrikeLightning { .await?; if !payment_result { - return Err(MokshaMintError::PayInvoiceStrike( + return Err(MokshaMintError::PayInvoice( payment_request, - StrikeError::PaymentFailed, + LightningError::PaymentFailed, )); } diff --git a/moksha-mint/src/strike.rs b/moksha-mint/src/lightning/strike.rs similarity index 83% rename from moksha-mint/src/strike.rs rename to moksha-mint/src/lightning/strike.rs index a4842f49..7eae72ba 100644 --- a/moksha-mint/src/strike.rs +++ b/moksha-mint/src/lightning/strike.rs @@ -1,29 +1,11 @@ use hyper::{header::CONTENT_TYPE, http::HeaderValue}; use serde::{Deserialize, Serialize}; + use url::Url; use crate::model::CreateInvoiceParams; -#[derive(Debug, thiserror::Error)] -pub enum StrikeError { - #[error("reqwest error: {0}")] - ReqwestError(#[from] reqwest::Error), - - #[error("url error: {0}")] - UrlError(#[from] url::ParseError), - - #[error("serde error: {0}")] - SerdeError(#[from] serde_json::Error), - - #[error("Not found")] - NotFound, - - #[error("Unauthorized")] - Unauthorized, - - #[error("Payment Failed")] - PaymentFailed, -} +use super::error::LightningError; #[derive(Clone)] pub struct StrikeClient { @@ -33,7 +15,7 @@ pub struct StrikeClient { } impl StrikeClient { - pub fn new(api_key: &str) -> Result { + pub fn new(api_key: &str) -> Result { let strike_url = Url::parse("https://api.strike.me")?; let reqwest_client = reqwest::Client::builder().build()?; @@ -47,7 +29,7 @@ impl StrikeClient { } impl StrikeClient { - pub async fn make_get(&self, endpoint: &str) -> Result { + pub async fn make_get(&self, endpoint: &str) -> Result { let url = self.strike_url.join(endpoint)?; let response = self .reqwest_client @@ -57,13 +39,13 @@ impl StrikeClient { .await?; if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(StrikeError::NotFound); + return Err(LightningError::NotFound); } Ok(response.text().await?) } - pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { + pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { let url = self.strike_url.join(endpoint)?; let response = self .reqwest_client @@ -78,17 +60,17 @@ impl StrikeClient { .await?; if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(StrikeError::NotFound); + return Err(LightningError::NotFound); } if response.status() == reqwest::StatusCode::UNAUTHORIZED { - return Err(StrikeError::Unauthorized); + return Err(LightningError::Unauthorized); } Ok(response.text().await?) } - pub async fn make_patch(&self, endpoint: &str, body: &str) -> Result { + pub async fn make_patch(&self, endpoint: &str, body: &str) -> Result { let url = self.strike_url.join(endpoint)?; let response = self .reqwest_client @@ -103,11 +85,11 @@ impl StrikeClient { .await?; if response.status() == reqwest::StatusCode::NOT_FOUND { - return Err(StrikeError::NotFound); + return Err(LightningError::NotFound); } if response.status() == reqwest::StatusCode::UNAUTHORIZED { - return Err(StrikeError::Unauthorized); + return Err(LightningError::Unauthorized); } Ok(response.text().await?) @@ -116,6 +98,7 @@ impl StrikeClient { #[derive(Debug, Serialize, Deserialize)] pub struct QuoteRequest { + #[serde(rename = "descriptionHash")] pub description_hash: String, } @@ -127,8 +110,8 @@ impl StrikeClient { pub async fn create_strike_invoice( &self, params: &CreateInvoiceParams, - ) -> Result { - let btc = params.amount / 100000000; + ) -> Result { + let btc = (params.amount as f64) / 100_000_000.0; let params = serde_json::json!({ "amount": { "amount": btc, @@ -150,13 +133,12 @@ impl StrikeClient { } // this is how you get the actual lightning invoice - pub async fn create_strike_quote(&self, invoice_id: &str) -> Result { + pub async fn create_strike_quote(&self, invoice_id: &str) -> Result { let endpoint = format!("v1/invoices/{}/quote", invoice_id); let description_hash = format!( "{:0>64}", hex::encode(hex::decode(invoice_id.replace('-', "").as_bytes()).unwrap()) ); - let params = QuoteRequest { description_hash }; let body = self .make_post(&endpoint, &serde_json::to_string(¶ms)?) @@ -170,7 +152,7 @@ impl StrikeClient { Ok(payment_request) } - pub async fn create_ln_payment_quote(&self, bolt11: &str) -> Result { + pub async fn create_ln_payment_quote(&self, bolt11: &str) -> Result { let params = serde_json::json!({ "lnInvoice": bolt11, "sourceCurrency": "BTC", @@ -190,7 +172,7 @@ impl StrikeClient { Ok(payment_quote_id) } - pub async fn execute_ln_payment_quote(&self, quote_id: &str) -> Result { + pub async fn execute_ln_payment_quote(&self, quote_id: &str) -> Result { let endpoint = format!("v1/payment-quotes/{}/execute", quote_id); let body = self .make_patch(&endpoint, &serde_json::to_string(&serde_json::json!({}))?) @@ -200,8 +182,8 @@ impl StrikeClient { Ok(response["state"].as_str().unwrap_or("") == "COMPLETED") } - pub async fn is_invoice_paid(&self, invoice_id: &str) -> Result { - let body = self.make_get(&format!("invoices/{invoice_id}")).await?; + pub async fn is_invoice_paid(&self, invoice_id: &str) -> Result { + let body = self.make_get(&format!("v1/invoices/{invoice_id}")).await?; let response = serde_json::from_str::(&body)?; Ok(response["state"].as_str().unwrap_or("") == "PAID") diff --git a/moksha-mint/src/mint.rs b/moksha-mint/src/mint.rs index 0b9fc779..59c39dc3 100644 --- a/moksha-mint/src/mint.rs +++ b/moksha-mint/src/mint.rs @@ -243,8 +243,8 @@ impl Mint { #[cfg(test)] mod tests { + use crate::lightning::error::LightningError; use crate::lightning::{LightningType, MockLightning}; - use crate::lnbits::LNBitsError; use crate::model::{Invoice, PayInvoiceResult}; use crate::{database::MockDatabase, error::MokshaMintError, Mint}; use moksha_core::dhke; @@ -411,7 +411,7 @@ mod tests { Ok(PayInvoiceResult { payment_hash: "hash".to_string(), }) - .map_err(|_err: LNBitsError| MokshaMintError::InvoiceNotFound("".to_string())) + .map_err(|_err: LightningError| MokshaMintError::InvoiceNotFound("".to_string())) }); let mint = Mint::new(