Skip to main content

cdk_mintd/
setup.rs

1#[cfg(feature = "fakewallet")]
2use std::collections::HashMap;
3#[cfg(feature = "fakewallet")]
4use std::collections::HashSet;
5use std::path::Path;
6#[cfg(feature = "ldk-node")]
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use async_trait::async_trait;
11#[cfg(feature = "fakewallet")]
12use bip39::rand::{thread_rng, Rng};
13use cdk::cdk_database::KVStore;
14use cdk::cdk_payment::MintPayment;
15use cdk::nuts::CurrencyUnit;
16#[cfg(any(
17    feature = "lnbits",
18    feature = "cln",
19    feature = "lnd",
20    feature = "ldk-node",
21    feature = "fakewallet"
22))]
23use cdk::types::FeeReserve;
24
25use crate::config::{self, Settings};
26#[cfg(feature = "cln")]
27use crate::expand_path;
28
29#[async_trait]
30pub trait LnBackendSetup {
31    async fn setup(
32        &self,
33        settings: &Settings,
34        unit: CurrencyUnit,
35        runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
36        work_dir: &Path,
37        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
38    ) -> anyhow::Result<impl MintPayment>;
39}
40
41#[cfg(feature = "cln")]
42#[async_trait]
43impl LnBackendSetup for config::Cln {
44    async fn setup(
45        &self,
46        _settings: &Settings,
47        _unit: CurrencyUnit,
48        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
49        _work_dir: &Path,
50        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
51    ) -> anyhow::Result<cdk_cln::Cln> {
52        // Validate required connection field
53        if self.rpc_path.as_os_str().is_empty() {
54            return Err(anyhow::anyhow!(
55                "CLN rpc_path must be set via config or CDK_MINTD_CLN_RPC_PATH env var"
56            ));
57        }
58
59        let cln_socket = expand_path(
60            self.rpc_path
61                .to_str()
62                .ok_or(anyhow::anyhow!("cln socket not defined"))?,
63        )
64        .ok_or(anyhow::anyhow!("cln socket not defined"))?;
65
66        let fee_reserve = FeeReserve {
67            min_fee_reserve: self.reserve_fee_min,
68            percent_fee_reserve: self.fee_percent,
69        };
70
71        let cln = cdk_cln::Cln::new(
72            cln_socket,
73            fee_reserve,
74            self.expose_private_channels,
75            kv_store.expect("Cln needs kv store"),
76        )
77        .await?;
78
79        Ok(cln)
80    }
81}
82
83#[cfg(feature = "lnbits")]
84#[async_trait]
85impl LnBackendSetup for config::LNbits {
86    async fn setup(
87        &self,
88        _settings: &Settings,
89        _unit: CurrencyUnit,
90        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
91        _work_dir: &Path,
92        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
93    ) -> anyhow::Result<cdk_lnbits::LNbits> {
94        use anyhow::bail;
95
96        // Validate required connection fields
97        if self.admin_api_key.is_empty() {
98            bail!("LNbits admin_api_key must be set via config or CDK_MINTD_LNBITS_ADMIN_API_KEY env var");
99        }
100        if self.invoice_api_key.is_empty() {
101            bail!("LNbits invoice_api_key must be set via config or CDK_MINTD_LNBITS_INVOICE_API_KEY env var");
102        }
103        if self.lnbits_api.is_empty() {
104            bail!(
105                "LNbits lnbits_api must be set via config or CDK_MINTD_LNBITS_LNBITS_API env var"
106            );
107        }
108
109        let admin_api_key = &self.admin_api_key;
110        let invoice_api_key = &self.invoice_api_key;
111
112        let fee_reserve = FeeReserve {
113            min_fee_reserve: self.reserve_fee_min,
114            percent_fee_reserve: self.fee_percent,
115        };
116
117        let lnbits = cdk_lnbits::LNbits::new(
118            admin_api_key.clone(),
119            invoice_api_key.clone(),
120            self.lnbits_api.clone(),
121            fee_reserve,
122        )
123        .await?;
124
125        // Use v1 websocket API
126        lnbits.subscribe_ws().await?;
127
128        Ok(lnbits)
129    }
130}
131
132#[cfg(feature = "lnd")]
133#[async_trait]
134impl LnBackendSetup for config::Lnd {
135    async fn setup(
136        &self,
137        _settings: &Settings,
138        _unit: CurrencyUnit,
139        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
140        _work_dir: &Path,
141        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
142    ) -> anyhow::Result<cdk_lnd::Lnd> {
143        use anyhow::bail;
144        // Validate required connection fields
145        if self.address.is_empty() {
146            bail!("LND address must be set via config or CDK_MINTD_LND_ADDRESS env var");
147        }
148        if self.cert_file.as_os_str().is_empty() {
149            bail!("LND cert_file must be set via config or CDK_MINTD_LND_CERT_FILE env var");
150        }
151        if self.macaroon_file.as_os_str().is_empty() {
152            bail!(
153                "LND macaroon_file must be set via config or CDK_MINTD_LND_MACAROON_FILE env var"
154            );
155        }
156
157        let address = &self.address;
158        let cert_file = &self.cert_file;
159        let macaroon_file = &self.macaroon_file;
160
161        let fee_reserve = FeeReserve {
162            min_fee_reserve: self.reserve_fee_min,
163            percent_fee_reserve: self.fee_percent,
164        };
165
166        let lnd = cdk_lnd::Lnd::new(
167            address.to_string(),
168            cert_file.clone(),
169            macaroon_file.clone(),
170            fee_reserve,
171            kv_store.expect("Lnd needs kv store"),
172        )
173        .await?;
174
175        Ok(lnd)
176    }
177}
178
179#[cfg(feature = "fakewallet")]
180#[async_trait]
181impl LnBackendSetup for config::FakeWallet {
182    async fn setup(
183        &self,
184        _settings: &Settings,
185        unit: CurrencyUnit,
186        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
187        _work_dir: &Path,
188        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
189    ) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
190        let fee_reserve = FeeReserve {
191            min_fee_reserve: self.reserve_fee_min,
192            percent_fee_reserve: self.fee_percent,
193        };
194
195        // calculate random delay time
196        let mut rng = thread_rng();
197        let delay_time = rng.gen_range(self.min_delay_time..=self.max_delay_time);
198
199        let fake_wallet = cdk_fake_wallet::FakeWallet::new(
200            fee_reserve,
201            HashMap::default(),
202            HashSet::default(),
203            delay_time,
204            unit,
205        );
206
207        Ok(fake_wallet)
208    }
209}
210
211#[cfg(feature = "grpc-processor")]
212#[async_trait]
213impl LnBackendSetup for config::GrpcProcessor {
214    async fn setup(
215        &self,
216        _settings: &Settings,
217        _unit: CurrencyUnit,
218        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
219        _work_dir: &Path,
220        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
221    ) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
222        let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
223            &self.addr,
224            self.port,
225            self.tls_dir.clone(),
226        )
227        .await?;
228
229        Ok(payment_processor)
230    }
231}
232
233#[cfg(feature = "ldk-node")]
234#[async_trait]
235impl LnBackendSetup for config::LdkNode {
236    async fn setup(
237        &self,
238        settings: &Settings,
239        _unit: CurrencyUnit,
240        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
241        work_dir: &Path,
242        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
243    ) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
244        use std::net::SocketAddr;
245
246        use anyhow::bail;
247        use bip39::Mnemonic;
248        use bitcoin::Network;
249
250        let fee_reserve = FeeReserve {
251            min_fee_reserve: self.reserve_fee_min,
252            percent_fee_reserve: self.fee_percent,
253        };
254
255        // Parse network from config
256        let network = match self
257            .bitcoin_network
258            .as_ref()
259            .map(|n| n.to_lowercase())
260            .as_deref()
261            .unwrap_or("regtest")
262        {
263            "mainnet" | "bitcoin" => Network::Bitcoin,
264            "testnet" => Network::Testnet,
265            "signet" => Network::Signet,
266            _ => Network::Regtest,
267        };
268
269        // Parse chain source from config
270        let chain_source = match self
271            .chain_source_type
272            .as_ref()
273            .map(|s| s.to_lowercase())
274            .as_deref()
275            .unwrap_or("esplora")
276        {
277            "bitcoinrpc" => {
278                let host = self
279                    .bitcoind_rpc_host
280                    .clone()
281                    .unwrap_or_else(|| "127.0.0.1".to_string());
282                let port = self.bitcoind_rpc_port.unwrap_or(18443);
283                let user = self
284                    .bitcoind_rpc_user
285                    .clone()
286                    .unwrap_or_else(|| "testuser".to_string());
287                let password = self
288                    .bitcoind_rpc_password
289                    .clone()
290                    .unwrap_or_else(|| "testpass".to_string());
291
292                cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
293                    host,
294                    port,
295                    user,
296                    password,
297                })
298            }
299            _ => {
300                let esplora_url = self
301                    .esplora_url
302                    .clone()
303                    .unwrap_or_else(|| "https://mutinynet.com/api".to_string());
304                cdk_ldk_node::ChainSource::Esplora(esplora_url)
305            }
306        };
307
308        // Parse gossip source from config
309        let gossip_source = match self.rgs_url.clone() {
310            Some(rgs_url) => cdk_ldk_node::GossipSource::RapidGossipSync(rgs_url),
311            None => cdk_ldk_node::GossipSource::P2P,
312        };
313
314        // Get storage directory path
315        let storage_dir_path = if let Some(dir_path) = &self.storage_dir_path {
316            dir_path.clone()
317        } else {
318            let mut work_dir = work_dir.to_path_buf();
319            work_dir.push("ldk-node");
320            work_dir.to_string_lossy().to_string()
321        };
322
323        // Get LDK node listen address
324        let host = self
325            .ldk_node_host
326            .clone()
327            .unwrap_or_else(|| "127.0.0.1".to_string());
328        let port = self.ldk_node_port.unwrap_or(8090);
329
330        let socket_addr = SocketAddr::new(host.parse()?, port);
331
332        // Parse socket address using ldk_node's SocketAddress
333        // We need to get the actual socket address struct from ldk_node
334        // For now, let's construct it manually based on the cdk-ldk-node implementation
335        let listen_address = vec![socket_addr.into()];
336
337        // Check if ldk_node_mnemonic is provided in the ldk_node config
338        let mnemonic_opt = settings
339            .clone()
340            .ldk_node
341            .as_ref()
342            .and_then(|ldk_config| ldk_config.ldk_node_mnemonic.clone());
343
344        // Only set seed if mnemonic is explicitly provided
345        // This maintains backward compatibility with existing nodes that use LDK's default seed storage
346        let seed = if let Some(mnemonic_str) = mnemonic_opt {
347            Some(
348                mnemonic_str
349                    .parse::<Mnemonic>()
350                    .map_err(|e| anyhow::anyhow!("invalid ldk_node_mnemonic in config: {e}"))?,
351            )
352        } else {
353            // Check if this is a new node or an existing node
354            let storage_dir = PathBuf::from(&storage_dir_path);
355            let keys_seed_file = storage_dir.join("keys_seed");
356
357            if !keys_seed_file.exists() {
358                bail!("ldk_node_mnemonic should be set in the [ldk_node] configuration section.");
359            }
360
361            // Existing node with stored seed, don't set a mnemonic
362            None
363        };
364
365        let ldk_node_settings = settings
366            .ldk_node
367            .as_ref()
368            .ok_or_else(|| anyhow::anyhow!("ldk_node configuration is required"))?;
369        let announce_addrs: Vec<_> = ldk_node_settings
370            .ldk_node_announce_addresses
371            .as_ref()
372            .map(|addrs| addrs.iter().filter_map(|addr| addr.parse().ok()).collect())
373            .unwrap_or_default();
374
375        let mut ldk_node_builder = cdk_ldk_node::CdkLdkNodeBuilder::new(
376            network,
377            chain_source,
378            gossip_source,
379            storage_dir_path,
380            fee_reserve,
381            listen_address,
382        );
383
384        // Only set seed if provided
385        if let Some(mnemonic) = seed {
386            ldk_node_builder = ldk_node_builder.with_seed(mnemonic);
387        }
388
389        if !announce_addrs.is_empty() {
390            ldk_node_builder = ldk_node_builder.with_announcement_address(announce_addrs)
391        }
392        // Configure webserver address if specified
393        let webserver_addr = if let Some(host) = &self.webserver_host {
394            let port = self.webserver_port.unwrap_or(8091);
395            let socket_addr: SocketAddr = format!("{host}:{port}").parse()?;
396            Some(socket_addr)
397        } else if self.webserver_port.is_some() {
398            // If only port is specified, use default host
399            let port = self.webserver_port.unwrap_or(8091);
400            let socket_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?;
401            Some(socket_addr)
402        } else {
403            // Use default webserver address if nothing is configured
404            Some(cdk_ldk_node::CdkLdkNode::default_web_addr())
405        };
406
407        println!(
408            "webserver: {}",
409            webserver_addr.map_or("none".to_string(), |a| a.to_string())
410        );
411        if let Some(log_dir_path) = ldk_node_settings.log_dir_path.as_ref() {
412            ldk_node_builder = ldk_node_builder.with_log_dir_path(log_dir_path.clone());
413        }
414        let mut ldk_node = ldk_node_builder.build()?;
415        ldk_node.set_web_addr(webserver_addr);
416
417        Ok(ldk_node)
418    }
419}