mokshamint/lightning/
cln.rs1use 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") .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, })
169 }
170}
171
172