Skip to content

Commit

Permalink
Made a convenience method to pay with an LNURL object and exposed the…
Browse files Browse the repository at this point in the history
… methods needed to manually resolve an lnurl to an invoice.
  • Loading branch information
Randy808 committed Aug 14, 2023
1 parent 1e8f675 commit 6838ad2
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 48 deletions.
74 changes: 68 additions & 6 deletions libs/gl-client/src/lnurl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
pub struct LNURL<T: LnUrlHttpClient> {
http_client: T,
}

impl<T: LnUrlHttpClient> LNURL<T> {
pub fn new(http_client: T) -> Self {
LNURL { http_client }
}

pub fn new_with_clearnet_client() -> LNURL<LnUrlHttpClearnetClient> {
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<PayRequestResponse> {
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<PayRequestCallbackResponse> {
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<tonic::Response<Payment>> {
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))
}
}
85 changes: 43 additions & 42 deletions libs/gl-client/src/lnurl/pay/mod.rs
Original file line number Diff line number Diff line change
@@ -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<T: LnUrlHttpClient>(
http_client: T,

pub async fn resolve_lnurl_to_invoice<T: LnUrlHttpClient>(
http_client: &T,
lnurl: &str,
amount_msats: u64,
) -> Result<String> {
Expand All @@ -18,7 +21,11 @@ pub async fn resolve_to_invoice<T: LnUrlHttpClient>(
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());
Expand All @@ -34,13 +41,35 @@ pub async fn resolve_to_invoice<T: LnUrlHttpClient>(
.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> {
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
Expand All @@ -59,8 +88,6 @@ fn extract_description(lnurl_pay_request_response: &PayRequestResponse) -> Resul
}
}

ensure!(!description.is_empty(), "No description found");

Ok(description)
}

Expand All @@ -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<()> {
Expand All @@ -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(())
}

Expand Down Expand Up @@ -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]
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
7 changes: 7 additions & 0 deletions libs/gl-client/src/lnurl/utils.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
Expand All @@ -12,3 +14,8 @@ pub fn parse_lnurl(lnurl: &str) -> Result<String> {
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> {
Invoice::from_str(&invoice_str).map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e)))
}
2 changes: 2 additions & 0 deletions libs/gl-client/src/node/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
2 changes: 2 additions & 0 deletions libs/gl-client/src/scheduler.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use crate::pb::scheduler::scheduler_client::SchedulerClient;
use crate::tls::{self, TlsConfig};

Expand Down

0 comments on commit 6838ad2

Please sign in to comment.