bright_lightning/lnd/
rest_client.rs

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