1use async_trait::async_trait;
4use base64::engine::general_purpose::STANDARD as B64_STD;
5use base64::Engine;
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8
9use crate::error::Error;
10use crate::lightning::{
11 Invoice, InvoiceCreateRequest, InvoiceLookup, LightningNode, PaymentResult,
12};
13
14pub struct LndRestConfig {
15 pub url: String,
16 pub macaroon_hex: String,
17}
18
19pub struct LndRestNode {
20 cfg: LndRestConfig,
21 client: Client,
22}
23
24impl LndRestNode {
25 pub fn new(cfg: LndRestConfig) -> Result<Self, Error> {
26 let client = Client::builder()
27 .danger_accept_invalid_certs(true)
28 .build()
29 .map_err(|e| Error::Http(e.to_string()))?;
30 Ok(Self { cfg, client })
31 }
32
33 pub fn with_client(cfg: LndRestConfig, client: Client) -> Self {
34 Self { cfg, client }
35 }
36}
37
38#[derive(Serialize)]
39struct CreateInvoiceReq<'a> {
40 value_msat: String,
41 memo: &'a str,
42 expiry: String,
43}
44
45#[derive(Deserialize)]
46struct CreateInvoiceResp {
47 payment_request: String,
48 r_hash: String,
49}
50
51#[derive(Deserialize)]
52struct LookupResp {
53 settled: Option<bool>,
54 value_msat: Option<String>,
55 r_preimage: Option<String>,
56}
57
58#[derive(Serialize)]
59struct PayReq<'a> {
60 payment_request: &'a str,
61}
62
63#[derive(Deserialize)]
64struct PayResp {
65 payment_error: Option<String>,
66 payment_preimage: Option<String>,
67 payment_route: Option<PaymentRoute>,
68}
69
70#[derive(Deserialize)]
71struct PaymentRoute {
72 total_fees_msat: Option<String>,
73}
74
75#[async_trait]
76impl LightningNode for LndRestNode {
77 async fn create_invoice(&self, req: InvoiceCreateRequest) -> Result<Invoice, Error> {
78 let body = CreateInvoiceReq {
79 value_msat: req.amount_msat.to_string(),
80 memo: req.memo.as_deref().unwrap_or(""),
81 expiry: req.expiry_seconds.unwrap_or(300).to_string(),
82 };
83 let res = self
84 .client
85 .post(format!("{}/v1/invoices", self.cfg.url))
86 .header("grpc-metadata-macaroon", &self.cfg.macaroon_hex)
87 .json(&body)
88 .send()
89 .await
90 .map_err(|e| Error::Http(e.to_string()))?;
91 if !res.status().is_success() {
92 let s = res.status();
93 let t = res.text().await.unwrap_or_default();
94 return Err(Error::Http(format!("LND createInvoice {s}: {t}")));
95 }
96 let resp: CreateInvoiceResp = res.json().await.map_err(|e| Error::Http(e.to_string()))?;
97 let r_hash_bytes = B64_STD
98 .decode(&resp.r_hash)
99 .map_err(|e| Error::Http(format!("r_hash b64: {e}")))?;
100 Ok(Invoice {
101 bolt11: resp.payment_request,
102 payment_hash: hex::encode(r_hash_bytes),
103 })
104 }
105
106 async fn lookup_invoice(&self, payment_hash: &str) -> Result<InvoiceLookup, Error> {
107 let bytes =
108 hex::decode(payment_hash).map_err(|e| Error::Http(format!("payment_hash hex: {e}")))?;
109 let b64 = B64_STD.encode(&bytes);
110 let path = urlencoding::encode(&b64).into_owned();
111 let res = self
112 .client
113 .get(format!("{}/v1/invoice/{}", self.cfg.url, path))
114 .header("grpc-metadata-macaroon", &self.cfg.macaroon_hex)
115 .send()
116 .await
117 .map_err(|e| Error::Http(e.to_string()))?;
118 if !res.status().is_success() {
119 let s = res.status();
120 let t = res.text().await.unwrap_or_default();
121 return Err(Error::Http(format!("LND lookupInvoice {s}: {t}")));
122 }
123 let resp: LookupResp = res.json().await.map_err(|e| Error::Http(e.to_string()))?;
124 let preimage = if let Some(b64) = resp.r_preimage {
125 Some(
126 B64_STD
127 .decode(b64)
128 .map_err(|e| Error::Http(format!("preimage b64: {e}")))?,
129 )
130 } else {
131 None
132 };
133 Ok(InvoiceLookup {
134 settled: resp.settled.unwrap_or(false),
135 amount_msat: resp
136 .value_msat
137 .as_deref()
138 .unwrap_or("0")
139 .parse()
140 .unwrap_or(0),
141 preimage,
142 })
143 }
144
145 async fn pay_invoice(&self, bolt11: &str) -> Result<PaymentResult, Error> {
146 let res = self
147 .client
148 .post(format!("{}/v1/channels/transactions", self.cfg.url))
149 .header("grpc-metadata-macaroon", &self.cfg.macaroon_hex)
150 .json(&PayReq {
151 payment_request: bolt11,
152 })
153 .send()
154 .await
155 .map_err(|e| Error::Http(e.to_string()))?;
156 if !res.status().is_success() {
157 let s = res.status();
158 let t = res.text().await.unwrap_or_default();
159 return Err(Error::Http(format!("LND payInvoice {s}: {t}")));
160 }
161 let resp: PayResp = res.json().await.map_err(|e| Error::Http(e.to_string()))?;
162 if let Some(err) = resp.payment_error {
163 return Err(Error::Http(format!("LND payment_error: {err}")));
164 }
165 let preimage = resp
166 .payment_preimage
167 .ok_or_else(|| Error::Http("missing payment_preimage".into()))?;
168 let bytes = B64_STD
169 .decode(&preimage)
170 .map_err(|e| Error::Http(format!("preimage b64: {e}")))?;
171 Ok(PaymentResult {
172 preimage: bytes,
173 fee_msat: resp
174 .payment_route
175 .and_then(|r| r.total_fees_msat)
176 .and_then(|s| s.parse().ok())
177 .unwrap_or(0),
178 })
179 }
180}