bright_ln_client/
rest_client.rs

1use base64::prelude::*;
2use reqwest::header::{HeaderMap, HeaderValue};
3use std::io::Read;
4
5use bright_ln_models::{
6    LndAddressProperty, LndHodlInvoice, LndHodlInvoiceState, LndInfo, LndInvoice, LndInvoiceList,
7    LndInvoiceRequestBody, LndListAddressesResponse, LndNewAddress, LndNextAddressRequest,
8    LndPaymentInvoice, OnchainAddressType,
9};
10
11#[derive(Debug, thiserror::Error)]
12pub enum LndRestClientError {
13    #[error("Base64Error: {0}")]
14    Base64Error(#[from] base64::DecodeError),
15    #[error("String Error: {0}")]
16    FormatError(#[from] std::fmt::Error),
17    #[error("ReqwestError: {0}")]
18    ReqwestError(#[from] reqwest::Error),
19
20    #[error("SerdeJsonError: {0}")]
21    LndModel(#[from] bright_ln_models::LndHodlInvoiceError),
22
23    #[error("SerdeJsonError: {0}")]
24    SerdeJson(#[from] serde_json::Error),
25    #[error("SerdeJsonError: {0}")]
26    Io(#[from] std::io::Error),
27
28    #[error("WebSocketError: {0}")]
29    WebSocket(#[from] crate::websocket::LndWebsocketError),
30    #[error("Unknown")]
31    Unknown,
32}
33
34type Result<T> = std::result::Result<T, LndRestClientError>;
35
36#[derive(Clone)]
37pub struct LndRestClient {
38    url: String,
39    data_dir: String,
40    pub client: reqwest::Client,
41}
42
43impl LndRestClient {
44    pub fn dud_server() -> Result<Self> {
45        let client = reqwest::Client::builder()
46            .danger_accept_invalid_certs(true)
47            .build()?;
48        Ok(Self {
49            url: "localhost:10009".to_string(),
50            client,
51            data_dir: String::new(),
52        })
53    }
54    pub fn new(url: &str, data_dir: &str) -> Result<Self> {
55        let mut default_header = HeaderMap::new();
56        let macaroon = Self::macaroon(data_dir)?;
57        let mut header_value = HeaderValue::from_str(&macaroon).unwrap();
58        header_value.set_sensitive(true);
59        default_header.insert("Grpc-Metadata-macaroon", header_value);
60        default_header.insert("Accept", HeaderValue::from_static("application/json"));
61        default_header.insert("Content-Type", HeaderValue::from_static("application/json"));
62        let client = reqwest::Client::builder()
63            .danger_accept_invalid_certs(true)
64            .default_headers(default_header)
65            .build()?;
66        Ok(Self {
67            url: url.to_string(),
68            client,
69            data_dir: data_dir.to_string(),
70        })
71    }
72    fn macaroon(data_dir: &str) -> Result<String> {
73        use std::fmt::Write;
74        let mut macaroon = vec![];
75        let mut file = std::fs::File::open(data_dir)?;
76        file.read_to_end(&mut macaroon)?;
77        macaroon.iter().try_fold(String::new(), |mut new_str, b| {
78            write!(new_str, "{b:02x}")?;
79            Ok(new_str)
80        })
81    }
82    pub async fn get_info(&self) -> Result<LndInfo> {
83        let url = format!("https://{}/v1/getinfo", self.url);
84        let response = self.client.get(&url).send().await?;
85        let response = response.text().await?;
86        Ok(response.parse::<LndInfo>()?)
87    }
88    pub async fn channel_balance(&self) -> Result<()> {
89        let url = format!("https://{}/v1/balance/channels", self.url);
90        let response = self.client.get(&url).send().await?;
91        let _response = response.text().await?;
92        Ok(())
93    }
94    pub async fn get_invoice(&self, form: LndInvoiceRequestBody) -> Result<LndPaymentInvoice> {
95        let url = format!("https://{}/v1/invoices", self.url);
96        let response = self.client.post(&url).body(form.to_string());
97        println!("Invoice request: {response:#?}");
98        let response = response.send().await?;
99        println!("Invoice response: {response:#?}");
100        let response = response.json::<LndPaymentInvoice>().await?;
101        Ok(response)
102    }
103    pub async fn list_invoices(&self) -> Result<Vec<LndInvoice>> {
104        let url = format!("https://{}/v1/invoices", self.url);
105        let response = self.client.get(&url).send().await?;
106        let response = response.json::<LndInvoiceList>().await?;
107        Ok(response.invoices)
108    }
109    pub async fn new_onchain_address(
110        &self,
111        request: LndNextAddressRequest,
112    ) -> Result<LndNewAddress> {
113        let url = format!("https://{}/v2/wallet/address/next", self.url);
114        let request_str: String = request.try_into()?;
115        let response = self.client.post(&url).body(request_str).send().await?;
116        tracing::info!("{:?}", response);
117        let response = response.json::<LndNewAddress>().await?;
118        Ok(response)
119    }
120    pub async fn list_onchain_addresses(
121        &self,
122        account: &str,
123        address_type: &OnchainAddressType,
124    ) -> Result<Vec<LndAddressProperty>> {
125        let url = format!("https://{}/v2/wallet/addresses", self.url);
126        let response = self.client.get(&url).send().await?;
127        let response = response
128            .json::<LndListAddressesResponse>()
129            .await?
130            .find_addresses(account, address_type);
131        Ok(response)
132    }
133    pub async fn invoice_channel(&self) -> Result<crate::LndWebsocket> {
134        let url = format!("wss://{}/v2/router/send?method=POST", self.url);
135        let lnd_ws = crate::LndWebsocket::default()
136            .connect(self.url.to_string(), Self::macaroon(&self.data_dir)?, url)
137            .await?;
138        Ok(lnd_ws)
139    }
140    pub async fn lookup_invoice(&self, r_hash_url_safe: String) -> Result<LndHodlInvoiceState> {
141        let query = format!(
142            "https://{}/v2/invoices/lookup?payment_hash={}",
143            self.url, r_hash_url_safe
144        );
145        let response = self.client.get(&query).send().await?;
146        let response = response.json::<LndHodlInvoiceState>().await?;
147        Ok(response)
148    }
149    pub async fn subscribe_to_invoice(
150        &self,
151        r_hash_url_safe: String,
152    ) -> Result<crate::LndWebsocket> {
153        let query = format!(
154            "wss://{}/v2/invoices/subscribe/{}",
155            self.url, r_hash_url_safe
156        );
157        let lnd_ws = crate::LndWebsocket::default()
158            .connect(self.url.to_string(), Self::macaroon(&self.data_dir)?, query)
159            .await?;
160        Ok(lnd_ws)
161    }
162    pub async fn get_hodl_invoice(
163        &self,
164        payment_hash: String,
165        amount: u64,
166    ) -> Result<LndHodlInvoice> {
167        let url = format!("https://{}/v2/invoices/hodl", self.url);
168
169        let response = self
170            .client
171            .post(&url)
172            .json(&serde_json::json!({ "value": amount, "hash": payment_hash }))
173            .send()
174            .await?;
175        let response = response.text().await?;
176        Ok(response.parse::<LndHodlInvoice>()?)
177    }
178    pub async fn settle_htlc(&self, preimage: String) -> Result<()> {
179        let url = format!("https://{}/v2/invoices/settle", self.url);
180        let hex_bytes = preimage.chars().collect::<Vec<char>>();
181        let preimage = hex_bytes
182            .chunks(2)
183            .map(|chunk| {
184                let s: String = chunk.iter().collect();
185                u8::from_str_radix(&s, 16).unwrap()
186            })
187            .collect::<Vec<u8>>();
188        let preimage = BASE64_URL_SAFE.encode(&preimage);
189        let response = self
190            .client
191            .post(&url)
192            .json(&serde_json::json!({ "preimage": preimage }))
193            .send()
194            .await?;
195        let _test = response.text().await?;
196        Ok(())
197    }
198    pub async fn cancel_htlc(&self, payment_hash: String) -> Result<()> {
199        let url = format!("https://{}/v2/invoices/cancel", self.url);
200        let response = self
201            .client
202            .post(&url)
203            .json(&serde_json::json!({ "payment_hash": payment_hash }))
204            .send()
205            .await?;
206        response.text().await?;
207        Ok(())
208    }
209}
210
211#[cfg(test)]
212mod test {
213
214    use bright_ln_models::{
215        InvoicePaymentState, LndHodlInvoiceState, LndInvoice, LndInvoiceRequestBody,
216        LndInvoiceState, LndNextAddressRequest, LndPaymentRequest, LndPaymentResponse,
217    };
218    use tracing::{error, info};
219    use tracing_test::traced_test;
220
221    use crate::websocket::LndWebsocketMessage;
222
223    use super::{LndRestClient, Result};
224    #[tokio::test]
225    #[traced_test]
226    async fn next_onchain() -> Result<()> {
227        let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
228        let invoices = client
229            .new_onchain_address(LndNextAddressRequest::default())
230            .await?;
231
232        info!("{:?}", invoices);
233        Ok(())
234    }
235    #[tokio::test]
236    #[traced_test]
237    async fn onchain_list() -> Result<()> {
238        let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
239        let invoices = client
240            .list_onchain_addresses(
241                "default",
242                &bright_ln_models::OnchainAddressType::TaprootPubkey,
243            )
244            .await?;
245        info!("{:?}", invoices);
246        Ok(())
247    }
248
249    #[tokio::test]
250    #[traced_test]
251    async fn test_invoice_list() -> Result<()> {
252        let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
253        let invoices = client.list_invoices().await?;
254        info!("{:?}", invoices);
255        Ok(())
256    }
257    #[tokio::test]
258    #[traced_test]
259    async fn test_connection() -> Result<()> {
260        let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
261        let invoice = client
262            .get_invoice(LndInvoiceRequestBody {
263                value: 1000.to_string(),
264                memo: Some("Hello".to_string()),
265                ..Default::default()
266            })
267            .await?;
268        info!("{:?}", invoice);
269        let subscription = client
270            .subscribe_to_invoice(invoice.r_hash_url_safe().expect("No hash"))
271            .await?;
272        loop {
273            match subscription.receiver.read::<LndInvoice>().await {
274                Some(LndWebsocketMessage::Response(state)) => {
275                    info!("{:?}", state);
276                    match state.state {
277                        LndInvoiceState::Open => {
278                            break;
279                        }
280                        LndInvoiceState::Canceled => {
281                            break;
282                        }
283                        _ => {}
284                    }
285                }
286                Some(LndWebsocketMessage::Error(e)) => {
287                    tracing::error!("{:#?}", e);
288                    Err(super::LndRestClientError::Unknown)?;
289                }
290                Some(LndWebsocketMessage::Ping) => {
291                    info!("Ping");
292                }
293                None => {
294                    Err(super::LndRestClientError::Unknown)?;
295                }
296            }
297        }
298        Ok(())
299    }
300    // #[tokio::test]
301    // #[traced_test]
302    // async fn get_hodl_invoice() -> Result<()> {
303    //     let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
304    //     // let ln_address = LightningAddress("42pupusas@blink.sv");
305    //     let pay_request = ln_address.get_invoice(&client.client, 1000).await?;
306    //     let _hodl_invoice = client.get_hodl_invoice(pay_request.r_hash()?, 100).await?;
307    //     let states = client
308    //         .subscribe_to_invoice(pay_request.r_hash_url_safe()?)
309    //         .await?;
310    //     let mut correct_state = false;
311    //     assert!(!correct_state);
312    //     loop {
313    //         if let Some(LndWebsocketMessage::Response(state)) =
314    //             states.receiver.read::<LndHodlInvoiceState>().await
315    //         {
316    //             info!("{:?}", state.state());
317    //             match state.state() {
318    //                 HodlState::OPEN => {
319    //                     match client.cancel_htlc(pay_request.r_hash_url_safe()?).await {
320    //                         Ok(_) => {
321    //                             info!("Canceled");
322    //                             correct_state = true;
323    //                             break;
324    //                         }
325    //                         Err(e) => {
326    //                             error!("{}", e);
327    //                         }
328    //                     }
329    //                 }
330    //                 _ => {}
331    //             }
332    //         }
333    //     }
334    //     assert!(correct_state);
335    //     Ok(())
336    // }
337
338    #[tokio::test]
339    #[traced_test]
340    async fn pay_invoice() -> Result<()> {
341        let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
342        let ln_address = "42pupusas@blink.sv";
343        //let pay_request = LightningAddress(ln_address)
344        //    .get_invoice(&client.client, 100_000)
345        //    .await?;
346        let pr = LndPaymentRequest::new("".to_string(), 10, 10.to_string(), false);
347        let lnd_ws = client.invoice_channel().await?;
348        lnd_ws.sender.send(pr).await.unwrap();
349        while let Some(LndWebsocketMessage::Response(state)) =
350            lnd_ws.receiver.read::<LndPaymentResponse>().await
351        {
352            match state.status() {
353                InvoicePaymentState::Initiaited => {
354                    info!("Initiated");
355                }
356                InvoicePaymentState::InFlight => {
357                    info!("InFlight");
358                }
359                InvoicePaymentState::Succeeded => {
360                    info!("Succeeded");
361                    break;
362                }
363                InvoicePaymentState::Failed => {
364                    error!("Failed");
365                    break;
366                }
367            }
368        }
369        Ok(())
370    }
371    // #[tokio::test]
372    // #[traced_test]
373    // async fn settle_htlc() -> Result<()> {
374    //     use std::sync::Arc;
375    //     use tokio::sync::Mutex;
376    //     let client = LndRestClient::new("lnd.illuminodes.com", "./admin.macaroon")?;
377    //     let ln_address = "42pupusas@blink.sv";
378    //     let pay_request = LightningAddress(ln_address)
379    //         .get_invoice(&client.client, 10000)
380    //         .await?;
381
382    //     let hodl_invoice = client.get_hodl_invoice(pay_request.r_hash()?, 20).await?;
383    //     info!("{:?}", hodl_invoice.payment_request());
384    //     let correct_state = Arc::new(Mutex::new(false));
385    //     let states = client
386    //         .subscribe_to_invoice(hodl_invoice.r_hash_url_safe()?)
387    //         .await?;
388
389    //     let pr = LndPaymentRequest::new(pay_request.pr.clone(), 1000, 10.to_string(), false);
390    //     let lnd_ws = client.invoice_channel().await?;
391    //     tokio::spawn(async move {
392    //         loop {
393    //             match lnd_ws.receiver.read::<LndPaymentResponse>().await {
394    //                 Some(LndWebsocketMessage::Response(state)) => {
395    //                     info!("Listening for payment state");
396    //                     match state.status() {
397    //                         InvoicePaymentState::Initiaited => {
398    //                             info!("Initiated");
399    //                         }
400    //                         InvoicePaymentState::InFlight => {
401    //                             info!("InFlight");
402    //                         }
403    //                         InvoicePaymentState::Succeeded => {
404    //                             client.settle_htlc(state.preimage()).await.unwrap();
405    //                             break;
406    //                         }
407    //                         InvoicePaymentState::Failed => {
408    //                             error!("Failed");
409    //                         }
410    //                     }
411    //                 }
412    //                 others => {
413    //                     info!("{:?}", others);
414    //                 }
415    //             }
416    //         }
417    //     });
418    //     let correct_state_c = correct_state.clone();
419    //     loop {
420    //         info!("Waiting for state");
421    //         if let Some(LndWebsocketMessage::Response(state)) =
422    //             states.receiver.read::<LndHodlInvoiceState>().await
423    //         {
424    //             match state.state() {
425    //                 HodlState::OPEN => {
426    //                     info!("Open");
427    //                 }
428    //                 HodlState::ACCEPTED => {
429    //                     lnd_ws.sender.send(pr.clone()).await.unwrap();
430    //                     info!("Sent payment");
431    //                 }
432    //                 HodlState::SETTLED => {
433    //                     info!("REALLY Settled");
434    //                     *correct_state_c.lock().await = true;
435    //                     break;
436    //                 }
437    //                 HodlState::CANCELED => {}
438    //             }
439    //         }
440    //     }
441    //     assert!(*correct_state.lock().await);
442    //     Ok(())
443    // }
444}