Skip to content

Commit

Permalink
Added code that implements the lnurl lud03 and lud08 spec.
Browse files Browse the repository at this point in the history
  • Loading branch information
Randy808 authored and cdecker committed Sep 10, 2023
1 parent 3034d82 commit 9a0a46a
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 7 deletions.
59 changes: 54 additions & 5 deletions libs/gl-client/src/lnurl/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
mod models;
mod pay;
mod utils;
mod withdraw;

use crate::node::Client;
use crate::pb::{PayRequest, Payment};
use crate::pb::{Amount, InvoiceRequest, PayRequest, Payment};
use crate::pb::amount::Unit;
use anyhow::{anyhow, Result};
use models::LnUrlHttpClearnetClient;
use pay::resolve_lnurl_to_invoice;
use pay::{resolve_lnurl_to_invoice, validate_invoice_from_callback_response};
use url::Url;

use self::models::{LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse};
use self::pay::validate_invoice_from_callback_response;
use withdraw::{build_withdraw_request_callback_url, parse_withdraw_request_response_from_url};
use self::models::{
LnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse, WithdrawRequestResponse,
};
use self::utils::{parse_invoice, parse_lnurl};


pub struct LNURL<T: LnUrlHttpClient> {
http_client: T,
}
Expand Down Expand Up @@ -75,4 +79,49 @@ impl<T: LnUrlHttpClient> LNURL<T> {
.await
.map_err(|e| anyhow!(e))
}

pub async fn get_withdraw_request_response(
&self,
lnurl: &str,
) -> Result<WithdrawRequestResponse> {
let url = parse_lnurl(lnurl)?;
let withdrawal_request_response = parse_withdraw_request_response_from_url(&url);

//If it's not a quick withdraw, then get the withdrawal_request_response from the web.
let withdrawal_request_response = match withdrawal_request_response {
Some(w) => w,
None => {
self.http_client
.get_withdrawal_request_response(&url)
.await?
}
};

Ok(withdrawal_request_response)
}

pub async fn withdraw(&self, lnurl: &str, amount_msats: u64, node: &mut Client) -> Result<()> {
let withdraw_request_response = self.get_withdraw_request_response(lnurl).await?;

let amount = Amount {
unit: Some(Unit::Millisatoshi(amount_msats))
};
let invoice = node
.create_invoice(InvoiceRequest {
amount: Some(amount),
description: withdraw_request_response.default_description.clone(),
..Default::default()
})
.await
.map_err(|e| anyhow!(e))?
.into_inner();

let callback_url =
build_withdraw_request_callback_url(&withdraw_request_response, invoice.bolt11)?;

let _ =self.http_client
.send_invoice_for_withdraw_request(&callback_url);

Ok(())
}
}
30 changes: 29 additions & 1 deletion libs/gl-client/src/lnurl/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,29 @@ pub struct PayRequestCallbackResponse {
}

#[derive(Debug, Deserialize, Serialize)]
struct ErrorResponse {
pub struct OkResponse {
status: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ErrorResponse {
status: String,
reason: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct WithdrawRequestResponse {
pub tag: String,
pub callback: String,
pub k1: String,
#[serde(rename = "defaultDescription")]
pub default_description: String,
#[serde(rename = "minWithdrawable")]
pub min_withdrawable: u64,
#[serde(rename = "maxWithdrawable")]
pub max_withdrawable: u64,
}

#[async_trait]
#[automock]
pub trait LnUrlHttpClient {
Expand All @@ -37,6 +55,8 @@ pub trait LnUrlHttpClient {
&self,
callback_url: &str,
) -> Result<PayRequestCallbackResponse>;
async fn get_withdrawal_request_response(&self, url: &str) -> Result<WithdrawRequestResponse>;
async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result<OkResponse>;
}

pub struct LnUrlHttpClearnetClient {
Expand Down Expand Up @@ -74,4 +94,12 @@ impl LnUrlHttpClient for LnUrlHttpClearnetClient {
) -> Result<PayRequestCallbackResponse> {
self.get::<PayRequestCallbackResponse>(callback_url).await
}

async fn get_withdrawal_request_response(&self, url: &str) -> Result<WithdrawRequestResponse> {
self.get::<WithdrawRequestResponse>(url).await
}

async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result<OkResponse>{
self.get::<OkResponse>(url).await
}
}
1 change: 0 additions & 1 deletion libs/gl-client/src/lnurl/pay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use log::debug;
use reqwest::Url;
use sha256;


pub async fn resolve_lnurl_to_invoice<T: LnUrlHttpClient>(
http_client: &T,
lnurl: &str,
Expand Down
115 changes: 115 additions & 0 deletions libs/gl-client/src/lnurl/withdraw/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use super::models::WithdrawRequestResponse;
use anyhow::{anyhow, Result};
use log::debug;
use reqwest::Url;
use serde_json::{to_value, Map, Value};

pub fn build_withdraw_request_callback_url(
lnurl_pay_request_response: &WithdrawRequestResponse,
invoice: String,
) -> Result<String> {
let mut url = Url::parse(&lnurl_pay_request_response.callback)?;
url.query_pairs_mut()
.append_pair("k1", &lnurl_pay_request_response.k1)
.append_pair("pr", &invoice);

Ok(url.to_string())
}

fn convert_value_field_from_str_to_u64(
value: &mut Map<String, Value>,
field_name: &str,
) -> Result<()> {
match value.get(field_name) {
Some(field_value) => match field_value.as_str() {
Some(field_value_str) => {
let converted_field_value = field_value_str.parse::<u64>()?;

//overwrites old type value
value.insert(
String::from(field_name),
to_value(converted_field_value).unwrap(),
);
return Ok(());
}
None => return Err(anyhow!("Failed to convert {} into a str", field_name)),
},
None => return Err(anyhow!("Failed to find {} in map", field_name)),
}
}

pub fn parse_withdraw_request_response_from_url(url: &str) -> Option<WithdrawRequestResponse> {
let url = Url::parse(url).unwrap();
let query_params: Value = url.query_pairs().clone().collect();

if let Some(mut query_params) = query_params.as_object().cloned() {
if convert_value_field_from_str_to_u64(&mut query_params, "minWithdrawable").is_err() {
debug!("minWithdrawable could not be parsed into a number");
return None;
};

if convert_value_field_from_str_to_u64(&mut query_params, "maxWithdrawable").is_err() {
debug!("maxWithdrawable could not be parsed into a number");
return None;
};

match serde_json::from_value(Value::Object(query_params)) {
Ok(w) => {
return w;
},
Err(e) => {
debug!("{:?}", e);
return None;
}
}
}

None
}

mod tests {
use super::*;

#[test]
fn test_build_withdraw_request_callback_url() -> Result<()> {

let k1 = String::from("unique");
let invoice = String::from("invoice");

let built_withdraw_request_callback_url = build_withdraw_request_callback_url(&WithdrawRequestResponse {
tag: String::from("withdraw"),
callback: String::from("https://cipherpunk.com/"),
k1: k1.clone(),
default_description: String::from(""),
min_withdrawable: 2,
max_withdrawable: 300,
}, invoice.clone());

let url = Url::parse(&built_withdraw_request_callback_url.unwrap())?;
let query_pairs = url.query_pairs().collect::<Value>();
let query_params: &Map<String, Value> = query_pairs.as_object().unwrap();

assert_eq!(query_params.get("k1").unwrap().as_str().unwrap(), k1);
assert_eq!(query_params.get("pr").unwrap().as_str().unwrap(), invoice);

Ok(())
}

#[test]
fn test_parse_withdraw_request_response_from_url() {
let withdraw_request = parse_withdraw_request_response_from_url("https://cipherpunk.com?tag=withdraw&callback=cipherpunk.com&k1=42&minWithdrawable=1&maxWithdrawable=100&defaultDescription=");
assert!(withdraw_request.is_some());
}

#[test]
fn test_parse_withdraw_request_response_from_url_fails_when_field_is_missing() {
let withdraw_request = parse_withdraw_request_response_from_url("https://cipherpunk.com?tag=withdraw&callback=cipherpunk.com&k1=42&minWithdrawable=1&maxWithdrawable=100");
assert!(withdraw_request.is_none());
}

#[test]
fn test_parse_withdraw_request_response_from_url_fails_when_min_withdrawable_is_wrong_type() {
let withdraw_request = parse_withdraw_request_response_from_url("https://cipherpunk.com?tag=withdraw&callback=cipherpunk.com&k1=42&minWithdrawable=one&maxWithdrawable=100&defaultDescription=");
assert!(withdraw_request.is_none());
}
}

0 comments on commit 9a0a46a

Please sign in to comment.