use async_trait::async_trait;
use base64::engine::general_purpose::STANDARD as B64_STD;
use base64::Engine;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::lightning::{
Invoice, InvoiceCreateRequest, InvoiceLookup, LightningNode, PaymentResult,
};
pub struct LndRestConfig {
pub url: String,
pub macaroon_hex: String,
}
pub struct LndRestNode {
cfg: LndRestConfig,
client: Client,
}
impl LndRestNode {
pub fn new(cfg: LndRestConfig) -> Result<Self, Error> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| Error::Http(e.to_string()))?;
Ok(Self { cfg, client })
}
pub fn with_client(cfg: LndRestConfig, client: Client) -> Self {
Self { cfg, client }
}
}
#[derive(Serialize)]
struct CreateInvoiceReq<'a> {
value_msat: String,
memo: &'a str,
expiry: String,
}
#[derive(Deserialize)]
struct CreateInvoiceResp {
payment_request: String,
r_hash: String,
}
#[derive(Deserialize)]
struct LookupResp {
settled: Option<bool>,
value_msat: Option<String>,
r_preimage: Option<String>,
}
#[derive(Serialize)]
struct PayReq<'a> {
payment_request: &'a str,
}
#[derive(Deserialize)]
struct PayResp {
payment_error: Option<String>,
payment_preimage: Option<String>,
payment_route: Option<PaymentRoute>,
}
#[derive(Deserialize)]
struct PaymentRoute {
total_fees_msat: Option<String>,
}
#[async_trait]
impl LightningNode for LndRestNode {
async fn create_invoice(&self, req: InvoiceCreateRequest) -> Result<Invoice, Error> {
let body = CreateInvoiceReq {
value_msat: req.amount_msat.to_string(),
memo: req.memo.as_deref().unwrap_or(""),
expiry: req.expiry_seconds.unwrap_or(300).to_string(),
};
let res = self
.client
.post(format!("{}/v1/invoices", self.cfg.url))
.header("grpc-metadata-macaroon", &self.cfg.macaroon_hex)
.json(&body)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if !res.status().is_success() {
let s = res.status();
let t = res.text().await.unwrap_or_default();
return Err(Error::Http(format!("LND createInvoice {s}: {t}")));
}
let resp: CreateInvoiceResp = res.json().await.map_err(|e| Error::Http(e.to_string()))?;
let r_hash_bytes = B64_STD
.decode(&resp.r_hash)
.map_err(|e| Error::Http(format!("r_hash b64: {e}")))?;
Ok(Invoice {
bolt11: resp.payment_request,
payment_hash: hex::encode(r_hash_bytes),
})
}
async fn lookup_invoice(&self, payment_hash: &str) -> Result<InvoiceLookup, Error> {
let bytes =
hex::decode(payment_hash).map_err(|e| Error::Http(format!("payment_hash hex: {e}")))?;
let b64 = B64_STD.encode(&bytes);
let path = urlencoding::encode(&b64).into_owned();
let res = self
.client
.get(format!("{}/v1/invoice/{}", self.cfg.url, path))
.header("grpc-metadata-macaroon", &self.cfg.macaroon_hex)
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if !res.status().is_success() {
let s = res.status();
let t = res.text().await.unwrap_or_default();
return Err(Error::Http(format!("LND lookupInvoice {s}: {t}")));
}
let resp: LookupResp = res.json().await.map_err(|e| Error::Http(e.to_string()))?;
let preimage = if let Some(b64) = resp.r_preimage {
Some(
B64_STD
.decode(b64)
.map_err(|e| Error::Http(format!("preimage b64: {e}")))?,
)
} else {
None
};
Ok(InvoiceLookup {
settled: resp.settled.unwrap_or(false),
amount_msat: resp
.value_msat
.as_deref()
.unwrap_or("0")
.parse()
.unwrap_or(0),
preimage,
})
}
async fn pay_invoice(&self, bolt11: &str) -> Result<PaymentResult, Error> {
let res = self
.client
.post(format!("{}/v1/channels/transactions", self.cfg.url))
.header("grpc-metadata-macaroon", &self.cfg.macaroon_hex)
.json(&PayReq {
payment_request: bolt11,
})
.send()
.await
.map_err(|e| Error::Http(e.to_string()))?;
if !res.status().is_success() {
let s = res.status();
let t = res.text().await.unwrap_or_default();
return Err(Error::Http(format!("LND payInvoice {s}: {t}")));
}
let resp: PayResp = res.json().await.map_err(|e| Error::Http(e.to_string()))?;
if let Some(err) = resp.payment_error {
return Err(Error::Http(format!("LND payment_error: {err}")));
}
let preimage = resp
.payment_preimage
.ok_or_else(|| Error::Http("missing payment_preimage".into()))?;
let bytes = B64_STD
.decode(&preimage)
.map_err(|e| Error::Http(format!("preimage b64: {e}")))?;
Ok(PaymentResult {
preimage: bytes,
fee_msat: resp
.payment_route
.and_then(|r| r.total_fees_msat)
.and_then(|s| s.parse().ok())
.unwrap_or(0),
})
}
}