Skip to content

Commit

Permalink
Moved models.rs to the module root and declared struct methods as asy…
Browse files Browse the repository at this point in the history
…nc to remove the tokio runtime.
  • Loading branch information
Randy808 committed Aug 9, 2023
1 parent 71d7bca commit 90797e6
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 2,493 deletions.
2,480 changes: 178 additions & 2,302 deletions libs/Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions libs/gl-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export = ["chacha20poly1305", "secp256k1"]

[dependencies]
anyhow = "*"
arti-client = {version="0.9.2", features=["onion-service-client"]}
base64 = "^0.21"
bech32 = "0.9.1"
bytes = "1.2.1"
Expand All @@ -21,7 +20,7 @@ lightning-invoice = "0.23.0"
log = "^0.4"
pin-project = "*"
prost = "0.11"
reqwest = {version="0.11.18", features=["blocking", "json"]}
reqwest = {version="0.11.18", features=["json"]}
ring = "*"
rustls-pemfile = "*"
sha256 = "1.1.4"
Expand All @@ -47,6 +46,7 @@ serde_bolt = "0.2"
secp256k1 = { version = "0.26.0", optional = true }
mockall = "0.11.4"
futures = "0.3.28"
async-trait = "0.1.72"

[build-dependencies]
tonic-build = "^0.8"
Expand Down
17 changes: 9 additions & 8 deletions libs/gl-client/src/lnurl/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
mod models;
mod pay;
mod tor_http_client;
mod utils;

use anyhow::{Result};
use anyhow::Result;
use models::LnUrlHttpClearnetClient;
use pay::resolve_to_invoice;
use pay::models::{LnUrlHttpClientImpl};

pub struct LNURL;

impl LNURL {
pub fn resolve_lnurl_to_invoice(&self, lnurl: &str, amount: u64) -> Result<String> {
let http_client = LnUrlHttpClientImpl::new();
resolve_to_invoice(http_client, lnurl, amount)
}
}
pub async fn resolve_lnurl_to_invoice(&self, lnurl: &str, amount: u64) -> Result<String> {
let http_client = LnUrlHttpClearnetClient::new();
resolve_to_invoice(http_client, lnurl, amount).await
}
}
79 changes: 79 additions & 0 deletions libs/gl-client/src/lnurl/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use log::debug;
use mockall::automock;
use reqwest::Response;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct PayRequestResponse {
pub callback: String,
#[serde(rename = "maxSendable")]
pub max_sendable: u64,
#[serde(rename = "minSendable")]
pub min_sendable: u64,
pub tag: String,
pub metadata: String,
}

#[derive(Deserialize)]
pub struct PayRequestCallbackResponse {
pub pr: String,
pub routes: Vec<String>,
}

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

#[async_trait]
#[automock]
pub trait LnUrlHttpClient {
async fn get_pay_request_response(&self, lnurl: &str) -> Result<PayRequestResponse>;
async fn get_pay_request_callback_response(
&self,
callback_url: &str,
amount: u64,
) -> Result<PayRequestCallbackResponse>;
}

pub struct LnUrlHttpClearnetClient {
client: reqwest::Client,
}

impl LnUrlHttpClearnetClient {
pub fn new() -> LnUrlHttpClearnetClient {
LnUrlHttpClearnetClient {
client: reqwest::Client::new(),
}
}

async fn get<T: DeserializeOwned + 'static>(&self, url: &str) -> Result<T> {
let response: Response = self.client.get(url).send().await?;
match response.json::<T>().await {
Ok(body) => Ok(body),
Err(e) => {
debug!("{}", e);
Err(anyhow!("Unable to parse http response body as json"))
}
}
}
}

#[async_trait]
impl LnUrlHttpClient for LnUrlHttpClearnetClient {
async fn get_pay_request_response(&self, lnurl: &str) -> Result<PayRequestResponse> {
self.get::<PayRequestResponse>(lnurl).await
}

async fn get_pay_request_callback_response(
&self,
callback_url: &str,
amount: u64,
) -> Result<PayRequestCallbackResponse> {
self.get::<PayRequestCallbackResponse>(callback_url).await
}
}
92 changes: 47 additions & 45 deletions libs/gl-client/src/lnurl/pay/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
pub mod models;

use super::models;
use super::utils::parse_lnurl;
use crate::lnurl::models::{
LnUrlHttpClient, MockLnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse,
};
use anyhow::{anyhow, ensure, Result};
use bech32::FromBase32;
use futures::future;
use lightning_invoice::{Invoice, InvoiceDescription};
use log::debug;
use models::{
LnUrlHttpClient, MockLnUrlHttpClient, PayRequestCallbackResponse, PayRequestResponse,
};
use reqwest::Url;
use sha256;
use std::str::FromStr;
use std::str::FromStr; // Import the future module

pub fn resolve_to_invoice<T: LnUrlHttpClient>(
pub async fn resolve_to_invoice<T: LnUrlHttpClient>(
http_client: T,
lnurl: &str,
amount: u64,
) -> Result<String> {
let url = decode_and_parse_lnurl(lnurl)?;
let url = parse_lnurl(lnurl)?;

let lnurl_pay_request_response: PayRequestResponse =
http_client.get_pay_request_response(&url)?;
http_client.get_pay_request_response(&url).await?;

validate_pay_request_response(&lnurl_pay_request_response, amount)?;
let description = extract_description(&lnurl_pay_request_response)?;
Expand All @@ -32,8 +32,9 @@ pub fn resolve_to_invoice<T: LnUrlHttpClient>(
);

let callback_url = build_callback_url(&lnurl_pay_request_response, amount)?;
let callback_response: PayRequestCallbackResponse =
http_client.get_pay_request_callback_response(&callback_url, amount)?;
let callback_response: PayRequestCallbackResponse = http_client
.get_pay_request_callback_response(&callback_url, amount)
.await?;

let invoice = parse_invoice(&callback_response.pr)?;
validate_invoice_from_callback_response(&invoice, amount, lnurl_pay_request_response)?;
Expand All @@ -45,18 +46,6 @@ fn parse_invoice(invoice_str: &str) -> Result<Invoice> {
Invoice::from_str(&invoice_str).map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e)))
}

// Function to decode and parse the lnurl into a URL
fn decode_and_parse_lnurl(lnurl: &str) -> Result<String> {
let (_hrp, data, _variant) =
bech32::decode(lnurl).map_err(|e| anyhow!("Failed to decode lnurl: {}", e))?;

let vec = Vec::<u8>::from_base32(&data)
.map_err(|e| anyhow!("Failed to base32 decode data: {}", e))?;

let url = String::from_utf8(vec).map_err(|e| anyhow!("Failed to convert to utf-8: {}", e))?;
Ok(url)
}

// Function to extract the description from the lnurl pay request response
fn extract_description(lnurl_pay_request_response: &PayRequestResponse) -> Result<String> {
let mut description = String::new();
Expand All @@ -83,9 +72,9 @@ fn build_callback_url(
lnurl_pay_request_response: &models::PayRequestResponse,
amount: u64,
) -> Result<String> {

let mut url = Url::parse(&lnurl_pay_request_response.callback)?;
url.query_pairs_mut().append_pair("amount", &amount.to_string());
url.query_pairs_mut()
.append_pair("amount", &amount.to_string());
Ok(url.to_string())
}

Expand Down Expand Up @@ -122,7 +111,7 @@ fn validate_invoice_from_callback_response(
lnurl_pay_request: PayRequestResponse,
) -> Result<()> {
ensure!(invoice.amount_milli_satoshis().unwrap_or_default() == amount ,
format!("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())
"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() {
Expand All @@ -139,8 +128,18 @@ fn validate_invoice_from_callback_response(

#[cfg(test)]
mod tests {
use futures::future::Ready;
use std::pin::Pin;

use super::*;

fn convert_to_async_return_value<T: Send + 'static>(
value: T,
) -> Pin<Box<dyn std::future::Future<Output = T> + Send>> {
let ready_future: Ready<_> = future::ready(value);
Pin::new(Box::new(ready_future)) as Pin<Box<dyn std::future::Future<Output = T> + Send>>
}

#[test]
fn test_parse_invoice() {
let invoice_str = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqclj9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9dha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58aguqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphmsywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0vp62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh38s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5j5r6drg6k6zcqj0fcwg";
Expand All @@ -152,35 +151,36 @@ mod tests {
assert_eq!(invoice.amount_milli_satoshis().unwrap(), 10);
}

#[test]
fn test_lnurl_pay() {
#[tokio::test]
async fn test_lnurl_pay() {
let mut mock_http_client = MockLnUrlHttpClient::new();

mock_http_client.expect_get_pay_request_response().returning(|_url| {
let x: PayRequestResponse = serde_json::from_str("{ \"callback\": \"https://cipherpunk.com/lnurlp/api/v1/lnurl/cb/1\", \"maxSendable\": 100000, \"minSendable\": 100, \"tag\": \"payRequest\", \"metadata\": \"[[\\\"text/plain\\\", \\\"Start the CoinTrain\\\"]]\" }").unwrap();
Ok(x)
convert_to_async_return_value(Ok(x))
});

mock_http_client.expect_get_pay_request_callback_response().returning(|_url, _amount| {
let invoice = "lnbc1u1pjv9qrvsp5e5wwexctzp9yklcrzx448c68q2a7kma55cm67ruajjwfkrswnqvqpp55x6mmz8ch6nahrcuxjsjvs23xkgt8eu748nukq463zhjcjk4s65shp5dd6hc533r655wtyz63jpf6ja08srn6rz6cjhwsjuyckrqwanhjtsxqzjccqpjrzjqw6lfdpjecp4d5t0gxk5khkrzfejjxyxtxg5exqsd95py6rhwwh72rpgrgqq3hcqqgqqqqlgqqqqqqgq9q9qxpqysgq95njz4sz6h7r2qh7txnevcrvg0jdsfpe72cecmjfka8mw5nvm7tydd0j34ps2u9q9h6v5u8h3vxs8jqq5fwehdda6a8qmpn93fm290cquhuc6r";
let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string();
Ok(serde_json::from_str(&callback_response_json).unwrap())
let x = serde_json::from_str(&callback_response_json).unwrap();
convert_to_async_return_value(Ok(x))
});

let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2";
let amount = 100000;

resolve_to_invoice(mock_http_client, lnurl, amount);
let _ = resolve_to_invoice(mock_http_client, lnurl, amount).await;
}

#[test]
fn test_lnurl_pay_returns_error_on_invalid_lnurl() {
#[tokio::test]
async fn test_lnurl_pay_returns_error_on_invalid_lnurl() {
let mock_http_client = MockLnUrlHttpClient::new();

let lnurl = "LNURL1111111111111111111111111111111111111111111111111111111111111111111";
let amount = 100000;

let result = resolve_to_invoice(mock_http_client, lnurl, amount);
let result = resolve_to_invoice(mock_http_client, lnurl, amount).await;

match result {
Err(err) => {
Expand All @@ -192,26 +192,27 @@ mod tests {
}
}

#[test]
fn test_lnurl_pay_returns_error_on_amount_less_than_min_sendable() {
#[tokio::test]
async fn test_lnurl_pay_returns_error_on_amount_less_than_min_sendable() {
let mut mock_http_client = MockLnUrlHttpClient::new();

// Set up expectations for the first two calls
mock_http_client.expect_get_pay_request_response().returning(|_url| {
let x: PayRequestResponse = serde_json::from_str("{ \"callback\": \"https://cipherpunk.com/lnurlp/api/v1/lnurl/cb/1\", \"maxSendable\": 100000, \"minSendable\": 100000, \"tag\": \"payRequest\", \"metadata\": \"[[\\\"text/plain\\\", \\\"Start the CoinTrain\\\"]]\" }").unwrap();
Ok(x)
convert_to_async_return_value(Ok(x))
});

mock_http_client.expect_get_pay_request_callback_response().returning(|_url, _amount| {
let invoice = "lnbc1u1pjv9qrvsp5e5wwexctzp9yklcrzx448c68q2a7kma55cm67ruajjwfkrswnqvqpp55x6mmz8ch6nahrcuxjsjvs23xkgt8eu748nukq463zhjcjk4s65shp5dd6hc533r655wtyz63jpf6ja08srn6rz6cjhwsjuyckrqwanhjtsxqzjccqpjrzjqw6lfdpjecp4d5t0gxk5khkrzfejjxyxtxg5exqsd95py6rhwwh72rpgrgqq3hcqqgqqqqlgqqqqqqgq9q9qxpqysgq95njz4sz6h7r2qh7txnevcrvg0jdsfpe72cecmjfka8mw5nvm7tydd0j34ps2u9q9h6v5u8h3vxs8jqq5fwehdda6a8qmpn93fm290cquhuc6r";
let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string();
Ok(serde_json::from_str(&callback_response_json).unwrap())
let callback_response = serde_json::from_str(&callback_response_json).unwrap();
convert_to_async_return_value(Ok(callback_response))
});

let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2";
let amount = 1;

let result = resolve_to_invoice(mock_http_client, lnurl, amount);
let result = resolve_to_invoice(mock_http_client, lnurl, amount).await;

match result {
Err(err) => {
Expand All @@ -221,25 +222,26 @@ mod tests {
}
}

#[test]
fn test_lnurl_pay_returns_error_on_amount_greater_than_max_sendable() {
#[tokio::test]
async fn test_lnurl_pay_returns_error_on_amount_greater_than_max_sendable() {
let mut mock_http_client = MockLnUrlHttpClient::new();

mock_http_client.expect_get_pay_request_response().returning(|_url| {
let x: PayRequestResponse = serde_json::from_str("{ \"callback\": \"https://cipherpunk.com/lnurlp/api/v1/lnurl/cb/1\", \"maxSendable\": 100000, \"minSendable\": 100000, \"tag\": \"payRequest\", \"metadata\": \"[[\\\"text/plain\\\", \\\"Start the CoinTrain\\\"]]\" }").unwrap();
Ok(x)
convert_to_async_return_value(Ok(x))
});

mock_http_client.expect_get_pay_request_callback_response().returning(|_url, _amount| {
let invoice = "lnbc1u1pjv9qrvsp5e5wwexctzp9yklcrzx448c68q2a7kma55cm67ruajjwfkrswnqvqpp55x6mmz8ch6nahrcuxjsjvs23xkgt8eu748nukq463zhjcjk4s65shp5dd6hc533r655wtyz63jpf6ja08srn6rz6cjhwsjuyckrqwanhjtsxqzjccqpjrzjqw6lfdpjecp4d5t0gxk5khkrzfejjxyxtxg5exqsd95py6rhwwh72rpgrgqq3hcqqgqqqqlgqqqqqqgq9q9qxpqysgq95njz4sz6h7r2qh7txnevcrvg0jdsfpe72cecmjfka8mw5nvm7tydd0j34ps2u9q9h6v5u8h3vxs8jqq5fwehdda6a8qmpn93fm290cquhuc6r";
let callback_response_json = format!("{{\"pr\":\"{}\",\"routes\":[]}}", invoice).to_string();
Ok(serde_json::from_str(&callback_response_json).unwrap())
let value = serde_json::from_str(&callback_response_json).unwrap();
convert_to_async_return_value(Ok(value))
});

let lnurl = "LNURL1DP68GURN8GHJ7CMFWP5X2UNSW4HXKTNRDAKJ7CTSDYHHVVF0D3H82UNV9UCSAXQZE2";
let amount = 1;

let result = resolve_to_invoice(mock_http_client, lnurl, amount);
let result = resolve_to_invoice(mock_http_client, lnurl, amount).await;

match result {
Err(err) => {
Expand Down
Loading

0 comments on commit 90797e6

Please sign in to comment.