Skip to content

Commit

Permalink
feat: attempt support at fast withdraw
Browse files Browse the repository at this point in the history
  • Loading branch information
lsunsi committed Dec 12, 2023
1 parent bd852eb commit cd2fba8
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 43 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ required-features = ["client", "server"]
name = "lud06"
required-features = ["client", "server"]

[[test]]
name = "lud08"
required-features = ["client", "server"]

[[test]]
name = "lud09"
required-features = ["client", "server"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This library works as a toolkit so you can serve and make your LNURL requests wi
- [LUD-05](https://github.com/lnurl/luds/blob/luds/05.md): 🆘 core 🆘 client 🆘 server 🆘 tests
- [LUD-06](https://github.com/lnurl/luds/blob/luds/06.md): ✅ core ✅ client ✅ server ✅ tests
- [LUD-07](https://github.com/lnurl/luds/blob/luds/07.md): 🆘 core 🆘 client 🆘 server 🆘 tests
- [LUD-08](https://github.com/lnurl/luds/blob/luds/08.md): 🆘 core 🆘 client 🆘 server 🆘 tests
- [LUD-08](https://github.com/lnurl/luds/blob/luds/08.md): core client server ⚠️ tests
- [LUD-09](https://github.com/lnurl/luds/blob/luds/09.md): ✅ core ✅ client ✅ server ✅ tests
- [LUD-10](https://github.com/lnurl/luds/blob/luds/10.md): 🆘 core 🆘 client 🆘 server 🆘 tests
- [LUD-11](https://github.com/lnurl/luds/blob/luds/11.md): ✅ core ✅ client ✅ server ✅ tests
Expand Down
10 changes: 8 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ impl Client {
///
/// Returns errors on network or deserialization failures.
pub async fn query(&self, s: &str) -> Result<Response, &'static str> {
let url = crate::resolve(s)?;

let client = &self.0;

let url = match crate::resolve(s)? {
crate::Resolved::Url(url) => url,
crate::Resolved::Withdraw(_, core) => {
return Ok(Response::Withdraw(Withdraw { client, core }))
}
};

let response = client.get(url).send().await.map_err(|_| "request failed")?;
let bytes = response.bytes().await.map_err(|_| "body failed")?;

Expand Down
110 changes: 81 additions & 29 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,36 @@ pub mod channel;
pub mod pay;
pub mod withdraw;

pub enum Resolved {
Url(url::Url),
Withdraw(url::Url, withdraw::client::Response),
}

/// # Errors
///
/// Returns error in case `s` cannot be understood.
pub fn resolve(s: &str) -> Result<url::Url, &'static str> {
if s.starts_with("lnurl1") || s.starts_with("LNURL1") {
pub fn resolve(s: &str) -> Result<Resolved, &'static str> {
let url = if s.starts_with("lnurl1") || s.starts_with("LNURL1") {
resolve_bech32(s)
} else if s.starts_with("lnurl") || s.starts_with("keyauth") {
resolve_scheme(s)
} else if s.contains('@') {
resolve_address(s)
} else {
Err("unknown")
}
}?;

let tag = url
.query_pairs()
.find_map(|(k, v)| (k == "tag").then_some(v));

Ok(match tag.as_deref() {
Some(withdraw::TAG) => match url.as_str().parse::<withdraw::client::Response>() {
Ok(w) => Resolved::Withdraw(url, w),
Err(_) => Resolved::Url(url),
},
_ => Resolved::Url(url),
})
}

fn resolve_bech32(s: &str) -> Result<url::Url, &'static str> {
Expand Down Expand Up @@ -106,50 +123,85 @@ mod tests {
#[test]
fn resolve_bech32() {
let input = "lnurl1dp68gurn8ghj7argv4ex2tnfwvhkumelwv7hqmm0dc6p3ztw";
assert_eq!(
super::resolve(input).unwrap().to_string(),
"https://there.is/no?s=poon"
);
let super::Resolved::Url(url) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/no?s=poon");

let input = "LNURL1DP68GURN8GHJ7ARGV4EX2TNFWVHKUMELWV7HQMM0DC6P3ZTW";
assert_eq!(
super::resolve(input).unwrap().to_string(),
"https://there.is/no?s=poon"
);
let super::Resolved::Url(url) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/no?s=poon");
}

#[test]
fn resolve_address() {
assert_eq!(
super::resolve("no-spoon@there.is").unwrap().to_string(),
"https://there.is/.well-known/lnurlp/no-spoon"
);
let super::Resolved::Url(url) = super::resolve("no-spoon@there.is").unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/.well-known/lnurlp/no-spoon");
}

#[test]
fn resolve_schemes() {
let input = "lnurlc://there.is/no?s=poon";
assert_eq!(
super::resolve(input).unwrap().to_string(),
"https://there.is/no?s=poon"
);
let super::Resolved::Url(url) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/no?s=poon");

let input = "lnurlw://there.is/no?s=poon";
assert_eq!(
super::resolve(input).unwrap().to_string(),
"https://there.is/no?s=poon"
);
let super::Resolved::Url(url) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/no?s=poon");

let input = "lnurlp://there.is/no?s=poon";
assert_eq!(
super::resolve(input).unwrap().to_string(),
"https://there.is/no?s=poon"
);
let super::Resolved::Url(url) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/no?s=poon");

let input = "keyauth://there.is/no?s=poon";
let super::Resolved::Url(url) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(url.as_str(), "https://there.is/no?s=poon");
}

#[test]
fn resolve_fast_withdraw() {
let input = "lnurlw://there.is/no\
?s=poon\
&tag=withdrawRequest\
&k1=caum\
&minWithdrawable=314\
&maxWithdrawable=315\
&defaultDescription=descrical\
&callback=https://call.back";

let super::Resolved::Withdraw(url, _) = super::resolve(input).unwrap() else {
panic!("expected resolved url");
};

assert_eq!(
super::resolve(input).unwrap().to_string(),
"https://there.is/no?s=poon"
url.as_str(),
"https://there.is/no\
?s=poon\
&tag=withdrawRequest\
&k1=caum\
&minWithdrawable=314\
&maxWithdrawable=315\
&defaultDescription=descrical\
&callback=https://call.back"
);
}
}
38 changes: 37 additions & 1 deletion src/core/withdraw/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ impl TryFrom<&[u8]> for Response {
}
}

impl std::str::FromStr for Response {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let d: de::Response = serde_urlencoded::from_str(s).map_err(|_| "deserialize failed")?;

Ok(Response {
k1: d.k1,
callback: d.callback,
description: d.default_description,
min: d.min_withdrawable,
max: d.max_withdrawable,
})
}
}

impl Response {
#[must_use]
pub fn callback<'a>(&'a self, pr: &'a str) -> CallbackQuery {
Expand Down Expand Up @@ -98,7 +114,7 @@ mod de {
#[cfg(test)]
mod tests {
#[test]
fn response_parse() {
fn response_bytes_parse() {
let input = r#"{
"callback": "https://yuri?o=callback",
"defaultDescription": "verde com bolinhas",
Expand All @@ -116,6 +132,26 @@ mod tests {
assert_eq!(parsed.min, 314);
}

#[test]
fn response_string_parse() {
let input = "lnurlw://there.is/no\
?s=poon\
&tag=withdrawRequest\
&k1=caum\
&minWithdrawable=314\
&maxWithdrawable=315\
&defaultDescription=descricao\
&callback=https://call.back";

let parsed: super::Response = input.parse().expect("parse");

assert_eq!(parsed.callback.to_string(), "https://call.back/");
assert_eq!(parsed.description, "descricao");
assert_eq!(parsed.k1, "caum");
assert_eq!(parsed.min, 314);
assert_eq!(parsed.max, 315);
}

#[test]
fn callback_query_render() {
let input = r#"{
Expand Down
15 changes: 15 additions & 0 deletions src/core/withdraw/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ impl TryFrom<Response> for Vec<u8> {
}
}

impl std::fmt::Display for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = serde_urlencoded::to_string(ser::Response {
tag: super::TAG,
callback: &self.callback,
default_description: &self.description,
min_withdrawable: self.min,
max_withdrawable: self.max,
k1: &self.k1,
})
.map_err(|_| std::fmt::Error)?;
f.write_str(&s)
}
}

pub struct CallbackQuery {
pub k1: String,
pub pr: String,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![cfg_attr(all(doc, docsrs), feature(doc_auto_cfg))]

mod core;
pub use core::{channel, pay, resolve, withdraw, Response};
pub use core::{channel, pay, resolve, withdraw, Resolved, Response};

#[cfg(feature = "client")]
pub mod client;
Expand Down
19 changes: 12 additions & 7 deletions tests/lud01.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ fn test() {
let input = "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";
let decoded = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";

assert_eq!(lnurlkit::resolve(input).unwrap().to_string(), decoded);
assert_eq!(
lnurlkit::resolve(&input.to_lowercase())
.unwrap()
.to_string(),
decoded
);
let lnurlkit::Resolved::Url(url) = lnurlkit::resolve(input).expect("resolve") else {
panic!("wrong resolved");
};

assert_eq!(url.as_str(), decoded);

let lnurlkit::Resolved::Url(url) = lnurlkit::resolve(&input.to_lowercase()).expect("resolve")
else {
panic!("wrong resolved");
};

assert_eq!(url.as_str(), decoded);
}
80 changes: 80 additions & 0 deletions tests/lud08.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#[tokio::test]
async fn test() {
let listener = tokio::net::TcpListener::bind("0.0.0.0:0")
.await
.expect("net");

let addr = listener.local_addr().expect("addr");

let callback = url::Url::parse(&format!("http://{addr}/lnurlw/callback")).expect("url");
let callback2 = url::Url::parse(&format!("http://{addr}/lnurlw/callback")).expect("url");

let w = lnurlkit::withdraw::server::Response {
description: String::from("descricao"),
k1: String::from("caum"),
callback: callback.clone(),
min: 314,
max: 315,
};

let query_url_slow = format!("http://{addr}/lnurlw");
let query_url_fast = format!("{query_url_slow}?{w}");

let router = lnurlkit::Server::default()
.withdraw_request(
move |()| {
let callback = callback.clone();
async move {
Ok(lnurlkit::withdraw::server::Response {
description: String::from("outra-descricao"),
k1: String::from("cadois"),
callback,
min: 123,
max: 321,
})
}
},
|_: lnurlkit::withdraw::server::CallbackQuery| async { unimplemented!() },
)
.build();

tokio::spawn(async move {
axum::serve(listener, router).await.expect("serve");
});

let client = lnurlkit::Client::default();

let lnurl = bech32::encode(
"lnurl",
bech32::ToBase32::to_base32(&query_url_slow),
bech32::Variant::Bech32,
)
.expect("lnurl");

let queried = client.query(&lnurl).await.expect("query");
let lnurlkit::client::Response::Withdraw(wr) = queried else {
panic!("not pay request");
};

assert_eq!(wr.core.min, 123);
assert_eq!(wr.core.max, 321);
assert_eq!(&wr.core.description as &str, "outra-descricao");
assert_eq!(wr.core.callback, callback2);

let lnurl = bech32::encode(
"lnurl",
bech32::ToBase32::to_base32(&query_url_fast),
bech32::Variant::Bech32,
)
.expect("lnurl");

let queried = client.query(&lnurl).await.expect("query");
let lnurlkit::client::Response::Withdraw(wr) = queried else {
panic!("not pay request");
};

assert_eq!(wr.core.min, 314);
assert_eq!(wr.core.max, 315);
assert_eq!(&wr.core.description as &str, "descricao");
assert_eq!(wr.core.callback, callback2);
}
Loading

0 comments on commit cd2fba8

Please sign in to comment.