l402_middleware 2.3.2

A middleware library for rust that provides handler functions to accept microtransactions before serving ad-free content or any paid APIs.
Documentation
use crate::lndrpc::lnrpc;
use lightning::types::payment::{PaymentHash};
use std::error::Error;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::future::Future;
use std::pin::Pin;

use crate::lnurl;
use crate::lnd;
use crate::nwc;
use crate::cln;
use crate::bolt12;
use crate::eclair;

const LND_CLIENT_TYPE: &str = "LND";
const LNURL_CLIENT_TYPE: &str = "LNURL";
const NWC_CLIENT_TYPE: &str = "NWC";
const CLN_CLIENT_TYPE: &str = "CLN";
const BOLT12_CLIENT_TYPE: &str = "BOLT12";
const ECLAIR_CLIENT_TYPE: &str = "ECLAIR";

#[derive(Debug, Clone)]
pub struct LNClientConfig {
    pub ln_client_type: String,
    pub lnd_config: Option<lnd::LNDOptions>,
    pub lnurl_config: Option<lnurl::LNURLOptions>,
    pub nwc_config: Option<nwc::NWCOptions>,
    pub cln_config: Option<cln::CLNOptions>,
    pub bolt12_config: Option<bolt12::Bolt12Options>,
    pub eclair_config: Option<eclair::EclairOptions>,
    pub root_key: Vec<u8>,
}

pub trait LNClient: Send + Sync + 'static {
    fn add_invoice(
        &self,
        invoice: lnrpc::Invoice,
    ) -> Pin<Box<dyn Future<Output = Result<lnrpc::AddInvoiceResponse, Box<dyn Error + Send + Sync>>> + Send>>;

    /// Server-side settlement lookup ("auto-detect"): given a payment hash, ask
    /// the node whether the invoice is settled and, if so, return its preimage —
    /// so a client that can't present a usable preimage still gets access.
    ///
    /// Returns `Ok(Some(preimage))` when settled, `Ok(None)` when the invoice
    /// exists but isn't settled yet, and `Err` when the backend can't answer.
    ///
    /// Default: unsupported. Backends that can query settlement override this
    /// (LND / CLN / Eclair / BOLT12 / NWC); LNURL keeps the default, as a
    /// lightning address gives no way to ask.
    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 backend".into())
        })
    }
}

pub struct LNClientConn {
    pub ln_client: Arc<Mutex<dyn LNClient>>,
}

impl LNClientConn {
    pub async fn init(ln_client_config: &LNClientConfig) -> Result<Arc<Mutex<dyn LNClient>>, Box<dyn Error + Send + Sync>> {
        let ln_client: Arc<Mutex<dyn LNClient>> = match ln_client_config.ln_client_type.as_str() {
            LND_CLIENT_TYPE => lnd::LNDWrapper::new_client(ln_client_config).await?,
            LNURL_CLIENT_TYPE => lnurl::LnAddressUrlResJson::new_client(ln_client_config).await?,
            NWC_CLIENT_TYPE => nwc::NWCWrapper::new_client(ln_client_config).await?,
            CLN_CLIENT_TYPE => cln::CLNWrapper::new_client(ln_client_config).await?,
            BOLT12_CLIENT_TYPE => bolt12::Bolt12Wrapper::new_client(ln_client_config).await?,
            ECLAIR_CLIENT_TYPE => eclair::EclairWrapper::new_client(ln_client_config).await?,
            _ => {
                return Err(format!(
                    "LN Client type not recognized: {}",
                    ln_client_config.ln_client_type
                )
                .into());
            }
        };

        Ok(ln_client)
    }

    pub async fn generate_invoice(
        &self,
        ln_invoice: lnrpc::Invoice,
    ) -> Result<(String, PaymentHash), Box<dyn Error + Send + Sync>> {
        let client = &mut self.ln_client.lock().await;
        let ln_client_invoice = &mut client.add_invoice(ln_invoice).await?;

        let invoice = &ln_client_invoice.payment_request;
        let hash: [u8; 32] = ln_client_invoice.r_hash.clone().try_into().map_err(|_| "Invalid length for r_hash, must be 32 bytes")?;
        let payment_hash = PaymentHash(hash);

        Ok((invoice.to_string(), payment_hash))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // A backend that only implements add_invoice; lookup_invoice falls back to
    // the trait default — which must REJECT auto-detect. This locks the contract
    // that LNURL/NWC/BOLT12 (no override) can't be used for settlement lookup.
    // The live LND/CLN/Eclair lookups need a real node and are integration-only.
    struct NoLookupClient;

    impl LNClient for NoLookupClient {
        fn add_invoice(
            &self,
            _invoice: lnrpc::Invoice,
        ) -> Pin<
            Box<
                dyn Future<Output = Result<lnrpc::AddInvoiceResponse, Box<dyn Error + Send + Sync>>>
                    + Send,
            >,
        > {
            Box::pin(async { unreachable!("add_invoice is not exercised in this test") })
        }
    }

    #[test]
    fn default_lookup_invoice_is_unsupported() {
        let rt = tokio::runtime::Builder::new_current_thread()
            .build()
            .unwrap();
        let client = NoLookupClient;
        let result = rt.block_on(client.lookup_invoice(vec![0u8; 32]));
        assert!(
            result.is_err(),
            "a backend without a lookup_invoice override must reject auto-detect"
        );
    }
}