diff --git a/flutter/native/Cargo.toml b/flutter/native/Cargo.toml index fed4676c..cadca9e4 100644 --- a/flutter/native/Cargo.toml +++ b/flutter/native/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["cdylib", "staticlib"] [dependencies] moksha-wallet = { version = "0.1.2", path = "../../moksha-wallet" } moksha-core = { ersion = "0.1.2", path = "../../moksha-core" } -moksha-fedimint = { ersion = "0.1.2", path = "../../moksha-fedimint" } +moksha-fedimint = { version = "0.1.2", path = "../../moksha-fedimint" } tracing = "0.1.40" anyhow = { version = "1.0.75", features = ["backtrace"] } diff --git a/integrationtests/tests/tests.rs b/integrationtests/tests/tests.rs index 37e9f5be..2b632f61 100644 --- a/integrationtests/tests/tests.rs +++ b/integrationtests/tests/tests.rs @@ -38,7 +38,7 @@ pub fn test_integration() -> anyhow::Result<()> { .with_fee(0.0, 0) .build(); - let result = mokshamint::run_server( + let result = mokshamint::server::run_server( mint.await.expect("Can not connect to lightning backend"), "127.0.0.1:8686".parse().expect("invalid address"), None, diff --git a/moksha-mint/src/bin/moksha-mint.rs b/moksha-mint/src/bin/moksha-mint.rs index 3e694290..7157f1c2 100644 --- a/moksha-mint/src/bin/moksha-mint.rs +++ b/moksha-mint/src/bin/moksha-mint.rs @@ -4,7 +4,7 @@ use mokshamint::{ AlbyLightningSettings, LightningType, LnbitsLightningSettings, LndLightningSettings, StrikeLightningSettings, }, - MintBuilder, + mint::MintBuilder, }; use std::{env, fmt, net::SocketAddr, path::PathBuf}; @@ -28,9 +28,6 @@ pub async fn main() -> anyhow::Result<()> { .unwrap_or_else(|_| "[::]:3338".to_string()) .parse()?; - // read optional env var - // MINT_API_PREFIX - let api_prefix = env::var("MINT_API_PREFIX").ok(); let ln_backend = get_env("MINT_LIGHTNING_BACKEND"); @@ -86,7 +83,7 @@ pub async fn main() -> anyhow::Result<()> { Err(_) => None, }; - mokshamint::run_server(mint?, host_port, serve_wallet_path, api_prefix).await + mokshamint::server::run_server(mint?, host_port, serve_wallet_path, api_prefix).await } #[derive(Debug, PartialEq)] diff --git a/moksha-mint/src/lib.rs b/moksha-mint/src/lib.rs index 995fb143..d3d17fea 100644 --- a/moksha-mint/src/lib.rs +++ b/moksha-mint/src/lib.rs @@ -1,395 +1,12 @@ -use std::collections::HashMap; -use std::convert::Infallible; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::Arc; - -use axum::extract::{Query, State}; -use axum::routing::{get_service, post}; -use axum::Router; -use axum::{routing::get, Json}; -use error::MokshaMintError; - -use hyper::http::{HeaderName, HeaderValue}; -use hyper::Method; -use info::{MintInfoResponse, MintInfoSettings, Parameter}; -use lightning::{AlbyLightning, Lightning, LightningType, LnbitsLightning, StrikeLightning}; -use mint::{LightningFeeConfig, Mint}; -use model::{GetMintQuery, PostMintQuery}; -use moksha_core::model::{ - CheckFeesRequest, CheckFeesResponse, Keysets, PaymentRequest, PostMeltRequest, - PostMeltResponse, PostMintRequest, PostMintResponse, PostSplitRequest, PostSplitResponse, -}; -use secp256k1::PublicKey; - -use tower_http::services::ServeDir; -use tower_http::set_header::SetResponseHeaderLayer; -use tower_http::{ - cors::{Any, CorsLayer}, - trace::TraceLayer, -}; -use tracing::{event, info, Level}; - -use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; - -mod database; -mod error; +// use std::collections::HashMap; +// use std::convert::Infallible; +// use std::net::SocketAddr; +// use std::path::PathBuf; +// use std::sync::Arc; +pub mod database; +pub mod error; pub mod info; pub mod lightning; pub mod mint; -mod model; - -#[derive(Debug, Default)] -pub struct MintBuilder { - private_key: Option, - lightning_type: Option, - db_path: Option, - fee_percent: Option, - fee_reserve_min: Option, - mint_info_settings: Option, -} - -impl MintBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn with_mint_info(mut self, mint_info: MintInfoSettings) -> MintBuilder { - self.mint_info_settings = Some(mint_info); - self - } - - pub fn with_private_key(mut self, private_key: String) -> MintBuilder { - self.private_key = Some(private_key); - self - } - - pub fn with_db(mut self, db_path: String) -> MintBuilder { - self.db_path = Some(db_path); - self - } - - pub fn with_lightning(mut self, lightning: LightningType) -> MintBuilder { - self.lightning_type = Some(lightning); - self - } - - pub fn with_fee(mut self, fee_percent: f32, fee_reserve_min: u64) -> MintBuilder { - self.fee_percent = Some(fee_percent); - self.fee_reserve_min = Some(fee_reserve_min); - self - } - - pub async fn build(self) -> Result { - let ln: Arc = match self.lightning_type.clone() { - Some(LightningType::Lnbits(lnbits_settings)) => Arc::new(LnbitsLightning::new( - lnbits_settings.admin_key.expect("LNBITS_ADMIN_KEY not set"), - lnbits_settings.url.expect("LNBITS_URL not set"), - )), - Some(LightningType::Alby(alby_settings)) => Arc::new(AlbyLightning::new( - alby_settings.api_key.expect("ALBY_API_KEY not set"), - )), - Some(LightningType::Strike(strike_settings)) => Arc::new(StrikeLightning::new( - strike_settings.api_key.expect("STRIKE_API_KEY not set"), - )), - Some(LightningType::Lnd(lnd_settings)) => Arc::new( - lightning::LndLightning::new( - lnd_settings.grpc_host.expect("LND_GRPC_HOST not set"), - &lnd_settings - .tls_cert_path - .expect("LND_TLS_CERT_PATH not set"), - &lnd_settings - .macaroon_path - .expect("LND_MACAROON_PATH not set"), - ) - .await?, - ), - None => panic!("Lightning backend not set"), - }; - - let db = Arc::new(database::RocksDB::new( - self.db_path.expect("MINT_DB_PATH not set"), - )); - - let fee_config = LightningFeeConfig::new( - self.fee_percent.expect("LIGHTNING_FEE_PERCENT not set"), - self.fee_reserve_min - .expect("LIGHTNING_RESERVE_FEE_MIN not set"), - ); - - Ok(Mint::new( - self.private_key.expect("MINT_PRIVATE_KEY not set"), - "".to_string(), - ln, - self.lightning_type.expect("Lightning backend not set"), - db, - fee_config, - self.mint_info_settings.unwrap_or_default(), - )) - } -} - -pub async fn run_server( - mint: Mint, - addr: SocketAddr, - serve_wallet_path: Option, - api_prefix: Option, -) -> anyhow::Result<()> { - tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .init(); - info!("listening on: {}", addr); - info!("mint_info: {:?}", mint.mint_info); - info!("lightning_backend: {}", mint.lightning_type); - if serve_wallet_path.is_some() { - info!( - "serving wallet from path: {:?}", - serve_wallet_path.clone().unwrap() - ); - } - - axum::Server::bind(&addr) - .serve( - app(mint, serve_wallet_path, api_prefix) - .layer( - CorsLayer::new() - .allow_origin(Any) - .allow_headers(Any) - .allow_methods([Method::GET, Method::POST]), - ) - .into_make_service(), - ) - .await?; - - Ok(()) -} - -fn app(mint: Mint, serve_wallet_path: Option, prefix: Option) -> Router { - let routes = Router::new() - .route("/keys", get(get_keys)) - .route("/keysets", get(get_keysets)) - .route("/mint", get(get_mint).post(post_mint)) - .route("/checkfees", post(post_check_fees)) - .route("/melt", post(post_melt)) - .route("/split", post(post_split)) - .route("/info", get(get_info)); - - let router = Router::new() - .nest(&prefix.unwrap_or_else(|| "".to_owned()), routes) - .with_state(mint) - .layer(TraceLayer::new_for_http()); - - if let Some(serve_wallet_path) = serve_wallet_path { - return router.nest_service( - "/", - get_service(ServeDir::new(serve_wallet_path)) - .layer::<_, _, Infallible>(SetResponseHeaderLayer::if_not_present( - HeaderName::from_static("cross-origin-embedder-policy"), - HeaderValue::from_static("require-corp"), - )) - .layer(SetResponseHeaderLayer::if_not_present( - HeaderName::from_static("cross-origin-opener-policy"), - HeaderValue::from_static("same-origin"), - )), - ); - } - router -} - -async fn post_split( - State(mint): State, - Json(split_request): Json, -) -> Result, MokshaMintError> { - let response = mint - .split( - split_request.amount, - &split_request.proofs, - &split_request.outputs, - ) - .await?; - - Ok(Json(response)) -} - -async fn post_melt( - State(mint): State, - Json(melt_request): Json, -) -> Result, MokshaMintError> { - let (paid, preimage, change) = mint - .melt(melt_request.pr, &melt_request.proofs, &melt_request.outputs) - .await?; - - Ok(Json(PostMeltResponse { - paid, - preimage, - change, - })) -} - -async fn post_check_fees( - State(mint): State, - Json(_check_fees): Json, -) -> Result, MokshaMintError> { - let invoice = mint.lightning.decode_invoice(_check_fees.pr).await?; - - Ok(Json(CheckFeesResponse { - fee: mint.fee_reserve( - invoice - .amount_milli_satoshis() - .ok_or_else(|| error::MokshaMintError::InvalidAmount)?, - ), - })) -} - -async fn get_info(State(mint): State) -> Result, MokshaMintError> { - let mint_info = MintInfoResponse { - name: mint.mint_info.name, - pubkey: mint.keyset.mint_pubkey, - version: match mint.mint_info.version { - true => Some(env!("CARGO_PKG_VERSION").to_owned()), - _ => None, - }, - description: mint.mint_info.description, - description_long: mint.mint_info.description_long, - contact: mint.mint_info.contact, - nuts: vec![ - "NUT-00".to_string(), - "NUT-01".to_string(), - "NUT-02".to_string(), - "NUT-03".to_string(), - "NUT-04".to_string(), - "NUT-05".to_string(), - "NUT-06".to_string(), - "NUT-09".to_string(), - ], - motd: mint.mint_info.motd, - parameter: Parameter { - peg_out_only: false, - }, - }; - Ok(Json(mint_info)) -} - -async fn get_mint( - State(mint): State, - Query(mint_query): Query, -) -> Result, MokshaMintError> { - let (pr, hash) = mint.create_invoice(mint_query.amount).await?; - Ok(Json(PaymentRequest { pr, hash })) -} - -async fn post_mint( - State(mint): State, - Query(mint_query): Query, - Json(blinded_messages): Json, -) -> Result, MokshaMintError> { - event!( - Level::INFO, - "post_mint: {mint_query:#?} {blinded_messages:#?}" - ); - - let promises = mint - .mint_tokens(mint_query.hash, &blinded_messages.outputs) - .await?; - Ok(Json(PostMintResponse { promises })) -} - -async fn get_keys( - State(mint): State, -) -> Result>, MokshaMintError> { - Ok(Json(mint.keyset.public_keys)) -} - -async fn get_keysets(State(mint): State) -> Result, MokshaMintError> { - Ok(Json(Keysets::new(vec![mint.keyset.keyset_id]))) -} - -#[cfg(test)] -mod tests { - use std::{collections::HashMap, sync::Arc}; - - use hyper::{Body, Request, StatusCode}; - use moksha_core::model::Keysets; - use secp256k1::PublicKey; - use tower::ServiceExt; - - use crate::{ - app, - database::MockDatabase, - info::{MintInfoResponse, MintInfoSettings}, - lightning::{LightningType, MockLightning}, - mint::{LightningFeeConfig, Mint}, - }; - - #[tokio::test] - async fn test_get_keys() -> anyhow::Result<()> { - let app = app(create_mock_mint(Default::default()), None, None); - let response = app - .oneshot(Request::builder().uri("/keys").body(Body::empty())?) - .await?; - - assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await?; - let keys: HashMap = serde_json::from_slice(&body)?; - assert_eq!(64, keys.len()); - Ok(()) - } - - #[tokio::test] - async fn test_get_keysets() -> anyhow::Result<()> { - let app = app(create_mock_mint(Default::default()), None, None); - let response = app - .oneshot(Request::builder().uri("/keysets").body(Body::empty())?) - .await?; - - assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await?; - let keysets = serde_json::from_slice::(&body)?; - assert_eq!(Keysets::new(vec!["53eJP2+qJyTd".to_string()]), keysets); - Ok(()) - } - - #[tokio::test] - async fn test_get_info() -> anyhow::Result<()> { - let mint_info_settings = MintInfoSettings { - name: Some("Bob's Cashu mint".to_string()), - version: true, - description: Some("A mint for testing".to_string()), - description_long: Some("A mint for testing long".to_string()), - ..Default::default() - }; - let app = app(create_mock_mint(mint_info_settings), None, None); - let response = app - .oneshot(Request::builder().uri("/info").body(Body::empty())?) - .await?; - - assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await?; - let info = serde_json::from_slice::(&body)?; - assert!(!info.parameter.peg_out_only); - assert_eq!(info.nuts.len(), 8); - assert_eq!(info.name, Some("Bob's Cashu mint".to_string())); - assert_eq!(info.description, Some("A mint for testing".to_string())); - assert_eq!( - info.description_long, - Some("A mint for testing long".to_string()) - ); - Ok(()) - } - - fn create_mock_mint(mint_info: MintInfoSettings) -> Mint { - let db = Arc::new(MockDatabase::new()); - let lightning = Arc::new(MockLightning::new()); - - Mint::new( - "mytestsecret".to_string(), - "".to_string(), - lightning, - LightningType::Lnbits(Default::default()), - db, - LightningFeeConfig::default(), - mint_info, - ) - } -} +pub mod model; +pub mod server; diff --git a/moksha-mint/src/lightning/alby.rs b/moksha-mint/src/lightning/alby.rs index d19fe7b4..312f040a 100644 --- a/moksha-mint/src/lightning/alby.rs +++ b/moksha-mint/src/lightning/alby.rs @@ -1,10 +1,8 @@ use hyper::{header::CONTENT_TYPE, http::HeaderValue}; +use tracing::info; use url::Url; -use crate::{ - info, - model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}, -}; +use crate::model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}; use super::error::LightningError; diff --git a/moksha-mint/src/mint.rs b/moksha-mint/src/mint.rs index 59c39dc3..0f818d4c 100644 --- a/moksha-mint/src/mint.rs +++ b/moksha-mint/src/mint.rs @@ -13,9 +13,8 @@ use crate::{ database::Database, error::MokshaMintError, info::MintInfoSettings, - lightning::{Lightning, LightningType}, + lightning::{AlbyLightning, Lightning, LightningType, LnbitsLightning, StrikeLightning}, model::Invoice, - MintBuilder, }; #[derive(Clone)] @@ -241,12 +240,103 @@ impl Mint { } } +#[derive(Debug, Default)] +pub struct MintBuilder { + private_key: Option, + lightning_type: Option, + db_path: Option, + fee_percent: Option, + fee_reserve_min: Option, + mint_info_settings: Option, +} + +impl MintBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn with_mint_info(mut self, mint_info: MintInfoSettings) -> MintBuilder { + self.mint_info_settings = Some(mint_info); + self + } + + pub fn with_private_key(mut self, private_key: String) -> MintBuilder { + self.private_key = Some(private_key); + self + } + + pub fn with_db(mut self, db_path: String) -> MintBuilder { + self.db_path = Some(db_path); + self + } + + pub fn with_lightning(mut self, lightning: LightningType) -> MintBuilder { + self.lightning_type = Some(lightning); + self + } + + pub fn with_fee(mut self, fee_percent: f32, fee_reserve_min: u64) -> MintBuilder { + self.fee_percent = Some(fee_percent); + self.fee_reserve_min = Some(fee_reserve_min); + self + } + + pub async fn build(self) -> Result { + let ln: Arc = match self.lightning_type.clone() { + Some(LightningType::Lnbits(lnbits_settings)) => Arc::new(LnbitsLightning::new( + lnbits_settings.admin_key.expect("LNBITS_ADMIN_KEY not set"), + lnbits_settings.url.expect("LNBITS_URL not set"), + )), + Some(LightningType::Alby(alby_settings)) => Arc::new(AlbyLightning::new( + alby_settings.api_key.expect("ALBY_API_KEY not set"), + )), + Some(LightningType::Strike(strike_settings)) => Arc::new(StrikeLightning::new( + strike_settings.api_key.expect("STRIKE_API_KEY not set"), + )), + Some(LightningType::Lnd(lnd_settings)) => Arc::new( + crate::lightning::LndLightning::new( + lnd_settings.grpc_host.expect("LND_GRPC_HOST not set"), + &lnd_settings + .tls_cert_path + .expect("LND_TLS_CERT_PATH not set"), + &lnd_settings + .macaroon_path + .expect("LND_MACAROON_PATH not set"), + ) + .await?, + ), + None => panic!("Lightning backend not set"), + }; + + let db = Arc::new(crate::database::RocksDB::new( + self.db_path.expect("MINT_DB_PATH not set"), + )); + + let fee_config = LightningFeeConfig::new( + self.fee_percent.expect("LIGHTNING_FEE_PERCENT not set"), + self.fee_reserve_min + .expect("LIGHTNING_RESERVE_FEE_MIN not set"), + ); + + Ok(Mint::new( + self.private_key.expect("MINT_PRIVATE_KEY not set"), + "".to_string(), + ln, + self.lightning_type.expect("Lightning backend not set"), + db, + fee_config, + self.mint_info_settings.unwrap_or_default(), + )) + } +} + #[cfg(test)] mod tests { use crate::lightning::error::LightningError; use crate::lightning::{LightningType, MockLightning}; + use crate::mint::Mint; use crate::model::{Invoice, PayInvoiceResult}; - use crate::{database::MockDatabase, error::MokshaMintError, Mint}; + use crate::{database::MockDatabase, error::MokshaMintError}; use moksha_core::dhke; use moksha_core::model::{BlindedMessage, TokenV3, TotalAmount}; use moksha_core::model::{PostSplitRequest, Proofs}; diff --git a/moksha-mint/src/server.rs b/moksha-mint/src/server.rs new file mode 100644 index 00000000..1d0b0146 --- /dev/null +++ b/moksha-mint/src/server.rs @@ -0,0 +1,297 @@ +use std::collections::HashMap; +use std::convert::Infallible; +use std::net::SocketAddr; +use std::path::PathBuf; + +use crate::error::MokshaMintError; +use axum::extract::{Query, State}; +use axum::routing::{get_service, post}; +use axum::Router; +use axum::{routing::get, Json}; + +use crate::info::{MintInfoResponse, Parameter}; + +use crate::mint::Mint; +use crate::model::{GetMintQuery, PostMintQuery}; +use hyper::http::{HeaderName, HeaderValue}; +use hyper::Method; +use moksha_core::model::{ + CheckFeesRequest, CheckFeesResponse, Keysets, PaymentRequest, PostMeltRequest, + PostMeltResponse, PostMintRequest, PostMintResponse, PostSplitRequest, PostSplitResponse, +}; +use secp256k1::PublicKey; + +use tower_http::services::ServeDir; +use tower_http::set_header::SetResponseHeaderLayer; +use tower_http::{ + cors::{Any, CorsLayer}, + trace::TraceLayer, +}; +use tracing::{event, info, Level}; + +use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +pub async fn run_server( + mint: Mint, + addr: SocketAddr, + serve_wallet_path: Option, + api_prefix: Option, +) -> anyhow::Result<()> { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .init(); + info!("listening on: {}", addr); + info!("mint_info: {:?}", mint.mint_info); + info!("lightning_backend: {}", mint.lightning_type); + if serve_wallet_path.is_some() { + info!( + "serving wallet from path: {:?}", + serve_wallet_path.clone().unwrap() + ); + } + + axum::Server::bind(&addr) + .serve( + app(mint, serve_wallet_path, api_prefix) + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_headers(Any) + .allow_methods([Method::GET, Method::POST]), + ) + .into_make_service(), + ) + .await?; + + Ok(()) +} + +fn app(mint: Mint, serve_wallet_path: Option, prefix: Option) -> Router { + let routes = Router::new() + .route("/keys", get(get_keys)) + .route("/keysets", get(get_keysets)) + .route("/mint", get(get_mint).post(post_mint)) + .route("/checkfees", post(post_check_fees)) + .route("/melt", post(post_melt)) + .route("/split", post(post_split)) + .route("/info", get(get_info)); + + let router = Router::new() + .nest(&prefix.unwrap_or_else(|| "".to_owned()), routes) + .with_state(mint) + .layer(TraceLayer::new_for_http()); + + if let Some(serve_wallet_path) = serve_wallet_path { + return router.nest_service( + "/", + get_service(ServeDir::new(serve_wallet_path)) + .layer::<_, _, Infallible>(SetResponseHeaderLayer::if_not_present( + HeaderName::from_static("cross-origin-embedder-policy"), + HeaderValue::from_static("require-corp"), + )) + .layer(SetResponseHeaderLayer::if_not_present( + HeaderName::from_static("cross-origin-opener-policy"), + HeaderValue::from_static("same-origin"), + )), + ); + } + router +} + +async fn post_split( + State(mint): State, + Json(split_request): Json, +) -> Result, MokshaMintError> { + let response = mint + .split( + split_request.amount, + &split_request.proofs, + &split_request.outputs, + ) + .await?; + + Ok(Json(response)) +} + +async fn post_melt( + State(mint): State, + Json(melt_request): Json, +) -> Result, MokshaMintError> { + let (paid, preimage, change) = mint + .melt(melt_request.pr, &melt_request.proofs, &melt_request.outputs) + .await?; + + Ok(Json(PostMeltResponse { + paid, + preimage, + change, + })) +} + +async fn post_check_fees( + State(mint): State, + Json(_check_fees): Json, +) -> Result, MokshaMintError> { + let invoice = mint.lightning.decode_invoice(_check_fees.pr).await?; + + Ok(Json(CheckFeesResponse { + fee: mint.fee_reserve( + invoice + .amount_milli_satoshis() + .ok_or_else(|| crate::error::MokshaMintError::InvalidAmount)?, + ), + })) +} + +async fn get_info(State(mint): State) -> Result, MokshaMintError> { + let mint_info = MintInfoResponse { + name: mint.mint_info.name, + pubkey: mint.keyset.mint_pubkey, + version: match mint.mint_info.version { + true => Some(env!("CARGO_PKG_VERSION").to_owned()), + _ => None, + }, + description: mint.mint_info.description, + description_long: mint.mint_info.description_long, + contact: mint.mint_info.contact, + nuts: vec![ + "NUT-00".to_string(), + "NUT-01".to_string(), + "NUT-02".to_string(), + "NUT-03".to_string(), + "NUT-04".to_string(), + "NUT-05".to_string(), + "NUT-06".to_string(), + "NUT-09".to_string(), + ], + motd: mint.mint_info.motd, + parameter: Parameter { + peg_out_only: false, + }, + }; + Ok(Json(mint_info)) +} + +async fn get_mint( + State(mint): State, + Query(mint_query): Query, +) -> Result, MokshaMintError> { + let (pr, hash) = mint.create_invoice(mint_query.amount).await?; + Ok(Json(PaymentRequest { pr, hash })) +} + +async fn post_mint( + State(mint): State, + Query(mint_query): Query, + Json(blinded_messages): Json, +) -> Result, MokshaMintError> { + event!( + Level::INFO, + "post_mint: {mint_query:#?} {blinded_messages:#?}" + ); + + let promises = mint + .mint_tokens(mint_query.hash, &blinded_messages.outputs) + .await?; + Ok(Json(PostMintResponse { promises })) +} + +async fn get_keys( + State(mint): State, +) -> Result>, MokshaMintError> { + Ok(Json(mint.keyset.public_keys)) +} + +async fn get_keysets(State(mint): State) -> Result, MokshaMintError> { + Ok(Json(Keysets::new(vec![mint.keyset.keyset_id]))) +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, sync::Arc}; + + use crate::server::app; + use hyper::{Body, Request, StatusCode}; + use moksha_core::model::Keysets; + use secp256k1::PublicKey; + use tower::ServiceExt; + + use crate::{ + database::MockDatabase, + info::{MintInfoResponse, MintInfoSettings}, + lightning::{LightningType, MockLightning}, + mint::{LightningFeeConfig, Mint}, + }; + + #[tokio::test] + async fn test_get_keys() -> anyhow::Result<()> { + let app = app(create_mock_mint(Default::default()), None, None); + let response = app + .oneshot(Request::builder().uri("/keys").body(Body::empty())?) + .await?; + + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await?; + let keys: HashMap = serde_json::from_slice(&body)?; + assert_eq!(64, keys.len()); + Ok(()) + } + + #[tokio::test] + async fn test_get_keysets() -> anyhow::Result<()> { + let app = app(create_mock_mint(Default::default()), None, None); + let response = app + .oneshot(Request::builder().uri("/keysets").body(Body::empty())?) + .await?; + + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await?; + let keysets = serde_json::from_slice::(&body)?; + assert_eq!(Keysets::new(vec!["53eJP2+qJyTd".to_string()]), keysets); + Ok(()) + } + + #[tokio::test] + async fn test_get_info() -> anyhow::Result<()> { + let mint_info_settings = MintInfoSettings { + name: Some("Bob's Cashu mint".to_string()), + version: true, + description: Some("A mint for testing".to_string()), + description_long: Some("A mint for testing long".to_string()), + ..Default::default() + }; + let app = app(create_mock_mint(mint_info_settings), None, None); + let response = app + .oneshot(Request::builder().uri("/info").body(Body::empty())?) + .await?; + + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await?; + let info = serde_json::from_slice::(&body)?; + assert!(!info.parameter.peg_out_only); + assert_eq!(info.nuts.len(), 8); + assert_eq!(info.name, Some("Bob's Cashu mint".to_string())); + assert_eq!(info.description, Some("A mint for testing".to_string())); + assert_eq!( + info.description_long, + Some("A mint for testing long".to_string()) + ); + Ok(()) + } + + fn create_mock_mint(mint_info: MintInfoSettings) -> Mint { + let db = Arc::new(MockDatabase::new()); + let lightning = Arc::new(MockLightning::new()); + + Mint::new( + "mytestsecret".to_string(), + "".to_string(), + lightning, + LightningType::Lnbits(Default::default()), + db, + LightningFeeConfig::default(), + mint_info, + ) + } +}