diff --git a/libs/gl-client/src/lnurl/mod.rs b/libs/gl-client/src/lnurl/mod.rs index f6208320e..19c2cb877 100644 --- a/libs/gl-client/src/lnurl/mod.rs +++ b/libs/gl-client/src/lnurl/mod.rs @@ -2,15 +2,77 @@ mod models; mod pay; mod utils; -use anyhow::Result; +use crate::node::Client; +use crate::pb::{PayRequest, Payment}; +use anyhow::{anyhow, Result}; use models::LnUrlHttpClearnetClient; -use pay::resolve_to_invoice; +use pay::resolve_lnurl_to_invoice; +use url::Url; -pub struct LNURL; +use self::models::{LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse}; +use self::pay::validate_invoice_from_callback_response; +use self::utils::{parse_invoice, parse_lnurl}; -impl LNURL { - pub async fn resolve_lnurl_to_invoice(&self, lnurl: &str, amount_msats: u64) -> Result { +pub struct LNURL { + http_client: T, +} + +impl LNURL { + pub fn new(http_client: T) -> Self { + LNURL { http_client } + } + + pub fn new_with_clearnet_client() -> LNURL { let http_client = LnUrlHttpClearnetClient::new(); - resolve_to_invoice(http_client, lnurl, amount_msats).await + LNURL { http_client } + } + + pub async fn get_pay_request_response(&self, lnurl: &str) -> Result { + let url = parse_lnurl(lnurl)?; + + let lnurl_pay_request_response: PayRequestResponse = + self.http_client.get_pay_request_response(&url).await?; + + if lnurl_pay_request_response.tag != "payRequest" { + return Err(anyhow!("Expected tag to say 'payRequest'")); + } + + Ok(lnurl_pay_request_response) + } + + pub async fn get_pay_request_callback_response( + &self, + base_callback_url: &str, + amount_msats: u64, + metadata: &str, + ) -> Result { + let mut url = Url::parse(base_callback_url)?; + url.query_pairs_mut() + .append_pair("amount", &amount_msats.to_string()); + + let callback_response: PayRequestCallbackResponse = self + .http_client + .get_pay_request_callback_response(&url.to_string()) + .await?; + + let invoice = parse_invoice(&callback_response.pr)?; + validate_invoice_from_callback_response(&invoice, amount_msats, metadata)?; + Ok(callback_response) + } + + pub async fn pay( + &self, + lnurl: &str, + amount_msats: u64, + node: &mut Client, + ) -> Result> { + let invoice = resolve_lnurl_to_invoice(&self.http_client, lnurl, amount_msats).await?; + + node.pay(PayRequest { + bolt11: invoice.to_string(), + ..Default::default() + }) + .await + .map_err(|e| anyhow!(e)) } } diff --git a/libs/gl-client/src/lnurl/pay/mod.rs b/libs/gl-client/src/lnurl/pay/mod.rs index e7f519d6c..5923d9cbb 100644 --- a/libs/gl-client/src/lnurl/pay/mod.rs +++ b/libs/gl-client/src/lnurl/pay/mod.rs @@ -1,15 +1,18 @@ use super::models; use super::utils::parse_lnurl; -use crate::lnurl::models::{LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse}; +use crate::lnurl::{ + models::{LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse}, + utils::parse_invoice, +}; use anyhow::{anyhow, ensure, Result}; use lightning_invoice::{Invoice, InvoiceDescription}; use log::debug; use reqwest::Url; use sha256; -use std::str::FromStr; -pub async fn resolve_to_invoice( - http_client: T, + +pub async fn resolve_lnurl_to_invoice( + http_client: &T, lnurl: &str, amount_msats: u64, ) -> Result { @@ -18,7 +21,11 @@ pub async fn resolve_to_invoice( let lnurl_pay_request_response: PayRequestResponse = http_client.get_pay_request_response(&url).await?; - validate_pay_request_response(&lnurl_pay_request_response, amount_msats)?; + if lnurl_pay_request_response.tag != "payRequest" { + return Err(anyhow!("Expected tag to say 'payRequest'")); + } + + ensure_amount_is_within_range(&lnurl_pay_request_response, amount_msats)?; let description = extract_description(&lnurl_pay_request_response)?; debug!("Domain: {}", Url::parse(&url).unwrap().host().unwrap()); @@ -34,13 +41,35 @@ pub async fn resolve_to_invoice( .await?; let invoice = parse_invoice(&callback_response.pr)?; - validate_invoice_from_callback_response(&invoice, amount_msats, lnurl_pay_request_response)?; + validate_invoice_from_callback_response( + &invoice, + amount_msats, + &lnurl_pay_request_response.metadata, + )?; Ok(invoice.to_string()) } -// Get an Invoice from a Lightning Network URL pay request -fn parse_invoice(invoice_str: &str) -> Result { - Invoice::from_str(&invoice_str).map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e))) +// Validates the invoice on the pay request's callback response +pub fn validate_invoice_from_callback_response( + invoice: &Invoice, + amount_msats: u64, + metadata: &str, +) -> Result<()> { + ensure!(invoice.amount_milli_satoshis().unwrap_or_default() == amount_msats , + "Amount found in invoice was not equal to the amount found in the original request\nRequest amount: {}\nInvoice amount:{:?}", amount_msats, invoice.amount_milli_satoshis().unwrap() + ); + + let description_hash: String = match invoice.description() { + InvoiceDescription::Direct(d) => sha256::digest(d.clone().into_inner()), + InvoiceDescription::Hash(h) => h.0.to_string(), + }; + + ensure!( + description_hash == sha256::digest(metadata), + "description_hash does not match the hash of the metadata" + ); + + Ok(()) } // Function to extract the description from the lnurl pay request response @@ -59,8 +88,6 @@ fn extract_description(lnurl_pay_request_response: &PayRequestResponse) -> Resul } } - ensure!(!description.is_empty(), "No description found"); - Ok(description) } @@ -76,7 +103,7 @@ fn build_callback_url( } // Validates the pay request response for expected values -fn validate_pay_request_response( +fn ensure_amount_is_within_range( lnurl_pay_request_response: &PayRequestResponse, amount: u64, ) -> Result<()> { @@ -94,32 +121,6 @@ fn validate_pay_request_response( )); } - if lnurl_pay_request_response.tag != "payRequest" { - return Err(anyhow!("Expected tag to say 'payRequest'")); - } - - Ok(()) -} - -// Validates the invoice on the pay request's callback response -fn validate_invoice_from_callback_response( - invoice: &Invoice, - amount: u64, - lnurl_pay_request_response: PayRequestResponse, -) -> Result<()> { - ensure!(invoice.amount_milli_satoshis().unwrap_or_default() == amount , - "Amount found in invoice was not equal to the amount found in the original request\nRequest amount: {}\nInvoice amount:{:?}", amount, invoice.amount_milli_satoshis().unwrap() - ); - - let description_hash: String = match invoice.description() { - InvoiceDescription::Direct(d) => sha256::digest(d.clone().into_inner()), - InvoiceDescription::Hash(h) => h.0.to_string(), - }; - - ensure!( - description_hash == sha256::digest(lnurl_pay_request_response.metadata), - "description_hash does not match the hash of the metadata" - ); Ok(()) } @@ -169,7 +170,7 @@ mod tests { let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; let amount = 100000; - let _ = resolve_to_invoice(mock_http_client, lnurl, amount).await; + let _ = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; } #[tokio::test] @@ -179,7 +180,7 @@ mod tests { let lnurl = "LNURL1111111111111111111111111111111111111111111111111111111111111111111"; let amount = 100000; - let result = resolve_to_invoice(mock_http_client, lnurl, amount).await; + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; match result { Err(err) => { @@ -211,7 +212,7 @@ mod tests { let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; let amount = 1; - let result = resolve_to_invoice(mock_http_client, lnurl, amount).await; + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; match result { Err(err) => { @@ -240,7 +241,7 @@ mod tests { let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2"; let amount = 1; - let result = resolve_to_invoice(mock_http_client, lnurl, amount).await; + let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, amount).await; match result { Err(err) => { diff --git a/libs/gl-client/src/lnurl/utils.rs b/libs/gl-client/src/lnurl/utils.rs index 8d55a65c1..d0994e28c 100644 --- a/libs/gl-client/src/lnurl/utils.rs +++ b/libs/gl-client/src/lnurl/utils.rs @@ -1,5 +1,7 @@ use anyhow::{anyhow, Result}; use bech32::FromBase32; +use lightning_invoice::Invoice; +use std::str::FromStr; // Function to decode and parse the lnurl into a URL pub fn parse_lnurl(lnurl: &str) -> Result { @@ -12,3 +14,8 @@ pub fn parse_lnurl(lnurl: &str) -> Result { let url = String::from_utf8(vec).map_err(|e| anyhow!("Failed to convert to utf-8: {}", e))?; Ok(url) } + +// Get an Invoice from a Lightning Network URL pay request +pub fn parse_invoice(invoice_str: &str) -> Result { + Invoice::from_str(&invoice_str).map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e))) +} diff --git a/libs/gl-client/src/node/mod.rs b/libs/gl-client/src/node/mod.rs index e5ba9c9be..bd68b6408 100644 --- a/libs/gl-client/src/node/mod.rs +++ b/libs/gl-client/src/node/mod.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::pb::cln::node_client as cln_client; use crate::pb::node_client::NodeClient; use crate::pb::scheduler::{scheduler_client::SchedulerClient, ScheduleRequest}; diff --git a/libs/gl-client/src/scheduler.rs b/libs/gl-client/src/scheduler.rs index d8b15f26a..7895142f2 100644 --- a/libs/gl-client/src/scheduler.rs +++ b/libs/gl-client/src/scheduler.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::pb::scheduler::scheduler_client::SchedulerClient; use crate::tls::{self, TlsConfig};