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 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 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 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 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 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 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 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 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 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 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 let listen_address = vec![socket_addr.into()];
336
337 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 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 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 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 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 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 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 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}