#[cfg(feature = "fakewallet")] use std::collections::HashMap; #[cfg(feature = "fakewallet")] use std::collections::HashSet; use std::path::Path; #[cfg(feature = "ldk-node")] use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; #[cfg(feature = "fakewallet")] use bip39::rand::{thread_rng, Rng}; use cdk::cdk_database::KVStore; use cdk::cdk_payment::MintPayment; use cdk::nuts::CurrencyUnit; #[cfg(any( feature = "lnbits", feature = "cln", feature = "lnd", feature = "ldk-node", feature = "fakewallet" ))] use cdk::types::FeeReserve; use crate::config::{self, Settings}; #[cfg(feature = "cln")] use crate::expand_path; #[async_trait] pub trait LnBackendSetup { async fn setup( &self, settings: &Settings, unit: CurrencyUnit, runtime: Option>, work_dir: &Path, kv_store: Option + Send + Sync>>, ) -> anyhow::Result; } #[cfg(feature = "cln")] #[async_trait] impl LnBackendSetup for config::Cln { async fn setup( &self, _settings: &Settings, _unit: CurrencyUnit, _runtime: Option>, _work_dir: &Path, kv_store: Option + Send + Sync>>, ) -> anyhow::Result { // Validate required connection field if self.rpc_path.as_os_str().is_empty() { return Err(anyhow::anyhow!( "CLN rpc_path must be set via config or CDK_MINTD_CLN_RPC_PATH env var" )); } let cln_socket = expand_path( self.rpc_path .to_str() .ok_or(anyhow::anyhow!("cln socket not defined"))?, ) .ok_or(anyhow::anyhow!("cln socket not defined"))?; let fee_reserve = FeeReserve { min_fee_reserve: self.reserve_fee_min, percent_fee_reserve: self.fee_percent, }; let cln = cdk_cln::Cln::new( cln_socket, fee_reserve, kv_store.expect("Cln needs kv store"), ) .await?; Ok(cln) } } #[cfg(feature = "lnbits")] #[async_trait] impl LnBackendSetup for config::LNbits { async fn setup( &self, _settings: &Settings, _unit: CurrencyUnit, _runtime: Option>, _work_dir: &Path, _kv_store: Option + Send + Sync>>, ) -> anyhow::Result { use anyhow::bail; // Validate required connection fields if self.admin_api_key.is_empty() { bail!("LNbits admin_api_key must be set via config or CDK_MINTD_LNBITS_ADMIN_API_KEY env var"); } if self.invoice_api_key.is_empty() { bail!("LNbits invoice_api_key must be set via config or CDK_MINTD_LNBITS_INVOICE_API_KEY env var"); } if self.lnbits_api.is_empty() { bail!( "LNbits lnbits_api must be set via config or CDK_MINTD_LNBITS_LNBITS_API env var" ); } let admin_api_key = &self.admin_api_key; let invoice_api_key = &self.invoice_api_key; let fee_reserve = FeeReserve { min_fee_reserve: self.reserve_fee_min, percent_fee_reserve: self.fee_percent, }; let lnbits = cdk_lnbits::LNbits::new( admin_api_key.clone(), invoice_api_key.clone(), self.lnbits_api.clone(), fee_reserve, ) .await?; // Use v1 websocket API lnbits.subscribe_ws().await?; Ok(lnbits) } } #[cfg(feature = "lnd")] #[async_trait] impl LnBackendSetup for config::Lnd { async fn setup( &self, _settings: &Settings, _unit: CurrencyUnit, _runtime: Option>, _work_dir: &Path, kv_store: Option + Send + Sync>>, ) -> anyhow::Result { use anyhow::bail; // Validate required connection fields if self.address.is_empty() { bail!("LND address must be set via config or CDK_MINTD_LND_ADDRESS env var"); } if self.cert_file.as_os_str().is_empty() { bail!("LND cert_file must be set via config or CDK_MINTD_LND_CERT_FILE env var"); } if self.macaroon_file.as_os_str().is_empty() { bail!( "LND macaroon_file must be set via config or CDK_MINTD_LND_MACAROON_FILE env var" ); } let address = &self.address; let cert_file = &self.cert_file; let macaroon_file = &self.macaroon_file; let fee_reserve = FeeReserve { min_fee_reserve: self.reserve_fee_min, percent_fee_reserve: self.fee_percent, }; let lnd = cdk_lnd::Lnd::new( address.to_string(), cert_file.clone(), macaroon_file.clone(), fee_reserve, kv_store.expect("Lnd needs kv store"), ) .await?; Ok(lnd) } } #[cfg(feature = "fakewallet")] #[async_trait] impl LnBackendSetup for config::FakeWallet { async fn setup( &self, _settings: &Settings, unit: CurrencyUnit, _runtime: Option>, _work_dir: &Path, _kv_store: Option + Send + Sync>>, ) -> anyhow::Result { let fee_reserve = FeeReserve { min_fee_reserve: self.reserve_fee_min, percent_fee_reserve: self.fee_percent, }; // calculate random delay time let mut rng = thread_rng(); let delay_time = rng.gen_range(self.min_delay_time..=self.max_delay_time); let fake_wallet = cdk_fake_wallet::FakeWallet::new( fee_reserve, HashMap::default(), HashSet::default(), delay_time, unit, ); Ok(fake_wallet) } } #[cfg(feature = "grpc-processor")] #[async_trait] impl LnBackendSetup for config::GrpcProcessor { async fn setup( &self, _settings: &Settings, _unit: CurrencyUnit, _runtime: Option>, _work_dir: &Path, _kv_store: Option + Send + Sync>>, ) -> anyhow::Result { let payment_processor = cdk_payment_processor::PaymentProcessorClient::new( &self.addr, self.port, self.tls_dir.clone(), ) .await?; Ok(payment_processor) } } #[cfg(feature = "ldk-node")] #[async_trait] impl LnBackendSetup for config::LdkNode { async fn setup( &self, settings: &Settings, _unit: CurrencyUnit, _runtime: Option>, work_dir: &Path, _kv_store: Option + Send + Sync>>, ) -> anyhow::Result { use std::net::SocketAddr; use anyhow::bail; use bip39::Mnemonic; use bitcoin::Network; let fee_reserve = FeeReserve { min_fee_reserve: self.reserve_fee_min, percent_fee_reserve: self.fee_percent, }; // Parse network from config let network = match self .bitcoin_network .as_ref() .map(|n| n.to_lowercase()) .as_deref() .unwrap_or("regtest") { "mainnet" | "bitcoin" => Network::Bitcoin, "testnet" => Network::Testnet, "signet" => Network::Signet, _ => Network::Regtest, }; // Parse chain source from config let chain_source = match self .chain_source_type .as_ref() .map(|s| s.to_lowercase()) .as_deref() .unwrap_or("esplora") { "bitcoinrpc" => { let host = self .bitcoind_rpc_host .clone() .unwrap_or_else(|| "127.0.0.1".to_string()); let port = self.bitcoind_rpc_port.unwrap_or(18443); let user = self .bitcoind_rpc_user .clone() .unwrap_or_else(|| "testuser".to_string()); let password = self .bitcoind_rpc_password .clone() .unwrap_or_else(|| "testpass".to_string()); cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig { host, port, user, password, }) } _ => { let esplora_url = self .esplora_url .clone() .unwrap_or_else(|| "https://mutinynet.com/api".to_string()); cdk_ldk_node::ChainSource::Esplora(esplora_url) } }; // Parse gossip source from config let gossip_source = match self.rgs_url.clone() { Some(rgs_url) => cdk_ldk_node::GossipSource::RapidGossipSync(rgs_url), None => cdk_ldk_node::GossipSource::P2P, }; // Get storage directory path let storage_dir_path = if let Some(dir_path) = &self.storage_dir_path { dir_path.clone() } else { let mut work_dir = work_dir.to_path_buf(); work_dir.push("ldk-node"); work_dir.to_string_lossy().to_string() }; // Get LDK node listen address let host = self .ldk_node_host .clone() .unwrap_or_else(|| "127.0.0.1".to_string()); let port = self.ldk_node_port.unwrap_or(8090); let socket_addr = SocketAddr::new(host.parse()?, port); // Parse socket address using ldk_node's SocketAddress // We need to get the actual socket address struct from ldk_node // For now, let's construct it manually based on the cdk-ldk-node implementation let listen_address = vec![socket_addr.into()]; // Check if ldk_node_mnemonic is provided in the ldk_node config let mnemonic_opt = settings .clone() .ldk_node .as_ref() .and_then(|ldk_config| ldk_config.ldk_node_mnemonic.clone()); // Only set seed if mnemonic is explicitly provided // This maintains backward compatibility with existing nodes that use LDK's default seed storage let seed = if let Some(mnemonic_str) = mnemonic_opt { Some( mnemonic_str .parse::() .map_err(|e| anyhow::anyhow!("invalid ldk_node_mnemonic in config: {e}"))?, ) } else { // Check if this is a new node or an existing node let storage_dir = PathBuf::from(&storage_dir_path); let keys_seed_file = storage_dir.join("keys_seed"); if !keys_seed_file.exists() { bail!("ldk_node_mnemonic should be set in the [ldk_node] configuration section."); } // Existing node with stored seed, don't set a mnemonic None }; let ldk_node_settings = settings .ldk_node .as_ref() .ok_or_else(|| anyhow::anyhow!("ldk_node configuration is required"))?; let announce_addrs: Vec<_> = ldk_node_settings .ldk_node_announce_addresses .as_ref() .map(|addrs| addrs.iter().filter_map(|addr| addr.parse().ok()).collect()) .unwrap_or_default(); let mut ldk_node_builder = cdk_ldk_node::CdkLdkNodeBuilder::new( network, chain_source, gossip_source, storage_dir_path, fee_reserve, listen_address, ); // Only set seed if provided if let Some(mnemonic) = seed { ldk_node_builder = ldk_node_builder.with_seed(mnemonic); } if !announce_addrs.is_empty() { ldk_node_builder = ldk_node_builder.with_announcement_address(announce_addrs) } // Configure webserver address if specified let webserver_addr = if let Some(host) = &self.webserver_host { let port = self.webserver_port.unwrap_or(8091); let socket_addr: SocketAddr = format!("{host}:{port}").parse()?; Some(socket_addr) } else if self.webserver_port.is_some() { // If only port is specified, use default host let port = self.webserver_port.unwrap_or(8091); let socket_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?; Some(socket_addr) } else { // Use default webserver address if nothing is configured Some(cdk_ldk_node::CdkLdkNode::default_web_addr()) }; println!( "webserver: {}", webserver_addr.map_or("none".to_string(), |a| a.to_string()) ); if let Some(log_dir_path) = ldk_node_settings.log_dir_path.as_ref() { ldk_node_builder = ldk_node_builder.with_log_dir_path(log_dir_path.clone()); } let mut ldk_node = ldk_node_builder.build()?; ldk_node.set_web_addr(webserver_addr); Ok(ldk_node) } }