diff --git a/CHANGELOG.md b/CHANGELOG.md index 7726fd85..c03aa5be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ - cashu: NUT-19 support in the wallet ([crodas]). - cdk: SIG_ALL support for swap and melt operations ([thesimplekid]). - cdk-sql-common: Add cache to SQL statements for better performance ([crodas]). +- cdk-integration-tests: New binary `start_fake_auth_mint` for testing fake mint with authentication ([thesimplekid]). +- cdk-integration-tests: New binary `start_fake_mint` for testing fake mint instances ([thesimplekid]). +- cdk-integration-tests: New binary `start_regtest_mints` for testing regtest mints ([thesimplekid]). +- cdk-integration-tests: Shared utilities module for common integration test functionality ([thesimplekid]). ### Changed - cdk: Refactored wallet keyset management methods for better clarity and separation of concerns ([thesimplekid]). @@ -28,6 +32,12 @@ - cdk-integration-tests: Updated test utilities to use new mint lifecycle management ([thesimplekid]). - cdk-sqlite: Introduce `cdk-sql-common` crate for shared SQL storage codebase ([crodas]). - cdk-sqlite: Rename `still_active` to `stale` for better clarity ([crodas]). +- cdk-integration-tests: Refactored regtest setup to use Rust binaries instead of shell scripts ([thesimplekid]). +- cdk-integration-tests: Improved environment variable handling for test configurations ([thesimplekid]). +- cdk-integration-tests: Enhanced CLN client connection with retry logic ([thesimplekid]). +- cdk-integration-tests: Updated integration tests to use proper temp directory management ([thesimplekid]). +- cdk-integration-tests: Simplified regtest shell scripts to use new binaries ([thesimplekid]). +- crates/cdk-mintd: Moved mintd library functions to separate module for better organization and testability ([thesimplekid]). ### Fixed - cashu: Fixed CurrencyUnit custom units preserving original case instead of being converted to uppercase ([thesimplekid]). diff --git a/Cargo.toml b/Cargo.toml index 527ee25a..08fc3c4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0. cdk-sql-common = { path = "./crates/cdk-sql-common", default-features = true, version = "=0.11.0" } cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" } cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.11.0", default-features = false } +cdk-mintd = { path = "./crates/cdk-mintd", version = "=0.11.0", default-features = false } clap = { version = "4.5.31", features = ["derive"] } ciborium = { version = "0.2.2", default-features = false, features = ["std"] } cbor-diag = "0.1.12" diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index 26a9da98..ceaf6838 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -23,10 +23,12 @@ cashu = { workspace = true, features = ["mint", "wallet"] } cdk = { workspace = true, features = ["mint", "wallet", "auth"] } cdk-cln = { workspace = true } cdk-lnd = { workspace = true } -cdk-axum = { workspace = true } +cdk-axum = { workspace = true, features = ["auth"] } cdk-sqlite = { workspace = true } cdk-redb = { workspace = true } cdk-fake-wallet = { workspace = true } +cdk-common = { workspace = true, features = ["mint", "wallet", "auth"] } +cdk-mintd = { workspace = true, features = ["cln", "lnd", "fakewallet", "grpc-processor", "auth", "lnbits", "management-rpc"] } futures = { workspace = true, default-features = false, features = [ "executor", ] } @@ -44,6 +46,7 @@ tower-http = { workspace = true, features = ["cors"] } tower-service = "0.3.3" reqwest.workspace = true bitcoin = "0.32.0" +clap = { workspace = true, features = ["derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio.workspace = true diff --git a/crates/cdk-integration-tests/src/bin/start_fake_auth_mint.rs b/crates/cdk-integration-tests/src/bin/start_fake_auth_mint.rs new file mode 100644 index 00000000..4cb3ad9d --- /dev/null +++ b/crates/cdk-integration-tests/src/bin/start_fake_auth_mint.rs @@ -0,0 +1,163 @@ +//! Binary for starting a fake mint with authentication for testing +//! +//! This binary provides a programmatic way to start a fake mint instance with authentication for testing purposes: +//! 1. Sets up a fake mint instance with authentication using the cdk-mintd library +//! 2. Configures OpenID Connect authentication settings +//! 3. Waits for the mint to be ready and responsive +//! 4. Keeps it running until interrupted (Ctrl+C) +//! 5. Gracefully shuts down on receiving shutdown signal +//! +//! This approach offers better control and integration compared to external scripts, +//! making it easier to run authentication integration tests with consistent configuration. + +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use bip39::Mnemonic; +use cdk_integration_tests::cli::CommonArgs; +use cdk_integration_tests::shared; +use clap::Parser; +use tokio::sync::Notify; + +#[derive(Parser)] +#[command(name = "start-fake-auth-mint")] +#[command(about = "Start a fake mint with authentication for testing", long_about = None)] +struct Args { + #[command(flatten)] + common: CommonArgs, + + /// Database type (sqlite) + database_type: String, + + /// Working directory path + work_dir: String, + + /// OpenID discovery URL + openid_discovery: String, + + /// Port to listen on (default: 8087) + #[arg(default_value_t = 8087)] + port: u16, +} + +/// Start a fake mint with authentication using the library +async fn start_fake_auth_mint( + temp_dir: &Path, + port: u16, + openid_discovery: String, + shutdown: Arc, +) -> Result> { + println!("Starting fake auth mintd on port {port}"); + + // Create settings struct for fake mint with auth using shared function + let fake_wallet_config = cdk_mintd::config::FakeWallet { + supported_units: vec![cdk::nuts::CurrencyUnit::Sat, cdk::nuts::CurrencyUnit::Usd], + fee_percent: 0.0, + reserve_fee_min: cdk::Amount::from(1), + min_delay_time: 1, + max_delay_time: 3, + }; + + let mut settings = shared::create_fake_wallet_settings( + port, + Some(Mnemonic::generate(12)?.to_string()), + None, + Some(fake_wallet_config), + ); + + // Enable authentication + settings.auth = Some(cdk_mintd::config::Auth { + openid_discovery, + openid_client_id: "cashu-client".to_string(), + mint_max_bat: 50, + enabled_mint: true, + enabled_melt: true, + enabled_swap: true, + enabled_check_mint_quote: true, + enabled_check_melt_quote: true, + enabled_restore: true, + enabled_check_proof_state: true, + }); + + // Set description for the mint + settings.mint_info.description = "fake test mint with auth".to_string(); + + let temp_dir = temp_dir.to_path_buf(); + let shutdown_clone = shutdown.clone(); + + // Run the mint in a separate task + let handle = tokio::spawn(async move { + // Create a future that resolves when the shutdown signal is received + let shutdown_future = async move { + shutdown_clone.notified().await; + println!("Fake auth mint shutdown signal received"); + }; + + match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None).await + { + Ok(_) => println!("Fake auth mint exited normally"), + Err(e) => eprintln!("Fake auth mint exited with error: {e}"), + } + }); + + Ok(handle) +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + // Initialize logging based on CLI arguments + shared::setup_logging(&args.common); + + let temp_dir = shared::init_working_directory(&args.work_dir)?; + + // Start fake auth mint + let shutdown = shared::create_shutdown_handler(); + let shutdown_clone = shutdown.clone(); + + let handle = start_fake_auth_mint( + &temp_dir, + args.port, + args.openid_discovery.clone(), + shutdown_clone, + ) + .await?; + + // Wait for fake auth mint to be ready + if let Err(e) = shared::wait_for_mint_ready(args.port, 100).await { + eprintln!("Error waiting for fake auth mint: {e}"); + return Err(e); + } + + println!("Fake auth mint started successfully!"); + println!("Fake auth mint: http://127.0.0.1:{}", args.port); + println!("Temp directory: {temp_dir:?}"); + println!("Database type: {}", args.database_type); + println!("OpenID Discovery: {}", args.openid_discovery); + println!(); + println!("Environment variables needed for tests:"); + println!(" CDK_TEST_OIDC_USER="); + println!(" CDK_TEST_OIDC_PASSWORD="); + println!(); + println!("You can now run auth integration tests with:"); + println!(" cargo test -p cdk-integration-tests --test fake_auth"); + println!(); + + println!("Press Ctrl+C to stop the mint..."); + + // Wait for Ctrl+C signal + shared::wait_for_shutdown_signal(shutdown).await; + + println!("\nReceived Ctrl+C, shutting down mint..."); + + // Wait for mint to finish gracefully + if let Err(e) = handle.await { + eprintln!("Error waiting for mint to shut down: {e}"); + } + + println!("Mint shut down successfully"); + + Ok(()) +} diff --git a/crates/cdk-integration-tests/src/bin/start_fake_mint.rs b/crates/cdk-integration-tests/src/bin/start_fake_mint.rs new file mode 100644 index 00000000..2d1d490b --- /dev/null +++ b/crates/cdk-integration-tests/src/bin/start_fake_mint.rs @@ -0,0 +1,178 @@ +//! Binary for starting a fake mint for testing +//! +//! This binary provides a programmatic way to start a fake mint instance for testing purposes: +//! 1. Sets up a fake mint instance using the cdk-mintd library +//! 2. Configures the mint with fake wallet backend for testing Lightning Network interactions +//! 3. Waits for the mint to be ready and responsive +//! 4. Keeps it running until interrupted (Ctrl+C) +//! 5. Gracefully shuts down on receiving shutdown signal +//! +//! This approach offers better control and integration compared to external scripts, +//! making it easier to run integration tests with consistent configuration. + +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use cdk::nuts::CurrencyUnit; +use cdk_integration_tests::cli::CommonArgs; +use cdk_integration_tests::shared; +use clap::Parser; +use tokio::sync::Notify; + +#[derive(Parser)] +#[command(name = "start-fake-mint")] +#[command(about = "Start a fake mint for testing", long_about = None)] +struct Args { + #[command(flatten)] + common: CommonArgs, + + /// Database type (sqlite) + database_type: String, + + /// Working directory path + work_dir: String, + + /// Port to listen on (default: 8086) + #[arg(default_value_t = 8086)] + port: u16, + + /// Use external signatory + #[arg(long, default_value_t = false)] + external_signatory: bool, +} + +/// Start a fake mint using the library +async fn start_fake_mint( + temp_dir: &Path, + port: u16, + shutdown: Arc, + external_signatory: bool, +) -> Result> { + let signatory_config = if external_signatory { + println!("Configuring external signatory"); + Some(( + "https://127.0.0.1:15060".to_string(), // Default signatory URL + temp_dir.to_string_lossy().to_string(), // Certs directory as string + )) + } else { + None + }; + + let mnemonic = if external_signatory { + None + } else { + Some( + "eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal" + .to_string(), + ) + }; + + let fake_wallet_config = Some(cdk_mintd::config::FakeWallet { + supported_units: vec![CurrencyUnit::Sat, CurrencyUnit::Usd], + fee_percent: 0.0, + reserve_fee_min: 1.into(), + min_delay_time: 1, + max_delay_time: 3, + }); + + // Create settings struct for fake mint using shared function + let settings = + shared::create_fake_wallet_settings(port, mnemonic, signatory_config, fake_wallet_config); + + println!("Starting fake mintd on port {port}"); + + let temp_dir = temp_dir.to_path_buf(); + let shutdown_clone = shutdown.clone(); + + // Run the mint in a separate task + let handle = tokio::spawn(async move { + // Create a future that resolves when the shutdown signal is received + let shutdown_future = async move { + shutdown_clone.notified().await; + println!("Fake mint shutdown signal received"); + }; + + match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None).await + { + Ok(_) => println!("Fake mint exited normally"), + Err(e) => eprintln!("Fake mint exited with error: {e}"), + } + }); + + Ok(handle) +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + // Initialize logging based on CLI arguments + shared::setup_logging(&args.common); + + let temp_dir = shared::init_working_directory(&args.work_dir)?; + + // Write environment variables to a .env file in the temp_dir BEFORE starting the mint + let mint_url = format!("http://127.0.0.1:{}", args.port); + let itests_dir = temp_dir.display().to_string(); + let env_vars: Vec<(&str, &str)> = vec![ + ("CDK_TEST_MINT_URL", &mint_url), + ("CDK_ITESTS_DIR", &itests_dir), + ]; + + shared::write_env_file(&temp_dir, &env_vars)?; + + // Start fake mint + let shutdown = shared::create_shutdown_handler(); + let shutdown_clone = shutdown.clone(); + + let handle = start_fake_mint( + &temp_dir, + args.port, + shutdown_clone, + args.external_signatory, + ) + .await?; + + // Wait for fake mint to be ready + if let Err(e) = shared::wait_for_mint_ready(args.port, 100).await { + eprintln!("Error waiting for fake mint: {e}"); + return Err(e); + } + + shared::display_mint_info(args.port, &temp_dir, &args.database_type); + + println!(); + println!( + "Environment variables written to: {}/.env", + temp_dir.display() + ); + println!("You can source these variables with:"); + println!(" source {}/.env", temp_dir.display()); + println!(); + println!("Environment variables set:"); + println!(" CDK_TEST_MINT_URL=http://127.0.0.1:{}", args.port); + println!(" CDK_ITESTS_DIR={}", temp_dir.display()); + println!(); + println!("You can now run integration tests with:"); + println!(" cargo test -p cdk-integration-tests --test fake_wallet"); + println!(" cargo test -p cdk-integration-tests --test happy_path_mint_wallet"); + println!(" etc."); + println!(); + + println!("Press Ctrl+C to stop the mint..."); + + // Wait for Ctrl+C signal + shared::wait_for_shutdown_signal(shutdown).await; + + println!("\nReceived Ctrl+C, shutting down mint..."); + + // Wait for mint to finish gracefully + if let Err(e) = handle.await { + eprintln!("Error waiting for mint to shut down: {e}"); + } + + println!("Mint shut down successfully"); + + Ok(()) +} diff --git a/crates/cdk-integration-tests/src/bin/start_regtest.rs b/crates/cdk-integration-tests/src/bin/start_regtest.rs index e63a8cdc..cec63d88 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest.rs @@ -1,20 +1,33 @@ use std::fs::OpenOptions; use std::io::Write; +use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anyhow::{bail, Result}; -use cdk_integration_tests::init_regtest::{get_temp_dir, start_regtest_end}; +use anyhow::Result; +use cdk_integration_tests::cli::{init_logging, CommonArgs}; +use cdk_integration_tests::init_regtest::start_regtest_end; +use clap::Parser; use tokio::signal; use tokio::sync::{oneshot, Notify}; use tokio::time::timeout; -use tracing_subscriber::EnvFilter; -fn signal_progress() { - let temp_dir = get_temp_dir(); +#[derive(Parser)] +#[command(name = "start-regtest")] +#[command(about = "Start regtest environment", long_about = None)] +struct Args { + #[command(flatten)] + common: CommonArgs, + + /// Working directory path + work_dir: String, +} + +fn signal_progress(work_dir: &Path) { let mut pipe = OpenOptions::new() .write(true) - .open(temp_dir.join("progress_pipe")) + .open(work_dir.join("progress_pipe")) .expect("Failed to open pipe"); pipe.write_all(b"checkpoint1\n") @@ -23,24 +36,21 @@ fn signal_progress() { #[tokio::main] async fn main() -> Result<()> { - let default_filter = "debug"; + let args = Args::parse(); - let sqlx_filter = "sqlx=warn"; - let hyper_filter = "hyper=warn"; - let h2_filter = "h2=warn"; - let rustls_filter = "rustls=warn"; + // Initialize logging based on CLI arguments + init_logging(args.common.enable_logging, args.common.log_level); - let env_filter = EnvFilter::new(format!( - "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter}" - )); - - tracing_subscriber::fmt().with_env_filter(env_filter).init(); + let temp_dir = PathBuf::from_str(&args.work_dir)?; + let temp_dir_clone = temp_dir.clone(); let shutdown_regtest = Arc::new(Notify::new()); - let shutdown_clone = shutdown_regtest.clone(); + let shutdown_clone = Arc::clone(&shutdown_regtest); + let shutdown_clone_two = Arc::clone(&shutdown_regtest); + let (tx, rx) = oneshot::channel(); tokio::spawn(async move { - start_regtest_end(tx, shutdown_clone) + start_regtest_end(&temp_dir_clone, tx, shutdown_clone) .await .expect("Error starting regtest"); }); @@ -48,15 +58,25 @@ async fn main() -> Result<()> { match timeout(Duration::from_secs(300), rx).await { Ok(_) => { tracing::info!("Regtest set up"); - signal_progress(); + signal_progress(&temp_dir); } Err(_) => { tracing::error!("regtest setup timed out after 5 minutes"); - bail!("Could not set up regtest"); + anyhow::bail!("Could not set up regtest"); } } - signal::ctrl_c().await?; + let shutdown_future = async { + // Wait for Ctrl+C signal + signal::ctrl_c() + .await + .expect("failed to install CTRL+C handler"); + tracing::info!("Shutdown signal received"); + println!("\nReceived Ctrl+C, shutting down mints..."); + shutdown_clone_two.notify_waiters(); + }; + + shutdown_future.await; Ok(()) } diff --git a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs new file mode 100644 index 00000000..ccef911b --- /dev/null +++ b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs @@ -0,0 +1,305 @@ +//! Binary for starting regtest mints +//! +//! This binary provides a programmatic way to start regtest mints for testing purposes: +//! 1. Sets up a regtest environment with CLN and LND nodes +//! 2. Starts CLN and LND mint instances using the cdk-mintd library +//! 3. Configures the mints to connect to the respective Lightning Network backends +//! 4. Waits for both mints to be ready and responsive +//! 5. Keeps them running until interrupted (Ctrl+C) +//! 6. Gracefully shuts down all services on receiving shutdown signal +//! +//! This approach offers better control and integration compared to external scripts, +//! making it easier to run integration tests with consistent configuration. + +use std::fs; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use cdk_integration_tests::cli::CommonArgs; +use cdk_integration_tests::init_regtest::start_regtest_end; +use cdk_integration_tests::shared; +use clap::Parser; +use tokio::signal::unix::SignalKind; +use tokio::signal::{self}; +use tokio::sync::{oneshot, Notify}; +use tokio::time::timeout; + +#[derive(Parser)] +#[command(name = "start-regtest-mints")] +#[command(about = "Start regtest mints", long_about = None)] +struct Args { + #[command(flatten)] + common: CommonArgs, + + /// Database type (sqlite) + database_type: String, + + /// Working directory path + work_dir: String, + + /// Mint address (default: 127.0.0.1) + #[arg(default_value = "127.0.0.1")] + mint_addr: String, + + /// CLN port (default: 8085) + #[arg(default_value_t = 8085)] + cln_port: u16, + + /// LND port (default: 8087) + #[arg(default_value_t = 8087)] + lnd_port: u16, +} + +/// Start regtest CLN mint using the library +async fn start_cln_mint( + temp_dir: &Path, + port: u16, + shutdown: Arc, +) -> Result> { + let cln_rpc_path = temp_dir + .join("cln") + .join("one") + .join("regtest") + .join("lightning-rpc"); + + let cln_config = cdk_mintd::config::Cln { + rpc_path: cln_rpc_path, + bolt12: false, + fee_percent: 0.0, + reserve_fee_min: 0.into(), + }; + + // Create settings struct for CLN mint using shared function + let settings = shared::create_cln_settings( + port, + temp_dir + .join("cln") + .join("one") + .join("regtest") + .join("lightning-rpc"), + "eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal".to_string(), + cln_config, + ); + + println!("Starting CLN mintd on port {port}"); + + let temp_dir = temp_dir.to_path_buf(); + let shutdown_clone = shutdown.clone(); + + // Run the mint in a separate task + let handle = tokio::spawn(async move { + // Create a future that resolves when the shutdown signal is received + let shutdown_future = async move { + shutdown_clone.notified().await; + println!("CLN mint shutdown signal received"); + }; + + match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None).await + { + Ok(_) => println!("CLN mint exited normally"), + Err(e) => eprintln!("CLN mint exited with error: {e}"), + } + }); + + Ok(handle) +} + +/// Start regtest LND mint using the library +async fn start_lnd_mint( + temp_dir: &Path, + port: u16, + shutdown: Arc, +) -> Result> { + let lnd_cert_file = temp_dir.join("lnd").join("two").join("tls.cert"); + let lnd_macaroon_file = temp_dir + .join("lnd") + .join("two") + .join("data") + .join("chain") + .join("bitcoin") + .join("regtest") + .join("admin.macaroon"); + let lnd_work_dir = temp_dir.join("lnd_mint"); + + // Create work directory for LND mint + fs::create_dir_all(&lnd_work_dir)?; + + let lnd_config = cdk_mintd::config::Lnd { + address: "https://localhost:10010".to_string(), + cert_file: lnd_cert_file, + macaroon_file: lnd_macaroon_file, + fee_percent: 0.0, + reserve_fee_min: 0.into(), + }; + + // Create settings struct for LND mint using shared function + let settings = shared::create_lnd_settings( + port, + lnd_config, + "cattle gold bind busy sound reduce tone addict baby spend february strategy".to_string(), + ); + + println!("Starting LND mintd on port {port}"); + + let lnd_work_dir = lnd_work_dir.clone(); + let shutdown_clone = shutdown.clone(); + + // Run the mint in a separate task + let handle = tokio::spawn(async move { + // Create a future that resolves when the shutdown signal is received + let shutdown_future = async move { + shutdown_clone.notified().await; + println!("LND mint shutdown signal received"); + }; + + match cdk_mintd::run_mintd_with_shutdown(&lnd_work_dir, &settings, shutdown_future, None) + .await + { + Ok(_) => println!("LND mint exited normally"), + Err(e) => eprintln!("LND mint exited with error: {e}"), + } + }); + + Ok(handle) +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + // Initialize logging based on CLI arguments + shared::setup_logging(&args.common); + + let temp_dir = shared::init_working_directory(&args.work_dir)?; + + // Write environment variables to a .env file in the temp_dir + let mint_url_1 = format!("http://{}:{}", args.mint_addr, args.cln_port); + let mint_url_2 = format!("http://{}:{}", args.mint_addr, args.lnd_port); + let env_vars: Vec<(&str, &str)> = vec![ + ("CDK_TEST_MINT_URL", &mint_url_1), + ("CDK_TEST_MINT_URL_2", &mint_url_2), + ]; + + shared::write_env_file(&temp_dir, &env_vars)?; + + // Start regtest + println!("Starting regtest..."); + + let shutdown_regtest = shared::create_shutdown_handler(); + let shutdown_clone = shutdown_regtest.clone(); + let (tx, rx) = oneshot::channel(); + + let shutdown_clone_one = Arc::clone(&shutdown_clone); + + let temp_dir_clone = temp_dir.clone(); + tokio::spawn(async move { + start_regtest_end(&temp_dir_clone, tx, shutdown_clone_one) + .await + .expect("Error starting regtest"); + }); + + match timeout(Duration::from_secs(300), rx).await { + Ok(_) => { + tracing::info!("Regtest set up"); + } + Err(_) => { + tracing::error!("regtest setup timed out after 5 minutes"); + anyhow::bail!("Could not set up regtest"); + } + } + + // Start CLN mint + let cln_handle = start_cln_mint(&temp_dir, args.cln_port, shutdown_clone.clone()).await?; + + // Wait for CLN mint to be ready + if let Err(e) = shared::wait_for_mint_ready(args.cln_port, 100).await { + eprintln!("Error waiting for CLN mint: {e}"); + return Err(e); + } + + // Start LND mint + let lnd_handle = start_lnd_mint(&temp_dir, args.lnd_port, shutdown_clone.clone()).await?; + + // Wait for LND mint to be ready + if let Err(e) = shared::wait_for_mint_ready(args.lnd_port, 100).await { + eprintln!("Error waiting for LND mint: {e}"); + return Err(e); + } + + println!("All regtest mints started successfully!"); + println!("CLN mint: http://{}:{}", args.mint_addr, args.cln_port); + println!("LND mint: http://{}:{}", args.mint_addr, args.lnd_port); + shared::display_mint_info(args.cln_port, &temp_dir, &args.database_type); // Using CLN port for display + println!(); + println!("Environment variables set:"); + println!( + " CDK_TEST_MINT_URL=http://{}:{}", + args.mint_addr, args.cln_port + ); + println!( + " CDK_TEST_MINT_URL_2=http://{}:{}", + args.mint_addr, args.lnd_port + ); + println!(" CDK_ITESTS_DIR={}", temp_dir.display()); + println!(); + println!("You can now run integration tests with:"); + println!(" cargo test -p cdk-integration-tests --test regtest"); + println!(" cargo test -p cdk-integration-tests --test happy_path_mint_wallet"); + println!(" etc."); + println!(); + + println!("Press Ctrl+C to stop the mints..."); + + // Create a future to wait for either Ctrl+C signal or unexpected mint termination + let shutdown_future = async { + // Wait for either SIGINT (Ctrl+C) or SIGTERM + let mut sigterm = signal::unix::signal(SignalKind::terminate()) + .expect("Failed to create SIGTERM signal handler"); + tokio::select! { + _ = signal::ctrl_c() => { + tracing::info!("Received SIGINT (Ctrl+C), shutting down mints..."); + } + _ = sigterm.recv() => { + tracing::info!("Received SIGTERM, shutting down mints..."); + } + } + println!("\nShutdown signal received, shutting down mints..."); + shutdown_clone.notify_waiters(); + }; + + // Monitor mint handles for unexpected termination + let monitor_mints = async { + loop { + if cln_handle.is_finished() { + println!("CLN mint finished unexpectedly"); + return; + } + if lnd_handle.is_finished() { + println!("LND mint finished unexpectedly"); + return; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }; + + // Wait for either shutdown signal or mint termination + tokio::select! { + _ = shutdown_future => { + println!("Shutdown signal received, waiting for mints to stop..."); + } + _ = monitor_mints => { + println!("One or more mints terminated unexpectedly"); + } + } + + // Wait for mints to finish gracefully + if let Err(e) = tokio::try_join!(cln_handle, lnd_handle) { + eprintln!("Error waiting for mints to shut down: {e}"); + } + + println!("All services shut down successfully"); + + Ok(()) +} diff --git a/crates/cdk-integration-tests/src/cli.rs b/crates/cdk-integration-tests/src/cli.rs new file mode 100644 index 00000000..f803da75 --- /dev/null +++ b/crates/cdk-integration-tests/src/cli.rs @@ -0,0 +1,42 @@ +//! Common CLI and logging utilities for CDK integration test binaries +//! +//! This module provides standardized CLI argument parsing and logging setup +//! for integration test binaries. + +use clap::Parser; +use tracing_subscriber::EnvFilter; + +/// Common CLI arguments for CDK integration test binaries +#[derive(Parser, Debug)] +pub struct CommonArgs { + /// Enable logging (default is false) + #[arg(long, default_value_t = false)] + pub enable_logging: bool, + + /// Logging level when enabled (default is debug) + #[arg(long, default_value = "debug")] + pub log_level: tracing::Level, +} + +/// Initialize logging based on CLI arguments +pub fn init_logging(enable_logging: bool, log_level: tracing::Level) { + if enable_logging { + let default_filter = log_level.to_string(); + + // Common filters to reduce noise + let sqlx_filter = "sqlx=warn"; + let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; + let rustls_filter = "rustls=warn"; + let reqwest_filter = "reqwest=warn"; + + let env_filter = EnvFilter::new(format!( + "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter},{reqwest_filter}" + )); + + // Ok if successful, Err if already initialized + let _ = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .try_init(); + } +} diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index 853dddb1..e3d86b83 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -211,10 +211,10 @@ impl MintConnector for DirectMintConnection { pub fn setup_tracing() { let default_filter = "debug"; - let sqlx_filter = "sqlx=warn"; + let h2_filter = "h2=warn"; let hyper_filter = "hyper=warn"; - let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter},{hyper_filter}")); + let env_filter = EnvFilter::new(format!("{default_filter},{h2_filter},{hyper_filter}")); // Ok if successful, Err if already initialized // Allows us to setup tracing at the start of several parallel tests diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index fa021f8e..538bd869 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -31,21 +31,41 @@ pub const LND_TWO_RPC_ADDR: &str = "localhost:10010"; pub const CLN_ADDR: &str = "127.0.0.1:19846"; pub const CLN_TWO_ADDR: &str = "127.0.0.1:19847"; -pub fn get_mint_addr() -> String { - env::var("CDK_ITESTS_MINT_ADDR").expect("Mint address not set") +/// Configuration for regtest environment +pub struct RegtestConfig { + pub mint_addr: String, + pub cln_port: u16, + pub lnd_port: u16, + pub temp_dir: PathBuf, } -pub fn get_mint_port(which: &str) -> u16 { - let dir = env::var(format!("CDK_ITESTS_MINT_PORT_{which}")).expect("Mint port not set"); - dir.parse().unwrap() +impl Default for RegtestConfig { + fn default() -> Self { + Self { + mint_addr: "127.0.0.1".to_string(), + cln_port: 8085, + lnd_port: 8087, + temp_dir: std::env::temp_dir().join("cdk-itests-default"), + } + } } -pub fn get_mint_url(which: &str) -> String { - format!("http://{}:{}", get_mint_addr(), get_mint_port(which)) +pub fn get_mint_url_with_config(config: &RegtestConfig, which: &str) -> String { + let port = match which { + "0" => config.cln_port, + "1" => config.lnd_port, + _ => panic!("Unknown mint identifier: {which}"), + }; + format!("http://{}:{}", config.mint_addr, port) } -pub fn get_mint_ws_url(which: &str) -> String { - format!("ws://{}:{}/v1/ws", get_mint_addr(), get_mint_port(which)) +pub fn get_mint_ws_url_with_config(config: &RegtestConfig, which: &str) -> String { + let port = match which { + "0" => config.cln_port, + "1" => config.lnd_port, + _ => panic!("Unknown mint identifier: {which}"), + }; + format!("ws://{}:{}/v1/ws", config.mint_addr, port) } pub fn get_temp_dir() -> PathBuf { @@ -54,15 +74,19 @@ pub fn get_temp_dir() -> PathBuf { dir.parse().expect("Valid path buf") } -pub fn get_bitcoin_dir() -> PathBuf { - let dir = get_temp_dir().join(BITCOIN_DIR); +pub fn get_temp_dir_with_config(config: &RegtestConfig) -> &PathBuf { + &config.temp_dir +} + +pub fn get_bitcoin_dir(temp_dir: &Path) -> PathBuf { + let dir = temp_dir.join(BITCOIN_DIR); std::fs::create_dir_all(&dir).unwrap(); dir } -pub fn init_bitcoind() -> Bitcoind { +pub fn init_bitcoind(work_dir: &Path) -> Bitcoind { Bitcoind::new( - get_bitcoin_dir(), + get_bitcoin_dir(work_dir), BITCOIND_ADDR.parse().unwrap(), BITCOIN_RPC_USER.to_string(), BITCOIN_RPC_PASS.to_string(), @@ -81,14 +105,14 @@ pub fn init_bitcoin_client() -> Result { ) } -pub fn get_cln_dir(name: &str) -> PathBuf { - let dir = get_temp_dir().join("cln").join(name); +pub fn get_cln_dir(work_dir: &Path, name: &str) -> PathBuf { + let dir = work_dir.join("cln").join(name); std::fs::create_dir_all(&dir).unwrap(); dir } -pub fn get_lnd_dir(name: &str) -> PathBuf { - let dir = get_temp_dir().join("lnd").join(name); +pub fn get_lnd_dir(work_dir: &Path, name: &str) -> PathBuf { + let dir = work_dir.join("lnd").join(name); std::fs::create_dir_all(&dir).unwrap(); dir } @@ -101,9 +125,14 @@ pub fn get_lnd_macaroon_path(lnd_dir: &Path) -> PathBuf { lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon") } -pub async fn init_lnd(lnd_dir: PathBuf, lnd_addr: &str, lnd_rpc_addr: &str) -> Lnd { +pub async fn init_lnd( + work_dir: &Path, + lnd_dir: PathBuf, + lnd_addr: &str, + lnd_rpc_addr: &str, +) -> Lnd { Lnd::new( - get_bitcoin_dir(), + get_bitcoin_dir(work_dir), lnd_dir, lnd_addr.parse().unwrap(), lnd_rpc_addr.to_string(), @@ -192,8 +221,12 @@ where Ok(()) } -pub async fn start_regtest_end(sender: Sender<()>, notify: Arc) -> anyhow::Result<()> { - let mut bitcoind = init_bitcoind(); +pub async fn start_regtest_end( + work_dir: &Path, + sender: Sender<()>, + notify: Arc, +) -> anyhow::Result<()> { + let mut bitcoind = init_bitcoind(work_dir); bitcoind.start_bitcoind()?; let bitcoin_client = init_bitcoin_client()?; @@ -203,9 +236,9 @@ pub async fn start_regtest_end(sender: Sender<()>, notify: Arc) -> anyho let new_add = bitcoin_client.get_new_address()?; bitcoin_client.generate_blocks(&new_add, 200).unwrap(); - let cln_one_dir = get_cln_dir("one"); + let cln_one_dir = get_cln_dir(work_dir, "one"); let mut clnd = Clnd::new( - get_bitcoin_dir(), + get_bitcoin_dir(work_dir), cln_one_dir.clone(), CLN_ADDR.into(), BITCOIN_RPC_USER.to_string(), @@ -220,9 +253,9 @@ pub async fn start_regtest_end(sender: Sender<()>, notify: Arc) -> anyho fund_ln(&bitcoin_client, &cln_client).await.unwrap(); // Create second cln - let cln_two_dir = get_cln_dir("two"); + let cln_two_dir = get_cln_dir(work_dir, "two"); let mut clnd_two = Clnd::new( - get_bitcoin_dir(), + get_bitcoin_dir(work_dir), cln_two_dir.clone(), CLN_TWO_ADDR.into(), BITCOIN_RPC_USER.to_string(), @@ -236,10 +269,10 @@ pub async fn start_regtest_end(sender: Sender<()>, notify: Arc) -> anyho fund_ln(&bitcoin_client, &cln_two_client).await.unwrap(); - let lnd_dir = get_lnd_dir("one"); + let lnd_dir = get_lnd_dir(work_dir, "one"); println!("{}", lnd_dir.display()); - let mut lnd = init_lnd(lnd_dir.clone(), LND_ADDR, LND_RPC_ADDR).await; + let mut lnd = init_lnd(work_dir, lnd_dir.clone(), LND_ADDR, LND_RPC_ADDR).await; lnd.start_lnd().unwrap(); tracing::info!("Started lnd node"); @@ -255,8 +288,15 @@ pub async fn start_regtest_end(sender: Sender<()>, notify: Arc) -> anyho fund_ln(&bitcoin_client, &lnd_client).await.unwrap(); // create second lnd node - let lnd_two_dir = get_lnd_dir("two"); - let mut lnd_two = init_lnd(lnd_two_dir.clone(), LND_TWO_ADDR, LND_TWO_RPC_ADDR).await; + let work_dir = get_temp_dir(); + let lnd_two_dir = get_lnd_dir(&work_dir, "two"); + let mut lnd_two = init_lnd( + &work_dir, + lnd_two_dir.clone(), + LND_TWO_ADDR, + LND_TWO_RPC_ADDR, + ) + .await; lnd_two.start_lnd().unwrap(); tracing::info!("Started second lnd node"); diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index 2fb7d062..41ccc0d7 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -1,4 +1,23 @@ +//! Integration Test Library +//! +//! This crate provides shared functionality for CDK integration tests. +//! It includes utilities for setting up test environments, funding wallets, +//! and common test operations across different test scenarios. +//! +//! Test Categories Supported: +//! - Pure in-memory tests (no external dependencies) +//! - Regtest environment tests (with actual Lightning nodes) +//! - Authenticated mint tests +//! - Multi-mint scenarios +//! +//! Key Components: +//! - Test environment initialization +//! - Wallet funding utilities +//! - Lightning Network client helpers +//! - Proof state management utilities + use std::env; +use std::path::Path; use std::sync::Arc; use anyhow::{anyhow, bail, Result}; @@ -8,13 +27,15 @@ use cdk::nuts::{MintQuoteState, NotificationPayload, State}; use cdk::wallet::WalletSubscription; use cdk::Wallet; use cdk_fake_wallet::create_fake_invoice; -use init_regtest::{get_lnd_dir, get_mint_url, LND_RPC_ADDR}; +use init_regtest::{get_lnd_dir, LND_RPC_ADDR}; use ln_regtest_rs::ln_client::{LightningClient, LndClient}; use tokio::time::{sleep, timeout, Duration}; +pub mod cli; pub mod init_auth_mint; pub mod init_pure_tests; pub mod init_regtest; +pub mod shared; pub async fn fund_wallet(wallet: Arc, amount: Amount) { let quote = wallet @@ -32,6 +53,20 @@ pub async fn fund_wallet(wallet: Arc, amount: Amount) { .expect("Could not mint"); } +pub fn get_mint_url_from_env() -> String { + match env::var("CDK_TEST_MINT_URL") { + Ok(url) => url, + Err(_) => panic!("Mint url not set"), + } +} + +pub fn get_second_mint_url_from_env() -> String { + match env::var("CDK_TEST_MINT_URL_2") { + Ok(url) => url, + Err(_) => panic!("Mint url not set"), + } +} + // Get all pending from wallet and attempt to swap // Will panic if there are no pending // Will return Ok if swap fails as expected @@ -151,33 +186,9 @@ pub async fn wait_for_mint_to_be_paid( } } -/// Gets the mint URL from environment variable or falls back to default -/// -/// Checks the CDK_TEST_MINT_URL environment variable: -/// - If set, returns that URL -/// - Otherwise falls back to the default URL from get_mint_url("0") -pub fn get_mint_url_from_env() -> String { - match env::var("CDK_TEST_MINT_URL") { - Ok(url) => url, - Err(_) => get_mint_url("0"), - } -} - -/// Gets the second mint URL from environment variable or falls back to default -/// -/// Checks the CDK_TEST_MINT_URL_2 environment variable: -/// - If set, returns that URL -/// - Otherwise falls back to the default URL from get_mint_url("1") -pub fn get_second_mint_url_from_env() -> String { - match env::var("CDK_TEST_MINT_URL_2") { - Ok(url) => url, - Err(_) => get_mint_url("1"), - } -} - // This is the ln wallet we use to send/receive ln payements as the wallet -pub async fn init_lnd_client() -> LndClient { - let lnd_dir = get_lnd_dir("one"); +pub async fn init_lnd_client(work_dir: &Path) -> LndClient { + let lnd_dir = get_lnd_dir(work_dir, "one"); let cert_file = lnd_dir.join("tls.cert"); let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); LndClient::new(format!("https://{LND_RPC_ADDR}"), cert_file, macaroon_file) @@ -189,10 +200,10 @@ pub async fn init_lnd_client() -> LndClient { /// /// This is useful for tests that need to pay invoices in regtest mode but /// should be skipped in other environments. -pub async fn pay_if_regtest(invoice: &Bolt11Invoice) -> Result<()> { +pub async fn pay_if_regtest(work_dir: &Path, invoice: &Bolt11Invoice) -> Result<()> { // Check if the invoice is for the regtest network if invoice.network() == bitcoin::Network::Regtest { - let lnd_client = init_lnd_client().await; + let lnd_client = init_lnd_client(work_dir).await; lnd_client.pay_invoice(invoice.to_string()).await?; Ok(()) } else { @@ -220,10 +231,10 @@ pub fn is_regtest_env() -> bool { /// /// Uses the is_regtest_env() function to determine whether to /// create a real regtest invoice or a fake one for testing. -pub async fn create_invoice_for_env(amount_sat: Option) -> Result { +pub async fn create_invoice_for_env(work_dir: &Path, amount_sat: Option) -> Result { if is_regtest_env() { // In regtest mode, create a real invoice - let lnd_client = init_lnd_client().await; + let lnd_client = init_lnd_client(work_dir).await; lnd_client .create_invoice(amount_sat) .await diff --git a/crates/cdk-integration-tests/src/shared.rs b/crates/cdk-integration-tests/src/shared.rs new file mode 100644 index 00000000..2657c135 --- /dev/null +++ b/crates/cdk-integration-tests/src/shared.rs @@ -0,0 +1,266 @@ +//! Shared utilities for mint integration tests +//! +//! This module provides common functionality used across different +//! integration test binaries to reduce code duplication. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use cdk_axum::cache; +use tokio::signal; +use tokio::sync::Notify; + +use crate::cli::{init_logging, CommonArgs}; + +/// Default minimum mint amount for test mints +const DEFAULT_MIN_MINT: u64 = 1; +/// Default maximum mint amount for test mints +const DEFAULT_MAX_MINT: u64 = 500_000; +/// Default minimum melt amount for test mints +const DEFAULT_MIN_MELT: u64 = 1; +/// Default maximum melt amount for test mints +const DEFAULT_MAX_MELT: u64 = 500_000; + +/// Wait for mint to be ready by checking its info endpoint +pub async fn wait_for_mint_ready(port: u16, timeout_secs: u64) -> Result<()> { + let url = format!("http://127.0.0.1:{port}/v1/info"); + let start_time = std::time::Instant::now(); + + println!("Waiting for mint on port {port} to be ready..."); + + loop { + // Check if timeout has been reached + if start_time.elapsed().as_secs() > timeout_secs { + return Err(anyhow::anyhow!("Timeout waiting for mint on port {}", port)); + } + + // Try to make a request to the mint info endpoint + match reqwest::get(&url).await { + Ok(response) => { + if response.status().is_success() { + println!("Mint on port {port} is ready"); + return Ok(()); + } else { + println!( + "Mint on port {} returned status: {}", + port, + response.status() + ); + } + } + Err(e) => { + println!("Error connecting to mint on port {port}: {e}"); + } + } + + tokio::time::sleep(Duration::from_secs(2)).await; + } +} + +/// Initialize working directory +pub fn init_working_directory(work_dir: &str) -> Result { + let temp_dir = PathBuf::from_str(work_dir)?; + + // Create the temp directory if it doesn't exist + fs::create_dir_all(&temp_dir)?; + + Ok(temp_dir) +} + +/// Write environment variables to .env file +pub fn write_env_file(temp_dir: &Path, env_vars: &[(&str, &str)]) -> Result<()> { + let mut env_content = String::new(); + for (key, value) in env_vars { + env_content.push_str(&format!("{key}={value}\n")); + } + + let env_file_path = temp_dir.join(".env"); + + fs::write(&env_file_path, &env_content) + .map(|_| { + println!( + "Environment variables written to: {}", + env_file_path.display() + ); + }) + .map_err(|e| anyhow::anyhow!("Could not write .env file: {}", e)) +} + +/// Wait for .env file to be created +pub async fn wait_for_env_file(temp_dir: &Path, timeout_secs: u64) -> Result<()> { + let env_file_path = temp_dir.join(".env"); + let start_time = std::time::Instant::now(); + + println!( + "Waiting for .env file to be created at: {}", + env_file_path.display() + ); + + loop { + // Check if timeout has been reached + if start_time.elapsed().as_secs() > timeout_secs { + return Err(anyhow::anyhow!( + "Timeout waiting for .env file at {}", + env_file_path.display() + )); + } + + // Check if the file exists + if env_file_path.exists() { + println!(".env file found at: {}", env_file_path.display()); + return Ok(()); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + } +} + +/// Setup common logging based on CLI arguments +pub fn setup_logging(common_args: &CommonArgs) { + init_logging(common_args.enable_logging, common_args.log_level); +} + +/// Create shutdown handler for graceful termination +pub fn create_shutdown_handler() -> Arc { + Arc::new(Notify::new()) +} + +/// Wait for Ctrl+C signal +pub async fn wait_for_shutdown_signal(shutdown: Arc) { + signal::ctrl_c() + .await + .expect("failed to install CTRL+C handler"); + + println!("\nReceived Ctrl+C, shutting down..."); + shutdown.notify_waiters(); +} + +/// Common mint information display +pub fn display_mint_info(port: u16, temp_dir: &Path, database_type: &str) { + println!("Mint started successfully!"); + println!("Mint URL: http://127.0.0.1:{port}"); + println!("Temp directory: {temp_dir:?}"); + println!("Database type: {database_type}"); +} + +/// Create settings for a fake wallet mint +pub fn create_fake_wallet_settings( + port: u16, + mnemonic: Option, + signatory_config: Option<(String, String)>, // (url, certs_dir) + fake_wallet_config: Option, +) -> cdk_mintd::config::Settings { + cdk_mintd::config::Settings { + info: cdk_mintd::config::Info { + url: format!("http://127.0.0.1:{port}"), + listen_host: "127.0.0.1".to_string(), + listen_port: port, + mnemonic, + signatory_url: signatory_config.as_ref().map(|(url, _)| url.clone()), + signatory_certs: signatory_config + .as_ref() + .map(|(_, certs_dir)| certs_dir.clone()), + input_fee_ppk: None, + http_cache: cache::Config::default(), + enable_swagger_ui: None, + }, + mint_info: cdk_mintd::config::MintInfo::default(), + ln: cdk_mintd::config::Ln { + ln_backend: cdk_mintd::config::LnBackend::FakeWallet, + invoice_description: None, + min_mint: DEFAULT_MIN_MINT.into(), + max_mint: DEFAULT_MAX_MINT.into(), + min_melt: DEFAULT_MIN_MELT.into(), + max_melt: DEFAULT_MAX_MELT.into(), + }, + cln: None, + lnbits: None, + lnd: None, + fake_wallet: fake_wallet_config, + grpc_processor: None, + database: cdk_mintd::config::Database::default(), + mint_management_rpc: None, + auth: None, + } +} + +/// Create settings for a CLN mint +pub fn create_cln_settings( + port: u16, + _cln_rpc_path: PathBuf, + mnemonic: String, + cln_config: cdk_mintd::config::Cln, +) -> cdk_mintd::config::Settings { + cdk_mintd::config::Settings { + info: cdk_mintd::config::Info { + url: format!("http://127.0.0.1:{port}"), + listen_host: "127.0.0.1".to_string(), + listen_port: port, + mnemonic: Some(mnemonic), + signatory_url: None, + signatory_certs: None, + input_fee_ppk: None, + http_cache: cache::Config::default(), + enable_swagger_ui: None, + }, + mint_info: cdk_mintd::config::MintInfo::default(), + ln: cdk_mintd::config::Ln { + ln_backend: cdk_mintd::config::LnBackend::Cln, + invoice_description: None, + min_mint: DEFAULT_MIN_MINT.into(), + max_mint: DEFAULT_MAX_MINT.into(), + min_melt: DEFAULT_MIN_MELT.into(), + max_melt: DEFAULT_MAX_MELT.into(), + }, + cln: Some(cln_config), + lnbits: None, + lnd: None, + fake_wallet: None, + grpc_processor: None, + database: cdk_mintd::config::Database::default(), + mint_management_rpc: None, + auth: None, + } +} + +/// Create settings for an LND mint +pub fn create_lnd_settings( + port: u16, + lnd_config: cdk_mintd::config::Lnd, + mnemonic: String, +) -> cdk_mintd::config::Settings { + cdk_mintd::config::Settings { + info: cdk_mintd::config::Info { + url: format!("http://127.0.0.1:{port}"), + listen_host: "127.0.0.1".to_string(), + listen_port: port, + mnemonic: Some(mnemonic), + signatory_url: None, + signatory_certs: None, + input_fee_ppk: None, + http_cache: cache::Config::default(), + enable_swagger_ui: None, + }, + mint_info: cdk_mintd::config::MintInfo::default(), + ln: cdk_mintd::config::Ln { + ln_backend: cdk_mintd::config::LnBackend::Lnd, + invoice_description: None, + min_mint: DEFAULT_MIN_MINT.into(), + max_mint: DEFAULT_MAX_MINT.into(), + min_melt: DEFAULT_MIN_MELT.into(), + max_melt: DEFAULT_MAX_MELT.into(), + }, + cln: None, + lnbits: None, + lnd: Some(lnd_config), + fake_wallet: None, + grpc_processor: None, + database: cdk_mintd::config::Database::default(), + mint_management_rpc: None, + auth: None, + } +} diff --git a/crates/cdk-integration-tests/tests/bolt12.rs b/crates/cdk-integration-tests/tests/bolt12.rs index fc279885..2c642256 100644 --- a/crates/cdk-integration-tests/tests/bolt12.rs +++ b/crates/cdk-integration-tests/tests/bolt12.rs @@ -1,3 +1,5 @@ +use std::env; +use std::path::PathBuf; use std::sync::Arc; use anyhow::{bail, Result}; @@ -6,11 +8,45 @@ use cashu::amount::SplitTarget; use cashu::nut23::Amountless; use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods}; use cdk::wallet::{HttpClient, MintConnector, Wallet}; -use cdk_integration_tests::init_regtest::get_cln_dir; +use cdk_integration_tests::init_regtest::{get_cln_dir, get_temp_dir}; use cdk_integration_tests::{get_mint_url_from_env, wait_for_mint_to_be_paid}; use cdk_sqlite::wallet::memory; use ln_regtest_rs::ln_client::ClnClient; +// Helper function to get temp directory from environment or fallback +fn get_test_temp_dir() -> PathBuf { + match env::var("CDK_ITESTS_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => get_temp_dir(), // fallback to default + } +} + +// Helper function to create CLN client with retries +async fn create_cln_client_with_retry(cln_dir: PathBuf) -> Result { + let mut retries = 0; + let max_retries = 10; + loop { + match ClnClient::new(cln_dir.clone(), None).await { + Ok(client) => return Ok(client), + Err(e) => { + retries += 1; + if retries >= max_retries { + bail!( + "Could not connect to CLN client after {} retries: {}", + max_retries, + e + ); + } + println!( + "Failed to connect to CLN (attempt {}/{}): {}. Retrying in 7 seconds...", + retries, max_retries, e + ); + tokio::time::sleep(tokio::time::Duration::from_secs(7)).await; + } + } + } +} + /// Tests basic BOLT12 minting functionality: /// - Creates a wallet /// - Gets a BOLT12 quote for a specific amount (100 sats) @@ -36,8 +72,11 @@ async fn test_regtest_bolt12_mint() { assert_eq!(mint_quote.amount, Some(mint_amount)); - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await.unwrap(); + let work_dir = get_test_temp_dir(); + let cln_one_dir = get_cln_dir(&work_dir, "one"); + let cln_client = create_cln_client_with_retry(cln_one_dir.clone()) + .await + .unwrap(); cln_client .pay_bolt12_offer(None, mint_quote.request) .await @@ -68,8 +107,9 @@ async fn test_regtest_bolt12_mint_multiple() -> Result<()> { let mint_quote = wallet.mint_bolt12_quote(None, None).await?; - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + let work_dir = get_test_temp_dir(); + let cln_one_dir = get_cln_dir(&work_dir, "one"); + let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?; cln_client .pay_bolt12_offer(Some(10000), mint_quote.request.clone()) .await @@ -131,8 +171,9 @@ async fn test_regtest_bolt12_multiple_wallets() -> Result<()> { )?; // Create a BOLT12 offer that both wallets will use - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + let work_dir = get_test_temp_dir(); + let cln_one_dir = get_cln_dir(&work_dir, "one"); + let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?; // First wallet payment let quote_one = wallet_one .mint_bolt12_quote(Some(10_000.into()), None) @@ -222,8 +263,9 @@ async fn test_regtest_bolt12_melt() -> Result<()> { assert_eq!(mint_quote.amount, Some(mint_amount)); // Pay the quote - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + let work_dir = get_test_temp_dir(); + let cln_one_dir = get_cln_dir(&work_dir, "one"); + let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?; cln_client .pay_bolt12_offer(None, mint_quote.request.clone()) .await?; @@ -281,8 +323,9 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> { let pay_amount_msats = 10_000; - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + let work_dir = get_test_temp_dir(); + let cln_one_dir = get_cln_dir(&work_dir, "one"); + let cln_client = create_cln_client_with_retry(cln_one_dir.clone()).await?; cln_client .pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone()) .await?; diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index a4843463..1c185b72 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -1,3 +1,19 @@ +//! Fake Wallet Integration Tests +//! +//! This file contains tests for the fake wallet backend functionality. +//! The fake wallet simulates Lightning Network behavior for testing purposes, +//! allowing verification of mint behavior in various payment scenarios without +//! requiring a real Lightning node. +//! +//! Test Scenarios: +//! - Pending payment states and proof handling +//! - Payment failure cases and proof state management +//! - Change output verification in melt operations +//! - Witness signature validation +//! - Cross-unit transaction validation +//! - Overflow and balance validation +//! - Duplicate proof detection + use std::sync::Arc; use bip39::Mnemonic; @@ -408,40 +424,6 @@ async fn test_fake_melt_change_in_quote() { assert_eq!(melt_change, check); } -/// Tests that the correct database type is used based on environment variables -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_database_type() { - // Get the database type and work dir from environment - let db_type = std::env::var("CDK_MINTD_DATABASE").expect("MINT_DATABASE env var should be set"); - let work_dir = - std::env::var("CDK_MINTD_WORK_DIR").expect("CDK_MINTD_WORK_DIR env var should be set"); - - // Check that the correct database file exists - match db_type.as_str() { - "REDB" => { - let db_path = std::path::Path::new(&work_dir).join("cdk-mintd.redb"); - assert!( - db_path.exists(), - "Expected redb database file to exist at {:?}", - db_path - ); - } - "SQLITE" => { - let db_path = std::path::Path::new(&work_dir).join("cdk-mintd.sqlite"); - assert!( - db_path.exists(), - "Expected sqlite database file to exist at {:?}", - db_path - ); - } - "MEMORY" => { - // Memory database has no file to check - println!("Memory database in use - no file to check"); - } - _ => panic!("Unknown database type: {}", db_type), - } -} - /// Tests minting tokens with a valid witness signature #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_mint_with_witness() { diff --git a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs index 5d42aff7..96d79d82 100644 --- a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs +++ b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs @@ -11,6 +11,7 @@ use core::panic; use std::env; use std::fmt::Debug; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -32,6 +33,14 @@ use tokio::time::timeout; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::protocol::Message; +// Helper function to get temp directory from environment or fallback +fn get_test_temp_dir() -> PathBuf { + match env::var("CDK_ITESTS_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => panic!("Unknown test dir"), + } +} + async fn get_notification> + Unpin, E: Debug>( reader: &mut T, timeout_to_wait: Duration, @@ -98,7 +107,9 @@ async fn test_happy_mint_melt_round_trip() { let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap(); let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - pay_if_regtest(&invoice).await.unwrap(); + pay_if_regtest(&get_test_temp_dir(), &invoice) + .await + .unwrap(); wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10) .await @@ -113,7 +124,9 @@ async fn test_happy_mint_melt_round_trip() { assert!(mint_amount == 100.into()); - let invoice = create_invoice_for_env(Some(50)).await.unwrap(); + let invoice = create_invoice_for_env(&get_test_temp_dir(), Some(50)) + .await + .unwrap(); let melt = wallet.melt_quote(invoice, None).await.unwrap(); @@ -217,7 +230,9 @@ async fn test_happy_mint() { assert_eq!(mint_quote.amount, Some(mint_amount)); let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - pay_if_regtest(&invoice).await.unwrap(); + pay_if_regtest(&get_test_temp_dir(), &invoice) + .await + .unwrap(); wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60) .await @@ -262,7 +277,9 @@ async fn test_restore() { let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap(); let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - pay_if_regtest(&invoice).await.unwrap(); + pay_if_regtest(&get_test_temp_dir(), &invoice) + .await + .unwrap(); wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60) .await @@ -341,7 +358,7 @@ async fn test_fake_melt_change_in_quote() { let bolt11 = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - pay_if_regtest(&bolt11).await.unwrap(); + pay_if_regtest(&get_test_temp_dir(), &bolt11).await.unwrap(); wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60) .await @@ -352,7 +369,9 @@ async fn test_fake_melt_change_in_quote() { .await .unwrap(); - let invoice = create_invoice_for_env(Some(9)).await.unwrap(); + let invoice = create_invoice_for_env(&get_test_temp_dir(), Some(9)) + .await + .unwrap(); let proofs = wallet.get_unspent_proofs().await.unwrap(); @@ -408,7 +427,7 @@ async fn test_pay_invoice_twice() { let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap(); - pay_if_regtest(&mint_quote.request.parse().unwrap()) + pay_if_regtest(&get_test_temp_dir(), &mint_quote.request.parse().unwrap()) .await .unwrap(); @@ -425,7 +444,9 @@ async fn test_pay_invoice_twice() { assert_eq!(mint_amount, 100.into()); - let invoice = create_invoice_for_env(Some(25)).await.unwrap(); + let invoice = create_invoice_for_env(&get_test_temp_dir(), Some(25)) + .await + .unwrap(); let melt_quote = wallet.melt_quote(invoice.clone(), None).await.unwrap(); diff --git a/crates/cdk-integration-tests/tests/integration_tests_pure.rs b/crates/cdk-integration-tests/tests/integration_tests_pure.rs index 7ed954b8..b98cd8a1 100644 --- a/crates/cdk-integration-tests/tests/integration_tests_pure.rs +++ b/crates/cdk-integration-tests/tests/integration_tests_pure.rs @@ -3,6 +3,11 @@ //! These tests verify the interaction between mint and wallet components, simulating real-world usage scenarios. //! They test the complete flow of operations including wallet funding, token swapping, sending tokens between wallets, //! and other operations that require client-mint interaction. +//! +//! Test Environment: +//! - Uses pure in-memory mint instances for fast execution +//! - Tests run concurrently with multi-threaded tokio runtime +//! - No external dependencies (Lightning nodes, databases) required use std::assert_eq; use std::collections::{HashMap, HashSet}; diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index da6fa8c8..3dd881ce 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -1,8 +1,15 @@ -//! Mint tests +//! Mint Tests //! //! This file contains tests that focus on the mint's internal functionality without client interaction. //! These tests verify the mint's behavior in isolation, such as keyset management, database operations, //! and other mint-specific functionality that doesn't require wallet clients. +//! +//! Test Categories: +//! - Keyset rotation and management +//! - Database transaction handling +//! - Internal state transitions +//! - Fee calculation and enforcement +//! - Proof validation and state management use std::collections::{HashMap, HashSet}; use std::sync::Arc; diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index df80f155..25fd33cb 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -1,4 +1,20 @@ -use std::str::FromStr; +//! Regtest Integration Tests +//! +//! This file contains tests that run against actual Lightning Network nodes in regtest mode. +//! These tests require a local development environment with LND nodes configured for regtest. +//! +//! Test Environment Setup: +//! - Uses actual LND nodes connected to a regtest Bitcoin network +//! - Tests real Lightning payment flows including invoice creation and payment +//! - Verifies mint behavior with actual Lightning Network interactions +//! +//! Running Tests: +//! - Requires CDK_TEST_REGTEST=1 environment variable to be set +//! - Requires properly configured LND nodes with TLS certificates and macaroons +//! - Uses real Bitcoin transactions in regtest mode + +use std::env; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -10,32 +26,46 @@ use cdk::nuts::{ NotificationPayload, PreMintSecrets, }; use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription}; -use cdk_integration_tests::init_regtest::{ - get_cln_dir, get_lnd_cert_file_path, get_lnd_dir, get_lnd_macaroon_path, get_mint_port, - LND_RPC_ADDR, LND_TWO_RPC_ADDR, -}; +use cdk_integration_tests::init_regtest::{get_lnd_dir, LND_RPC_ADDR}; use cdk_integration_tests::{ get_mint_url_from_env, get_second_mint_url_from_env, wait_for_mint_to_be_paid, }; use cdk_sqlite::wallet::{self, memory}; use futures::join; -use lightning_invoice::Bolt11Invoice; -use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient}; -use ln_regtest_rs::InvoiceStatus; +use ln_regtest_rs::ln_client::{LightningClient, LndClient}; use tokio::time::timeout; // This is the ln wallet we use to send/receive ln payements as the wallet async fn init_lnd_client() -> LndClient { - let lnd_dir = get_lnd_dir("one"); + // Try to get the temp directory from environment variable first (from .env file) + let temp_dir = match env::var("CDK_ITESTS_DIR") { + Ok(dir) => { + let path = PathBuf::from(dir); + println!("Using temp directory from CDK_ITESTS_DIR: {:?}", path); + path + } + Err(_) => { + panic!("Unknown temp dir"); + } + }; + + // The LND mint uses the second LND node (LND_TWO_RPC_ADDR = localhost:10010) + let lnd_dir = get_lnd_dir(&temp_dir, "one"); let cert_file = lnd_dir.join("tls.cert"); let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); + + println!("Looking for LND cert file: {:?}", cert_file); + println!("Looking for LND macaroon file: {:?}", macaroon_file); + println!("Connecting to LND at: https://{}", LND_RPC_ADDR); + + // Connect to LND LndClient::new( format!("https://{}", LND_RPC_ADDR), - cert_file, - macaroon_file, + cert_file.clone(), + macaroon_file.clone(), ) .await - .unwrap() + .expect("Could not connect to lnd rpc") } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -98,41 +128,41 @@ async fn test_internal_payment() { .await .unwrap(); - let check_paid = match get_mint_port("0") { - 8085 => { - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await.unwrap(); + // let check_paid = match get_mint_port("0") { + // 8085 => { + // let cln_one_dir = get_cln_dir(&get_temp_dir(), "one"); + // let cln_client = ClnClient::new(cln_one_dir.clone(), None).await.unwrap(); - let payment_hash = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - cln_client - .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) - .await - .expect("Could not check invoice") - } - 8087 => { - let lnd_two_dir = get_lnd_dir("two"); - let lnd_client = LndClient::new( - format!("https://{}", LND_TWO_RPC_ADDR), - get_lnd_cert_file_path(&lnd_two_dir), - get_lnd_macaroon_path(&lnd_two_dir), - ) - .await - .unwrap(); - let payment_hash = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - lnd_client - .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) - .await - .expect("Could not check invoice") - } - _ => panic!("Unknown mint port"), - }; + // let payment_hash = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); + // cln_client + // .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) + // .await + // .expect("Could not check invoice") + // } + // 8087 => { + // let lnd_two_dir = get_lnd_dir(&get_temp_dir(), "two"); + // let lnd_client = LndClient::new( + // format!("https://{}", LND_TWO_RPC_ADDR), + // get_lnd_cert_file_path(&lnd_two_dir), + // get_lnd_macaroon_path(&lnd_two_dir), + // ) + // .await + // .unwrap(); + // let payment_hash = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); + // lnd_client + // .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) + // .await + // .expect("Could not check invoice") + // } + // _ => panic!("Unknown mint port"), + // }; - match check_paid { - InvoiceStatus::Unpaid => (), - _ => { - panic!("Invoice has incorrect status: {:?}", check_paid); - } - } + // match check_paid { + // InvoiceStatus::Unpaid => (), + // _ => { + // panic!("Invoice has incorrect status: {:?}", check_paid); + // } + // } let wallet_2_balance = wallet_2.total_balance().await.unwrap(); diff --git a/crates/cdk-integration-tests/tests/test_fees.rs b/crates/cdk-integration-tests/tests/test_fees.rs index 1433030a..f4d30ded 100644 --- a/crates/cdk-integration-tests/tests/test_fees.rs +++ b/crates/cdk-integration-tests/tests/test_fees.rs @@ -6,6 +6,7 @@ use cashu::{Bolt11Invoice, ProofsMethods}; use cdk::amount::{Amount, SplitTarget}; use cdk::nuts::CurrencyUnit; use cdk::wallet::{ReceiveOptions, SendKind, SendOptions, Wallet}; +use cdk_integration_tests::init_regtest::get_temp_dir; use cdk_integration_tests::{ create_invoice_for_env, get_mint_url_from_env, pay_if_regtest, wait_for_mint_to_be_paid, }; @@ -26,7 +27,7 @@ async fn test_swap() { let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap(); let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - pay_if_regtest(&invoice).await.unwrap(); + pay_if_regtest(&get_temp_dir(), &invoice).await.unwrap(); wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10) .await @@ -93,7 +94,7 @@ async fn test_fake_melt_change_in_quote() { let bolt11 = Bolt11Invoice::from_str(&mint_quote.request).unwrap(); - pay_if_regtest(&bolt11).await.unwrap(); + pay_if_regtest(&get_temp_dir(), &bolt11).await.unwrap(); wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60) .await @@ -106,7 +107,9 @@ async fn test_fake_melt_change_in_quote() { let invoice_amount = 9; - let invoice = create_invoice_for_env(Some(invoice_amount)).await.unwrap(); + let invoice = create_invoice_for_env(&get_temp_dir(), Some(invoice_amount)) + .await + .unwrap(); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap(); diff --git a/crates/cdk-mint-rpc/Cargo.toml b/crates/cdk-mint-rpc/Cargo.toml index 810ac0c0..9f3ed089 100644 --- a/crates/cdk-mint-rpc/Cargo.toml +++ b/crates/cdk-mint-rpc/Cargo.toml @@ -23,7 +23,7 @@ anyhow.workspace = true cdk = { workspace = true, features = [ "mint", ] } -cdk-common = { workspace = true } +cdk-common.workspace = true clap.workspace = true tonic = { workspace = true, features = ["transport"] } tracing.workspace = true diff --git a/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs b/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs index f7a61942..293ae634 100644 --- a/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs +++ b/crates/cdk-mint-rpc/src/bin/mint_rpc_cli.rs @@ -7,22 +7,55 @@ use cdk_mint_rpc::GetInfoRequest; use clap::{Parser, Subcommand}; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tonic::Request; -use tracing::Level; use tracing_subscriber::EnvFilter; +/// Common CLI arguments for CDK binaries +#[derive(Parser, Debug)] +pub struct CommonArgs { + /// Enable logging (default is false) + #[arg(long, default_value_t = false)] + pub enable_logging: bool, + + /// Logging level when enabled (default is debug) + #[arg(long, default_value = "debug")] + pub log_level: tracing::Level, +} + +/// Initialize logging based on CLI arguments +pub fn init_logging(enable_logging: bool, log_level: tracing::Level) { + if enable_logging { + let default_filter = log_level.to_string(); + + // Common filters to reduce noise + let sqlx_filter = "sqlx=warn"; + let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; + let rustls_filter = "rustls=warn"; + let reqwest_filter = "reqwest=warn"; + + let env_filter = EnvFilter::new(format!( + "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter},{reqwest_filter}" + )); + + // Ok if successful, Err if already initialized + let _ = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .try_init(); + } +} + const DEFAULT_WORK_DIR: &str = ".cdk-mint-rpc-cli"; #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { + #[command(flatten)] + common: CommonArgs, + /// Address of RPC server #[arg(short, long, default_value = "https://127.0.0.1:8086")] addr: String, - /// Logging level - #[arg(short, long, default_value = "debug")] - log_level: Level, - /// Path to working dir #[arg(short, long)] work_dir: Option, @@ -70,14 +103,9 @@ enum Commands { #[tokio::main] async fn main() -> Result<()> { let args: Cli = Cli::parse(); - let default_filter = args.log_level; - let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn"; - - let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter}")); - - // Parse input - tracing_subscriber::fmt().with_env_filter(env_filter).init(); + // Initialize logging based on CLI arguments + init_logging(args.common.enable_logging, args.common.log_level); let cli = Cli::parse(); diff --git a/crates/cdk-mintd/src/cli.rs b/crates/cdk-mintd/src/cli.rs index 20d07086..9da08831 100644 --- a/crates/cdk-mintd/src/cli.rs +++ b/crates/cdk-mintd/src/cli.rs @@ -24,4 +24,12 @@ pub struct CLIArgs { pub config: Option, #[arg(short, long, help = "Recover Greenlight from seed", required = false)] pub recover: Option, + #[arg( + long, + help = "Enable logging output", + required = false, + action = clap::ArgAction::SetTrue, + default_value = "true" + )] + pub enable_logging: bool, } diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index a8268fd9..86adbf1b 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -1,13 +1,66 @@ //! Cdk mintd lib -#[cfg(feature = "cln")] -use std::path::PathBuf; +// std +#[cfg(feature = "auth")] +use std::collections::HashMap; +use std::env; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; + +// external crates +use anyhow::{anyhow, bail, Result}; +use axum::Router; +use bip39::Mnemonic; +// internal crate modules +use cdk::cdk_database::{self, MintDatabase, MintKeysDatabase}; +use cdk::cdk_payment; +use cdk::cdk_payment::MintPayment; +use cdk::mint::{Mint, MintBuilder, MintMeltLimits}; +#[cfg(any( + feature = "cln", + feature = "lnbits", + feature = "lnd", + feature = "fakewallet", + feature = "grpc-processor" +))] +use cdk::nuts::nut17::SupportedMethods; +use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}; +#[cfg(any( + feature = "cln", + feature = "lnbits", + feature = "lnd", + feature = "fakewallet" +))] +use cdk::nuts::CurrencyUnit; +#[cfg(feature = "auth")] +use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath}; +use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod}; +use cdk::types::QuoteTTL; +use cdk_axum::cache::HttpCache; +#[cfg(feature = "auth")] +use cdk_sqlite::mint::MintSqliteAuthDatabase; +use cdk_sqlite::MintSqliteDatabase; +use cli::CLIArgs; +use config::{DatabaseEngine, LnBackend}; +use env_vars::ENV_WORK_DIR; +use setup::LnBackendSetup; +use tower::ServiceBuilder; +use tower_http::compression::CompressionLayer; +use tower_http::decompression::RequestDecompressionLayer; +use tower_http::trace::TraceLayer; +use tracing_subscriber::EnvFilter; +#[cfg(feature = "swagger")] +use utoipa::OpenApi; pub mod cli; pub mod config; pub mod env_vars; pub mod setup; +const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); + #[cfg(feature = "cln")] fn expand_path(path: &str) -> Option { if path.starts_with('~') { @@ -23,3 +76,751 @@ fn expand_path(path: &str) -> Option { Some(PathBuf::from(path)) } } + +/// Performs the initial setup for the application, including configuring tracing, +/// parsing CLI arguments, setting up the working directory, loading settings, +/// and initializing the database connection. +async fn initial_setup( + work_dir: &Path, + settings: &config::Settings, + db_password: Option, +) -> Result<( + Arc + Send + Sync>, + Arc + Send + Sync>, +)> { + let (localstore, keystore) = setup_database(settings, work_dir, db_password).await?; + Ok((localstore, keystore)) +} + +/// Sets up and initializes a tracing subscriber with custom log filtering. +pub fn setup_tracing() { + let default_filter = "debug"; + let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; + let tower_http = "tower_http=warn"; + + let env_filter = EnvFilter::new(format!( + "{default_filter},{hyper_filter},{h2_filter},{tower_http}" + )); + + tracing_subscriber::fmt().with_env_filter(env_filter).init(); +} + +/// Retrieves the work directory based on command-line arguments, environment variables, or system defaults. +pub async fn get_work_directory(args: &CLIArgs) -> Result { + let work_dir = if let Some(work_dir) = &args.work_dir { + tracing::info!("Using work dir from cmd arg"); + work_dir.clone() + } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) { + tracing::info!("Using work dir from env var"); + env_work_dir.into() + } else { + work_dir()? + }; + tracing::info!("Using work dir: {}", work_dir.display()); + Ok(work_dir) +} + +/// Loads the application settings based on a configuration file and environment variables. +pub fn load_settings(work_dir: &Path, config_path: Option) -> Result { + // get config file name from args + let config_file_arg = match config_path { + Some(c) => c, + None => work_dir.join("config.toml"), + }; + + let mut settings = if config_file_arg.exists() { + config::Settings::new(Some(config_file_arg)) + } else { + tracing::info!("Config file does not exist. Attempting to read env vars"); + config::Settings::default() + }; + + // This check for any settings defined in ENV VARs + // ENV VARS will take **priority** over those in the config + settings.from_env() +} + +async fn setup_database( + settings: &config::Settings, + work_dir: &Path, + db_password: Option, +) -> Result<( + Arc + Send + Sync>, + Arc + Send + Sync>, +)> { + match settings.database.engine { + DatabaseEngine::Sqlite => { + let db = setup_sqlite_database(work_dir, db_password).await?; + let localstore: Arc + Send + Sync> = db.clone(); + let keystore: Arc + Send + Sync> = db; + Ok((localstore, keystore)) + } + } +} + +async fn setup_sqlite_database( + work_dir: &Path, + _password: Option, +) -> Result> { + let sql_db_path = work_dir.join("cdk-mintd.sqlite"); + + #[cfg(not(feature = "sqlcipher"))] + let db = MintSqliteDatabase::new(&sql_db_path).await?; + #[cfg(feature = "sqlcipher")] + let db = { + // Get password from command line arguments for sqlcipher + MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await? + }; + + Ok(Arc::new(db)) +} + +/** + * Configures a `MintBuilder` instance with provided settings and initializes + * routers for Lightning Network backends. + */ +async fn configure_mint_builder( + settings: &config::Settings, + mint_builder: MintBuilder, +) -> Result<(MintBuilder, Vec)> { + let mut ln_routers = vec![]; + + // Configure basic mint information + let mint_builder = configure_basic_info(settings, mint_builder); + + // Configure lightning backend + let mint_builder = configure_lightning_backend(settings, mint_builder, &mut ln_routers).await?; + + // Configure caching + let mint_builder = configure_cache(settings, mint_builder); + + Ok((mint_builder, ln_routers)) +} + +/// Configures basic mint information (name, contact info, descriptions, etc.) +fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder { + // Add contact information + let mut contacts = Vec::new(); + if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key { + contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string())); + } + if let Some(email) = &settings.mint_info.contact_email { + contacts.push(ContactInfo::new("email".to_string(), email.to_string())); + } + + // Add version information + let mint_version = MintVersion::new( + "cdk-mintd".to_string(), + CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(), + ); + + // Configure mint builder with basic info + let mut builder = mint_builder + .with_name(settings.mint_info.name.clone()) + .with_version(mint_version) + .with_description(settings.mint_info.description.clone()); + + // Add optional information + if let Some(long_description) = &settings.mint_info.description_long { + builder = builder.with_long_description(long_description.to_string()); + } + + for contact in contacts { + builder = builder.with_contact_info(contact); + } + + if let Some(pubkey) = settings.mint_info.pubkey { + builder = builder.with_pubkey(pubkey); + } + + if let Some(icon_url) = &settings.mint_info.icon_url { + builder = builder.with_icon_url(icon_url.to_string()); + } + + if let Some(motd) = &settings.mint_info.motd { + builder = builder.with_motd(motd.to_string()); + } + + if let Some(tos_url) = &settings.mint_info.tos_url { + builder = builder.with_tos_url(tos_url.to_string()); + } + + builder +} +/// Configures Lightning Network backend based on the specified backend type +async fn configure_lightning_backend( + settings: &config::Settings, + mut mint_builder: MintBuilder, + ln_routers: &mut Vec, +) -> Result { + let mint_melt_limits = MintMeltLimits { + mint_min: settings.ln.min_mint, + mint_max: settings.ln.max_mint, + melt_min: settings.ln.min_melt, + melt_max: settings.ln.max_melt, + }; + + tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend); + + match settings.ln.ln_backend { + #[cfg(feature = "cln")] + LnBackend::Cln => { + let cln_settings = settings + .cln + .clone() + .expect("Config checked at load that cln is some"); + let cln = cln_settings + .setup(ln_routers, settings, CurrencyUnit::Msat) + .await?; + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + CurrencyUnit::Sat, + mint_melt_limits, + Arc::new(cln), + ) + .await?; + } + #[cfg(feature = "lnbits")] + LnBackend::LNbits => { + let lnbits_settings = settings.clone().lnbits.expect("Checked on config load"); + let lnbits = lnbits_settings + .setup(ln_routers, settings, CurrencyUnit::Sat) + .await?; + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + CurrencyUnit::Sat, + mint_melt_limits, + Arc::new(lnbits), + ) + .await?; + } + #[cfg(feature = "lnd")] + LnBackend::Lnd => { + let lnd_settings = settings.clone().lnd.expect("Checked at config load"); + let lnd = lnd_settings + .setup(ln_routers, settings, CurrencyUnit::Msat) + .await?; + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + CurrencyUnit::Sat, + mint_melt_limits, + Arc::new(lnd), + ) + .await?; + } + #[cfg(feature = "fakewallet")] + LnBackend::FakeWallet => { + let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); + tracing::info!("Using fake wallet: {:?}", fake_wallet); + + for unit in fake_wallet.clone().supported_units { + let fake = fake_wallet + .setup(ln_routers, settings, CurrencyUnit::Sat) + .await?; + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + unit.clone(), + mint_melt_limits, + Arc::new(fake), + ) + .await?; + } + } + #[cfg(feature = "grpc-processor")] + LnBackend::GrpcProcessor => { + let grpc_processor = settings + .clone() + .grpc_processor + .expect("grpc processor config defined"); + + tracing::info!( + "Attempting to start with gRPC payment processor at {}:{}.", + grpc_processor.addr, + grpc_processor.port + ); + + for unit in grpc_processor.clone().supported_units { + tracing::debug!("Adding unit: {:?}", unit); + let processor = grpc_processor + .setup(ln_routers, settings, unit.clone()) + .await?; + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + unit.clone(), + mint_melt_limits, + Arc::new(processor), + ) + .await?; + } + } + LnBackend::None => { + tracing::error!( + "Payment backend was not set or feature disabled. {:?}", + settings.ln.ln_backend + ); + bail!("Lightning backend must be configured"); + } + }; + + Ok(mint_builder) +} + +/// Helper function to configure a mint builder with a lightning backend for a specific currency unit +async fn configure_backend_for_unit( + settings: &config::Settings, + mut mint_builder: MintBuilder, + unit: cdk::nuts::CurrencyUnit, + mint_melt_limits: MintMeltLimits, + backend: Arc + Send + Sync>, +) -> Result { + let payment_settings = backend.get_settings().await?; + + if let Some(bolt12) = payment_settings.get("bolt12") { + if bolt12.as_bool().unwrap_or_default() { + mint_builder + .add_payment_processor( + unit.clone(), + PaymentMethod::Bolt12, + mint_melt_limits, + Arc::clone(&backend), + ) + .await?; + } + } + + mint_builder + .add_payment_processor( + unit.clone(), + PaymentMethod::Bolt11, + mint_melt_limits, + backend, + ) + .await?; + + if let Some(input_fee) = settings.info.input_fee_ppk { + mint_builder.set_unit_fee(&unit, input_fee)?; + } + + let nut17_supported = SupportedMethods::default_bolt11(unit); + mint_builder = mint_builder.with_supported_websockets(nut17_supported); + + Ok(mint_builder) +} + +/// Configures cache settings +fn configure_cache(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder { + let cached_endpoints = vec![ + CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), + CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11), + CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap), + ]; + + let cache: HttpCache = settings.info.http_cache.clone().into(); + mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints) +} + +#[cfg(feature = "auth")] +async fn setup_authentication( + settings: &config::Settings, + work_dir: &Path, + mut mint_builder: MintBuilder, + _password: Option, +) -> Result { + if let Some(auth_settings) = settings.auth.clone() { + tracing::info!("Auth settings are defined. {:?}", auth_settings); + let auth_localstore: Arc< + dyn cdk_database::MintAuthDatabase + Send + Sync, + > = match settings.database.engine { + DatabaseEngine::Sqlite => { + let sql_db_path = work_dir.join("cdk-mintd-auth.sqlite"); + #[cfg(not(feature = "sqlcipher"))] + let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?; + #[cfg(feature = "sqlcipher")] + let sqlite_db = { + // Get password from command line arguments for sqlcipher + MintSqliteAuthDatabase::new((sql_db_path, _password.unwrap())).await? + }; + + Arc::new(sqlite_db) + } + }; + + let mint_blind_auth_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth); + + let mut protected_endpoints = HashMap::new(); + + protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear); + + let mut blind_auth_endpoints = vec![]; + let mut unprotected_endpoints = vec![]; + + { + let mint_quote_protected_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11); + let mint_protected_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11); + if auth_settings.enabled_mint { + protected_endpoints.insert(mint_quote_protected_endpoint, AuthRequired::Blind); + + protected_endpoints.insert(mint_protected_endpoint, AuthRequired::Blind); + + blind_auth_endpoints.push(mint_quote_protected_endpoint); + blind_auth_endpoints.push(mint_protected_endpoint); + } else { + unprotected_endpoints.push(mint_protected_endpoint); + unprotected_endpoints.push(mint_quote_protected_endpoint); + } + } + + { + let melt_quote_protected_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11); + let melt_protected_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11); + + if auth_settings.enabled_melt { + protected_endpoints.insert(melt_quote_protected_endpoint, AuthRequired::Blind); + protected_endpoints.insert(melt_protected_endpoint, AuthRequired::Blind); + + blind_auth_endpoints.push(melt_quote_protected_endpoint); + blind_auth_endpoints.push(melt_protected_endpoint); + } else { + unprotected_endpoints.push(melt_quote_protected_endpoint); + unprotected_endpoints.push(melt_protected_endpoint); + } + } + + { + let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap); + + if auth_settings.enabled_swap { + protected_endpoints.insert(swap_protected_endpoint, AuthRequired::Blind); + blind_auth_endpoints.push(swap_protected_endpoint); + } else { + unprotected_endpoints.push(swap_protected_endpoint); + } + } + + { + let check_mint_protected_endpoint = + ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11); + + if auth_settings.enabled_check_mint_quote { + protected_endpoints.insert(check_mint_protected_endpoint, AuthRequired::Blind); + blind_auth_endpoints.push(check_mint_protected_endpoint); + } else { + unprotected_endpoints.push(check_mint_protected_endpoint); + } + } + + { + let check_melt_protected_endpoint = + ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11); + + if auth_settings.enabled_check_melt_quote { + protected_endpoints.insert(check_melt_protected_endpoint, AuthRequired::Blind); + blind_auth_endpoints.push(check_melt_protected_endpoint); + } else { + unprotected_endpoints.push(check_melt_protected_endpoint); + } + } + + { + let restore_protected_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::Restore); + + if auth_settings.enabled_restore { + protected_endpoints.insert(restore_protected_endpoint, AuthRequired::Blind); + blind_auth_endpoints.push(restore_protected_endpoint); + } else { + unprotected_endpoints.push(restore_protected_endpoint); + } + } + + { + let state_protected_endpoint = + ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate); + + if auth_settings.enabled_check_proof_state { + protected_endpoints.insert(state_protected_endpoint, AuthRequired::Blind); + blind_auth_endpoints.push(state_protected_endpoint); + } else { + unprotected_endpoints.push(state_protected_endpoint); + } + } + + mint_builder = mint_builder.with_auth( + auth_localstore.clone(), + auth_settings.openid_discovery, + auth_settings.openid_client_id, + vec![mint_blind_auth_endpoint], + ); + mint_builder = + mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints); + + let mut tx = auth_localstore.begin_transaction().await?; + + tx.remove_protected_endpoints(unprotected_endpoints).await?; + tx.add_protected_endpoints(protected_endpoints).await?; + tx.commit().await?; + } + Ok(mint_builder) +} + +/// Build mints with the configured the signing method (remote signatory or local seed) +async fn build_mint( + settings: &config::Settings, + keystore: Arc + Send + Sync>, + mint_builder: MintBuilder, +) -> Result { + if let Some(signatory_url) = settings.info.signatory_url.clone() { + tracing::info!( + "Connecting to remote signatory to {} with certs {:?}", + signatory_url, + settings.info.signatory_certs.clone() + ); + + Ok(mint_builder + .build_with_signatory(Arc::new( + cdk_signatory::SignatoryRpcClient::new( + signatory_url, + settings.info.signatory_certs.clone(), + ) + .await?, + )) + .await?) + } else if let Some(mnemonic) = settings + .info + .mnemonic + .clone() + .map(|s| Mnemonic::from_str(&s)) + .transpose()? + { + Ok(mint_builder + .build_with_seed(keystore, &mnemonic.to_seed_normalized("")) + .await?) + } else { + bail!("No seed nor remote signatory set"); + } +} + +async fn start_services_with_shutdown( + mint: Arc, + settings: &config::Settings, + ln_routers: Vec, + work_dir: &Path, + mint_builder_info: cdk::nuts::MintInfo, + shutdown_signal: impl std::future::Future + Send + 'static, +) -> Result<()> { + let listen_addr = settings.info.listen_host.clone(); + let listen_port = settings.info.listen_port; + let cache: HttpCache = settings.info.http_cache.clone().into(); + + #[cfg(feature = "management-rpc")] + let mut rpc_enabled = false; + #[cfg(not(feature = "management-rpc"))] + let rpc_enabled = false; + + #[cfg(feature = "management-rpc")] + let mut rpc_server: Option = None; + + #[cfg(feature = "management-rpc")] + { + if let Some(rpc_settings) = settings.mint_management_rpc.clone() { + if rpc_settings.enabled { + let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string()); + let port = rpc_settings.port.unwrap_or(8086); + let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?; + + let tls_dir = rpc_settings.tls_dir_path.unwrap_or(work_dir.join("tls")); + + if !tls_dir.exists() { + tracing::error!("TLS directory does not exist: {}", tls_dir.display()); + bail!("Cannot start RPC server: TLS directory does not exist"); + } + + mint_rpc.start(Some(tls_dir)).await?; + + rpc_server = Some(mint_rpc); + + rpc_enabled = true; + } + } + } + + if rpc_enabled { + if mint.mint_info().await.is_err() { + tracing::info!("Mint info not set on mint, setting."); + mint.set_mint_info(mint_builder_info).await?; + mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; + } else { + if mint.localstore().get_quote_ttl().await.is_err() { + mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; + } + // Add version information + let mint_version = MintVersion::new( + "cdk-mintd".to_string(), + CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(), + ); + let mut stored_mint_info = mint.mint_info().await?; + stored_mint_info.version = Some(mint_version); + mint.set_mint_info(stored_mint_info).await?; + + tracing::info!("Mint info already set, not using config file settings."); + } + } else { + tracing::warn!("RPC not enabled, using mint info from config."); + mint.set_mint_info(mint_builder_info).await?; + mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; + } + + let mint_info = mint.mint_info().await?; + let nut04_methods = mint_info.nuts.nut04.supported_methods(); + let nut05_methods = mint_info.nuts.nut05.supported_methods(); + + let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12) + || nut05_methods.contains(&&PaymentMethod::Bolt12); + + let v1_service = + cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported) + .await?; + + let mut mint_service = Router::new() + .merge(v1_service) + .layer( + ServiceBuilder::new() + .layer(RequestDecompressionLayer::new()) + .layer(CompressionLayer::new()), + ) + .layer(TraceLayer::new_for_http()); + + #[cfg(feature = "swagger")] + { + if settings.info.enable_swagger_ui.unwrap_or(false) { + mint_service = mint_service.merge( + utoipa_swagger_ui::SwaggerUi::new("/swagger-ui") + .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()), + ); + } + } + + for router in ln_routers { + mint_service = mint_service.merge(router); + } + + mint.start().await?; + + let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?; + + let listener = tokio::net::TcpListener::bind(socket_addr).await?; + + tracing::debug!("listening on {}", listener.local_addr().unwrap()); + + // Wait for axum server to complete with custom shutdown signal + let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(shutdown_signal); + + match axum_result.await { + Ok(_) => { + tracing::info!("Axum server stopped with okay status"); + } + Err(err) => { + tracing::warn!("Axum server stopped with error"); + tracing::error!("{}", err); + bail!("Axum exited with error") + } + } + + mint.stop().await?; + + #[cfg(feature = "management-rpc")] + { + if let Some(rpc_server) = rpc_server { + rpc_server.stop().await?; + } + } + + Ok(()) +} + +async fn shutdown_signal() { + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C handler"); + tracing::info!("Shutdown signal received"); +} + +fn work_dir() -> Result { + let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; + let dir = home_dir.join(".cdk-mintd"); + + std::fs::create_dir_all(&dir)?; + + Ok(dir) +} + +/// The main entry point for the application when used as a library. +/// +/// This asynchronous function performs the following steps: +/// 1. Executes the initial setup, including loading configurations and initializing the database. +/// 2. Configures a `MintBuilder` instance with the local store and keystore based on the database. +/// 3. Applies additional custom configurations and authentication setup for the `MintBuilder`. +/// 4. Constructs a `Mint` instance from the configured `MintBuilder`. +/// 5. Checks and resolves the status of any pending mint and melt quotes. +pub async fn run_mintd( + work_dir: &Path, + settings: &config::Settings, + db_password: Option, +) -> Result<()> { + run_mintd_with_shutdown(work_dir, settings, shutdown_signal(), db_password).await +} + +/// Run mintd with a custom shutdown signal +pub async fn run_mintd_with_shutdown( + work_dir: &Path, + settings: &config::Settings, + shutdown_signal: impl std::future::Future + Send + 'static, + db_password: Option, +) -> Result<()> { + let (localstore, keystore) = initial_setup(work_dir, settings, db_password.clone()).await?; + + let mint_builder = MintBuilder::new(localstore); + + let (mint_builder, ln_routers) = configure_mint_builder(settings, mint_builder).await?; + #[cfg(feature = "auth")] + let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?; + + let mint = build_mint(settings, keystore, mint_builder).await?; + + tracing::debug!("Mint built from builder."); + + let mint = Arc::new(mint); + + // Checks the status of all pending melt quotes + // Pending melt quotes where the payment has gone through inputs are burnt + // Pending melt quotes where the payment has **failed** inputs are reset to unspent + mint.check_pending_melt_quotes().await?; + + start_services_with_shutdown( + mint.clone(), + settings, + ln_routers, + work_dir, + mint.mint_info().await?, + shutdown_signal, + ) + .await?; + + Ok(()) +} diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index ab332c6b..8e8bdaeb 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -2,63 +2,6 @@ #![warn(missing_docs)] #![warn(rustdoc::bare_urls)] -// std -#[cfg(feature = "auth")] -use std::collections::HashMap; -use std::env; -use std::net::SocketAddr; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::Arc; - -// external crates -use anyhow::{anyhow, bail, Result}; -use axum::Router; -use bip39::Mnemonic; -// internal crate modules -use cdk::cdk_database::{self, MintDatabase, MintKeysDatabase}; -use cdk::cdk_payment; -use cdk::cdk_payment::MintPayment; -use cdk::mint::{Mint, MintBuilder, MintMeltLimits}; -#[cfg(any( - feature = "cln", - feature = "lnbits", - feature = "lnd", - feature = "fakewallet", - feature = "grpc-processor" -))] -use cdk::nuts::nut17::SupportedMethods; -use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}; -#[cfg(any( - feature = "cln", - feature = "lnbits", - feature = "lnd", - feature = "fakewallet" -))] -use cdk::nuts::CurrencyUnit; -#[cfg(feature = "auth")] -use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath}; -use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod}; -use cdk::types::QuoteTTL; -use cdk_axum::cache::HttpCache; -use cdk_mintd::cli::CLIArgs; -use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; -use cdk_mintd::env_vars::ENV_WORK_DIR; -use cdk_mintd::setup::LnBackendSetup; -#[cfg(feature = "auth")] -use cdk_sqlite::mint::MintSqliteAuthDatabase; -use cdk_sqlite::MintSqliteDatabase; -use clap::Parser; -use tower::ServiceBuilder; -use tower_http::compression::CompressionLayer; -use tower_http::decompression::RequestDecompressionLayer; -use tower_http::trace::TraceLayer; -use tracing_subscriber::EnvFilter; -#[cfg(feature = "swagger")] -use utoipa::OpenApi; - -const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); - // Ensure at least one lightning backend is enabled at compile time #[cfg(not(any( feature = "cln", @@ -71,737 +14,29 @@ compile_error!( "At least one lightning backend feature must be enabled: cln, lnbits, lnd, fakewallet, or grpc-processor" ); -/// The main entry point for the application. -/// -/// This asynchronous function performs the following steps: -/// 1. Executes the initial setup, including loading configurations and initializing the database. -/// 2. Configures a `MintBuilder` instance with the local store and keystore based on the database. -/// 3. Applies additional custom configurations and authentication setup for the `MintBuilder`. -/// 4. Constructs a `Mint` instance from the configured `MintBuilder`. -/// 5. Checks and resolves the status of any pending mint and melt quotes. -#[tokio::main] +use anyhow::Result; +use cdk_mintd::cli::CLIArgs; +use cdk_mintd::{get_work_directory, load_settings, setup_tracing}; +use clap::Parser; +use tokio::main; + +#[main] async fn main() -> Result<()> { - let (work_dir, settings, localstore, keystore) = initial_setup().await?; - - let mint_builder = MintBuilder::new(localstore); - - let (mint_builder, ln_routers) = configure_mint_builder(&settings, mint_builder).await?; - #[cfg(feature = "auth")] - let mint_builder = setup_authentication(&settings, &work_dir, mint_builder).await?; - - let mint = build_mint(&settings, keystore, mint_builder).await?; - - tracing::debug!("Mint built from builder."); - - let mint = Arc::new(mint); - - // Checks the status of all pending melt quotes - // Pending melt quotes where the payment has gone through inputs are burnt - // Pending melt quotes where the payment has **failed** inputs are reset to unspent - mint.check_pending_melt_quotes().await?; - - start_services( - mint.clone(), - &settings, - ln_routers, - &work_dir, - mint.mint_info().await?, - ) - .await?; - - Ok(()) -} - -/// Performs the initial setup for the application, including configuring tracing, -/// parsing CLI arguments, setting up the working directory, loading settings, -/// and initializing the database connection. -async fn initial_setup() -> Result<( - PathBuf, - config::Settings, - Arc + Send + Sync>, - Arc + Send + Sync>, -)> { - setup_tracing(); let args = CLIArgs::parse(); + + if args.enable_logging { + setup_tracing(); + } + let work_dir = get_work_directory(&args).await?; let settings = load_settings(&work_dir, args.config)?; - let (localstore, keystore) = setup_database(&settings, &work_dir).await?; - Ok((work_dir, settings, localstore, keystore)) -} -/// Sets up and initializes a tracing subscriber with custom log filtering. -fn setup_tracing() { - let default_filter = "debug"; - let sqlx_filter = "sqlx=warn"; - let hyper_filter = "hyper=warn"; - let h2_filter = "h2=warn"; - let tower_http = "tower_http=warn"; - - let env_filter = EnvFilter::new(format!( - "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{tower_http}" - )); - - tracing_subscriber::fmt().with_env_filter(env_filter).init(); -} - -/// Retrieves the work directory based on command-line arguments, environment variables, or system defaults. -async fn get_work_directory(args: &CLIArgs) -> Result { - let work_dir = if let Some(work_dir) = &args.work_dir { - tracing::info!("Using work dir from cmd arg"); - work_dir.clone() - } else if let Ok(env_work_dir) = env::var(ENV_WORK_DIR) { - tracing::info!("Using work dir from env var"); - env_work_dir.into() - } else { - work_dir()? - }; - tracing::info!("Using work dir: {}", work_dir.display()); - Ok(work_dir) -} - -/// Loads the application settings based on a configuration file and environment variables. -fn load_settings(work_dir: &Path, config_path: Option) -> Result { - // get config file name from args - let config_file_arg = match config_path { - Some(c) => c, - None => work_dir.join("config.toml"), - }; - - let mut settings = if config_file_arg.exists() { - config::Settings::new(Some(config_file_arg)) - } else { - tracing::info!("Config file does not exist. Attempting to read env vars"); - config::Settings::default() - }; - - // This check for any settings defined in ENV VARs - // ENV VARS will take **priority** over those in the config - settings.from_env() -} - -async fn setup_database( - settings: &config::Settings, - work_dir: &Path, -) -> Result<( - Arc + Send + Sync>, - Arc + Send + Sync>, -)> { - match settings.database.engine { - DatabaseEngine::Sqlite => { - #[cfg(feature = "sqlcipher")] - let password = CLIArgs::parse().password; - #[cfg(not(feature = "sqlcipher"))] - let password = String::new(); - let db = setup_sqlite_database(work_dir, Some(password)).await?; - let localstore: Arc + Send + Sync> = db.clone(); - let keystore: Arc + Send + Sync> = db; - Ok((localstore, keystore)) - } - } -} - -async fn setup_sqlite_database( - work_dir: &Path, - _password: Option, -) -> Result> { - let sql_db_path = work_dir.join("cdk-mintd.sqlite"); - #[cfg(not(feature = "sqlcipher"))] - let db = MintSqliteDatabase::new(&sql_db_path).await?; #[cfg(feature = "sqlcipher")] - let db = { - // Get password from command line arguments for sqlcipher - MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await? - }; - Ok(Arc::new(db)) -} - -/** - * Configures a `MintBuilder` instance with provided settings and initializes - * routers for Lightning Network backends. - */ -async fn configure_mint_builder( - settings: &config::Settings, - mint_builder: MintBuilder, -) -> Result<(MintBuilder, Vec)> { - let mut ln_routers = vec![]; - - // Configure basic mint information - let mint_builder = configure_basic_info(settings, mint_builder); - - // Configure lightning backend - let mint_builder = configure_lightning_backend(settings, mint_builder, &mut ln_routers).await?; - - // Configure caching - let mint_builder = configure_cache(settings, mint_builder); - - Ok((mint_builder, ln_routers)) -} - -/// Configures basic mint information (name, contact info, descriptions, etc.) -fn configure_basic_info(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder { - // Add contact information - let mut contacts = Vec::new(); - if let Some(nostr_key) = &settings.mint_info.contact_nostr_public_key { - contacts.push(ContactInfo::new("nostr".to_string(), nostr_key.to_string())); - } - if let Some(email) = &settings.mint_info.contact_email { - contacts.push(ContactInfo::new("email".to_string(), email.to_string())); - } - - // Add version information - let mint_version = MintVersion::new( - "cdk-mintd".to_string(), - CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(), - ); - - // Configure mint builder with basic info - let mut builder = mint_builder - .with_name(settings.mint_info.name.clone()) - .with_version(mint_version) - .with_description(settings.mint_info.description.clone()); - - // Add optional information - if let Some(long_description) = &settings.mint_info.description_long { - builder = builder.with_long_description(long_description.to_string()); - } - - for contact in contacts { - builder = builder.with_contact_info(contact); - } - - if let Some(pubkey) = settings.mint_info.pubkey { - builder = builder.with_pubkey(pubkey); - } - - if let Some(icon_url) = &settings.mint_info.icon_url { - builder = builder.with_icon_url(icon_url.to_string()); - } - - if let Some(motd) = &settings.mint_info.motd { - builder = builder.with_motd(motd.to_string()); - } - - if let Some(tos_url) = &settings.mint_info.tos_url { - builder = builder.with_tos_url(tos_url.to_string()); - } - - builder -} -/// Configures Lightning Network backend based on the specified backend type -async fn configure_lightning_backend( - settings: &config::Settings, - mut mint_builder: MintBuilder, - ln_routers: &mut Vec, -) -> Result { - let mint_melt_limits = MintMeltLimits { - mint_min: settings.ln.min_mint, - mint_max: settings.ln.max_mint, - melt_min: settings.ln.min_melt, - melt_max: settings.ln.max_melt, - }; - - tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend); - - match settings.ln.ln_backend { - #[cfg(feature = "cln")] - LnBackend::Cln => { - let cln_settings = settings - .cln - .clone() - .expect("Config checked at load that cln is some"); - let cln = cln_settings - .setup(ln_routers, settings, CurrencyUnit::Msat) - .await?; - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(cln), - ) - .await?; - } - #[cfg(feature = "lnbits")] - LnBackend::LNbits => { - let lnbits_settings = settings.clone().lnbits.expect("Checked on config load"); - let lnbits = lnbits_settings - .setup(ln_routers, settings, CurrencyUnit::Sat) - .await?; - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(lnbits), - ) - .await?; - } - #[cfg(feature = "lnd")] - LnBackend::Lnd => { - let lnd_settings = settings.clone().lnd.expect("Checked at config load"); - let lnd = lnd_settings - .setup(ln_routers, settings, CurrencyUnit::Msat) - .await?; - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(lnd), - ) - .await?; - } - #[cfg(feature = "fakewallet")] - LnBackend::FakeWallet => { - let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); - tracing::info!("Using fake wallet: {:?}", fake_wallet); - - for unit in fake_wallet.clone().supported_units { - let fake = fake_wallet - .setup(ln_routers, settings, CurrencyUnit::Sat) - .await?; - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - unit.clone(), - mint_melt_limits, - Arc::new(fake), - ) - .await?; - } - } - #[cfg(feature = "grpc-processor")] - LnBackend::GrpcProcessor => { - let grpc_processor = settings - .clone() - .grpc_processor - .expect("grpc processor config defined"); - - tracing::info!( - "Attempting to start with gRPC payment processor at {}:{}.", - grpc_processor.addr, - grpc_processor.port - ); - - for unit in grpc_processor.clone().supported_units { - tracing::debug!("Adding unit: {:?}", unit); - let processor = grpc_processor - .setup(ln_routers, settings, unit.clone()) - .await?; - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - unit.clone(), - mint_melt_limits, - Arc::new(processor), - ) - .await?; - } - } - LnBackend::None => { - tracing::error!( - "Payment backend was not set or feature disabled. {:?}", - settings.ln.ln_backend - ); - bail!("Lightning backend must be configured"); - } - }; - - Ok(mint_builder) -} - -/// Helper function to configure a mint builder with a lightning backend for a specific currency unit -async fn configure_backend_for_unit( - settings: &config::Settings, - mut mint_builder: MintBuilder, - unit: cdk::nuts::CurrencyUnit, - mint_melt_limits: MintMeltLimits, - backend: Arc + Send + Sync>, -) -> Result { - let payment_settings = backend.get_settings().await?; - - if let Some(bolt12) = payment_settings.get("bolt12") { - if bolt12.as_bool().unwrap_or_default() { - mint_builder - .add_payment_processor( - unit.clone(), - PaymentMethod::Bolt12, - mint_melt_limits, - Arc::clone(&backend), - ) - .await?; - } - } - - mint_builder - .add_payment_processor( - unit.clone(), - PaymentMethod::Bolt11, - mint_melt_limits, - backend, - ) - .await?; - - if let Some(input_fee) = settings.info.input_fee_ppk { - mint_builder.set_unit_fee(&unit, input_fee)?; - } - - let nut17_supported = SupportedMethods::default_bolt11(unit); - mint_builder = mint_builder.with_supported_websockets(nut17_supported); - - Ok(mint_builder) -} - -/// Configures cache settings -fn configure_cache(settings: &config::Settings, mint_builder: MintBuilder) -> MintBuilder { - let cached_endpoints = vec![ - CachedEndpoint::new(NUT19Method::Post, NUT19Path::MintBolt11), - CachedEndpoint::new(NUT19Method::Post, NUT19Path::MeltBolt11), - CachedEndpoint::new(NUT19Method::Post, NUT19Path::Swap), - ]; - - let cache: HttpCache = settings.info.http_cache.clone().into(); - mint_builder.with_cache(Some(cache.ttl.as_secs()), cached_endpoints) -} - -#[cfg(feature = "auth")] -async fn setup_authentication( - settings: &config::Settings, - work_dir: &Path, - mut mint_builder: MintBuilder, -) -> Result { - if let Some(auth_settings) = settings.auth.clone() { - tracing::info!("Auth settings are defined. {:?}", auth_settings); - let auth_localstore: Arc< - dyn cdk_database::MintAuthDatabase + Send + Sync, - > = match settings.database.engine { - DatabaseEngine::Sqlite => { - let sql_db_path = work_dir.join("cdk-mintd-auth.sqlite"); - #[cfg(feature = "sqlcipher")] - let password = CLIArgs::parse().password; - #[cfg(feature = "sqlcipher")] - let sqlite_db = MintSqliteAuthDatabase::new((sql_db_path, password)).await?; - #[cfg(not(feature = "sqlcipher"))] - let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?; - Arc::new(sqlite_db) - } - }; - - let mint_blind_auth_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth); - - let mut protected_endpoints = HashMap::new(); - - protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear); - - let mut blind_auth_endpoints = vec![]; - let mut unprotected_endpoints = vec![]; - - { - let mint_quote_protected_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11); - let mint_protected_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11); - if auth_settings.enabled_mint { - protected_endpoints.insert(mint_quote_protected_endpoint, AuthRequired::Blind); - - protected_endpoints.insert(mint_protected_endpoint, AuthRequired::Blind); - - blind_auth_endpoints.push(mint_quote_protected_endpoint); - blind_auth_endpoints.push(mint_protected_endpoint); - } else { - unprotected_endpoints.push(mint_protected_endpoint); - unprotected_endpoints.push(mint_quote_protected_endpoint); - } - } - - { - let melt_quote_protected_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11); - let melt_protected_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11); - - if auth_settings.enabled_melt { - protected_endpoints.insert(melt_quote_protected_endpoint, AuthRequired::Blind); - protected_endpoints.insert(melt_protected_endpoint, AuthRequired::Blind); - - blind_auth_endpoints.push(melt_quote_protected_endpoint); - blind_auth_endpoints.push(melt_protected_endpoint); - } else { - unprotected_endpoints.push(melt_quote_protected_endpoint); - unprotected_endpoints.push(melt_protected_endpoint); - } - } - - { - let swap_protected_endpoint = ProtectedEndpoint::new(Method::Post, RoutePath::Swap); - - if auth_settings.enabled_swap { - protected_endpoints.insert(swap_protected_endpoint, AuthRequired::Blind); - blind_auth_endpoints.push(swap_protected_endpoint); - } else { - unprotected_endpoints.push(swap_protected_endpoint); - } - } - - { - let check_mint_protected_endpoint = - ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11); - - if auth_settings.enabled_check_mint_quote { - protected_endpoints.insert(check_mint_protected_endpoint, AuthRequired::Blind); - blind_auth_endpoints.push(check_mint_protected_endpoint); - } else { - unprotected_endpoints.push(check_mint_protected_endpoint); - } - } - - { - let check_melt_protected_endpoint = - ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11); - - if auth_settings.enabled_check_melt_quote { - protected_endpoints.insert(check_melt_protected_endpoint, AuthRequired::Blind); - blind_auth_endpoints.push(check_melt_protected_endpoint); - } else { - unprotected_endpoints.push(check_melt_protected_endpoint); - } - } - - { - let restore_protected_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::Restore); - - if auth_settings.enabled_restore { - protected_endpoints.insert(restore_protected_endpoint, AuthRequired::Blind); - blind_auth_endpoints.push(restore_protected_endpoint); - } else { - unprotected_endpoints.push(restore_protected_endpoint); - } - } - - { - let state_protected_endpoint = - ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate); - - if auth_settings.enabled_check_proof_state { - protected_endpoints.insert(state_protected_endpoint, AuthRequired::Blind); - blind_auth_endpoints.push(state_protected_endpoint); - } else { - unprotected_endpoints.push(state_protected_endpoint); - } - } - - mint_builder = mint_builder.with_auth( - auth_localstore.clone(), - auth_settings.openid_discovery, - auth_settings.openid_client_id, - vec![mint_blind_auth_endpoint], - ); - mint_builder = - mint_builder.with_blind_auth(auth_settings.mint_max_bat, blind_auth_endpoints); - - let mut tx = auth_localstore.begin_transaction().await?; - - tx.remove_protected_endpoints(unprotected_endpoints).await?; - tx.add_protected_endpoints(protected_endpoints).await?; - tx.commit().await?; - } - Ok(mint_builder) -} - -/// Build mints with the configured the signing method (remote signatory or local seed) -async fn build_mint( - settings: &config::Settings, - keystore: Arc + Send + Sync>, - mint_builder: MintBuilder, -) -> Result { - if let Some(signatory_url) = settings.info.signatory_url.clone() { - tracing::info!( - "Connecting to remote signatory to {} with certs {:?}", - signatory_url, - settings.info.signatory_certs.clone() - ); - - Ok(mint_builder - .build_with_signatory(Arc::new( - cdk_signatory::SignatoryRpcClient::new( - signatory_url, - settings.info.signatory_certs.clone(), - ) - .await?, - )) - .await?) - } else if let Some(mnemonic) = settings - .info - .mnemonic - .clone() - .map(|s| Mnemonic::from_str(&s)) - .transpose()? - { - Ok(mint_builder - .build_with_seed(keystore, &mnemonic.to_seed_normalized("")) - .await?) - } else { - bail!("No seed nor remote signatory set"); - } -} - -async fn start_services( - mint: Arc, - settings: &config::Settings, - ln_routers: Vec, - _work_dir: &Path, - mint_builder_info: cdk::nuts::MintInfo, -) -> Result<()> { - let listen_addr = settings.info.listen_host.clone(); - let listen_port = settings.info.listen_port; - let cache: HttpCache = settings.info.http_cache.clone().into(); - - #[cfg(feature = "management-rpc")] - let mut rpc_enabled = false; - #[cfg(not(feature = "management-rpc"))] - let rpc_enabled = false; - - #[cfg(feature = "management-rpc")] - let mut rpc_server: Option = None; - - #[cfg(feature = "management-rpc")] - { - if let Some(rpc_settings) = settings.mint_management_rpc.clone() { - if rpc_settings.enabled { - let addr = rpc_settings.address.unwrap_or("127.0.0.1".to_string()); - let port = rpc_settings.port.unwrap_or(8086); - let mut mint_rpc = cdk_mint_rpc::MintRPCServer::new(&addr, port, mint.clone())?; - - let tls_dir = rpc_settings.tls_dir_path.unwrap_or(_work_dir.join("tls")); - - if !tls_dir.exists() { - tracing::error!("TLS directory does not exist: {}", tls_dir.display()); - bail!("Cannot start RPC server: TLS directory does not exist"); - } - - mint_rpc.start(Some(tls_dir)).await?; - - rpc_server = Some(mint_rpc); - - rpc_enabled = true; - } - } - } - - if rpc_enabled { - if mint.mint_info().await.is_err() { - tracing::info!("Mint info not set on mint, setting."); - mint.set_mint_info(mint_builder_info).await?; - mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; - } else { - if mint.localstore().get_quote_ttl().await.is_err() { - mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; - } - // Add version information - let mint_version = MintVersion::new( - "cdk-mintd".to_string(), - CARGO_PKG_VERSION.unwrap_or("Unknown").to_string(), - ); - let mut stored_mint_info = mint.mint_info().await?; - stored_mint_info.version = Some(mint_version); - mint.set_mint_info(stored_mint_info).await?; - - tracing::info!("Mint info already set, not using config file settings."); - } - } else { - tracing::warn!("RPC not enabled, using mint info from config."); - mint.set_mint_info(mint_builder_info).await?; - mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?; - } - - let mint_info = mint.mint_info().await?; - let nut04_methods = mint_info.nuts.nut04.supported_methods(); - let nut05_methods = mint_info.nuts.nut05.supported_methods(); - - let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12) - || nut05_methods.contains(&&PaymentMethod::Bolt12); - - let v1_service = - cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported) - .await?; - - let mut mint_service = Router::new() - .merge(v1_service) - .layer( - ServiceBuilder::new() - .layer(RequestDecompressionLayer::new()) - .layer(CompressionLayer::new()), - ) - .layer(TraceLayer::new_for_http()); - - #[cfg(feature = "swagger")] - { - if settings.info.enable_swagger_ui.unwrap_or(false) { - mint_service = mint_service.merge( - utoipa_swagger_ui::SwaggerUi::new("/swagger-ui") - .url("/api-docs/openapi.json", cdk_axum::ApiDoc::openapi()), - ); - } - } - - for router in ln_routers { - mint_service = mint_service.merge(router); - } - - mint.start().await?; - - let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?; - - let listener = tokio::net::TcpListener::bind(socket_addr).await?; - - tracing::debug!("listening on {}", listener.local_addr().unwrap()); - - // Wait for axum server to complete - let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(shutdown_signal()); - - match axum_result.await { - Ok(_) => { - tracing::info!("Axum server stopped with okay status"); - } - Err(err) => { - tracing::warn!("Axum server stopped with error"); - tracing::error!("{}", err); - bail!("Axum exited with error") - } - } - - mint.stop().await?; - - #[cfg(feature = "management-rpc")] - { - if let Some(rpc_server) = rpc_server { - rpc_server.stop().await?; - } - } - - Ok(()) -} - -async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C handler"); - tracing::info!("Shutdown signal received"); -} - -fn work_dir() -> Result { - let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; - let dir = home_dir.join(".cdk-mintd"); - - std::fs::create_dir_all(&dir)?; - - Ok(dir) + let password = Some(CLIArgs::parse().password); + + #[cfg(not(feature = "sqlcipher"))] + let password = None; + + cdk_mintd::run_mintd(&work_dir, &settings, password).await } diff --git a/crates/cdk-payment-processor/Cargo.toml b/crates/cdk-payment-processor/Cargo.toml index 605ff211..db798352 100644 --- a/crates/cdk-payment-processor/Cargo.toml +++ b/crates/cdk-payment-processor/Cargo.toml @@ -30,6 +30,7 @@ cdk-common = { workspace = true, features = ["mint"] } cdk-cln = { workspace = true, optional = true } cdk-lnd = { workspace = true, optional = true } cdk-fake-wallet = { workspace = true, optional = true } +clap = { workspace = true, features = ["derive"] } serde.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/cdk-payment-processor/src/bin/payment_processor.rs b/crates/cdk-payment-processor/src/bin/payment_processor.rs index 46d11bef..030ef0bb 100644 --- a/crates/cdk-payment-processor/src/bin/payment_processor.rs +++ b/crates/cdk-payment-processor/src/bin/payment_processor.rs @@ -14,11 +14,47 @@ use cdk_common::payment::{self, MintPayment}; use cdk_common::Amount; #[cfg(feature = "fake")] use cdk_fake_wallet::FakeWallet; +use clap::Parser; use serde::{Deserialize, Serialize}; #[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] use tokio::signal; use tracing_subscriber::EnvFilter; +/// Common CLI arguments for CDK binaries +#[derive(Parser, Debug)] +pub struct CommonArgs { + /// Enable logging (default is false) + #[arg(long, default_value_t = false)] + pub enable_logging: bool, + + /// Logging level when enabled (default is debug) + #[arg(long, default_value = "debug")] + pub log_level: tracing::Level, +} + +/// Initialize logging based on CLI arguments +pub fn init_logging(enable_logging: bool, log_level: tracing::Level) { + if enable_logging { + let default_filter = log_level.to_string(); + + // Common filters to reduce noise + let sqlx_filter = "sqlx=warn"; + let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; + let rustls_filter = "rustls=warn"; + let reqwest_filter = "reqwest=warn"; + + let env_filter = EnvFilter::new(format!( + "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter},{reqwest_filter}" + )); + + // Ok if successful, Err if already initialized + let _ = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .try_init(); + } +} + pub const ENV_LN_BACKEND: &str = "CDK_PAYMENT_PROCESSOR_LN_BACKEND"; pub const ENV_LISTEN_HOST: &str = "CDK_PAYMENT_PROCESSOR_LISTEN_HOST"; pub const ENV_LISTEN_PORT: &str = "CDK_PAYMENT_PROCESSOR_LISTEN_PORT"; @@ -36,20 +72,20 @@ pub const ENV_LND_ADDRESS: &str = "CDK_PAYMENT_PROCESSOR_LND_ADDRESS"; pub const ENV_LND_CERT_FILE: &str = "CDK_PAYMENT_PROCESSOR_LND_CERT_FILE"; pub const ENV_LND_MACAROON_FILE: &str = "CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE"; +#[derive(Parser)] +#[command(name = "payment-processor")] +#[command(about = "CDK Payment Processor", long_about = None)] +struct Args { + #[command(flatten)] + common: CommonArgs, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { - let default_filter = "debug"; + let args = Args::parse(); - let sqlx_filter = "sqlx=warn"; - let hyper_filter = "hyper=warn"; - let h2_filter = "h2=warn"; - let rustls_filter = "rustls=warn"; - - let env_filter = EnvFilter::new(format!( - "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter}" - )); - - tracing_subscriber::fmt().with_env_filter(env_filter).init(); + // Initialize logging based on CLI arguments + init_logging(args.common.enable_logging, args.common.log_level); #[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] { diff --git a/crates/cdk-signatory/src/bin/cli/mod.rs b/crates/cdk-signatory/src/bin/cli/mod.rs index b389b31c..6dc90579 100644 --- a/crates/cdk-signatory/src/bin/cli/mod.rs +++ b/crates/cdk-signatory/src/bin/cli/mod.rs @@ -17,9 +17,43 @@ use cdk_signatory::{db_signatory, start_grpc_server}; #[cfg(feature = "sqlite")] use cdk_sqlite::MintSqliteDatabase; use clap::Parser; -use tracing::Level; use tracing_subscriber::EnvFilter; +/// Common CLI arguments for CDK binaries +#[derive(Parser, Debug)] +pub struct CommonArgs { + /// Enable logging (default is false) + #[arg(long, default_value_t = false)] + pub enable_logging: bool, + + /// Logging level when enabled (default is debug) + #[arg(long, default_value = "debug")] + pub log_level: tracing::Level, +} + +/// Initialize logging based on CLI arguments +pub fn init_logging(enable_logging: bool, log_level: tracing::Level) { + if enable_logging { + let default_filter = log_level.to_string(); + + // Common filters to reduce noise + let sqlx_filter = "sqlx=warn"; + let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; + let rustls_filter = "rustls=warn"; + let reqwest_filter = "reqwest=warn"; + + let env_filter = EnvFilter::new(format!( + "{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter},{reqwest_filter}" + )); + + // Ok if successful, Err if already initialized + let _ = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .try_init(); + } +} + const DEFAULT_WORK_DIR: &str = ".cdk-signatory"; const ENV_MNEMONIC: &str = "CDK_MINTD_MNEMONIC"; @@ -30,6 +64,9 @@ const ENV_MNEMONIC: &str = "CDK_MINTD_MNEMONIC"; #[command(version = "0.1.0")] #[command(author, version, about, long_about = None)] struct Cli { + #[command(flatten)] + common: CommonArgs, + /// Database engine to use (sqlite/redb) #[arg(short, long, default_value = "sqlite")] engine: String, @@ -39,9 +76,6 @@ struct Cli { /// Path to working dir #[arg(short, long)] work_dir: Option, - /// Logging level - #[arg(short, long, default_value = "debug")] - log_level: Level, #[arg(long, default_value = "127.0.0.1")] listen_addr: String, #[arg(long, default_value = "15060")] @@ -56,7 +90,10 @@ struct Cli { /// Main function for the signatory standalone binary pub async fn cli_main() -> Result<()> { let args: Cli = Cli::parse(); - let default_filter = args.log_level; + + // Initialize logging based on CLI arguments + init_logging(args.common.enable_logging, args.common.log_level); + let supported_units = args .units .into_iter() @@ -74,13 +111,6 @@ pub async fn cli_main() -> Result<()> { }) .collect::, _>>()?; - let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn"; - - let env_filter = EnvFilter::new(format!("{default_filter},{sqlx_filter}")); - - // Parse input - tracing_subscriber::fmt().with_env_filter(env_filter).init(); - let work_dir = match &args.work_dir { Some(work_dir) => work_dir.clone(), None => { diff --git a/misc/fake_auth_itests.sh b/misc/fake_auth_itests.sh index cd70345b..02531104 100755 --- a/misc/fake_auth_itests.sh +++ b/misc/fake_auth_itests.sh @@ -1,31 +1,37 @@ - #!/usr/bin/env bash # Function to perform cleanup cleanup() { echo "Cleaning up..." - echo "Killing the cdk mintd" - kill -2 $cdk_mintd_pid - wait $cdk_mintd_pid + if [ -n "$FAKE_AUTH_MINT_PID" ]; then + echo "Killing the fake auth mint process" + kill -2 $FAKE_AUTH_MINT_PID 2>/dev/null || true + wait $FAKE_AUTH_MINT_PID 2>/dev/null || true + fi echo "Mint binary terminated" # Remove the temporary directory - rm -rf "$CDK_ITESTS_DIR" - echo "Temp directory removed: $CDK_ITESTS_DIR" + if [ -n "$CDK_ITESTS_DIR" ] && [ -d "$CDK_ITESTS_DIR" ]; then + rm -rf "$CDK_ITESTS_DIR" + echo "Temp directory removed: $CDK_ITESTS_DIR" + fi + + # Unset all environment variables unset CDK_ITESTS_DIR unset CDK_ITESTS_MINT_ADDR unset CDK_ITESTS_MINT_PORT + unset FAKE_AUTH_MINT_PID } # Set up trap to call cleanup on script exit -trap cleanup EXIT +trap cleanup EXIT INT TERM # Create a temporary directory export CDK_ITESTS_DIR=$(mktemp -d) -export CDK_ITESTS_MINT_ADDR="127.0.0.1"; -export CDK_ITESTS_MINT_PORT=8087; +export CDK_ITESTS_MINT_ADDR="127.0.0.1" +export CDK_ITESTS_MINT_PORT=8087 # Check if the temporary directory was created successfully if [[ ! -d "$CDK_ITESTS_DIR" ]]; then @@ -34,72 +40,41 @@ if [[ ! -d "$CDK_ITESTS_DIR" ]]; then fi echo "Temp directory created: $CDK_ITESTS_DIR" -export MINT_DATABASE="$1"; -export OPENID_DISCOVERY="$2"; +# Check if a database type was provided as first argument, default to sqlite +export MINT_DATABASE="${1:-sqlite}" + +# Check if OPENID_DISCOVERY was provided as second argument, default to a test value +export OPENID_DISCOVERY="${2:-http://127.0.0.1:8080/realms/cdk-test-realm/.well-known/openid-configuration}" + +# Build the project cargo build -p cdk-integration-tests -export CDK_MINTD_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT"; -export CDK_MINTD_WORK_DIR="$CDK_ITESTS_DIR"; -export CDK_MINTD_LISTEN_HOST=$CDK_ITESTS_MINT_ADDR; -export CDK_MINTD_LISTEN_PORT=$CDK_ITESTS_MINT_PORT; -export CDK_MINTD_LN_BACKEND="fakewallet"; -export CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS="sat"; -export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal"; -export CDK_MINTD_FAKE_WALLET_FEE_PERCENT="0"; -export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1"; -export CDK_MINTD_DATABASE=$MINT_DATABASE; - # Auth configuration -export CDK_TEST_OIDC_USER="cdk-test"; -export CDK_TEST_OIDC_PASSWORD="cdkpassword"; +export CDK_TEST_OIDC_USER="cdk-test" +export CDK_TEST_OIDC_PASSWORD="cdkpassword" -export CDK_MINTD_AUTH_OPENID_DISCOVERY=$OPENID_DISCOVERY; -export CDK_MINTD_AUTH_OPENID_CLIENT_ID="cashu-client"; -export CDK_MINTD_AUTH_MINT_MAX_BAT="50"; -export CDK_MINTD_AUTH_ENABLED_MINT="true"; -export CDK_MINTD_AUTH_ENABLED_MELT="true"; -export CDK_MINTD_AUTH_ENABLED_SWAP="true"; -export CDK_MINTD_AUTH_ENABLED_CHECK_MINT_QUOTE="true"; -export CDK_MINTD_AUTH_ENABLED_CHECK_MELT_QUOTE="true"; -export CDK_MINTD_AUTH_ENABLED_RESTORE="true"; -export CDK_MINTD_AUTH_ENABLED_CHECK_PROOF_STATE="true"; +# Start the fake auth mint in the background +echo "Starting fake auth mint with discovery URL: $OPENID_DISCOVERY" +echo "Using temp directory: $CDK_ITESTS_DIR" +cargo run -p cdk-integration-tests --bin start_fake_auth_mint -- --enable-logging "$MINT_DATABASE" "$CDK_ITESTS_DIR" "$OPENID_DISCOVERY" "$CDK_ITESTS_MINT_PORT" & -echo "Starting auth mintd"; -cargo run --bin cdk-mintd --features redb & -cdk_mintd_pid=$! +# Store the PID of the mint process +FAKE_AUTH_MINT_PID=$! -URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT/v1/info" -TIMEOUT=100 -START_TIME=$(date +%s) -# Loop until the endpoint returns a 200 OK status or timeout is reached -while true; do - # Get the current time - CURRENT_TIME=$(date +%s) - - # Calculate the elapsed time - ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) +# Wait a moment for the mint to start +sleep 5 - # Check if the elapsed time exceeds the timeout - if [ $ELAPSED_TIME -ge $TIMEOUT ]; then - echo "Timeout of $TIMEOUT seconds reached. Exiting..." - exit 1 - fi +# Check if the mint is running +if ! kill -0 $FAKE_AUTH_MINT_PID 2>/dev/null; then + echo "Failed to start fake auth mint" + exit 1 +fi - # Make a request to the endpoint and capture the HTTP status code - HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL) - - # Check if the HTTP status is 200 OK - if [ "$HTTP_STATUS" -eq 200 ]; then - echo "Received 200 OK from $URL" - break - else - echo "Waiting for 200 OK response, current status: $HTTP_STATUS" - sleep 2 # Wait for 2 seconds before retrying - fi -done +echo "Fake auth mint started with PID: $FAKE_AUTH_MINT_PID" # Run cargo test +echo "Running fake auth integration tests..." cargo test -p cdk-integration-tests --test fake_auth # Capture the exit status of cargo test diff --git a/misc/fake_itests.sh b/misc/fake_itests.sh index 9ff24284..d6a4199e 100755 --- a/misc/fake_itests.sh +++ b/misc/fake_itests.sh @@ -1,46 +1,45 @@ #!/usr/bin/env bash +# Script to run fake mint tests with proper handling of race conditions +# This script ensures the .env file is properly created and available +# before running tests + # Function to perform cleanup cleanup() { echo "Cleaning up..." - echo "Killing the cdk mintd" - kill -2 $CDK_MINTD_PID - wait $CDK_MINTD_PID - kill -9 $CDK_SIGNATORY_PID - wait $CDK_SIGNATORY_PID + if [ -n "$FAKE_MINT_PID" ]; then + echo "Killing the fake mint process" + kill -2 $FAKE_MINT_PID 2>/dev/null || true + wait $FAKE_MINT_PID 2>/dev/null || true + fi + + if [ -n "$CDK_SIGNATORY_PID" ]; then + echo "Killing the signatory process" + kill -9 $CDK_SIGNATORY_PID 2>/dev/null || true + wait $CDK_SIGNATORY_PID 2>/dev/null || true + fi echo "Mint binary terminated" # Remove the temporary directory - rm -rf "$CDK_ITESTS_DIR" - echo "Temp directory removed: $CDK_ITESTS_DIR" + if [ -n "$CDK_ITESTS_DIR" ] && [ -d "$CDK_ITESTS_DIR" ]; then + rm -rf "$CDK_ITESTS_DIR" + echo "Temp directory removed: $CDK_ITESTS_DIR" + fi # Unset all environment variables unset CDK_ITESTS_DIR - unset CDK_ITESTS_MINT_ADDR - unset CDK_ITESTS_MINT_PORT - unset CDK_MINTD_DATABASE unset CDK_TEST_MINT_URL - unset CDK_MINTD_URL - unset CDK_MINTD_WORK_DIR - unset CDK_MINTD_LISTEN_HOST - unset CDK_MINTD_LISTEN_PORT - unset CDK_MINTD_LN_BACKEND - unset CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS - unset CDK_MINTD_MNEMONIC - unset CDK_MINTD_FAKE_WALLET_FEE_PERCENT - unset CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN - unset CDK_MINTD_PID + unset FAKE_MINT_PID + unset CDK_SIGNATORY_PID } # Set up trap to call cleanup on script exit -trap cleanup EXIT +trap cleanup EXIT INT TERM # Create a temporary directory export CDK_ITESTS_DIR=$(mktemp -d) -export CDK_ITESTS_MINT_ADDR="127.0.0.1" -export CDK_ITESTS_MINT_PORT=8086 # Check if the temporary directory was created successfully if [[ ! -d "$CDK_ITESTS_DIR" ]]; then @@ -49,36 +48,74 @@ if [[ ! -d "$CDK_ITESTS_DIR" ]]; then fi echo "Temp directory created: $CDK_ITESTS_DIR" -export CDK_MINTD_DATABASE="$1" + +# Check if a database type was provided as first argument, default to sqlite +export CDK_MINTD_DATABASE="${1:-sqlite}" cargo build -p cdk-integration-tests - -export CDK_MINTD_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT" -export CDK_MINTD_WORK_DIR="$CDK_ITESTS_DIR" -export CDK_MINTD_LISTEN_HOST=$CDK_ITESTS_MINT_ADDR -export CDK_MINTD_LISTEN_PORT=$CDK_ITESTS_MINT_PORT -export CDK_MINTD_LN_BACKEND="fakewallet" -export CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS="sat,usd" -export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal" -export CDK_MINTD_FAKE_WALLET_FEE_PERCENT="0" -export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1" - +# Start the fake mint binary with the new Rust-based approach +echo "Starting fake mint using Rust binary..." if [ "$2" = "external_signatory" ]; then - export CDK_MINTD_SIGNATORY_URL="https://127.0.0.1:15060" - export CDK_MINTD_SIGNATORY_CERTS="$CDK_ITESTS_DIR" + echo "Starting with external signatory support" + bash -x `dirname $0`/../crates/cdk-signatory/generate_certs.sh $CDK_ITESTS_DIR + cargo build --bin signatory cargo run --bin signatory -- -w $CDK_ITESTS_DIR -u "sat" -u "usd" & export CDK_SIGNATORY_PID=$! sleep 5 + + cargo run --bin start_fake_mint -- --enable-logging --external-signatory "$CDK_MINTD_DATABASE" "$CDK_ITESTS_DIR" & +else + cargo run --bin start_fake_mint -- --enable-logging "$CDK_MINTD_DATABASE" "$CDK_ITESTS_DIR" & +fi +export FAKE_MINT_PID=$! + +# Give the mint a moment to start +sleep 3 + +# Look for the .env file in the temp directory +ENV_FILE_PATH="$CDK_ITESTS_DIR/.env" + +# Wait for the .env file to be created (with longer timeout) +max_wait=200 +wait_count=0 +while [ $wait_count -lt $max_wait ]; do + if [ -f "$ENV_FILE_PATH" ]; then + echo ".env file found at: $ENV_FILE_PATH" + break + fi + echo "Waiting for .env file to be created... ($wait_count/$max_wait)" + wait_count=$((wait_count + 1)) + sleep 1 +done + +# Check if we found the .env file +if [ ! -f "$ENV_FILE_PATH" ]; then + echo "ERROR: Could not find .env file at $ENV_FILE_PATH after $max_wait seconds" + exit 1 fi -echo "Starting fake mintd" -cargo run --bin cdk-mintd & -export CDK_MINTD_PID=$! +# Source the environment variables from the .env file +echo "Sourcing environment variables from $ENV_FILE_PATH" +source "$ENV_FILE_PATH" -URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT/v1/info" -TIMEOUT=100 +echo "Sourced environment variables:" +echo "CDK_TEST_MINT_URL=$CDK_TEST_MINT_URL" +echo "CDK_ITESTS_DIR=$CDK_ITESTS_DIR" + +# Validate that we sourced the variables +if [ -z "$CDK_TEST_MINT_URL" ] || [ -z "$CDK_ITESTS_DIR" ]; then + echo "ERROR: Failed to source environment variables from the .env file" + exit 1 +fi + +# Export all variables so they're available to the tests +export CDK_TEST_MINT_URL + +URL="$CDK_TEST_MINT_URL/v1/info" + +TIMEOUT=120 START_TIME=$(date +%s) # Loop until the endpoint returns a 200 OK status or timeout is reached while true; do @@ -107,10 +144,8 @@ while true; do fi done - -export CDK_TEST_MINT_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT" - # Run first test +echo "Running fake_wallet test" cargo test -p cdk-integration-tests --test fake_wallet status1=$? @@ -121,6 +156,7 @@ if [ $status1 -ne 0 ]; then fi # Run second test only if the first one succeeded +echo "Running happy_path_mint_wallet test" cargo test -p cdk-integration-tests --test happy_path_mint_wallet status2=$? diff --git a/misc/interactive_regtest_mprocs.sh b/misc/interactive_regtest_mprocs.sh index 72aceef7..c1f263e0 100755 --- a/misc/interactive_regtest_mprocs.sh +++ b/misc/interactive_regtest_mprocs.sh @@ -123,7 +123,7 @@ cargo build -p cdk-integration-tests --bin start_regtest cargo build --bin cdk-mintd echo "Starting regtest network (Bitcoin + Lightning nodes)..." -cargo run --bin start_regtest & +cargo run --bin start_regtest -- --enable-logging "$CDK_ITESTS_DIR" & export CDK_REGTEST_PID=$! # Create named pipe for progress tracking diff --git a/misc/itests.sh b/misc/itests.sh index dd2755c1..7fc490f4 100755 --- a/misc/itests.sh +++ b/misc/itests.sh @@ -4,25 +4,29 @@ cleanup() { echo "Cleaning up..." - echo "Killing the cdk mintd" - kill -2 $CDK_MINTD_PID - wait $CDK_MINTD_PID - - - echo "Killing the cdk lnd mintd" - kill -2 $CDK_MINTD_LND_PID - wait $CDK_MINTD_LND_PID - - echo "Killing the cdk regtest" - kill -2 $CDK_REGTEST_PID - wait $CDK_REGTEST_PID - + echo "Killing the cdk regtest and mints" + if [ ! -z "$CDK_REGTEST_PID" ]; then + # First try graceful shutdown with SIGTERM + kill -15 $CDK_REGTEST_PID 2>/dev/null + sleep 2 + + # Check if process is still running, if so force kill with SIGKILL + if ps -p $CDK_REGTEST_PID > /dev/null 2>&1; then + echo "Process still running, force killing..." + kill -9 $CDK_REGTEST_PID 2>/dev/null + fi + + # Wait for process to terminate + wait $CDK_REGTEST_PID 2>/dev/null || true + fi echo "Mint binary terminated" # Remove the temporary directory - rm -rf "$CDK_ITESTS_DIR" - echo "Temp directory removed: $CDK_ITESTS_DIR" + if [ ! -z "$CDK_ITESTS_DIR" ] && [ -d "$CDK_ITESTS_DIR" ]; then + rm -rf "$CDK_ITESTS_DIR" + echo "Temp directory removed: $CDK_ITESTS_DIR" + fi # Unset all environment variables unset CDK_ITESTS_DIR @@ -32,18 +36,6 @@ cleanup() { unset CDK_MINTD_DATABASE unset CDK_TEST_MINT_URL unset CDK_TEST_MINT_URL_2 - unset CDK_MINTD_URL - unset CDK_MINTD_WORK_DIR - unset CDK_MINTD_LISTEN_HOST - unset CDK_MINTD_LISTEN_PORT - unset CDK_MINTD_LN_BACKEND - unset CDK_MINTD_MNEMONIC - unset CDK_MINTD_CLN_RPC_PATH - unset CDK_MINTD_LND_ADDRESS - unset CDK_MINTD_LND_CERT_FILE - unset CDK_MINTD_LND_MACAROON_FILE - unset CDK_MINTD_PID - unset CDK_MINTD_LND_PID unset CDK_REGTEST_PID unset RUST_BACKTRACE unset CDK_TEST_REGTEST @@ -69,53 +61,59 @@ fi echo "Temp directory created: $CDK_ITESTS_DIR" export CDK_MINTD_DATABASE="$1" -cargo build -p cdk-integration-tests - -cargo run --bin start_regtest & +cargo build -p cdk-integration-tests +cargo build --bin start_regtest_mints +echo "Starting regtest and mints" +# Run the binary in background +cargo r --bin start_regtest_mints -- --enable-logging "$CDK_MINTD_DATABASE" "$CDK_ITESTS_DIR" "$CDK_ITESTS_MINT_ADDR" "$CDK_ITESTS_MINT_PORT_0" "$CDK_ITESTS_MINT_PORT_1" & export CDK_REGTEST_PID=$! -mkfifo "$CDK_ITESTS_DIR/progress_pipe" -rm -f "$CDK_ITESTS_DIR/signal_received" # Ensure clean state -# Start reading from pipe in background -(while read line; do - case "$line" in - "checkpoint1") - echo "Reached first checkpoint" - touch "$CDK_ITESTS_DIR/signal_received" - exit 0 - ;; - esac -done < "$CDK_ITESTS_DIR/progress_pipe") & -# Wait for up to 120 seconds -for ((i=0; i<120; i++)); do - if [ -f "$CDK_ITESTS_DIR/signal_received" ]; then - echo "break signal received" + +# Give it a moment to start - reduced from 5 to 2 seconds since we have better waiting mechanisms now +sleep 2 + +# Look for the .env file in the current directory +ENV_FILE_PATH="$CDK_ITESTS_DIR/.env" + +# Wait for the .env file to be created in the current directory +max_wait=120 +wait_count=0 +while [ $wait_count -lt $max_wait ]; do + if [ -f "$ENV_FILE_PATH" ]; then + echo ".env file found at: $ENV_FILE_PATH" break fi + wait_count=$((wait_count + 1)) sleep 1 done -echo "Regtest set up continuing" -echo "Starting regtest mint" -# cargo run --bin regtest_mint & +# Check if we found the .env file +if [ ! -f "$ENV_FILE_PATH" ]; then + echo "ERROR: Could not find .env file at $ENV_FILE_PATH" + exit 1 +fi -export CDK_MINTD_CLN_RPC_PATH="$CDK_ITESTS_DIR/cln/one/regtest/lightning-rpc" -export CDK_MINTD_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_0" -export CDK_MINTD_WORK_DIR="$CDK_ITESTS_DIR" -export CDK_MINTD_LISTEN_HOST=$CDK_ITESTS_MINT_ADDR -export CDK_MINTD_LISTEN_PORT=$CDK_ITESTS_MINT_PORT_0 -export CDK_MINTD_LN_BACKEND="cln" -export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal" -export RUST_BACKTRACE=1 +# Source the environment variables from the .env file +echo "Sourcing environment variables from $ENV_FILE_PATH" +source "$ENV_FILE_PATH" -echo "Starting cln mintd" -cargo run --bin cdk-mintd & -export CDK_MINTD_PID=$! +echo "Sourced environment variables:" +echo "CDK_TEST_MINT_URL=$CDK_TEST_MINT_URL" +echo "CDK_TEST_MINT_URL_2=$CDK_TEST_MINT_URL_2" +echo "CDK_ITESTS_DIR=$CDK_ITESTS_DIR" +# Validate that we sourced the variables +if [ -z "$CDK_TEST_MINT_URL" ] || [ -z "$CDK_TEST_MINT_URL_2" ] || [ -z "$CDK_ITESTS_DIR" ]; then + echo "ERROR: Failed to source environment variables from the .env file" + exit 1 +fi -echo $CDK_ITESTS_DIR +# Export all variables so they're available to the tests +export CDK_TEST_MINT_URL +export CDK_TEST_MINT_URL_2 + +URL="$CDK_TEST_MINT_URL/v1/info" -URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_0/v1/info" TIMEOUT=100 START_TIME=$(date +%s) @@ -146,24 +144,8 @@ while true; do fi done +URL="$CDK_TEST_MINT_URL_2/v1/info" -export CDK_MINTD_LND_ADDRESS="https://localhost:10010" -export CDK_MINTD_LND_CERT_FILE="$CDK_ITESTS_DIR/lnd/two/tls.cert" -export CDK_MINTD_LND_MACAROON_FILE="$CDK_ITESTS_DIR/lnd/two/data/chain/bitcoin/regtest/admin.macaroon" - -export CDK_MINTD_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_1" -mkdir -p "$CDK_ITESTS_DIR/lnd_mint" -export CDK_MINTD_WORK_DIR="$CDK_ITESTS_DIR/lnd_mint" -export CDK_MINTD_LISTEN_HOST=$CDK_ITESTS_MINT_ADDR -export CDK_MINTD_LISTEN_PORT=$CDK_ITESTS_MINT_PORT_1 -export CDK_MINTD_LN_BACKEND="lnd" -export CDK_MINTD_MNEMONIC="cattle gold bind busy sound reduce tone addict baby spend february strategy" - -echo "Starting lnd mintd" -cargo run --bin cdk-mintd & -export CDK_MINTD_LND_PID=$! - -URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_1/v1/info" TIMEOUT=100 START_TIME=$(date +%s) @@ -194,13 +176,6 @@ while true; do fi done - - -export CDK_TEST_MINT_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_0" -export CDK_TEST_MINT_URL_2="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_1" - -# Run tests and exit immediately on failure - # Run cargo test echo "Running regtest test with CLN mint" cargo test -p cdk-integration-tests --test regtest @@ -216,7 +191,7 @@ if [ $? -ne 0 ]; then exit 1 fi -# # Run cargo test with the http_subscription feature +# Run cargo test with the http_subscription feature echo "Running regtest test with http_subscription feature" cargo test -p cdk-integration-tests --test regtest --features http_subscription if [ $? -ne 0 ]; then @@ -233,12 +208,13 @@ fi # Switch Mints: Run tests with LND mint echo "Switching to LND mint for tests" -export CDK_ITESTS_MINT_PORT_0=8087 -export CDK_ITESTS_MINT_PORT_1=8085 -export CDK_TEST_MINT_URL="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_0" -export CDK_TEST_MINT_URL_2="http://$CDK_ITESTS_MINT_ADDR:$CDK_ITESTS_MINT_PORT_1" echo "Running regtest test with LND mint" +CDK_TEST_MINT_URL_SWITCHED=$CDK_TEST_MINT_URL_2 +CDK_TEST_MINT_URL_2_SWITCHED=$CDK_TEST_MINT_URL +export CDK_TEST_MINT_URL=$CDK_TEST_MINT_URL_SWITCHED +export CDK_TEST_MINT_URL_2=$CDK_TEST_MINT_URL_2_SWITCHED + cargo test -p cdk-integration-tests --test regtest if [ $? -ne 0 ]; then echo "regtest test with LND mint failed, exiting" diff --git a/misc/mintd_payment_processor.sh b/misc/mintd_payment_processor.sh index d00dc740..47904594 100755 --- a/misc/mintd_payment_processor.sh +++ b/misc/mintd_payment_processor.sh @@ -80,7 +80,7 @@ cargo build -p cdk-integration-tests export CDK_TEST_REGTEST=0 if [ "$LN_BACKEND" != "FAKEWALLET" ]; then export CDK_TEST_REGTEST=1 - cargo run --bin start_regtest & + cargo run --bin start_regtest "$CDK_ITESTS_DIR" & CDK_REGTEST_PID=$! mkfifo "$CDK_ITESTS_DIR/progress_pipe" rm -f "$CDK_ITESTS_DIR/signal_received" # Ensure clean state diff --git a/misc/nutshell_wallet_itest.sh b/misc/nutshell_wallet_itest.sh index e54717d6..bcc0f65a 100755 --- a/misc/nutshell_wallet_itest.sh +++ b/misc/nutshell_wallet_itest.sh @@ -58,6 +58,9 @@ export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1" export CDK_MINTD_INPUT_FEE_PPK="100" +export CDK_ITESTS_DIR="$CDK_ITESTS" + + echo "Starting fake mintd" cargo run --bin cdk-mintd & CDK_MINTD_PID=$!