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