use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use cdk::mint::Mint; use cdk::nuts::nut04::MintMethodSettings; use cdk::nuts::nut05::MeltMethodSettings; use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod}; use cdk::types::QuoteTTL; use cdk::Amount; use thiserror::Error; use tokio::sync::Notify; use tokio::task::JoinHandle; use tokio::time::Duration; use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; use tonic::{Request, Response, Status}; use crate::cdk_mint_server::{CdkMint, CdkMintServer}; use crate::{ ContactInfo, GetInfoRequest, GetInfoResponse, RotateNextKeysetRequest, RotateNextKeysetResponse, UpdateContactRequest, UpdateDescriptionRequest, UpdateIconUrlRequest, UpdateMotdRequest, UpdateNameRequest, UpdateNut04QuoteRequest, UpdateNut04Request, UpdateNut05Request, UpdateQuoteTtlRequest, UpdateResponse, UpdateUrlRequest, }; /// Error #[derive(Debug, Error)] pub enum Error { /// Parse error #[error(transparent)] Parse(#[from] std::net::AddrParseError), /// Transport error #[error(transparent)] Transport(#[from] tonic::transport::Error), /// Io error #[error(transparent)] Io(#[from] std::io::Error), } /// CDK Mint RPC Server #[derive(Clone)] pub struct MintRPCServer { socket_addr: SocketAddr, mint: Arc<Mint>, shutdown: Arc<Notify>, handle: Option<Arc<JoinHandle<Result<(), Error>>>>, } impl MintRPCServer { pub fn new(addr: &str, port: u16, mint: Arc<Mint>) -> Result<Self, Error> { Ok(Self { socket_addr: format!("{addr}:{port}").parse()?, mint, shutdown: Arc::new(Notify::new()), handle: None, }) } pub async fn start(&mut self, tls_dir: Option<PathBuf>) -> Result<(), Error> { 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(CdkMintServer::new(self.clone())) } None => { tracing::warn!("No valid TLS configuration found, starting insecure server"); Server::builder().add_service(CdkMintServer::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(()) } pub async fn stop(&self) -> Result<(), Error> { 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 MintRPCServer { fn drop(&mut self) { tracing::debug!("Dropping mint rpc server"); self.shutdown.notify_one(); } } #[tonic::async_trait] impl CdkMint for MintRPCServer { async fn get_info( &self, _request: Request<GetInfoRequest>, ) -> Result<Response<GetInfoResponse>, Status> { let info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; let total_issued = self .mint .total_issued() .await .map_err(|err| Status::internal(err.to_string()))?; let total_issued: Amount = Amount::try_sum(total_issued.values().cloned()) .map_err(|_| Status::internal("Overflow".to_string()))?; let total_redeemed = self .mint .total_redeemed() .await .map_err(|err| Status::internal(err.to_string()))?; let total_redeemed: Amount = Amount::try_sum(total_redeemed.values().cloned()) .map_err(|_| Status::internal("Overflow".to_string()))?; let contact = info .contact .unwrap_or_default() .into_iter() .map(|c| ContactInfo { method: c.method, info: c.info, }) .collect(); Ok(Response::new(GetInfoResponse { name: info.name, description: info.description, long_description: info.description_long, version: info.version.map(|v| v.to_string()), contact, motd: info.motd, icon_url: info.icon_url, urls: info.urls.unwrap_or_default(), total_issued: total_issued.into(), total_redeemed: total_redeemed.into(), })) } async fn update_motd( &self, request: Request<UpdateMotdRequest>, ) -> Result<Response<UpdateResponse>, Status> { let motd = request.into_inner().motd; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; info.motd = Some(motd); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_short_description( &self, request: Request<UpdateDescriptionRequest>, ) -> Result<Response<UpdateResponse>, Status> { let description = request.into_inner().description; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; info.description = Some(description); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_long_description( &self, request: Request<UpdateDescriptionRequest>, ) -> Result<Response<UpdateResponse>, Status> { let description = request.into_inner().description; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; info.description = Some(description); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_name( &self, request: Request<UpdateNameRequest>, ) -> Result<Response<UpdateResponse>, Status> { let name = request.into_inner().name; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; info.name = Some(name); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_icon_url( &self, request: Request<UpdateIconUrlRequest>, ) -> Result<Response<UpdateResponse>, Status> { let icon_url = request.into_inner().icon_url; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; info.icon_url = Some(icon_url); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn add_url( &self, request: Request<UpdateUrlRequest>, ) -> Result<Response<UpdateResponse>, Status> { let url = request.into_inner().url; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; let urls = info.urls; urls.clone().unwrap_or_default().push(url); info.urls = urls; self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn remove_url( &self, request: Request<UpdateUrlRequest>, ) -> Result<Response<UpdateResponse>, Status> { let url = request.into_inner().url; let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; let urls = info.urls; urls.clone().unwrap_or_default().push(url); info.urls = urls; self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn add_contact( &self, request: Request<UpdateContactRequest>, ) -> Result<Response<UpdateResponse>, Status> { let request_inner = request.into_inner(); let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; info.contact .get_or_insert_with(Vec::new) .push(cdk::nuts::ContactInfo::new( request_inner.method, request_inner.info, )); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn remove_contact( &self, request: Request<UpdateContactRequest>, ) -> Result<Response<UpdateResponse>, Status> { let request_inner = request.into_inner(); let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; if let Some(contact) = info.contact.as_mut() { let contact_info = cdk::nuts::ContactInfo::new(request_inner.method, request_inner.info); contact.retain(|x| x != &contact_info); self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; } Ok(Response::new(UpdateResponse {})) } async fn update_nut04( &self, request: Request<UpdateNut04Request>, ) -> Result<Response<UpdateResponse>, Status> { let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; let mut nut04_settings = info.nuts.nut04.clone(); let request_inner = request.into_inner(); let unit = CurrencyUnit::from_str(&request_inner.unit) .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?; let payment_method = PaymentMethod::from_str(&request_inner.method) .map_err(|_| Status::invalid_argument("Invalid method".to_string()))?; let current_nut04_settings = nut04_settings.remove_settings(&unit, &payment_method); let mut methods = nut04_settings.methods.clone(); let updated_method_settings = MintMethodSettings { method: payment_method, unit, min_amount: request_inner .min .map(Amount::from) .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.min_amount)), max_amount: request_inner .max .map(Amount::from) .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.max_amount)), description: request_inner.description.unwrap_or( current_nut04_settings .map(|c| c.description) .unwrap_or_default(), ), }; methods.push(updated_method_settings); nut04_settings.methods = methods; if let Some(disabled) = request_inner.disabled { nut04_settings.disabled = disabled; } info.nuts.nut04 = nut04_settings; self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_nut05( &self, request: Request<UpdateNut05Request>, ) -> Result<Response<UpdateResponse>, Status> { let mut info = self .mint .mint_info() .await .map_err(|err| Status::internal(err.to_string()))?; let mut nut05_settings = info.nuts.nut05.clone(); let request_inner = request.into_inner(); let unit = CurrencyUnit::from_str(&request_inner.unit) .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?; let payment_method = PaymentMethod::from_str(&request_inner.method) .map_err(|_| Status::invalid_argument("Invalid method".to_string()))?; let current_nut05_settings = nut05_settings.remove_settings(&unit, &payment_method); let mut methods = nut05_settings.methods; let updated_method_settings = MeltMethodSettings { method: payment_method, unit, min_amount: request_inner .min .map(Amount::from) .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.min_amount)), max_amount: request_inner .max .map(Amount::from) .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)), }; methods.push(updated_method_settings); nut05_settings.methods = methods; if let Some(disabled) = request_inner.disabled { nut05_settings.disabled = disabled; } info.nuts.nut05 = nut05_settings; self.mint .set_mint_info(info) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_quote_ttl( &self, request: Request<UpdateQuoteTtlRequest>, ) -> Result<Response<UpdateResponse>, Status> { let current_ttl = self .mint .quote_ttl() .await .map_err(|err| Status::internal(err.to_string()))?; let request = request.into_inner(); let quote_ttl = QuoteTTL { mint_ttl: request.mint_ttl.unwrap_or(current_ttl.mint_ttl), melt_ttl: request.melt_ttl.unwrap_or(current_ttl.melt_ttl), }; self.mint .set_quote_ttl(quote_ttl) .await .map_err(|err| Status::internal(err.to_string()))?; Ok(Response::new(UpdateResponse {})) } async fn update_nut04_quote( &self, request: Request<UpdateNut04QuoteRequest>, ) -> Result<Response<UpdateNut04QuoteRequest>, Status> { let request = request.into_inner(); let quote_id = request .quote_id .parse() .map_err(|_| Status::invalid_argument("Invalid quote id".to_string()))?; let state = MintQuoteState::from_str(&request.state) .map_err(|_| Status::invalid_argument("Invalid quote state".to_string()))?; let mut tx = self.mint.localstore.begin_transaction().await?; let mint_quote = db .get_mint_quote("e_id) .await .map_err(|_| Status::invalid_argument("Could not find quote".to_string()))? .ok_or(Status::invalid_argument("Could not find quote".to_string()))?; match state { MintQuoteState::Paid => { self.mint .pay_mint_quote(&mut tx, &mint_quote) .await .map_err(|_| Status::internal("Could not find quote".to_string()))?; } _ => { tx.update_mint_quote_state(&mint_quote.id, state).await?; } } tx.commit().await?; Ok(Response::new(UpdateNut04QuoteRequest { state: state.to_string(), quote_id: mint_quote.id.to_string(), })) } async fn rotate_next_keyset( &self, request: Request<RotateNextKeysetRequest>, ) -> Result<Response<RotateNextKeysetResponse>, Status> { let request = request.into_inner(); let unit = CurrencyUnit::from_str(&request.unit) .map_err(|_| Status::invalid_argument("Invalid unit".to_string()))?; let keyset_info = self .mint .rotate_next_keyset( unit, request.max_order.map(|a| a as u8).unwrap_or(32), request.input_fee_ppk.unwrap_or(0), ) .await .map_err(|_| Status::invalid_argument("Could not rotate keyset".to_string()))?; Ok(Response::new(RotateNextKeysetResponse { id: keyset_info.id.to_string(), unit: keyset_info.unit.to_string(), max_order: keyset_info.max_order.into(), input_fee_ppk: keyset_info.input_fee_ppk, })) } }