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}