From 1a0196fbe9206965bef53fe7171e32e98d3a4d36 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 13 Feb 2025 16:56:29 +0000 Subject: [PATCH] feat: payment process server --- crates/cdk-common/src/proto/mod.rs | 67 ++- .../src/proto/payment_processor.proto | 1 + crates/cdk-fake-wallet/src/lib.rs | 516 ++++-------------- crates/cdk/src/mint/mod.rs | 3 +- .../client.rs} | 0 crates/cdk/src/mint/payment_processor/mod.rs | 4 + .../cdk/src/mint/payment_processor/server.rs | 201 +++++++ 7 files changed, 393 insertions(+), 399 deletions(-) rename crates/cdk/src/mint/{payment_processor.rs => payment_processor/client.rs} (100%) create mode 100644 crates/cdk/src/mint/payment_processor/mod.rs create mode 100644 crates/cdk/src/mint/payment_processor/server.rs diff --git a/crates/cdk-common/src/proto/mod.rs b/crates/cdk-common/src/proto/mod.rs index 6ced975b7..76b8f36ed 100644 --- a/crates/cdk-common/src/proto/mod.rs +++ b/crates/cdk-common/src/proto/mod.rs @@ -3,10 +3,20 @@ use std::str::FromStr; use cashu::{Bolt11Invoice, CurrencyUnit, MeltQuoteBolt11Request}; use melt_options::Options; -use crate::lightning::{CreateInvoiceResponse, PayInvoiceResponse}; +use crate::lightning::{CreateInvoiceResponse, PayInvoiceResponse, Settings}; tonic::include_proto!("cdk_payment_processor"); +impl From for SettingsResponse { + fn from(value: Settings) -> Self { + Self { + mpp: value.mpp, + unit: value.unit.to_string(), + invoice_description: value.invoice_description, + } + } +} + impl TryFrom for PayInvoiceResponse { type Error = crate::error::Error; fn try_from(value: MakePaymentResponse) -> Result { @@ -32,6 +42,16 @@ impl From for MakePaymentResponse { } } +impl From for CreatePaymentResponse { + fn from(value: CreateInvoiceResponse) -> Self { + Self { + request_lookup_id: value.request_lookup_id, + request: value.request.to_string(), + expiry: value.expiry, + } + } +} + impl TryFrom for CreateInvoiceResponse { type Error = crate::error::Error; @@ -54,6 +74,17 @@ impl From<&MeltQuoteBolt11Request> for PaymentQuoteRequest { } } +impl From for PaymentQuoteResponse { + fn from(value: crate::lightning::PaymentQuoteResponse) -> Self { + Self { + request_lookup_id: value.request_lookup_id, + amount: value.amount.into(), + fee: value.fee.into(), + state: QuoteState::from(value.state).into(), + } + } +} + impl From for MeltOptions { fn from(value: cashu::nut05::MeltOptions) -> Self { Self { @@ -100,6 +131,7 @@ impl From for cashu::nut05::QuoteState { QuoteState::Pending => Self::Pending, QuoteState::Unknown => Self::Unknown, QuoteState::Failed => Self::Failed, + QuoteState::Issued => Self::Unknown, } } } @@ -116,6 +148,17 @@ impl From for QuoteState { } } +impl From for QuoteState { + fn from(value: cashu::nut04::QuoteState) -> Self { + match value { + cashu::MintQuoteState::Unpaid => Self::Unpaid, + cashu::MintQuoteState::Paid => Self::Paid, + cashu::MintQuoteState::Pending => Self::Pending, + cashu::MintQuoteState::Issued => Self::Issued, + } + } +} + impl From for MeltQuote { fn from(value: cashu::mint::MeltQuote) -> Self { Self { @@ -133,6 +176,28 @@ impl From for MeltQuote { } } +impl TryFrom for cashu::mint::MeltQuote { + type Error = crate::error::Error; + + fn try_from(value: MeltQuote) -> Result { + Ok(Self { + id: value + .id + .parse() + .map_err(|_| crate::error::Error::Internal)?, + unit: value.unit.parse()?, + amount: value.amount.into(), + request: value.request.clone(), + fee_reserve: value.fee_reserve.into(), + state: cashu::nut05::QuoteState::from(value.state()), + expiry: value.expiry, + payment_preimage: value.payment_preimage, + request_lookup_id: value.request_lookup_id, + msat_to_pay: value.msat_to_pay.map(|a| a.into()), + }) + } +} + impl TryFrom for MeltQuoteBolt11Request { type Error = crate::error::Error; diff --git a/crates/cdk-common/src/proto/payment_processor.proto b/crates/cdk-common/src/proto/payment_processor.proto index d242bc552..30d0fb707 100644 --- a/crates/cdk-common/src/proto/payment_processor.proto +++ b/crates/cdk-common/src/proto/payment_processor.proto @@ -54,6 +54,7 @@ enum QuoteState { PENDING = 2; UNKNOWN = 3; FAILED = 4; + ISSUED = 5; } diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 6d0855563..2bed50d7a 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -6,31 +6,31 @@ #![warn(rustdoc::bare_urls)] use std::collections::{HashMap, HashSet}; -use std::net::SocketAddr; -use std::path::PathBuf; +use std::pin::Pin; use std::str::FromStr; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::time::Duration; -use anyhow::Result; +use async_trait::async_trait; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::rand::{thread_rng, Rng}; use bitcoin::secp256k1::{Secp256k1, SecretKey}; use cdk::amount::{Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::PayInvoiceResponse; +use cdk::cdk_lightning::{ + self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, +}; +use cdk::mint; use cdk::mint::FeeReserve; -use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState}; -use cdk::proto::cdk_payment_processor_server::{CdkPaymentProcessor, CdkPaymentProcessorServer}; -use cdk::proto::*; -use cdk::tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; -use cdk::tonic::{async_trait, Request, Response, Status}; +use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::util::unix_time; +use error::Error; +use futures::stream::StreamExt; +use futures::Stream; use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret}; use serde::{Deserialize, Serialize}; -use tokio::sync::{Mutex, Notify}; -use tokio::task::JoinHandle; +use tokio::sync::Mutex; use tokio::time; +use tokio_stream::wrappers::ReceiverStream; use tokio_util::sync::CancellationToken; pub mod error; @@ -40,15 +40,12 @@ pub mod error; pub struct FakeWallet { fee_reserve: FeeReserve, sender: tokio::sync::mpsc::Sender, - _receiver: Arc>>>, + receiver: Arc>>>, payment_states: Arc>>, failed_payment_check: Arc>>, payment_delay: u64, - _wait_invoice_cancel_token: CancellationToken, - _wait_invoice_is_active: Arc, - socket_addr: SocketAddr, - shutdown: Arc, - handle: Option>>>, + wait_invoice_cancel_token: CancellationToken, + wait_invoice_is_active: Arc, } impl FakeWallet { @@ -58,86 +55,19 @@ impl FakeWallet { payment_states: HashMap, fail_payment_check: HashSet, payment_delay: u64, - addr: &str, - port: u16, - ) -> Result { + ) -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(8); - Ok(Self { + Self { fee_reserve, sender, - _receiver: Arc::new(Mutex::new(Some(receiver))), + receiver: Arc::new(Mutex::new(Some(receiver))), payment_states: Arc::new(Mutex::new(payment_states)), failed_payment_check: Arc::new(Mutex::new(fail_payment_check)), payment_delay, - _wait_invoice_cancel_token: CancellationToken::new(), - _wait_invoice_is_active: Arc::new(AtomicBool::new(false)), - shutdown: Arc::new(Notify::new()), - handle: None, - socket_addr: format!("{addr}:{port}").parse()?, - }) - } - - /// Start fake wallet grpc server - pub async fn start(&mut self, tls_dir: Option) -> Result<()> { - tracing::info!("Starting RPC server {}", self.socket_addr); - - let server = match tls_dir { - Some(tls_dir) => { - tracing::info!("TLS configuration found, starting secure server"); - let cert = std::fs::read_to_string(tls_dir.join("server.pem"))?; - let key = std::fs::read_to_string(tls_dir.join("server.key"))?; - let client_ca_cert = std::fs::read_to_string(tls_dir.join("ca.pem"))?; - let client_ca_cert = Certificate::from_pem(client_ca_cert); - let server_identity = Identity::from_pem(cert, key); - let tls_config = ServerTlsConfig::new() - .identity(server_identity) - .client_ca_root(client_ca_cert); - - Server::builder() - .tls_config(tls_config)? - .add_service(CdkPaymentProcessorServer::new(self.clone())) - } - None => { - tracing::warn!("No valid TLS configuration found, starting insecure server"); - Server::builder().add_service(CdkPaymentProcessorServer::new(self.clone())) - } - }; - - let shutdown = self.shutdown.clone(); - let addr = self.socket_addr; - - self.handle = Some(Arc::new(tokio::spawn(async move { - let server = server.serve_with_shutdown(addr, async { - shutdown.notified().await; - }); - - server.await?; - Ok(()) - }))); - - Ok(()) - } - - /// Stop fake wallet grpc server - pub async fn stop(&self) -> Result<()> { - self.shutdown.notify_one(); - if let Some(handle) = &self.handle { - while !handle.is_finished() { - tracing::info!("Waitning for mint rpc server to stop"); - tokio::time::sleep(Duration::from_millis(100)).await; - } + wait_invoice_cancel_token: CancellationToken::new(), + wait_invoice_is_active: Arc::new(AtomicBool::new(false)), } - - tracing::info!("Mint rpc server stopped"); - Ok(()) - } -} - -impl Drop for FakeWallet { - fn drop(&mut self) { - tracing::debug!("Dropping fake wallet rpc server"); - self.shutdown.notify_one(); } } @@ -166,85 +96,38 @@ impl Default for FakeInvoiceDescription { } #[async_trait] -impl CdkPaymentProcessor for FakeWallet { - async fn get_settings( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(SettingsResponse { +impl MintLightning for FakeWallet { + type Err = cdk_lightning::Error; + + async fn get_settings(&self) -> Result { + Ok(Settings { mpp: true, - unit: CurrencyUnit::Msat.to_string(), + unit: CurrencyUnit::Msat, invoice_description: true, - })) + }) } - async fn create_payment( - &self, - request: Request, - ) -> Result, Status> { - let CreatePaymentRequest { - amount, - unit, - description, - unix_expiry, - } = request.into_inner(); - - let time_now = unix_time(); - - if unit != CurrencyUnit::Msat.to_string() { - return Err(Status::invalid_argument("Unsupported unit")); - } - - if let Some(unix_expiry) = unix_expiry { - if time_now > unix_expiry { - return Err(Status::invalid_argument("Invalid unix time")); - } - } - - // Since this is fake we just use the amount no matter the unit to create an invoice - let amount_msat = amount; - - let invoice = create_fake_invoice(amount_msat, description); - - let sender = self.sender.clone(); - - let payment_hash = invoice.payment_hash(); - - let payment_hash_clone = payment_hash.to_string(); - - let duration = time::Duration::from_secs(self.payment_delay); - - tokio::spawn(async move { - // Wait for the random delay to elapse - time::sleep(duration).await; - - // Send the message after waiting for the specified duration - if sender.send(payment_hash_clone.clone()).await.is_err() { - tracing::error!("Failed to send label: {}", payment_hash_clone); - } - }); + fn is_wait_invoice_active(&self) -> bool { + self.wait_invoice_is_active.load(Ordering::SeqCst) + } - let expiry = invoice.expires_at().map(|t| t.as_secs()); + fn cancel_wait_invoice(&self) { + self.wait_invoice_cancel_token.cancel() + } - Ok(Response::new(CreatePaymentResponse { - request_lookup_id: payment_hash.to_string(), - request: invoice.to_string(), - expiry, - })) + async fn wait_any_invoice( + &self, + ) -> Result + Send>>, Self::Err> { + let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; + let receiver_stream = ReceiverStream::new(receiver); + Ok(Box::pin(receiver_stream.map(|label| label))) } async fn get_payment_quote( &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let melt_quote_request: MeltQuoteBolt11Request = request - .try_into() - .map_err(|_| Status::invalid_argument("invalid request"))?; - let amount = melt_quote_request - .amount_msat() - .map_err(|_| Status::internal("Could not get amount"))?; + melt_quote_request: &MeltQuoteBolt11Request, + ) -> Result { + let amount = melt_quote_request.amount_msat()?; let amount = amount / MSAT_IN_SAT.into(); @@ -258,28 +141,21 @@ impl CdkPaymentProcessor for FakeWallet { false => absolute_fee_reserve, }; - Ok(Response::new(PaymentQuoteResponse { + Ok(PaymentQuoteResponse { request_lookup_id: melt_quote_request.request.payment_hash().to_string(), - amount: amount.into(), - fee, - state: QuoteState::from(MeltQuoteState::Unpaid).into(), - })) + amount, + fee: fee.into(), + state: MeltQuoteState::Unpaid, + }) } - async fn make_payment( + async fn pay_invoice( &self, - request: Request, - ) -> Result, Status> { - let MakePaymentRequest { - melt_quote, - partial_amount: _, - max_fee_amount: _, - } = request.into_inner(); - - let melt_quote: MeltQuote = melt_quote.ok_or(Status::invalid_argument("No melt quote"))?; - - let bolt11 = Bolt11Invoice::from_str(&melt_quote.request) - .map_err(|_| Status::internal("Invalid bolt11"))?; + melt_quote: mint::MeltQuote, + _partial_msats: Option, + _max_fee_msats: Option, + ) -> Result { + let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; let payment_hash = bolt11.payment_hash().to_string(); @@ -307,248 +183,94 @@ impl CdkPaymentProcessor for FakeWallet { } if description.pay_err { - return Err(Status::invalid_argument("Description not supported")); + return Err(Error::UnknownInvoice.into()); } } - Ok(Response::new( - PayInvoiceResponse { - payment_preimage: Some("".to_string()), - payment_lookup_id: payment_hash, - status: payment_status, - total_spent: melt_quote.amount.into(), - unit: melt_quote - .unit - .parse() - .map_err(|_| Status::invalid_argument("Invalid unit"))?, + Ok(PayInvoiceResponse { + payment_preimage: Some("".to_string()), + payment_lookup_id: payment_hash, + status: payment_status, + total_spent: melt_quote.amount, + unit: melt_quote.unit, + }) + } + + async fn create_invoice( + &self, + amount: Amount, + _unit: &CurrencyUnit, + description: String, + unix_expiry: u64, + ) -> Result { + let time_now = unix_time(); + assert!(unix_expiry > time_now); + + // Since this is fake we just use the amount no matter the unit to create an invoice + let amount_msat = amount; + + let invoice = create_fake_invoice(amount_msat.into(), description); + + let sender = self.sender.clone(); + + let payment_hash = invoice.payment_hash(); + + let payment_hash_clone = payment_hash.to_string(); + + let duration = time::Duration::from_secs(self.payment_delay); + + tokio::spawn(async move { + // Wait for the random delay to elapse + time::sleep(duration).await; + + // Send the message after waiting for the specified duration + if sender.send(payment_hash_clone.clone()).await.is_err() { + tracing::error!("Failed to send label: {}", payment_hash_clone); } - .into(), - )) + }); + + let expiry = invoice.expires_at().map(|t| t.as_secs()); + + Ok(CreateInvoiceResponse { + request_lookup_id: payment_hash.to_string(), + request: invoice, + expiry, + }) } - async fn check_incoming_payment( + async fn check_incoming_invoice_status( &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(CheckIncomingPaymentResponse { - status: QuoteState::Paid.into(), - })) + _request_lookup_id: &str, + ) -> Result { + Ok(MintQuoteState::Paid) } async fn check_outgoing_payment( &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - let request_lookup_id = request.request_lookup_id; - + request_lookup_id: &str, + ) -> Result { // For fake wallet if the state is not explicitly set default to paid let states = self.payment_states.lock().await; - let status = states.get(&request_lookup_id).cloned(); + let status = states.get(request_lookup_id).cloned(); let status = status.unwrap_or(MeltQuoteState::Paid); let fail_payments = self.failed_payment_check.lock().await; - if fail_payments.contains(&request_lookup_id) { - return Err(Status::internal("Could not pay invoice")); + if fail_payments.contains(request_lookup_id) { + return Err(cdk_lightning::Error::InvoicePaymentPending); } - Ok(Response::new( - PayInvoiceResponse { - payment_preimage: Some("".to_string()), - payment_lookup_id: request_lookup_id.to_string(), - status, - total_spent: Amount::ZERO, - unit: self - .get_settings(Request::new(SettingsRequest {})) - .await? - .into_inner() - .unit - .parse() - .expect("Valid unit set"), - } - .into(), - )) + Ok(PayInvoiceResponse { + payment_preimage: Some("".to_string()), + payment_lookup_id: request_lookup_id.to_string(), + status, + total_spent: Amount::ZERO, + unit: self.get_settings().await?.unit, + }) } } -// #[async_trait] -// impl MintLightning for FakeWallet { -// type Err = cdk_lightning::Error; - -// async fn get_settings(&self) -> Result { -// Ok(Settings { -// mpp: true, -// unit: CurrencyUnit::Msat, -// invoice_description: true, -// }) -// } - -// fn is_wait_invoice_active(&self) -> bool { -// self.wait_invoice_is_active.load(Ordering::SeqCst) -// } - -// fn cancel_wait_invoice(&self) { -// self.wait_invoice_cancel_token.cancel() -// } - -// async fn wait_any_invoice( -// &self, -// ) -> Result + Send>>, Self::Err> { -// let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; -// let receiver_stream = ReceiverStream::new(receiver); -// Ok(Box::pin(receiver_stream.map(|label| label))) -// } - -// async fn get_payment_quote( -// &self, -// melt_quote_request: &MeltQuoteBolt11Request, -// ) -> Result { -// let amount = melt_quote_request.amount_msat()?; - -// let amount = amount / MSAT_IN_SAT.into(); - -// let relative_fee_reserve = -// (self.fee_reserve.percent_fee_reserve * u64::from(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.payment_hash().to_string(), -// amount, -// fee: fee.into(), -// state: MeltQuoteState::Unpaid, -// }) -// } - -// async fn pay_invoice( -// &self, -// melt_quote: mint::MeltQuote, -// _partial_msats: Option, -// _max_fee_msats: Option, -// ) -> Result { -// let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; - -// let payment_hash = bolt11.payment_hash().to_string(); - -// let description = bolt11.description().to_string(); - -// let status: Option = serde_json::from_str(&description).ok(); - -// let mut payment_states = self.payment_states.lock().await; -// let payment_status = status -// .clone() -// .map(|s| s.pay_invoice_state) -// .unwrap_or(MeltQuoteState::Paid); - -// let checkout_going_status = status -// .clone() -// .map(|s| s.check_payment_state) -// .unwrap_or(MeltQuoteState::Paid); - -// payment_states.insert(payment_hash.clone(), checkout_going_status); - -// if let Some(description) = status { -// if description.check_err { -// let mut fail = self.failed_payment_check.lock().await; -// fail.insert(payment_hash.clone()); -// } - -// if description.pay_err { -// return Err(Error::UnknownInvoice.into()); -// } -// } - -// Ok(PayInvoiceResponse { -// payment_preimage: Some("".to_string()), -// payment_lookup_id: payment_hash, -// status: payment_status, -// total_spent: melt_quote.amount, -// unit: melt_quote.unit, -// }) -// } - -// async fn create_invoice( -// &self, -// amount: Amount, -// _unit: &CurrencyUnit, -// description: String, -// unix_expiry: u64, -// ) -> Result { -// let time_now = unix_time(); -// assert!(unix_expiry > time_now); - -// // Since this is fake we just use the amount no matter the unit to create an invoice -// let amount_msat = amount; - -// let invoice = create_fake_invoice(amount_msat.into(), description); - -// let sender = self.sender.clone(); - -// let payment_hash = invoice.payment_hash(); - -// let payment_hash_clone = payment_hash.to_string(); - -// let duration = time::Duration::from_secs(self.payment_delay); - -// tokio::spawn(async move { -// // Wait for the random delay to elapse -// time::sleep(duration).await; - -// // Send the message after waiting for the specified duration -// if sender.send(payment_hash_clone.clone()).await.is_err() { -// tracing::error!("Failed to send label: {}", payment_hash_clone); -// } -// }); - -// let expiry = invoice.expires_at().map(|t| t.as_secs()); - -// Ok(CreateInvoiceResponse { -// request_lookup_id: payment_hash.to_string(), -// request: invoice, -// expiry, -// }) -// } - -// async fn check_incoming_invoice_status( -// &self, -// _request_lookup_id: &str, -// ) -> Result { -// Ok(MintQuoteState::Paid) -// } - -// async fn check_outgoing_payment( -// &self, -// request_lookup_id: &str, -// ) -> Result { -// // For fake wallet if the state is not explicitly set default to paid -// let states = self.payment_states.lock().await; -// let status = states.get(request_lookup_id).cloned(); - -// let status = status.unwrap_or(MeltQuoteState::Paid); - -// let fail_payments = self.failed_payment_check.lock().await; - -// if fail_payments.contains(request_lookup_id) { -// return Err(cdk_lightning::Error::InvoicePaymentPending); -// } - -// Ok(PayInvoiceResponse { -// payment_preimage: Some("".to_string()), -// payment_lookup_id: request_lookup_id.to_string(), -// status, -// total_spent: Amount::ZERO, -// unit: self.get_settings().await?.unit, -// }) -// } -// } - /// Create fake invoice pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice { let private_key = SecretKey::from_slice( diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 77d0b31ed..94d249084 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -38,7 +38,8 @@ mod verification; pub use builder::{MintBuilder, MintMeltLimits}; pub use cdk_common::mint::{MeltQuote, MintQuote}; -pub use payment_processor::PaymentProcessor; +pub use payment_processor::client::PaymentProcessor; +pub use payment_processor::PaymentProcessorServer; /// Cashu Mint #[derive(Clone)] diff --git a/crates/cdk/src/mint/payment_processor.rs b/crates/cdk/src/mint/payment_processor/client.rs similarity index 100% rename from crates/cdk/src/mint/payment_processor.rs rename to crates/cdk/src/mint/payment_processor/client.rs diff --git a/crates/cdk/src/mint/payment_processor/mod.rs b/crates/cdk/src/mint/payment_processor/mod.rs new file mode 100644 index 000000000..55fce55ae --- /dev/null +++ b/crates/cdk/src/mint/payment_processor/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod server; + +pub use server::PaymentProcessorServer; diff --git a/crates/cdk/src/mint/payment_processor/server.rs b/crates/cdk/src/mint/payment_processor/server.rs new file mode 100644 index 000000000..dbee4f218 --- /dev/null +++ b/crates/cdk/src/mint/payment_processor/server.rs @@ -0,0 +1,201 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use cdk_common::lightning::MintLightning; +use cdk_common::proto::cdk_payment_processor_server::CdkPaymentProcessor; +use cdk_common::{CdkPaymentProcessorServer, CurrencyUnit, MeltQuoteBolt11Request}; +use tokio::sync::Notify; +use tokio::task::JoinHandle; +use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; +use tonic::{async_trait, Request, Response, Status}; + +use crate::proto::*; + +/// Payment Processor +#[derive(Clone)] +pub struct PaymentProcessorServer { + inner: Arc + Send + Sync>, + socket_addr: SocketAddr, + shutdown: Arc, + handle: Option>>>, +} + +impl PaymentProcessorServer { + /// Start fake wallet grpc server + pub async fn start(&mut self, tls_dir: Option) -> anyhow::Result<()> { + tracing::info!("Starting RPC server {}", self.socket_addr); + + let server = match tls_dir { + Some(tls_dir) => { + tracing::info!("TLS configuration found, starting secure server"); + let cert = std::fs::read_to_string(tls_dir.join("server.pem"))?; + let key = std::fs::read_to_string(tls_dir.join("server.key"))?; + let client_ca_cert = std::fs::read_to_string(tls_dir.join("ca.pem"))?; + let client_ca_cert = Certificate::from_pem(client_ca_cert); + let server_identity = Identity::from_pem(cert, key); + let tls_config = ServerTlsConfig::new() + .identity(server_identity) + .client_ca_root(client_ca_cert); + + Server::builder() + .tls_config(tls_config)? + .add_service(CdkPaymentProcessorServer::new(self.clone())) + } + None => { + tracing::warn!("No valid TLS configuration found, starting insecure server"); + Server::builder().add_service(CdkPaymentProcessorServer::new(self.clone())) + } + }; + + let shutdown = self.shutdown.clone(); + let addr = self.socket_addr; + + self.handle = Some(Arc::new(tokio::spawn(async move { + let server = server.serve_with_shutdown(addr, async { + shutdown.notified().await; + }); + + server.await?; + Ok(()) + }))); + + Ok(()) + } + + /// Stop fake wallet grpc server + pub async fn stop(&self) -> anyhow::Result<()> { + self.shutdown.notify_one(); + if let Some(handle) = &self.handle { + while !handle.is_finished() { + tracing::info!("Waitning for mint rpc server to stop"); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + + tracing::info!("Mint rpc server stopped"); + Ok(()) + } +} + +impl Drop for PaymentProcessorServer { + fn drop(&mut self) { + tracing::debug!("Dropping fake wallet rpc server"); + self.shutdown.notify_one(); + } +} + +#[async_trait] +impl CdkPaymentProcessor for PaymentProcessorServer { + async fn get_settings( + &self, + _request: Request, + ) -> Result, Status> { + let settings = self + .inner + .get_settings() + .await + .map_err(|_| Status::internal("Could not get settings"))?; + Ok(Response::new(settings.into())) + } + + async fn create_payment( + &self, + request: Request, + ) -> Result, Status> { + let CreatePaymentRequest { + amount, + unit, + description, + unix_expiry, + } = request.into_inner(); + + let unit = + CurrencyUnit::from_str(&unit).map_err(|_| Status::invalid_argument("Invalid unit"))?; + let invoice_response = self + .inner + .create_invoice(amount.into(), &unit, description, unix_expiry.unwrap()) + .await + .map_err(|_| Status::internal("Could not create invoice"))?; + + Ok(Response::new(invoice_response.into())) + } + + async fn get_payment_quote( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let bolt11_melt_quote: MeltQuoteBolt11Request = request + .try_into() + .map_err(|_| Status::invalid_argument("Invalid request"))?; + + let payment_quote = self + .inner + .get_payment_quote(&bolt11_melt_quote) + .await + .map_err(|err| { + tracing::error!("Could not get bolt11 melt quote: {}", err); + Status::internal("Could not get melt quote") + })?; + + Ok(Response::new(payment_quote.into())) + } + + async fn make_payment( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let pay_invoice = self + .inner + .pay_invoice( + request + .melt_quote + .ok_or(Status::invalid_argument("Meltquote is required"))? + .try_into() + .map_err(|_err| Status::invalid_argument("Invalid melt quote"))?, + request.partial_amount.map(|a| a.into()), + request.max_fee_amount.map(|a| a.into()), + ) + .await + .map_err(|_| Status::internal("Could not pay invoice"))?; + + Ok(Response::new(pay_invoice.into())) + } + + async fn check_incoming_payment( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let check_response = self + .inner + .check_incoming_invoice_status(&request.request_lookup_id) + .await + .map_err(|_| Status::internal("Could not check incoming payment status"))?; + + Ok(Response::new(CheckIncomingPaymentResponse { + status: QuoteState::from(check_response).into(), + })) + } + + async fn check_outgoing_payment( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let check_response = self + .inner + .check_outgoing_payment(&request.request_lookup_id) + .await + .map_err(|_| Status::internal("Could not check incoming payment status"))?; + + Ok(Response::new(check_response.into())) + } +}