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