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