소스 검색

feat: Add more ldk-node config settings (#1010)

This adds more ldk node config settings as well as support for bip39 mnemonic 

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
asmo 1 주 전
부모
커밋
a6dfadfa47

+ 1 - 0
Cargo.lock

@@ -1445,6 +1445,7 @@ version = "0.14.0"
 dependencies = [
  "async-trait",
  "axum 0.8.8",
+ "bip39",
  "cdk-common",
  "futures",
  "ldk-node",

+ 1 - 2
crates/cashu/src/nuts/nut23.rs

@@ -6,13 +6,12 @@ use std::str::FromStr;
 use lightning_invoice::Bolt11Invoice;
 use serde::de::DeserializeOwned;
 use serde::{Deserialize, Serialize};
-
-use crate::util::serde_helpers::deserialize_empty_string_as_none;
 use thiserror::Error;
 
 use super::{BlindSignature, CurrencyUnit, MeltQuoteState, Mpp, PublicKey};
 #[cfg(feature = "mint")]
 use crate::quote_id::QuoteId;
+use crate::util::serde_helpers::deserialize_empty_string_as_none;
 use crate::Amount;
 
 /// NUT023 Error

+ 1 - 2
crates/cashu/src/util/serde_helpers.rs

@@ -26,9 +26,8 @@ where
 mod tests {
     use serde::Deserialize;
 
-    use crate::PublicKey;
-
     use super::*;
+    use crate::PublicKey;
 
     #[derive(Debug, Deserialize, PartialEq)]
     struct TestStruct {

+ 4 - 3
crates/cdk-integration-tests/src/bin/start_regtest.rs

@@ -9,7 +9,7 @@ use anyhow::Result;
 use cashu::Amount;
 use cdk_integration_tests::cli::{init_logging, CommonArgs};
 use cdk_integration_tests::init_regtest::start_regtest_end;
-use cdk_ldk_node::CdkLdkNode;
+use cdk_ldk_node::CdkLdkNodeBuilder;
 use clap::Parser;
 use ldk_node::lightning::ln::msgs::SocketAddress;
 use tokio::signal;
@@ -51,7 +51,7 @@ async fn main() -> Result<()> {
     let shutdown_clone_two = Arc::clone(&shutdown_regtest);
 
     let ldk_work_dir = temp_dir.join("ldk_mint");
-    let cdk_ldk = CdkLdkNode::new(
+    let node_builder = CdkLdkNodeBuilder::new(
         bitcoin::Network::Regtest,
         cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
             host: "127.0.0.1".to_string(),
@@ -69,7 +69,8 @@ async fn main() -> Result<()> {
             addr: [127, 0, 0, 1],
             port: 8092,
         }],
-    )?;
+    );
+    let cdk_ldk = node_builder.build()?;
 
     let inner_node = cdk_ldk.node();
 

+ 68 - 7
crates/cdk-integration-tests/src/bin/start_regtest_mints.rs

@@ -22,7 +22,7 @@ use cashu::Amount;
 use cdk_integration_tests::cli::CommonArgs;
 use cdk_integration_tests::init_regtest::start_regtest_end;
 use cdk_integration_tests::shared;
-use cdk_ldk_node::CdkLdkNode;
+use cdk_ldk_node::{CdkLdkNode, CdkLdkNodeBuilder};
 use cdk_mintd::config::LoggingConfig;
 use clap::Parser;
 use ldk_node::lightning::ln::msgs::SocketAddress;
@@ -192,11 +192,14 @@ async fn start_lnd_mint(
 }
 
 /// Start regtest LDK mint using the library
+/// If `existing_node` is provided, it will be used instead of creating a new one.
+/// This allows the mint to use a node that was already set up (e.g., with channels).
 async fn start_ldk_mint(
     temp_dir: &Path,
     port: u16,
     shutdown: Arc<Notify>,
     runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
+    existing_node: Option<CdkLdkNode>,
 ) -> Result<tokio::task::JoinHandle<()>> {
     let ldk_work_dir = temp_dir.join("ldk_mint");
 
@@ -215,6 +218,8 @@ async fn start_ldk_mint(
         bitcoind_rpc_user: Some("testuser".to_string()),
         bitcoind_rpc_password: Some("testpass".to_string()),
         esplora_url: None,
+        log_dir_path: None,
+        ldk_node_announce_addresses: None,
         storage_dir_path: Some(ldk_work_dir.to_string_lossy().to_string()),
         ldk_node_host: Some("127.0.0.1".to_string()),
         ldk_node_port: Some(port + 10), // Use a different port for the LDK node P2P connections
@@ -222,10 +227,14 @@ async fn start_ldk_mint(
         rgs_url: None,
         webserver_host: Some("127.0.0.1".to_string()),
         webserver_port: Some(port + 1), // Use next port for web interface
+        ldk_node_mnemonic: Some(
+            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+                .to_string(),
+        ),
     };
 
     // Create settings struct for LDK mint using a new shared function
-    let settings = create_ldk_settings(port, ldk_config, Mnemonic::generate(12)?.to_string());
+    let settings = create_ldk_settings(port, ldk_config);
 
     println!("Starting LDK mintd on port {port}");
 
@@ -240,6 +249,13 @@ async fn start_ldk_mint(
             println!("LDK mint shutdown signal received");
         };
 
+        // Both nodes now use the same seed, so the standard flow should work.
+        // The existing_node parameter is kept for API compatibility but not used
+        // since run_mintd_with_shutdown will create its own node with the same seed.
+        if existing_node.is_some() {
+            println!("Using existing LDK node configuration (same seed as mint)");
+        }
+
         match cdk_mintd::run_mintd_with_shutdown(
             &ldk_work_dir,
             &settings,
@@ -262,7 +278,6 @@ async fn start_ldk_mint(
 fn create_ldk_settings(
     port: u16,
     ldk_config: cdk_mintd::config::LdkNode,
-    mnemonic: String,
 ) -> cdk_mintd::config::Settings {
     cdk_mintd::config::Settings {
         info: cdk_mintd::config::Info {
@@ -271,7 +286,10 @@ fn create_ldk_settings(
             listen_host: "127.0.0.1".to_string(),
             listen_port: port,
             seed: None,
-            mnemonic: Some(mnemonic),
+            mnemonic: Some(
+                "eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal"
+                    .to_string(),
+            ),
             signatory_url: None,
             signatory_certs: None,
             input_fee_ppk: None,
@@ -339,7 +357,11 @@ fn main() -> Result<()> {
         let shutdown_clone_one = Arc::clone(&shutdown_clone);
 
         let ldk_work_dir = temp_dir.join("ldk_mint");
-        let cdk_ldk = CdkLdkNode::new(
+        fs::create_dir_all(ldk_work_dir.join("logs"))?;
+        let test_mnemonic: Mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+            .parse()
+            .expect("Failed to parse test mnemonic");
+        let node_builder = CdkLdkNodeBuilder::new(
             bitcoin::Network::Regtest,
             cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
                 host: "127.0.0.1".to_string(),
@@ -357,7 +379,45 @@ fn main() -> Result<()> {
                 addr: [127, 0, 0, 1],
                 port: 8092,
             }],
-        )?;
+        )
+        .with_seed(test_mnemonic.clone());
+        let cdk_ldk = match node_builder.build() {
+            Ok(node) => node,
+            Err(e) => {
+                tracing::warn!(
+                    "Failed to start LDK node: {}. Attempting to wipe data and restart...",
+                    e
+                );
+                // Clean up the storage directory
+                if ldk_work_dir.exists() {
+                    fs::remove_dir_all(&ldk_work_dir)?;
+                    fs::create_dir_all(ldk_work_dir.join("logs"))?;
+                }
+                // Recreate builder since it was consumed
+                let node_builder = CdkLdkNodeBuilder::new(
+                    bitcoin::Network::Regtest,
+                    cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
+                        host: "127.0.0.1".to_string(),
+                        port: 18443,
+                        user: "testuser".to_string(),
+                        password: "testpass".to_string(),
+                    }),
+                    cdk_ldk_node::GossipSource::P2P,
+                    ldk_work_dir.to_string_lossy().to_string(),
+                    cdk_common::common::FeeReserve {
+                        min_fee_reserve: Amount::ZERO,
+                        percent_fee_reserve: 0.0,
+                    },
+                    vec![SocketAddress::TcpIpV4 {
+                        addr: [127, 0, 0, 1],
+                        port: 8092,
+                    }],
+                )
+                .with_seed(test_mnemonic);
+
+                node_builder.build()?
+            }
+        };
 
         let inner_node = cdk_ldk.node();
 
@@ -385,12 +445,13 @@ fn main() -> Result<()> {
         // Start LND mint
         let lnd_handle = start_lnd_mint(&temp_dir, args.lnd_port, shutdown_clone.clone()).await?;
 
-        // Start LDK mint
+        // Start LDK mint (using the existing node that was already set up with channels)
         let ldk_handle = start_ldk_mint(
             &temp_dir,
             args.ldk_port,
             shutdown_clone.clone(),
             Some(rt_clone),
+            Some(cdk_ldk),
         )
         .await?;
 

+ 1 - 0
crates/cdk-ldk-node/Cargo.toml

@@ -29,6 +29,7 @@ tower-http.workspace = true
 rust-embed = "8.5.0"
 serde_urlencoded = "0.7"
 urlencoding = "2.1"
+bip39.workspace = true
 
 [lints]
 workspace = true

+ 87 - 36
crates/cdk-ldk-node/src/lib.rs

@@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
 use async_trait::async_trait;
+use bip39::Mnemonic;
 use cdk_common::common::FeeReserve;
 use cdk_common::payment::{self, *};
 use cdk_common::util::{hex, unix_time};
@@ -20,6 +21,7 @@ use ldk_node::lightning::ln::msgs::SocketAddress;
 use ldk_node::lightning::routing::router::RouteParametersConfig;
 use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description};
 use ldk_node::lightning_types::payment::PaymentHash;
+use ldk_node::logger::{LogLevel, LogWriter};
 use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
 use ldk_node::{Builder, Event, Node};
 use tokio_stream::wrappers::BroadcastStream;
@@ -27,8 +29,10 @@ use tokio_util::sync::CancellationToken;
 use tracing::instrument;
 
 use crate::error::Error;
+use crate::log::StdoutLogWriter;
 
 mod error;
+mod log;
 mod web;
 
 /// CDK Lightning backend using LDK Node
@@ -102,40 +106,72 @@ pub enum GossipSource {
     /// Contains the URL of the RGS server for compressed gossip data
     RapidGossipSync(String),
 }
+/// A builder for an [`CdkLdkNode`] instance.
+#[derive(Debug)]
+pub struct CdkLdkNodeBuilder {
+    network: Network,
+    chain_source: ChainSource,
+    gossip_source: GossipSource,
+    log_dir_path: Option<String>,
+    storage_dir_path: String,
+    fee_reserve: FeeReserve,
+    listening_addresses: Vec<SocketAddress>,
+    seed: Option<Mnemonic>,
+    announcement_addresses: Option<Vec<SocketAddress>>,
+}
 
-impl CdkLdkNode {
-    /// Create a new CDK LDK Node instance
-    ///
-    /// # Arguments
-    /// * `network` - Bitcoin network (mainnet, testnet, regtest, signet)
-    /// * `chain_source` - Source of blockchain data (Esplora or Bitcoin RPC)
-    /// * `gossip_source` - Source of Lightning network gossip data
-    /// * `storage_dir_path` - Directory path for node data storage
-    /// * `fee_reserve` - Fee reserve configuration for payments
-    /// * `listening_address` - Socket addresses for peer connections
-    /// * `runtime` - Optional Tokio runtime to use for starting the node
-    ///
-    /// # Returns
-    /// A new `CdkLdkNode` instance ready to be started
-    ///
-    /// # Errors
-    /// Returns an error if the LDK node builder fails to create the node
+impl CdkLdkNodeBuilder {
+    /// Creates a new builder instance.
     pub fn new(
         network: Network,
         chain_source: ChainSource,
         gossip_source: GossipSource,
         storage_dir_path: String,
         fee_reserve: FeeReserve,
-        listening_address: Vec<SocketAddress>,
-    ) -> Result<Self, Error> {
-        let mut builder = Builder::new();
-        builder.set_network(network);
-        tracing::info!("Storage dir of node is {}", storage_dir_path);
-        builder.set_storage_dir_path(storage_dir_path);
-
-        match chain_source {
+        listening_addresses: Vec<SocketAddress>,
+    ) -> Self {
+        Self {
+            network,
+            chain_source,
+            gossip_source,
+            storage_dir_path,
+            fee_reserve,
+            listening_addresses,
+            seed: None,
+            announcement_addresses: None,
+            log_dir_path: None,
+        }
+    }
+
+    /// Configures the [`CdkLdkNode`] to use the Mnemonic for entropy source configuration
+    pub fn with_seed(mut self, seed: Mnemonic) -> Self {
+        self.seed = Some(seed);
+        self
+    }
+    /// Configures the [`CdkLdkNode`] to use announce this address to the lightning network
+    pub fn with_announcement_address(mut self, announcement_addresses: Vec<SocketAddress>) -> Self {
+        self.announcement_addresses = Some(announcement_addresses);
+        self
+    }
+    /// Configures the [`CdkLdkNode`] to use announce this address to the lightning network
+    pub fn with_log_dir_path(mut self, log_dir_path: String) -> Self {
+        self.log_dir_path = Some(log_dir_path);
+        self
+    }
+
+    /// Builds the [`CdkLdkNode`] instance
+    ///
+    /// # Errors
+    /// Returns an error if the LDK node builder fails to create the node
+    pub fn build(self) -> Result<CdkLdkNode, Error> {
+        let mut ldk = Builder::new();
+        ldk.set_network(self.network);
+        tracing::info!("Storage dir of node is {}", self.storage_dir_path);
+        ldk.set_storage_dir_path(self.storage_dir_path);
+
+        match self.chain_source {
             ChainSource::Esplora(esplora_url) => {
-                builder.set_chain_source_esplora(esplora_url, None);
+                ldk.set_chain_source_esplora(esplora_url, None);
             }
             ChainSource::BitcoinRpc(BitcoinRpcConfig {
                 host,
@@ -143,24 +179,37 @@ impl CdkLdkNode {
                 user,
                 password,
             }) => {
-                builder.set_chain_source_bitcoind_rpc(host, port, user, password);
+                ldk.set_chain_source_bitcoind_rpc(host, port, user, password);
             }
         }
 
-        match gossip_source {
+        match self.gossip_source {
             GossipSource::P2P => {
-                builder.set_gossip_source_p2p();
+                ldk.set_gossip_source_p2p();
             }
             GossipSource::RapidGossipSync(rgs_url) => {
-                builder.set_gossip_source_rgs(rgs_url);
+                ldk.set_gossip_source_rgs(rgs_url);
             }
         }
 
-        builder.set_listening_addresses(listening_address)?;
+        ldk.set_listening_addresses(self.listening_addresses)?;
+        if self.log_dir_path.is_some() {
+            ldk.set_filesystem_logger(self.log_dir_path, Some(LogLevel::Info));
+        } else {
+            ldk.set_custom_logger(Arc::new(StdoutLogWriter));
+        }
 
-        builder.set_node_alias("cdk-ldk-node".to_string())?;
+        ldk.set_node_alias("cdk-ldk-node".to_string())?;
+        // set the seed as bip39 entropy mnemonic
+        if let Some(seed) = self.seed {
+            ldk.set_entropy_bip39_mnemonic(seed, None);
+        }
+        // set the announcement addresses
+        if let Some(announcement_addresses) = self.announcement_addresses {
+            ldk.set_announcement_addresses(announcement_addresses)?;
+        }
 
-        let node = builder.build()?;
+        let node = ldk.build()?;
 
         tracing::info!("Creating tokio channel for payment notifications");
         let (sender, receiver) = tokio::sync::broadcast::channel(8);
@@ -173,12 +222,12 @@ impl CdkLdkNode {
             "Created node {} with address {:?} on network {}",
             id,
             adr,
-            network
+            self.network
         );
 
-        Ok(Self {
+        Ok(CdkLdkNode {
             inner: node.into(),
-            fee_reserve,
+            fee_reserve: self.fee_reserve,
             wait_invoice_cancel_token: CancellationToken::new(),
             wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
             sender,
@@ -187,7 +236,9 @@ impl CdkLdkNode {
             web_addr: None,
         })
     }
+}
 
+impl CdkLdkNode {
     /// Set the web server address for the LDK node management interface
     ///
     /// # Arguments

+ 65 - 0
crates/cdk-ldk-node/src/log.rs

@@ -0,0 +1,65 @@
+//! A logger implementation that writes log messages to stdout. This struct implements the
+//! `LogWriter` trait, which defines a way to handle log records. The log records are formatted,
+//! assigned a severity level, and emitted as structured tracing events.
+pub struct StdoutLogWriter;
+impl crate::LogWriter for StdoutLogWriter {
+    /// Logs a given `LogRecord` instance using structured tracing events.
+    fn log(&self, record: ldk_node::logger::LogRecord) {
+        let level = match record.level.to_string().to_ascii_lowercase().as_str() {
+            "error" => tracing::Level::ERROR,
+            "warn" | "warning" => tracing::Level::WARN,
+            "debug" => tracing::Level::DEBUG,
+            "trace" => tracing::Level::TRACE,
+            _ => tracing::Level::INFO,
+        };
+
+        // Format message once
+        let msg = record.args.to_string();
+        // Emit as a structured tracing event.
+        // Use level-specific macros (require compile-time level) and record the original module path as a field.
+        match level {
+            tracing::Level::ERROR => {
+                tracing::error!(
+                    module_path = record.module_path,
+                    line = record.line,
+                    "{msg}"
+                );
+            }
+            tracing::Level::WARN => {
+                tracing::warn!(
+                    module_path = record.module_path,
+                    line = record.line,
+                    "{msg}"
+                );
+            }
+            tracing::Level::INFO => {
+                tracing::info!(
+                    module_path = record.module_path,
+                    line = record.line,
+                    "{msg}"
+                );
+            }
+            tracing::Level::DEBUG => {
+                tracing::debug!(
+                    module_path = record.module_path,
+                    line = record.line,
+                    "{msg}"
+                );
+            }
+            tracing::Level::TRACE => {
+                tracing::trace!(
+                    module_path = record.module_path,
+                    line = record.line,
+                    "{msg}"
+                );
+            }
+        }
+    }
+}
+
+impl Default for StdoutLogWriter {
+    /// Provides the default implementation for the struct.
+    fn default() -> Self {
+        Self {}
+    }
+}

+ 10 - 0
crates/cdk-mintd/example.config.toml

@@ -127,11 +127,20 @@ ln_backend = "fakewallet"
 # bitcoin_network = "signet"  # mainnet, testnet, signet, regtest
 # chain_source_type = "esplora"  # esplora, bitcoinrpc  
 # 
+# # IMPORTANT: LDK Node Seed Configuration
+# # For NEW nodes: ldk_node_mnemonic MUST be set. This is the BIP39 mnemonic used to derive
+# # the LDK node's keys. Keep this secure and backed up!
+# # For EXISTING nodes: If omitted, the node will use its stored seed from the storage directory.
+# # This maintains backward compatibility with nodes created before this configuration was added.
+# ldk_node_mnemonic = "your twelve or twenty-four word mnemonic phrase here"
+# 
 # # Mutinynet configuration (recommended for testing)
 # esplora_url = "https://mutinynet.com/api"
 # gossip_source_type = "rgs"  # Use RGS for better performance
 # rgs_url = "https://rgs.mutinynet.com/snapshot/0"
 # storage_dir_path = "~/.cdk-ldk-node/mutinynet"
+# log_dir_path = ".cdk-ldk-node/ldk-node/ldk_node.log"
+
 # 
 # # Testnet configuration
 # # bitcoin_network = "testnet"
@@ -154,6 +163,7 @@ ln_backend = "fakewallet"
 # # Node configuration
 # ldk_node_host = "127.0.0.1"
 # ldk_node_port = 8090
+
 # 
 # # Gossip source configuration  
 # gossip_source_type = "p2p"  # p2p (direct peer-to-peer) or rgs (rapid gossip sync)

+ 10 - 0
crates/cdk-mintd/src/config.rs

@@ -290,10 +290,14 @@ pub struct LdkNode {
     pub bitcoind_rpc_password: Option<String>,
     /// Storage directory path
     pub storage_dir_path: Option<String>,
+    /// Log directory path (logging stdout if omitted)
+    pub log_dir_path: Option<String>,
     /// LDK node listening host
     pub ldk_node_host: Option<String>,
     /// LDK node listening port
     pub ldk_node_port: Option<u16>,
+    /// LDK node announcement addresses
+    pub ldk_node_announce_addresses: Option<Vec<String>>,
     /// Gossip source type (p2p or rgs)
     pub gossip_source_type: Option<String>,
     /// Rapid Gossip Sync URL (when gossip_source_type = "rgs")
@@ -304,6 +308,9 @@ pub struct LdkNode {
     /// Webserver port
     #[serde(default = "default_webserver_port")]
     pub webserver_port: Option<u16>,
+    /// LDK node mnemonic
+    /// If not set, LDK node will use its default seed storage mechanism
+    pub ldk_node_mnemonic: Option<String>,
 }
 
 #[cfg(feature = "ldk-node")]
@@ -318,14 +325,17 @@ impl Default for LdkNode {
             bitcoind_rpc_host: None,
             bitcoind_rpc_port: None,
             bitcoind_rpc_user: None,
+            ldk_node_announce_addresses: None,
             bitcoind_rpc_password: None,
             storage_dir_path: None,
             ldk_node_host: None,
+            log_dir_path: None,
             ldk_node_port: None,
             gossip_source_type: None,
             rgs_url: None,
             webserver_host: default_webserver_host(),
             webserver_port: default_webserver_port(),
+            ldk_node_mnemonic: None,
         }
     }
 }

+ 10 - 0
crates/cdk-mintd/src/env_vars/ldk_node.rs

@@ -15,12 +15,14 @@ pub const LDK_NODE_BITCOIND_RPC_PORT_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_BITCOIN
 pub const LDK_NODE_BITCOIND_RPC_USER_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_BITCOIND_RPC_USER";
 pub const LDK_NODE_BITCOIND_RPC_PASSWORD_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_BITCOIND_RPC_PASSWORD";
 pub const LDK_NODE_STORAGE_DIR_PATH_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_STORAGE_DIR_PATH";
+pub const LDK_NODE_LOG_DIR_PATH_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_LOG_DIR_PATH";
 pub const LDK_NODE_LDK_NODE_HOST_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_LDK_NODE_HOST";
 pub const LDK_NODE_LDK_NODE_PORT_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_LDK_NODE_PORT";
 pub const LDK_NODE_GOSSIP_SOURCE_TYPE_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_GOSSIP_SOURCE_TYPE";
 pub const LDK_NODE_RGS_URL_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_RGS_URL";
 pub const LDK_NODE_WEBSERVER_HOST_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_WEBSERVER_HOST";
 pub const LDK_NODE_WEBSERVER_PORT_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_WEBSERVER_PORT";
+pub const LDK_NODE_MNEMONIC_ENV_VAR: &str = "CDK_MINTD_LDK_NODE_MNEMONIC";
 
 impl LdkNode {
     pub fn from_env(mut self) -> Self {
@@ -70,6 +72,10 @@ impl LdkNode {
             self.storage_dir_path = Some(storage_dir_path);
         }
 
+        if let Ok(log_dir_path) = env::var(LDK_NODE_LOG_DIR_PATH_ENV_VAR) {
+            self.log_dir_path = Some(log_dir_path);
+        }
+
         if let Ok(ldk_node_host) = env::var(LDK_NODE_LDK_NODE_HOST_ENV_VAR) {
             self.ldk_node_host = Some(ldk_node_host);
         }
@@ -98,6 +104,10 @@ impl LdkNode {
             }
         }
 
+        if let Ok(ldk_node_mnemonic) = env::var(LDK_NODE_MNEMONIC_ENV_VAR) {
+            self.ldk_node_mnemonic = Some(ldk_node_mnemonic);
+        }
+
         self
     }
 }

+ 63 - 11
crates/cdk-mintd/src/setup.rs

@@ -3,12 +3,10 @@ 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;
 
-#[cfg(feature = "cln")]
-use anyhow::anyhow;
-#[cfg(any(feature = "lnbits", feature = "lnd"))]
-use anyhow::bail;
 use async_trait::async_trait;
 #[cfg(feature = "fakewallet")]
 use bip39::rand::{thread_rng, Rng};
@@ -53,7 +51,7 @@ impl LnBackendSetup for config::Cln {
     ) -> anyhow::Result<cdk_cln::Cln> {
         // Validate required connection field
         if self.rpc_path.as_os_str().is_empty() {
-            return Err(anyhow!(
+            return Err(anyhow::anyhow!(
                 "CLN rpc_path must be set via config or CDK_MINTD_CLN_RPC_PATH env var"
             ));
         }
@@ -61,9 +59,9 @@ impl LnBackendSetup for config::Cln {
         let cln_socket = expand_path(
             self.rpc_path
                 .to_str()
-                .ok_or(anyhow!("cln socket not defined"))?,
+                .ok_or(anyhow::anyhow!("cln socket not defined"))?,
         )
-        .ok_or(anyhow!("cln socket not defined"))?;
+        .ok_or(anyhow::anyhow!("cln socket not defined"))?;
 
         let fee_reserve = FeeReserve {
             min_fee_reserve: self.reserve_fee_min,
@@ -92,6 +90,8 @@ impl LnBackendSetup for config::LNbits {
         _work_dir: &Path,
         _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_lnbits::LNbits> {
+        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");
@@ -139,6 +139,7 @@ impl LnBackendSetup for config::Lnd {
         _work_dir: &Path,
         kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_lnd::Lnd> {
+        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");
@@ -233,7 +234,7 @@ impl LnBackendSetup for config::GrpcProcessor {
 impl LnBackendSetup for config::LdkNode {
     async fn setup(
         &self,
-        _settings: &Settings,
+        settings: &Settings,
         _unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         work_dir: &Path,
@@ -241,6 +242,8 @@ impl LnBackendSetup for config::LdkNode {
     ) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
         use std::net::SocketAddr;
 
+        use anyhow::bail;
+        use bip39::Mnemonic;
         use bitcoin::Network;
 
         let fee_reserve = FeeReserve {
@@ -330,15 +333,61 @@ impl LnBackendSetup for config::LdkNode {
         // For now, let's construct it manually based on the cdk-ldk-node implementation
         let listen_address = vec![socket_addr.into()];
 
-        let mut ldk_node = cdk_ldk_node::CdkLdkNode::new(
+        // 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::<Mnemonic>()
+                    .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);
@@ -358,7 +407,10 @@ impl LnBackendSetup for config::LdkNode {
             "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)

+ 1 - 0
crates/cdk/src/wallet/melt/mod.rs

@@ -439,6 +439,7 @@ impl<'a> PreparedMelt<'a> {
     /// ```rust,no_run
     /// # async fn example(wallet: &cdk::wallet::Wallet) -> anyhow::Result<()> {
     /// use std::collections::HashMap;
+    ///
     /// use cdk::nuts::PaymentMethod;
     /// use cdk::wallet::MeltOutcome;
     ///