1use std::collections::HashMap;
2use std::ops::ControlFlow;
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::time::SystemTime;
6
7use anyhow::{Context, Result, anyhow};
8use bitcoin::hashes::sha256;
9use chrono::{DateTime, Utc};
10use esplora_client::Txid;
11use fedimint_core::config::FederationId;
12use fedimint_core::secp256k1::PublicKey;
13use fedimint_core::util::{backoff_util, retry};
14use fedimint_core::{Amount, BitcoinAmountOrAll, BitcoinHash};
15use fedimint_gateway_common::envs::FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV;
16use fedimint_gateway_common::{
17 ChannelInfo, CreateOfferResponse, GatewayBalances, GetInvoiceResponse,
18 ListTransactionsResponse, MnemonicResponse, PaymentDetails, PaymentStatus,
19 PaymentSummaryResponse, V1_API_ENDPOINT,
20};
21use fedimint_ln_server::common::lightning_invoice::Bolt11Invoice;
22use fedimint_lnv2_common::gateway_api::PaymentFee;
23use fedimint_logging::LOG_DEVIMINT;
24use fedimint_testing_core::node_type::LightningNodeType;
25use rand::rngs::OsRng;
26use semver::Version;
27use tracing::info;
28
29use crate::cmd;
30use crate::envs::{
31 FM_GATEWAY_API_ADDR_ENV, FM_GATEWAY_DATA_DIR_ENV, FM_GATEWAY_IROH_LISTEN_ADDR_ENV,
32 FM_GATEWAY_LISTEN_ADDR_ENV, FM_PORT_LDK_ENV,
33};
34use crate::external::{Bitcoind, LightningNode};
35use crate::federation::Federation;
36use crate::util::{Command, ProcessHandle, ProcessManager, poll, supports_lnv2};
37use crate::vars::utf8;
38use crate::version_constants::VERSION_0_9_0_ALPHA;
39
40#[derive(Clone)]
41pub struct Gatewayd {
42 pub(crate) process: ProcessHandle,
43 pub ln: LightningNode,
44 pub addr: String,
45 pub(crate) lightning_node_addr: String,
46 pub gatewayd_version: Version,
47 pub gw_name: String,
48 pub log_path: PathBuf,
49 pub gw_port: u16,
50 pub ldk_port: u16,
51 pub gateway_id: String,
52 pub iroh_port: u16,
53 pub node_id: iroh_base::NodeId,
54}
55
56impl Gatewayd {
57 pub async fn new(process_mgr: &ProcessManager, ln: LightningNode) -> Result<Self> {
58 let ln_type = ln.ln_type();
59 let (gw_name, port, lightning_node_port, iroh_port) = match &ln {
60 LightningNode::Lnd(_) => (
61 "gatewayd-lnd".to_string(),
62 process_mgr.globals.FM_PORT_GW_LND,
63 process_mgr.globals.FM_PORT_LND_LISTEN,
64 process_mgr.globals.FM_PORT_GW_LND_IROH,
65 ),
66 LightningNode::Ldk {
67 name,
68 gw_port,
69 ldk_port,
70 iroh_port,
71 } => (
72 name.to_owned(),
73 gw_port.to_owned(),
74 ldk_port.to_owned(),
75 iroh_port.to_owned(),
76 ),
77 };
78 let test_dir = &process_mgr.globals.FM_TEST_DIR;
79 let addr = format!("http://127.0.0.1:{port}/{V1_API_ENDPOINT}");
80 let lightning_node_addr = format!("127.0.0.1:{lightning_node_port}");
81 let iroh_sk = iroh_base::SecretKey::generate(&mut OsRng);
82
83 let mut gateway_env: HashMap<String, String> = HashMap::from_iter([
84 (
85 FM_GATEWAY_DATA_DIR_ENV.to_owned(),
86 format!("{}/{gw_name}", utf8(test_dir)),
87 ),
88 (
89 FM_GATEWAY_LISTEN_ADDR_ENV.to_owned(),
90 format!("127.0.0.1:{port}"),
91 ),
92 (FM_GATEWAY_API_ADDR_ENV.to_owned(), addr.clone()),
93 (FM_PORT_LDK_ENV.to_owned(), lightning_node_port.to_string()),
94 (
95 FM_GATEWAY_IROH_LISTEN_ADDR_ENV.to_owned(),
96 format!("127.0.0.1:{iroh_port}"),
97 ),
98 (
99 FM_GATEWAY_IROH_SECRET_KEY_OVERRIDE_ENV.to_owned(),
100 iroh_sk.to_string(),
101 ),
102 ]);
103
104 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
105 if gatewayd_version < *VERSION_0_9_0_ALPHA {
106 let mode = if supports_lnv2() {
107 "All"
108 } else {
109 info!(target: LOG_DEVIMINT, "LNv2 is not supported, running gatewayd in LNv1 mode");
110 "LNv1"
111 };
112 gateway_env.insert(
113 "FM_GATEWAY_LIGHTNING_MODULE_MODE".to_owned(),
114 mode.to_string(),
115 );
116 }
117
118 if ln_type == LightningNodeType::Ldk {
119 gateway_env.insert("FM_LDK_ALIAS".to_owned(), gw_name.clone());
120
121 if gatewayd_version < *VERSION_0_9_0_ALPHA {
123 let btc_rpc_port = process_mgr.globals.FM_PORT_BTC_RPC;
124 gateway_env.insert(
125 "FM_LDK_BITCOIND_RPC_URL".to_owned(),
126 format!("http://bitcoin:bitcoin@127.0.0.1:{btc_rpc_port}"),
127 );
128 }
129 }
130
131 let process = process_mgr
132 .spawn_daemon(
133 &gw_name,
134 cmd!(crate::util::Gatewayd, ln_type).envs(gateway_env),
135 )
136 .await?;
137
138 let gateway_id = poll(
139 "waiting for gateway to be ready to respond to rpc",
140 || async {
141 let info = cmd!(
143 crate::util::get_gateway_cli_path(),
144 "--rpcpassword",
145 "theresnosecondbest",
146 "-a",
147 addr,
148 "info"
149 )
150 .out_json()
151 .await
152 .map_err(ControlFlow::Continue)?;
153 let gateway_id = info["gateway_id"]
154 .as_str()
155 .context("gateway_id must be a string")
156 .map_err(ControlFlow::Break)?
157 .to_owned();
158 Ok(gateway_id)
159 },
160 )
161 .await?;
162
163 let log_path = process_mgr
164 .globals
165 .FM_LOGS_DIR
166 .join(format!("{gw_name}.log"));
167 let gatewayd = Self {
168 process,
169 ln,
170 addr,
171 lightning_node_addr,
172 gatewayd_version,
173 gw_name,
174 log_path,
175 gw_port: port,
176 ldk_port: lightning_node_port,
177 gateway_id,
178 iroh_port,
179 node_id: iroh_sk.public(),
180 };
181
182 Ok(gatewayd)
183 }
184
185 pub async fn terminate(self) -> Result<()> {
186 self.process.terminate().await
187 }
188
189 pub fn set_lightning_node(&mut self, ln_node: LightningNode) {
190 self.ln = ln_node;
191 }
192
193 pub async fn stop_lightning_node(&mut self) -> Result<()> {
194 info!(target: LOG_DEVIMINT, "Stopping lightning node");
195 match self.ln.clone() {
196 LightningNode::Lnd(lnd) => lnd.terminate().await,
197 LightningNode::Ldk {
198 name: _,
199 gw_port: _,
200 ldk_port: _,
201 iroh_port: _,
202 } => {
203 unimplemented!("LDK node termination not implemented")
206 }
207 }
208 }
209
210 pub async fn restart_with_bin(
213 &mut self,
214 process_mgr: &ProcessManager,
215 gatewayd_path: &PathBuf,
216 gateway_cli_path: &PathBuf,
217 ) -> Result<()> {
218 let ln = self.ln.clone();
219
220 self.process.terminate().await?;
221 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", gatewayd_path) };
223 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", gateway_cli_path) };
225
226 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
227 if gatewayd_version < *VERSION_0_9_0_ALPHA && supports_lnv2() {
228 info!(target: LOG_DEVIMINT, "LNv2 is now supported, running in All mode");
229 unsafe { std::env::set_var("FM_GATEWAY_LIGHTNING_MODULE_MODE", "All") };
231 }
232
233 let new_ln = ln;
234 let new_gw = Self::new(process_mgr, new_ln.clone()).await?;
235 self.process = new_gw.process;
236 self.set_lightning_node(new_ln);
237 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
238 info!(
239 target: LOG_DEVIMINT,
240 ?gatewayd_version,
241 ?gateway_cli_version,
242 "upgraded gatewayd and gateway-cli"
243 );
244 Ok(())
245 }
246
247 pub fn cmd(&self) -> Command {
248 cmd!(
249 crate::util::get_gateway_cli_path(),
250 "--rpcpassword=theresnosecondbest",
251 "-a",
252 &self.addr
253 )
254 }
255
256 pub async fn gateway_id(&self) -> Result<String> {
257 let info = self.get_info().await?;
258 let gateway_id = info["gateway_id"]
259 .as_str()
260 .context("gateway_id must be a string")?
261 .to_owned();
262 Ok(gateway_id)
263 }
264
265 pub async fn get_info(&self) -> Result<serde_json::Value> {
266 retry(
267 "Getting gateway info via gateway-cli info",
268 backoff_util::aggressive_backoff(),
269 || async { cmd!(self, "info").out_json().await },
270 )
271 .await
272 .context("Getting gateway info via gateway-cli info")
273 }
274
275 pub async fn get_info_iroh(&self) -> Result<serde_json::Value> {
276 cmd!(
277 crate::util::get_gateway_cli_path(),
278 "--rpcpassword=theresnosecondbest",
279 "--address",
280 format!("iroh://{}", self.node_id),
281 "--connection-override",
282 format!("http://127.0.0.1:{}", self.iroh_port),
283 "info",
284 )
285 .out_json()
286 .await
287 }
288
289 pub async fn lightning_pubkey(&self) -> Result<PublicKey> {
290 let info = self.get_info().await?;
291 let lightning_pub_key = info["lightning_pub_key"]
292 .as_str()
293 .context("lightning_pub_key must be a string")?
294 .to_owned();
295 Ok(lightning_pub_key.parse()?)
296 }
297
298 pub async fn connect_fed(&self, fed: &Federation) -> Result<()> {
299 let invite_code = fed.invite_code()?;
300 poll("gateway connect-fed", || async {
301 cmd!(self, "connect-fed", invite_code.clone())
302 .run()
303 .await
304 .map_err(ControlFlow::Continue)?;
305 Ok(())
306 })
307 .await?;
308 Ok(())
309 }
310
311 pub async fn recover_fed(&self, fed: &Federation) -> Result<()> {
312 let federation_id = fed.calculate_federation_id();
313 let invite_code = fed.invite_code()?;
314 info!(target: LOG_DEVIMINT, federation_id = %federation_id, "Recovering...");
315 poll("gateway connect-fed --recover=true", || async {
316 cmd!(self, "connect-fed", invite_code.clone(), "--recover=true")
317 .run()
318 .await
319 .map_err(ControlFlow::Continue)?;
320 Ok(())
321 })
322 .await?;
323 Ok(())
324 }
325
326 pub async fn backup_to_fed(&self, fed: &Federation) -> Result<()> {
327 let federation_id = fed.calculate_federation_id();
328 cmd!(self, "ecash", "backup", "--federation-id", federation_id)
329 .run()
330 .await?;
331 Ok(())
332 }
333
334 pub async fn get_pegin_addr(&self, fed_id: &str) -> Result<String> {
335 Ok(cmd!(self, "ecash", "pegin", "--federation-id={fed_id}")
336 .out_json()
337 .await?
338 .as_str()
339 .context("address must be a string")?
340 .to_owned())
341 }
342
343 pub async fn get_ln_onchain_address(&self) -> Result<String> {
344 cmd!(self, "onchain", "address").out_string().await
345 }
346
347 pub async fn get_mnemonic(&self) -> Result<MnemonicResponse> {
348 let value = retry(
349 "Getting gateway mnemonic",
350 backoff_util::aggressive_backoff(),
351 || async { cmd!(self, "seed").out_json().await },
352 )
353 .await
354 .context("Getting gateway mnemonic")?;
355
356 Ok(serde_json::from_value(value)?)
357 }
358
359 pub async fn leave_federation(&self, federation_id: FederationId) -> Result<()> {
360 cmd!(self, "leave-fed", "--federation-id", federation_id)
361 .run()
362 .await?;
363 Ok(())
364 }
365
366 pub async fn create_invoice(&self, amount_msats: u64) -> Result<Bolt11Invoice> {
367 Ok(Bolt11Invoice::from_str(
368 &cmd!(self, "lightning", "create-invoice", amount_msats)
369 .out_string()
370 .await?,
371 )?)
372 }
373
374 pub async fn pay_invoice(&self, invoice: Bolt11Invoice) -> Result<()> {
375 cmd!(self, "lightning", "pay-invoice", invoice.to_string())
376 .run()
377 .await?;
378
379 Ok(())
380 }
381
382 pub async fn send_ecash(&self, federation_id: String, amount_msats: u64) -> Result<String> {
383 let value = cmd!(
384 self,
385 "ecash",
386 "send",
387 "--federation-id",
388 federation_id,
389 amount_msats
390 )
391 .out_json()
392 .await?;
393 let ecash: String = serde_json::from_value(
394 value
395 .get("notes")
396 .expect("notes key does not exist")
397 .clone(),
398 )?;
399 Ok(ecash)
400 }
401
402 pub async fn receive_ecash(&self, ecash: String) -> Result<()> {
403 cmd!(self, "ecash", "receive", "--notes", ecash)
404 .run()
405 .await?;
406 Ok(())
407 }
408
409 pub async fn get_balances(&self) -> Result<GatewayBalances> {
410 let value = cmd!(self, "get-balances").out_json().await?;
411 Ok(serde_json::from_value(value)?)
412 }
413
414 pub async fn ecash_balance(&self, federation_id: String) -> anyhow::Result<u64> {
415 let federation_id = FederationId::from_str(&federation_id)?;
416 let balances = self.get_balances().await?;
417 let ecash_balance = balances
418 .ecash_balances
419 .into_iter()
420 .find(|info| info.federation_id == federation_id)
421 .ok_or(anyhow::anyhow!("Gateway is not joined to federation"))?
422 .ecash_balance_msats
423 .msats;
424 Ok(ecash_balance)
425 }
426
427 pub async fn send_onchain(
428 &self,
429 bitcoind: &Bitcoind,
430 amount: BitcoinAmountOrAll,
431 fee_rate: u64,
432 ) -> Result<bitcoin::Txid> {
433 let withdraw_address = bitcoind.get_new_address().await?;
434 let value = cmd!(
435 self,
436 "onchain",
437 "send",
438 "--address",
439 withdraw_address,
440 "--amount",
441 amount,
442 "--fee-rate-sats-per-vbyte",
443 fee_rate
444 )
445 .out_json()
446 .await?;
447
448 let txid: bitcoin::Txid = serde_json::from_value(value)?;
449 Ok(txid)
450 }
451
452 pub async fn close_channel(&self, remote_pubkey: PublicKey, force: bool) -> Result<()> {
453 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
454 let mut close_channel = if force && gateway_cli_version >= *VERSION_0_9_0_ALPHA {
455 cmd!(
456 self,
457 "lightning",
458 "close-channels-with-peer",
459 "--pubkey",
460 remote_pubkey,
461 "--force",
462 )
463 } else {
464 cmd!(
465 self,
466 "lightning",
467 "close-channels-with-peer",
468 "--pubkey",
469 remote_pubkey,
470 )
471 };
472
473 close_channel.run().await?;
474
475 Ok(())
476 }
477
478 pub async fn close_all_channels(&self, force: bool) -> Result<()> {
479 let channels = self.list_channels().await?;
480
481 for chan in channels {
482 let remote_pubkey = chan.remote_pubkey;
483 self.close_channel(remote_pubkey, force).await?;
484 }
485
486 Ok(())
487 }
488
489 pub async fn open_channel(
492 &self,
493 gw: &Gatewayd,
494 channel_size_sats: u64,
495 push_amount_sats: Option<u64>,
496 ) -> Result<Txid> {
497 let pubkey = gw.lightning_pubkey().await?;
498
499 let mut command = cmd!(
500 self,
501 "lightning",
502 "open-channel",
503 "--pubkey",
504 pubkey,
505 "--host",
506 gw.lightning_node_addr,
507 "--channel-size-sats",
508 channel_size_sats,
509 "--push-amount-sats",
510 push_amount_sats.unwrap_or(0)
511 );
512
513 Ok(Txid::from_str(&command.out_string().await?)?)
514 }
515
516 pub async fn list_channels(&self) -> Result<Vec<ChannelInfo>> {
517 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
518 let channels = if gateway_cli_version >= *VERSION_0_9_0_ALPHA {
519 cmd!(self, "lightning", "list-channels").out_json().await?
520 } else {
521 cmd!(self, "lightning", "list-active-channels")
522 .out_json()
523 .await?
524 };
525
526 let channels = channels
527 .as_array()
528 .context("channels must be an array")?
529 .iter()
530 .map(|channel| {
531 let remote_pubkey = channel["remote_pubkey"]
532 .as_str()
533 .context("remote_pubkey must be a string")?
534 .to_owned();
535 let channel_size_sats = channel["channel_size_sats"]
536 .as_u64()
537 .context("channel_size_sats must be a u64")?;
538 let outbound_liquidity_sats = channel["outbound_liquidity_sats"]
539 .as_u64()
540 .context("outbound_liquidity_sats must be a u64")?;
541 let inbound_liquidity_sats = channel["inbound_liquidity_sats"]
542 .as_u64()
543 .context("inbound_liquidity_sats must be a u64")?;
544 let is_active = channel["is_active"].as_bool().unwrap_or(true);
545 Ok(ChannelInfo {
546 remote_pubkey: remote_pubkey
547 .parse()
548 .expect("Lightning node returned invalid remote channel pubkey"),
549 channel_size_sats,
550 outbound_liquidity_sats,
551 inbound_liquidity_sats,
552 is_active,
553 })
554 })
555 .collect::<Result<Vec<ChannelInfo>>>()?;
556 Ok(channels)
557 }
558
559 pub async fn wait_for_block_height(&self, target_block_height: u64) -> Result<()> {
560 poll("waiting for block height", || async {
561 let info = self.get_info().await.map_err(ControlFlow::Continue)?;
562 let value = info.get("block_height");
563 if let Some(height) = value {
564 let block_height: Option<u32> = serde_json::from_value(height.clone())
565 .context("Could not parse block height")
566 .map_err(ControlFlow::Continue)?;
567 let Some(block_height) = block_height else {
568 return Err(ControlFlow::Continue(anyhow!("Not synced any blocks yet")));
569 };
570 let synced = info["synced_to_chain"]
571 .as_bool()
572 .expect("Could not get synced_to_chain");
573 if block_height >= target_block_height as u32 && synced {
574 return Ok(());
575 }
576 }
577 Err(ControlFlow::Continue(anyhow!("Not synced to block")))
578 })
579 .await?;
580 Ok(())
581 }
582
583 pub async fn get_lightning_fee(&self, fed_id: String) -> Result<PaymentFee> {
584 let info_value = self.get_info().await?;
585 let federations = info_value["federations"]
586 .as_array()
587 .expect("federations is an array");
588
589 let fed = federations
590 .iter()
591 .find(|fed| {
592 serde_json::from_value::<String>(fed["federation_id"].clone())
593 .expect("could not deserialize federation_id")
594 == fed_id
595 })
596 .ok_or_else(|| anyhow!("Federation not found"))?;
597
598 let lightning_fee = fed["config"]["lightning_fee"].clone();
599 let base: Amount = serde_json::from_value(lightning_fee["base"].clone())
600 .map_err(|e| anyhow!("Couldnt parse base: {}", e))?;
601 let parts_per_million: u64 =
602 serde_json::from_value(lightning_fee["parts_per_million"].clone())
603 .map_err(|e| anyhow!("Couldnt parse parts_per_million: {}", e))?;
604
605 Ok(PaymentFee {
606 base,
607 parts_per_million,
608 })
609 }
610
611 pub async fn set_federation_routing_fee(
612 &self,
613 fed_id: String,
614 base: u64,
615 ppm: u64,
616 ) -> Result<()> {
617 cmd!(
618 self,
619 "cfg",
620 "set-fees",
621 "--federation-id",
622 fed_id,
623 "--ln-base",
624 base,
625 "--ln-ppm",
626 ppm
627 )
628 .run()
629 .await?;
630
631 Ok(())
632 }
633
634 pub async fn set_federation_transaction_fee(
635 &self,
636 fed_id: String,
637 base: u64,
638 ppm: u64,
639 ) -> Result<()> {
640 cmd!(
641 self,
642 "cfg",
643 "set-fees",
644 "--federation-id",
645 fed_id,
646 "--tx-base",
647 base,
648 "--tx-ppm",
649 ppm
650 )
651 .run()
652 .await?;
653
654 Ok(())
655 }
656
657 pub async fn payment_summary(&self) -> Result<PaymentSummaryResponse> {
658 let out_json = cmd!(self, "payment-summary").out_json().await?;
659 Ok(serde_json::from_value(out_json).expect("Could not deserialize PaymentSummaryResponse"))
660 }
661
662 pub async fn wait_bolt11_invoice(&self, payment_hash: Vec<u8>) -> Result<()> {
663 let payment_hash =
664 sha256::Hash::from_slice(&payment_hash).expect("Could not parse payment hash");
665 let invoice_val = cmd!(
666 self,
667 "lightning",
668 "get-invoice",
669 "--payment-hash",
670 payment_hash
671 )
672 .out_json()
673 .await?;
674 let invoice: GetInvoiceResponse =
675 serde_json::from_value(invoice_val).expect("Could not parse GetInvoiceResponse");
676 anyhow::ensure!(invoice.status == PaymentStatus::Succeeded);
677
678 Ok(())
679 }
680
681 pub async fn list_transactions(
682 &self,
683 start: SystemTime,
684 end: SystemTime,
685 ) -> Result<Vec<PaymentDetails>> {
686 let start_datetime: DateTime<Utc> = start.into();
687 let end_datetime: DateTime<Utc> = end.into();
688 let response = cmd!(
689 self,
690 "lightning",
691 "list-transactions",
692 "--start-time",
693 start_datetime.to_rfc3339(),
694 "--end-time",
695 end_datetime.to_rfc3339()
696 )
697 .out_json()
698 .await?;
699 let transactions = serde_json::from_value::<ListTransactionsResponse>(response)?;
700 Ok(transactions.transactions)
701 }
702
703 pub async fn create_offer(&self, amount: Option<Amount>) -> Result<String> {
704 let offer_value = if let Some(amount) = amount {
705 cmd!(
706 self,
707 "lightning",
708 "create-offer",
709 "--amount-msat",
710 amount.msats
711 )
712 .out_json()
713 .await?
714 } else {
715 cmd!(self, "lightning", "create-offer").out_json().await?
716 };
717 let offer_response = serde_json::from_value::<CreateOfferResponse>(offer_value)
718 .expect("Could not parse offer response");
719 Ok(offer_response.offer)
720 }
721
722 pub async fn pay_offer(&self, offer: String, amount: Option<Amount>) -> Result<()> {
723 if let Some(amount) = amount {
724 cmd!(
725 self,
726 "lightning",
727 "pay-offer",
728 "--offer",
729 offer,
730 "--amount-msat",
731 amount.msats
732 )
733 .run()
734 .await?;
735 } else {
736 cmd!(self, "lightning", "pay-offer", "--offer", offer)
737 .run()
738 .await?;
739 }
740
741 Ok(())
742 }
743}