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 crate::lndrpc::lnrpc;
use bitcoin::hashes::Hash;
use lightning_invoice::{Bolt11Invoice, SignedRawBolt11Invoice};
use reqwest::{Client, Error};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::Mutex;

use crate::lnclient;
use crate::utils;

#[derive(Debug, Clone)]
pub struct LNURLOptions {
    pub address: String,
}

#[derive(Debug, serde::Deserialize)]
pub struct LnAddressUrlResJson {
    callback: String,

    #[serde(rename = "maxSendable")]
    max_sendable: u64,

    #[serde(rename = "minSendable")]
    min_sendable: u64,

    metadata: String,

    #[serde(rename = "commentAllowed", default)]
    comment_allowed: u32,
    tag: String,
}

#[derive(Debug, serde::Deserialize)]
pub struct CallbackUrlResJson {
    pr: String,
}

#[derive(Debug, serde::Deserialize)]
pub struct DecodedPR {
    currency: String,
    created_at: i32,
    expiry: i32,
    payee: String,
    msatoshi: i64,
    description: Option<String>,
    description_hash: Option<String>,
    payment_hash: String,
    min_final_cltv_expiry: i32,
}

impl LnAddressUrlResJson {
    pub async fn new_client(ln_client_config: &lnclient::LNClientConfig) -> Result<Arc<Mutex<dyn lnclient::LNClient>>, Box<dyn std::error::Error + Send + Sync>> {
        let lnurl_options = ln_client_config
            .lnurl_config
            .clone()
            .ok_or("LN_CLIENT_TYPE is LNURL but lnurl_config is missing")?;
        let (username, domain) = utils::parse_ln_address(lnurl_options.address)?;

        // LUD-16: onion services are served over http; everything else https.
        let scheme = if domain.ends_with(".onion") { "http" } else { "https" };
        let ln_address_url = format!("{}://{}/.well-known/lnurlp/{}", scheme, domain, username);
        let ln_address_url_res_body = do_get_request(&ln_address_url).await?;

        let ln_address_url_res: LnAddressUrlResJson = serde_json::from_str(&ln_address_url_res_body)?;
        Ok(Arc::new(Mutex::new(ln_address_url_res)))
    }
}

impl lnclient::LNClient for LnAddressUrlResJson {
    fn add_invoice(
        &self,
        ln_invoice: lnrpc::Invoice,
    ) -> Pin<
        Box<
            dyn Future<
                    Output = Result<
                        lnrpc::AddInvoiceResponse,
                        Box<dyn std::error::Error + Send + Sync>,
                    >,
                > + Send,
        >,
    > {
        let callback_url = format!("{}?amount={}", self.callback, ln_invoice.value_msat);

        Box::pin(async move {
            let callback_url_res_body = do_get_request(&callback_url).await?;

            let callback_url_res_json: CallbackUrlResJson =
                serde_json::from_str(&callback_url_res_body)?;

            let invoice = callback_url_res_json.pr;
            // `pr` is whatever the remote provider returned — never unwrap it.
            let signed = invoice
                .parse::<SignedRawBolt11Invoice>()
                .map_err(|e| format!("LNURL callback returned an unparsable invoice: {:?}", e))?;
            let decoded_invoice = Bolt11Invoice::from_signed(signed)
                .map_err(|e| format!("LNURL callback invoice failed validation: {:?}", e))?;
            let payment_hash = decoded_invoice.payment_hash();
            let payment_addr = decoded_invoice.payment_secret();

            Ok(lnrpc::AddInvoiceResponse {
                r_hash: payment_hash.to_byte_array().to_vec(),
                payment_request: invoice,
                add_index: 0,
                payment_addr: payment_addr.0.to_vec(),
            })
        })
    }
}

async fn do_get_request(url: &str) -> Result<String, Error> {
    // The caller is holding a request open while this runs, so a provider that
    // stops answering has to fail rather than block indefinitely.
    let client = Client::builder()
        .timeout(std::time::Duration::from_secs(10))
        .build()?;

    let raw_resp = client.get(url).send().await?;
    let resp = raw_resp.error_for_status()?;

    let text = resp.text().await?;
    Ok(text)
}