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]
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 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 }