cdk_mintd/
setup.rs

1#[cfg(feature = "fakewallet")]
2use std::collections::HashMap;
3#[cfg(feature = "fakewallet")]
4use std::collections::HashSet;
5use std::path::Path;
6use std::sync::Arc;
7
8#[cfg(feature = "cln")]
9use anyhow::anyhow;
10#[cfg(any(feature = "lnbits", feature = "lnd"))]
11use anyhow::bail;
12use async_trait::async_trait;
13#[cfg(feature = "fakewallet")]
14use bip39::rand::{thread_rng, Rng};
15use cdk::cdk_database::MintKVStore;
16use cdk::cdk_payment::MintPayment;
17use cdk::nuts::CurrencyUnit;
18#[cfg(any(
19    feature = "lnbits",
20    feature = "cln",
21    feature = "lnd",
22    feature = "ldk-node",
23    feature = "fakewallet"
24))]
25use cdk::types::FeeReserve;
26
27use crate::config::{self, Settings};
28#[cfg(feature = "cln")]
29use crate::expand_path;
30
31#[async_trait]
32pub trait LnBackendSetup {
33    async fn setup(
34        &self,
35        settings: &Settings,
36        unit: CurrencyUnit,
37        runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
38        work_dir: &Path,
39        kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
40    ) -> anyhow::Result<impl MintPayment>;
41}
42
43#[cfg(feature = "cln")]
44#[async_trait]
45impl LnBackendSetup for config::Cln {
46    async fn setup(
47        &self,
48        _settings: &Settings,
49        _unit: CurrencyUnit,
50        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
51        _work_dir: &Path,
52        kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
53    ) -> anyhow::Result<cdk_cln::Cln> {
54        // Validate required connection field
55        if self.rpc_path.as_os_str().is_empty() {
56            return Err(anyhow!(
57                "CLN rpc_path must be set via config or CDK_MINTD_CLN_RPC_PATH env var"
58            ));
59        }
60
61        let cln_socket = expand_path(
62            self.rpc_path
63                .to_str()
64                .ok_or(anyhow!("cln socket not defined"))?,
65        )
66        .ok_or(anyhow!("cln socket not defined"))?;
67
68        let fee_reserve = FeeReserve {
69            min_fee_reserve: self.reserve_fee_min,
70            percent_fee_reserve: self.fee_percent,
71        };
72
73        let cln = cdk_cln::Cln::new(
74            cln_socket,
75            fee_reserve,
76            kv_store.expect("Cln needs kv store"),
77        )
78        .await?;
79
80        Ok(cln)
81    }
82}
83
84#[cfg(feature = "lnbits")]
85#[async_trait]
86impl LnBackendSetup for config::LNbits {
87    async fn setup(
88        &self,
89        _settings: &Settings,
90        _unit: CurrencyUnit,
91        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
92        _work_dir: &Path,
93        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
94    ) -> anyhow::Result<cdk_lnbits::LNbits> {
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 MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
141    ) -> anyhow::Result<cdk_lnd::Lnd> {
142        // Validate required connection fields
143        if self.address.is_empty() {
144            bail!("LND address must be set via config or CDK_MINTD_LND_ADDRESS env var");
145        }
146        if self.cert_file.as_os_str().is_empty() {
147            bail!("LND cert_file must be set via config or CDK_MINTD_LND_CERT_FILE env var");
148        }
149        if self.macaroon_file.as_os_str().is_empty() {
150            bail!(
151                "LND macaroon_file must be set via config or CDK_MINTD_LND_MACAROON_FILE env var"
152            );
153        }
154
155        let address = &self.address;
156        let cert_file = &self.cert_file;
157        let macaroon_file = &self.macaroon_file;
158
159        let fee_reserve = FeeReserve {
160            min_fee_reserve: self.reserve_fee_min,
161            percent_fee_reserve: self.fee_percent,
162        };
163
164        let lnd = cdk_lnd::Lnd::new(
165            address.to_string(),
166            cert_file.clone(),
167            macaroon_file.clone(),
168            fee_reserve,
169            kv_store.expect("Lnd needs kv store"),
170        )
171        .await?;
172
173        Ok(lnd)
174    }
175}
176
177#[cfg(feature = "fakewallet")]
178#[async_trait]
179impl LnBackendSetup for config::FakeWallet {
180    async fn setup(
181        &self,
182        _settings: &Settings,
183        unit: CurrencyUnit,
184        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
185        _work_dir: &Path,
186        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
187    ) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
188        let fee_reserve = FeeReserve {
189            min_fee_reserve: self.reserve_fee_min,
190            percent_fee_reserve: self.fee_percent,
191        };
192
193        // calculate random delay time
194        let mut rng = thread_rng();
195        let delay_time = rng.gen_range(self.min_delay_time..=self.max_delay_time);
196
197        let fake_wallet = cdk_fake_wallet::FakeWallet::new(
198            fee_reserve,
199            HashMap::default(),
200            HashSet::default(),
201            delay_time,
202            unit,
203        );
204
205        Ok(fake_wallet)
206    }
207}
208
209#[cfg(feature = "grpc-processor")]
210#[async_trait]
211impl LnBackendSetup for config::GrpcProcessor {
212    async fn setup(
213        &self,
214        _settings: &Settings,
215        _unit: CurrencyUnit,
216        _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
217        _work_dir: &Path,
218        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
219    ) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
220        let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
221            &self.addr,
222            self.port,
223            self.tls_dir.clone(),
224        )
225        .await?;
226
227        Ok(payment_processor)
228    }
229}
230
231#[cfg(feature = "ldk-node")]
232#[async_trait]
233impl LnBackendSetup for config::LdkNode {
234    async fn setup(
235        &self,
236        _settings: &Settings,
237        _unit: CurrencyUnit,
238        runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
239        work_dir: &Path,
240        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
241    ) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
242        use std::net::SocketAddr;
243
244        use bitcoin::Network;
245
246        let fee_reserve = FeeReserve {
247            min_fee_reserve: self.reserve_fee_min,
248            percent_fee_reserve: self.fee_percent,
249        };
250
251        // Parse network from config
252        let network = match self
253            .bitcoin_network
254            .as_ref()
255            .map(|n| n.to_lowercase())
256            .as_deref()
257            .unwrap_or("regtest")
258        {
259            "mainnet" | "bitcoin" => Network::Bitcoin,
260            "testnet" => Network::Testnet,
261            "signet" => Network::Signet,
262            _ => Network::Regtest,
263        };
264
265        // Parse chain source from config
266        let chain_source = match self
267            .chain_source_type
268            .as_ref()
269            .map(|s| s.to_lowercase())
270            .as_deref()
271            .unwrap_or("esplora")
272        {
273            "bitcoinrpc" => {
274                let host = self
275                    .bitcoind_rpc_host
276                    .clone()
277                    .unwrap_or_else(|| "127.0.0.1".to_string());
278                let port = self.bitcoind_rpc_port.unwrap_or(18443);
279                let user = self
280                    .bitcoind_rpc_user
281                    .clone()
282                    .unwrap_or_else(|| "testuser".to_string());
283                let password = self
284                    .bitcoind_rpc_password
285                    .clone()
286                    .unwrap_or_else(|| "testpass".to_string());
287
288                cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
289                    host,
290                    port,
291                    user,
292                    password,
293                })
294            }
295            _ => {
296                let esplora_url = self
297                    .esplora_url
298                    .clone()
299                    .unwrap_or_else(|| "https://mutinynet.com/api".to_string());
300                cdk_ldk_node::ChainSource::Esplora(esplora_url)
301            }
302        };
303
304        // Parse gossip source from config
305        let gossip_source = match self.rgs_url.clone() {
306            Some(rgs_url) => cdk_ldk_node::GossipSource::RapidGossipSync(rgs_url),
307            None => cdk_ldk_node::GossipSource::P2P,
308        };
309
310        // Get storage directory path
311        let storage_dir_path = if let Some(dir_path) = &self.storage_dir_path {
312            dir_path.clone()
313        } else {
314            let mut work_dir = work_dir.to_path_buf();
315            work_dir.push("ldk-node");
316            work_dir.to_string_lossy().to_string()
317        };
318
319        // Get LDK node listen address
320        let host = self
321            .ldk_node_host
322            .clone()
323            .unwrap_or_else(|| "127.0.0.1".to_string());
324        let port = self.ldk_node_port.unwrap_or(8090);
325
326        let socket_addr = SocketAddr::new(host.parse()?, port);
327
328        // Parse socket address using ldk_node's SocketAddress
329        // We need to get the actual socket address struct from ldk_node
330        // For now, let's construct it manually based on the cdk-ldk-node implementation
331        let listen_address = vec![socket_addr.into()];
332
333        let mut ldk_node = cdk_ldk_node::CdkLdkNode::new(
334            network,
335            chain_source,
336            gossip_source,
337            storage_dir_path,
338            fee_reserve,
339            listen_address,
340            runtime,
341        )?;
342
343        // Configure webserver address if specified
344        let webserver_addr = if let Some(host) = &self.webserver_host {
345            let port = self.webserver_port.unwrap_or(8091);
346            let socket_addr: SocketAddr = format!("{host}:{port}").parse()?;
347            Some(socket_addr)
348        } else if self.webserver_port.is_some() {
349            // If only port is specified, use default host
350            let port = self.webserver_port.unwrap_or(8091);
351            let socket_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?;
352            Some(socket_addr)
353        } else {
354            // Use default webserver address if nothing is configured
355            Some(cdk_ldk_node::CdkLdkNode::default_web_addr())
356        };
357
358        println!("webserver: {:?}", webserver_addr);
359
360        ldk_node.set_web_addr(webserver_addr);
361
362        Ok(ldk_node)
363    }
364}