use std::{error::Error, sync::Arc, path::Path};
use tokio::sync::Mutex;
use std::future::Future;
use std::pin::Pin;
use cln_rpc::ClnRpc;
use cln_rpc::model::requests::FetchinvoiceRequest;
use cln_rpc::model::responses::FetchinvoiceResponse;
use cln_rpc::primitives::Amount;
use crate::lndrpc::lnrpc;
use crate::lnclient;
#[derive(Debug, Clone)]
pub struct Bolt12Options {
pub lightning_dir: String,
pub offer: String,
}
pub trait Bolt12Backend: Send + Sync {
fn fetch_invoice(
&self,
offer: &str,
amount_msat: u64,
memo: Option<String>,
) -> Pin<Box<dyn Future<Output = Result<(String, Vec<u8>, Option<Vec<u8>>), Box<dyn Error + Send + Sync>>> + Send>>;
fn lookup_invoice(
&self,
_payment_hash: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Box<dyn Error + Send + Sync>>> + Send>> {
Box::pin(async {
Err("Server-side settlement lookup (auto-detect) is not supported for this BOLT12 backend".into())
})
}
}
struct ClnBolt12Backend {
client: Arc<Mutex<Option<ClnRpc>>>,
lightning_dir: String,
}
impl ClnBolt12Backend {
fn new(lightning_dir: String) -> Self {
Self {
client: Arc::new(Mutex::new(None)),
lightning_dir,
}
}
}
impl Bolt12Backend for ClnBolt12Backend {
fn fetch_invoice(
&self,
offer: &str,
amount_msat: u64,
memo: Option<String>,
) -> Pin<Box<dyn Future<Output = Result<(String, Vec<u8>, Option<Vec<u8>>), Box<dyn Error + Send + Sync>>> + Send>> {
let client = Arc::clone(&self.client);
let lightning_dir = self.lightning_dir.clone();
let offer = offer.to_string();
Box::pin(async move {
let mut client_guard = client.lock().await;
if client_guard.is_none() {
let new_client = ClnRpc::new(Path::new(&lightning_dir)).await
.map_err(|e| format!("CLN RPC error: {}", e))?;
*client_guard = Some(new_client);
}
let client = client_guard.as_mut().unwrap();
let fetch_invoice_request = FetchinvoiceRequest {
offer: offer,
amount_msat: Some(Amount::from_msat(amount_msat)),
quantity: None,
recurrence_counter: None,
recurrence_start: None,
recurrence_label: None,
timeout: None,
payer_note: memo,
bip353: None,
payer_metadata: None,
};
let response: FetchinvoiceResponse = match client.call_typed(&fetch_invoice_request).await {
Ok(res) => res,
Err(e) => {
*client_guard = None;
return Err(format!("CLN RPC error: {}", e).into());
}
};
let invoice_str = response.invoice;
let decode_request = cln_rpc::model::requests::DecodeRequest {
string: invoice_str.clone(),
};
let decode_response: cln_rpc::model::responses::DecodeResponse = match client.call_typed(&decode_request).await {
Ok(res) => res,
Err(e) => {
*client_guard = None;
return Err(format!("CLN RPC error during decode: {}", e).into());
}
};
let payment_hash_bytes = if let Some(ph) = decode_response.payment_hash {
<cln_rpc::primitives::Sha256 as AsRef<[u8]>>::as_ref(&ph).to_vec()
} else if let Some(ph_hex) = decode_response.invoice_payment_hash {
hex::decode(ph_hex).map_err(|e| format!("Invalid hex in invoice_payment_hash: {}", e))?
} else {
return Err("No payment hash in decode response".into());
};
let payment_secret = decode_response.payment_secret.map(|s| s.to_vec());
Ok((invoice_str, payment_hash_bytes, payment_secret))
})
}
fn lookup_invoice(
&self,
payment_hash: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Box<dyn Error + Send + Sync>>> + Send>> {
use cln_rpc::model::requests::ListinvoicesRequest;
use cln_rpc::model::responses::ListinvoicesInvoicesStatus;
let client = Arc::clone(&self.client);
let lightning_dir = self.lightning_dir.clone();
Box::pin(async move {
let mut client_guard = client.lock().await;
if client_guard.is_none() {
let new_client = ClnRpc::new(Path::new(&lightning_dir)).await
.map_err(|e| format!("CLN RPC error: {}", e))?;
*client_guard = Some(new_client);
}
let client = client_guard.as_mut().unwrap();
let request = ListinvoicesRequest {
payment_hash: Some(hex::encode(&payment_hash)),
label: None,
invstring: None,
offer_id: None,
index: None,
start: None,
limit: None,
};
let resp = match client.call_typed(&request).await {
Ok(res) => res,
Err(e) => {
*client_guard = None;
return Err(format!("CLN listinvoices error: {}", e).into());
}
};
match resp.invoices.into_iter().next() {
Some(inv) if inv.status == ListinvoicesInvoicesStatus::PAID => {
match inv.payment_preimage {
Some(preimage) => Ok(Some(preimage.to_vec())),
None => Err("CLN invoice settled but preimage missing".into()),
}
}
_ => Ok(None),
}
})
}
}
pub struct Bolt12Wrapper {
backend: Arc<dyn Bolt12Backend>,
offer: String,
}
impl Bolt12Wrapper {
pub async fn new_client(
ln_client_config: &lnclient::LNClientConfig,
) -> Result<Arc<Mutex<dyn lnclient::LNClient>>, Box<dyn Error + Send + Sync>> {
let bolt12_options = ln_client_config.bolt12_config.clone().unwrap();
println!("BOLT12 client {} with offer {}", bolt12_options.lightning_dir, bolt12_options.offer);
let backend = ClnBolt12Backend::new(bolt12_options.lightning_dir);
let wrapper = Bolt12Wrapper {
backend: Arc::new(backend),
offer: bolt12_options.offer,
};
Ok(Arc::new(Mutex::new(wrapper)))
}
}
impl lnclient::LNClient for Bolt12Wrapper {
fn add_invoice(
&self,
invoice: lnrpc::Invoice,
) -> Pin<Box<dyn Future<Output = Result<lnrpc::AddInvoiceResponse, Box<dyn Error + Send + Sync>>> + Send>> {
let backend = Arc::clone(&self.backend);
let offer = self.offer.clone();
Box::pin(async move {
let memo = if invoice.memo.is_empty() { None } else { Some(invoice.memo.clone()) };
let amount_msat = u64::try_from(invoice.value_msat)
.map_err(|_| format!("invalid value_msat: {}", invoice.value_msat))?;
let (payment_request, r_hash, payment_secret) = backend.fetch_invoice(
&offer,
amount_msat,
memo
).await?;
Ok(lnrpc::AddInvoiceResponse {
r_hash,
payment_request,
add_index: 0,
payment_addr: if let Some(secret) = payment_secret {
secret
} else {
vec![]
},
})
})
}
fn lookup_invoice(
&self,
payment_hash: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Box<dyn Error + Send + Sync>>> + Send>> {
let backend = Arc::clone(&self.backend);
Box::pin(async move { backend.lookup_invoice(payment_hash).await })
}
}