mokshamint/lightning/
cln.rs

1use async_trait::async_trait;
2use clap::Parser;
3use cln_grpc::pb::{amount_or_any, Amount, AmountOrAny};
4use cln_grpc::pb::{listinvoices_invoices::ListinvoicesInvoicesStatus, node_client::NodeClient};
5use serde::{Deserialize, Serialize};
6use std::fmt::{self};
7use std::{fmt::Formatter, path::PathBuf, sync::Arc};
8
9use crate::{
10    error::MokshaMintError,
11    model::{CreateInvoiceResult, PayInvoiceResult},
12};
13use tonic::transport::{Certificate, ClientTlsConfig, Identity};
14
15use super::Lightning;
16
17use secp256k1::rand;
18use std::fs::read;
19use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard};
20
21#[derive(Deserialize, Serialize, Debug, Clone, Default, Parser)]
22pub struct ClnLightningSettings {
23    #[clap(long, env = "MINT_LND_GRPC_HOST")]
24    pub grpc_host: Option<String>,
25    #[clap(long, env = "MINT_LND_CLIENT_CERT")]
26    pub client_cert: Option<PathBuf>,
27    #[clap(long, env = "MINT_LND_CLIENT_CERT")]
28    pub client_key: Option<PathBuf>,
29    #[clap(long, env = "MINT_LND_CA_CERT")]
30    pub ca_cert: Option<PathBuf>,
31}
32
33impl fmt::Display for ClnLightningSettings {
34    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
35        write!(f, "ClnLightningSettings")
36    }
37}
38
39pub struct ClnLightning(Arc<Mutex<NodeClient<tonic::transport::Channel>>>);
40
41impl ClnLightning {
42    pub async fn new(
43        grpc_host: String,
44        client_cert: &PathBuf,
45        client_key: &PathBuf,
46        ca_cert: &PathBuf,
47    ) -> Result<Self, MokshaMintError> {
48        let client_cert = read(client_cert).unwrap();
49        let client_key = read(client_key).unwrap();
50
51        let identity = Identity::from_pem(client_cert, client_key);
52        let ca_cert = read(ca_cert).unwrap();
53        let ca_certificate = Certificate::from_pem(ca_cert);
54
55        let tls_config = ClientTlsConfig::new()
56            .domain_name("localhost")
57            .identity(identity)
58            .ca_certificate(ca_certificate);
59        let url = grpc_host.to_owned();
60
61        let channel = tonic::transport::Channel::from_shared(url)
62            .unwrap()
63            .tls_config(tls_config)
64            .unwrap()
65            .connect()
66            .await
67            .unwrap();
68
69        let node = NodeClient::new(channel);
70        Ok(Self(Arc::new(Mutex::new(node))))
71    }
72
73    pub async fn client_lock(
74        &self,
75    ) -> anyhow::Result<MappedMutexGuard<'_, NodeClient<tonic::transport::Channel>>> {
76        let guard = self.0.lock().await;
77        Ok(MutexGuard::map(guard, |client| client))
78    }
79}
80
81#[async_trait]
82impl Lightning for ClnLightning {
83    async fn is_invoice_paid(&self, payment_request: String) -> Result<bool, MokshaMintError> {
84        let invoices = self
85            .client_lock()
86            .await
87            .expect("failed to lock client")
88            .list_invoices(cln_grpc::pb::ListinvoicesRequest {
89                invstring: Some(payment_request),
90                label: None,
91                payment_hash: None,
92                offer_id: None,
93                index: None,
94                start: None,
95                limit: None,
96            })
97            .await
98            .expect("failed to lookup invoice")
99            .into_inner();
100
101        let invoice = invoices
102            .invoices
103            .first()
104            .expect("no matching invoice found");
105
106        Ok(invoice.status() == ListinvoicesInvoicesStatus::Paid)
107    }
108
109    async fn create_invoice(&self, amount: u64) -> Result<CreateInvoiceResult, MokshaMintError> {
110        let amount_msat = Some(AmountOrAny {
111            value: Some(amount_or_any::Value::Amount(Amount {
112                msat: amount * 1_000,
113            })),
114        });
115        let invoice = self
116            .client_lock()
117            .await
118            .expect("failed to lock client")
119            .invoice(cln_grpc::pb::InvoiceRequest {
120                amount_msat,
121                description: format!("{:x}", rand::random::<u128>()),
122                label: format!("{:x}", rand::random::<u128>()),
123                expiry: None,
124                fallbacks: vec![],
125                preimage: None,
126                cltv: None,
127                deschashonly: None,
128            })
129            .await
130            .expect("failed to create invoice")
131            .into_inner();
132
133        Ok(CreateInvoiceResult {
134            payment_hash: invoice.payment_hash,
135            payment_request: invoice.bolt11,
136        })
137    }
138
139    async fn pay_invoice(
140        &self,
141        payment_request: String,
142    ) -> Result<PayInvoiceResult, MokshaMintError> {
143        let payment = self
144            .client_lock()
145            .await
146            .expect("failed to lock client") //FIXME map error
147            .pay(cln_grpc::pb::PayRequest {
148                bolt11: payment_request,
149                amount_msat: None,
150                label: None,
151                riskfactor: None,
152                maxfeepercent: None,
153                retry_for: None,
154                maxdelay: None,
155                exemptfee: None,
156                localinvreqid: None,
157                exclude: vec![],
158                maxfee: None,
159                description: None,
160            })
161            .await
162            .expect("failed to pay invoice")
163            .into_inner();
164
165        Ok(PayInvoiceResult {
166            payment_hash: hex::encode(payment.payment_hash),
167            total_fees: payment.amount_sent_msat.unwrap().msat - payment.amount_msat.unwrap().msat, // FIXME check if this is correct
168        })
169    }
170}
171
172// mod tests {
173//     use cln_grpc::pb::GetinfoRequest;
174
175//     #[tokio::test]
176//     async fn test_connect() -> anyhow::Result<()> {
177//         let path = std::path::PathBuf::from("/Users/steffen/.polar/networks/1/volumes/c-lightning/bob/lightningd/regtest/lightning-rpc");
178//         let client = super::ClnLightning::new(&path).await?;
179//         let info = client
180//             .client_lock()
181//             .await
182//             .expect("failed to lock client")
183//             .getinfo(GetinfoRequest {})
184//             .await?;
185//         println!("{:?}", info);
186//         Ok(())
187//     }
188// }