mokshamint/lightning/
lnd.rs

1use std::{
2    fmt::{self, Formatter},
3    path::PathBuf,
4    sync::Arc,
5};
6
7use crate::{
8    error::MokshaMintError,
9    model::{CreateInvoiceResult, PayInvoiceResult},
10    url_serialize::{deserialize_url, serialize_url},
11};
12use async_trait::async_trait;
13use clap::Parser;
14use fedimint_tonic_lnd::Client;
15use serde::{Deserialize, Serialize};
16use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard};
17use tracing::info;
18use url::Url;
19
20use super::Lightning;
21
22#[derive(Deserialize, Serialize, Debug, Clone, Default, Parser)]
23pub struct LndLightningSettings {
24    #[clap(long, env = "MINT_LND_GRPC_HOST")]
25    #[serde(serialize_with = "serialize_url", deserialize_with = "deserialize_url")]
26    pub grpc_host: Option<Url>,
27
28    #[clap(long, env = "MINT_LND_TLS_CERT_PATH")]
29    pub tls_cert_path: Option<PathBuf>,
30
31    #[clap(long, env = "MINT_LND_MACAROON_PATH")]
32    pub macaroon_path: Option<PathBuf>,
33}
34impl fmt::Display for LndLightningSettings {
35    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
36        write!(
37            f,
38            "grpc_host: {}, tls_cert_path: {}, macaroon_path: {}",
39            self.grpc_host.as_ref().unwrap(),
40            self.tls_cert_path
41                .as_ref()
42                .unwrap() // FIXME unwrap
43                .to_str()
44                .unwrap_or_default(),
45            self.macaroon_path
46                .as_ref()
47                .unwrap()
48                .to_str()
49                .unwrap_or_default()
50        )
51    }
52}
53
54pub struct LndLightning(Arc<Mutex<Client>>);
55
56impl LndLightning {
57    pub async fn new(
58        address: Url,
59        cert_file: &PathBuf,
60        macaroon_file: &PathBuf,
61    ) -> Result<Self, MokshaMintError> {
62        let client =
63            fedimint_tonic_lnd::connect(address.to_string(), cert_file, &macaroon_file).await;
64
65        Ok(Self(Arc::new(Mutex::new(
66            client.map_err(MokshaMintError::ConnectError)?,
67        ))))
68    }
69
70    pub async fn client_lock(
71        &self,
72    ) -> Result<MappedMutexGuard<'_, fedimint_tonic_lnd::LightningClient>, MokshaMintError> {
73        let guard = self.0.lock().await;
74        Ok(MutexGuard::map(guard, |client| client.lightning()))
75    }
76}
77
78#[allow(implied_bounds_entailment)]
79#[async_trait]
80impl Lightning for LndLightning {
81    async fn is_invoice_paid(&self, payment_request: String) -> Result<bool, MokshaMintError> {
82        let invoice = self.decode_invoice(payment_request).await?;
83        let payment_hash = invoice.payment_hash();
84        let invoice_request = fedimint_tonic_lnd::lnrpc::PaymentHash {
85            r_hash: payment_hash.to_vec(),
86            ..Default::default()
87        };
88
89        let invoice = self
90            .client_lock()
91            .await?
92            .lookup_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
93            .await?
94            .into_inner();
95
96        Ok(invoice.state == fedimint_tonic_lnd::lnrpc::invoice::InvoiceState::Settled as i32)
97    }
98
99    async fn create_invoice(&self, amount: u64) -> Result<CreateInvoiceResult, MokshaMintError> {
100        let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice {
101            value: amount as i64,
102            ..Default::default()
103        };
104
105        let invoice = self
106            .client_lock()
107            .await?
108            .add_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
109            .await?
110            .into_inner();
111
112        Ok(CreateInvoiceResult {
113            payment_hash: invoice.r_hash,
114            payment_request: invoice.payment_request,
115        })
116    }
117
118    async fn pay_invoice(
119        &self,
120        payment_request: String,
121    ) -> Result<PayInvoiceResult, MokshaMintError> {
122        let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
123            payment_request,
124            ..Default::default()
125        };
126        let payment_response = self
127            .client_lock()
128            .await?
129            .send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req))
130            .await?
131            .into_inner();
132
133        let total_fees = payment_response
134            .payment_route
135            .map_or(0, |route| route.total_fees_msat / 1_000) as u64;
136
137        info!("lnd total_fees: {}", total_fees);
138
139        Ok(PayInvoiceResult {
140            payment_hash: hex::encode(payment_response.payment_hash),
141            total_fees,
142        })
143    }
144}