lightning_cluster/
lnd.rs

1use crate::cluster::{self, ClusterAddInvoice, ClusterUtxo, ClusterUtxos};
2use anyhow::{Context, Result};
3use reqwest::header::{HeaderMap, HeaderValue};
4use reqwest::Response;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::io::Read;
8
9#[derive(Clone)]
10pub struct LndClient {
11    pub host: String,
12    pub cert_path: String,
13    pub macaroon_path: String,
14}
15
16#[derive(serde::Deserialize, Debug)]
17pub struct NewAddressResponse {
18    pub address: String,
19}
20
21#[derive(Serialize, Deserialize, Debug)]
22pub struct AddInvoiceLndRequest {
23    pub memo: String,
24    pub value: i64,
25    pub expiry: i64,
26}
27
28#[derive(Serialize, Deserialize, Debug)]
29pub struct ListUnspentRequest {
30    pub min_confs: i64,
31    pub max_confs: i64,
32    pub account: Option<String>,
33    pub unconfirmed_only: Option<bool>,
34}
35
36#[derive(Serialize, Deserialize, Debug)]
37pub struct ListUnspentResponse {
38    pub utxos: Vec<Utxo>,
39}
40
41impl ListUnspentResponse {
42    pub fn to_cluster(self, pubkey: String) -> Result<ClusterUtxos> {
43        let mut utxos = Vec::new();
44        for utxo in self.utxos {
45            utxos.push(utxo.to_cluster(pubkey.clone())?);
46        }
47
48        Ok(ClusterUtxos { utxos: utxos })
49    }
50}
51
52#[derive(Serialize, Deserialize, Debug)]
53pub struct Utxo {
54    pub address: String,
55    pub amount_sat: String,
56    pub confirmations: String,
57    pub outpoint: Outpoint,
58    pub pk_script: String,
59}
60
61impl Utxo {
62    pub fn to_cluster(self, pubkey: String) -> Result<ClusterUtxo> {
63        let amount = self.amount_sat.parse::<u64>()?;
64        Ok(ClusterUtxo {
65            pubkey: pubkey,
66            address: self.address,
67            amount: amount,
68            confirmations: self.confirmations.parse::<u64>()?,
69        })
70    }
71}
72
73#[derive(Serialize, Deserialize, Debug)]
74pub struct Outpoint {
75    pub txid_bytes: String,
76    pub txid_str: String,
77    pub output_index: u64,
78}
79
80#[derive(Serialize, Deserialize, Debug)]
81pub struct AddInvoiceResponse {
82    pub r_hash: String,
83    pub payment_request: String,
84    pub add_index: String,
85    pub payment_addr: String,
86}
87
88#[derive(Deserialize, Debug)]
89pub struct LookupInvoiceResponse {
90    pub memo: String,
91    pub r_preimage: String,
92    pub r_hash: String,
93    pub value: String,
94    pub settle_date: String,
95    pub payment_request: String,
96    pub description_hash: String,
97    pub expiry: String,
98    pub amt_paid_sat: String,
99    pub state: InvoiceState,
100}
101
102impl LookupInvoiceResponse {
103    pub fn to_cluster(self, pubkey: &str) -> cluster::ClusterLookupInvoice {
104        let state = self.state.to_cluster();
105        cluster::ClusterLookupInvoice {
106            pubkey: pubkey.to_string(),
107            memo: self.memo,
108            r_preimage: self.r_preimage,
109            r_hash: self.r_hash,
110            value: self.value,
111            settle_date: self.settle_date,
112            payment_request: self.payment_request,
113            description_hash: self.description_hash,
114            expiry: self.expiry,
115            amt_paid_sat: self.amt_paid_sat,
116            state: state,
117        }
118    }
119}
120
121#[derive(Deserialize, Debug)]
122pub enum InvoiceState {
123    #[serde(rename = "OPEN")]
124    Open = 0,
125    #[serde(rename = "SETTLED")]
126    Settled = 1,
127    #[serde(rename = "CANCELED")]
128    Canceled = 2,
129    #[serde(rename = "ACCEPTED")]
130    Accepted = 3,
131}
132
133#[derive(Deserialize, Serialize, Debug)]
134pub struct LndSendPaymentSyncReq {
135    pub payment_request: String,
136    pub amt: String,
137    pub fee_limit: FeeLimit,
138    pub allow_self_payment: bool,
139}
140
141#[derive(Deserialize, Serialize, Debug)]
142pub struct FeeLimit {
143    pub fixed: String,
144}
145
146#[derive(Serialize, Deserialize, Debug)]
147pub struct LndSendPaymentSyncRes {
148    pub payment_error: Option<String>,
149    pub payment_preimage: Option<String>,
150    pub payment_route: Option<Route>,
151    pub payment_hash: Option<String>,
152}
153
154#[derive(Deserialize, Serialize, Debug, Clone)]
155pub struct Route {
156    pub total_time_lock: u64,
157    pub total_fees: String,
158    pub total_amt: String,
159    pub hops: Vec<Hop>,
160}
161
162#[derive(Deserialize, Serialize, Debug, Clone)]
163pub struct Hop {
164    pub chan_id: String,
165    pub chan_capacity: String,
166    pub amt_to_forward: String,
167    pub fee: String,
168    pub expiry: i64,
169    pub amt_to_forward_msat: String,
170    pub fee_msat: String,
171    pub pub_key: String,
172    pub metadata: String,
173}
174
175impl LndSendPaymentSyncRes {
176    pub fn to_cluster(self, pubkey: String) -> cluster::ClusterPayPaymentRequestRes {
177        cluster::ClusterPayPaymentRequestRes {
178            pubkey: pubkey,
179            payment_error: self.payment_error,
180            payment_preimage: self.payment_preimage,
181            payment_route: self.payment_route,
182            payment_hash: self.payment_hash,
183        }
184    }
185}
186
187impl InvoiceState {
188    pub fn to_cluster(&self) -> cluster::ClusterInvoiceState {
189        match self {
190            InvoiceState::Open => cluster::ClusterInvoiceState::Open,
191            InvoiceState::Settled => cluster::ClusterInvoiceState::Settled,
192            InvoiceState::Canceled => cluster::ClusterInvoiceState::Canceled,
193            InvoiceState::Accepted => cluster::ClusterInvoiceState::Accepted,
194        }
195    }
196}
197
198impl LndClient {
199    pub fn new(host: String, cert_path: String, macaroon_path: String) -> LndClient {
200        Self {
201            host,
202            cert_path,
203            macaroon_path,
204        }
205    }
206
207    pub async fn new_address(&self) -> Result<NewAddressResponse> {
208        let url = format!("{}/v1/newaddress", self.host);
209        let response = LndClient::get(&self, &url)
210            .await
211            .map_err(|error| anyhow::Error::from(error))
212            .context("Failed to make request to LND API")?;
213
214        response
215            .json::<NewAddressResponse>()
216            .await
217            .map_err(|error| anyhow::Error::from(error))
218            .context("Failed to parse JSON response from LND API")
219    }
220
221    pub async fn add_invoice(&self, req: ClusterAddInvoice) -> Result<AddInvoiceResponse> {
222        let url = format!("{}/v1/invoices", self.host);
223        let body = AddInvoiceLndRequest {
224            memo: req.memo,
225            value: req.value,
226            expiry: req.expiry,
227        };
228        let response = LndClient::post(&self, &url, &body).await?;
229
230        response
231            .json::<AddInvoiceResponse>()
232            .await
233            .map_err(|error| anyhow::Error::from(error))
234            .context("Failed to parse JSON response from LND API")
235    }
236
237    pub async fn lookup_invoice(&self, r_hash: &str) -> Result<LookupInvoiceResponse> {
238        let url = format!("{}/v1/invoice/{}", self.host, r_hash);
239        let response = LndClient::get(&self, &url).await?;
240
241        response
242            .json::<LookupInvoiceResponse>()
243            .await
244            .map_err(|error| anyhow::Error::from(error))
245            .context("Failed to parse JSON response from LND API")
246    }
247
248    pub async fn send_payment_sync(
249        &self,
250        req: LndSendPaymentSyncReq,
251    ) -> Result<LndSendPaymentSyncRes> {
252        let url = format!("{}/v1/channels/transactions", self.host);
253        let res = LndClient::post(&self, &url, &req).await.unwrap();
254
255        let json_string = res.text().await.unwrap();
256
257        eprintln!("{}", json_string);
258
259        let json = serde_json::from_str::<serde_json::Value>(&json_string).unwrap();
260
261        let payment_hash = match &json["payment_hash"] {
262            serde_json::Value::Null => None,
263            serde_json::Value::String(s) if s.is_empty() => None,
264            serde_json::Value::String(s) => Some(to_hex(&s)?),
265            _ => None,
266        };
267
268        let payment_error = match &json["payment_error"] {
269            serde_json::Value::Null => None,
270            serde_json::Value::String(s) if s.is_empty() => None,
271            serde_json::Value::String(s) => Some(s.clone()),
272            _ => None,
273        };
274
275        let payment_route = match &json["payment_route"] {
276            serde_json::Value::Null => None,
277            _ => {
278                let route = serde_json::to_string(&json["payment_route"]).unwrap();
279                let route = serde_json::from_str::<Route>(&route).unwrap();
280                Some(route)
281            }
282        };
283
284        let payment_preimage = match &json["payment_preimage"] {
285            serde_json::Value::Null => None,
286            serde_json::Value::String(s) if s.is_empty() => None,
287            serde_json::Value::String(s) => Some(to_hex(&s)?),
288            _ => None,
289        };
290
291        let res = LndSendPaymentSyncRes {
292            payment_error,
293            payment_preimage,
294            payment_route,
295            payment_hash,
296        };
297
298        eprintln!("{:?}", res);
299
300        Ok(res)
301    }
302
303    pub async fn list_unspent(&self) -> Result<ListUnspentResponse> {
304        let url = format!("{}/v2/wallet/utxos", self.host);
305
306        let req = ListUnspentRequest {
307            min_confs: 0,
308            max_confs: 50000,
309            account: None,
310            unconfirmed_only: None,
311        };
312        let response = LndClient::post(&self, &url, &req).await?;
313
314        let json = response
315            .json::<ListUnspentResponse>()
316            .await
317            .map_err(|error| anyhow::Error::from(error))?;
318
319        Ok(json)
320    }
321
322    async fn get(&self, url: &str) -> Result<Response> {
323        let mut macaroon_data = Vec::new();
324        let mut macaroon_file = fs::File::open(&self.macaroon_path).unwrap();
325        macaroon_file.read_to_end(&mut macaroon_data).unwrap();
326        let macaroon_hex = hex::encode(macaroon_data);
327
328        let mut headers = HeaderMap::new();
329        headers.insert(
330            "Grpc-Metadata-macaroon",
331            HeaderValue::from_str(&macaroon_hex).unwrap(),
332        );
333
334        let mut buf = Vec::new();
335        fs::File::open(&self.cert_path)
336            .unwrap()
337            .read_to_end(&mut buf)
338            .unwrap();
339        let cert = reqwest::Certificate::from_pem(&buf).unwrap();
340
341        let client = reqwest::Client::builder()
342            .default_headers(headers)
343            .add_root_certificate(cert)
344            .build()
345            .unwrap();
346
347        let resp = client.get(url).send().await?;
348
349        Ok(resp)
350    }
351
352    async fn post<T: serde::Serialize>(&self, url: &str, body: &T) -> Result<Response> {
353        let mut macaroon_data = Vec::new();
354        let mut macaroon_file = fs::File::open(&self.macaroon_path).unwrap();
355        macaroon_file.read_to_end(&mut macaroon_data).unwrap();
356        let macaroon_hex = hex::encode(macaroon_data);
357
358        let mut headers = HeaderMap::new();
359        headers.insert(
360            "Grpc-Metadata-macaroon",
361            HeaderValue::from_str(&macaroon_hex).unwrap(),
362        );
363
364        let mut buf = Vec::new();
365        fs::File::open(&self.cert_path)
366            .unwrap()
367            .read_to_end(&mut buf)
368            .unwrap();
369        let cert = reqwest::Certificate::from_pem(&buf).unwrap();
370
371        let client = reqwest::Client::builder()
372            .default_headers(headers)
373            .add_root_certificate(cert)
374            .build()
375            .unwrap();
376
377        let resp = client.post(url).json(body).send().await?;
378
379        Ok(resp)
380    }
381}
382
383pub fn to_hex(str: &str) -> Result<String> {
384    let decoded_bytes = base64::decode(str)?;
385    let hex_string = hex::encode(decoded_bytes);
386
387    Ok(hex_string)
388}
389
390#[cfg(test)]
391mod tests {
392    use crate::lnd::{FeeLimit, LndClient, LndSendPaymentSyncReq};
393
394    #[tokio::test]
395    async fn test_send_payment_sync() {
396        let client = LndClient::new(
397            dotenvy::var("NODE1_HOST").unwrap(),
398            dotenvy::var("NODE1_CERT_PATH").unwrap(),
399            dotenvy::var("NODE1_MACAROON_PATH").unwrap(),
400        );
401
402        // can't self pay invoices, hardcoding for now, invoice already paid.
403        let payment_request = String::from("lntb10u1pjv4fjnpp5vnx7xwnqmaceg3kkeayhq7yk4zp7ppdvakdfuxj959k7d3s5gzmqdqqcqzzsxqr23ssp5vjnsq8jy5fw8ynq842ta8lppf4esh72m4mn79z46jxf93ncw7gus9qyyssqterg9uuet8uzqt63ehwha5pdv2ted8r2f8u4s35lg5yedrfutvkqjfxyf76zaskmycn9m05vnjy6ctytluxn639u2qdtydzzzn09r4qpv6uahm");
404
405        let payment_req = LndSendPaymentSyncReq {
406            payment_request: payment_request,
407            amt: String::from("1000"),
408            fee_limit: FeeLimit {
409                fixed: 10.to_string(),
410            },
411            allow_self_payment: true,
412        };
413
414        let payment = client.send_payment_sync(payment_req).await;
415
416        eprintln!("{:?}", payment);
417    }
418}