l402_middleware 2.3.4

A middleware library for rust that provides handler functions to accept microtransactions before serving ad-free content or any paid APIs.
Documentation
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,
}

/// Trait for fetching BOLT12 invoices.
/// This allows us to swap the backend (CLN, LND, etc.) transparently.
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>>;

    /// Return the preimage once the invoice for `payment_hash` is settled.
    ///
    /// Default: unsupported, for backends that can't query their own node.
    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())
        })
    }
}

/// CLN Implementation of Bolt12Backend
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;

            // Decode to extract payment hash
            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());
                 }
            };

            // BOLT12 invoices return `invoice_payment_hash` (hex) instead of `payment_hash` (Sha256)
            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))
        })
    }

    /// Invoices fetched from the offer settle on this CLN node, so they are in
    /// its local database like any other.
    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()
            .ok_or("LN_CLIENT_TYPE is BOLT12 but bolt12_config is missing")?;

        println!("BOLT12 client {} with offer {}", bolt12_options.lightning_dir, bolt12_options.offer);

        // In the future, we can check config to decide which backend to instantiate
        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 })
    }
}