diff --git a/src/client.rs b/src/client.rs index 9cef789..596eff7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -32,19 +32,19 @@ pub enum Query<'a> { #[derive(Clone, Debug)] pub struct Channel<'a> { client: &'a reqwest::Client, - pub core: crate::channel::Query, + pub core: crate::channel::client::Query, } #[derive(Clone, Debug)] pub struct Pay<'a> { client: &'a reqwest::Client, - pub core: crate::pay::Query, + pub core: crate::pay::client::Query, } #[derive(Clone, Debug)] pub struct Withdraw<'a> { client: &'a reqwest::Client, - pub core: crate::withdraw::Query, + pub core: crate::withdraw::client::Query, } impl Channel<'_> { @@ -52,15 +52,15 @@ impl Channel<'_> { /// /// Returns errors on network or deserialization failures. pub async fn callback_accept( - self, - remoteid: String, + &self, + remoteid: &str, private: bool, - ) -> Result { - let callback = self.core.callback_accept(remoteid, private).url(); + ) -> Result { + let callback = self.core.callback_accept(remoteid, private); let response = self .client - .get(callback) + .get(callback.to_string()) .send() .await .map_err(|_| "request failed")?; @@ -73,14 +73,14 @@ impl Channel<'_> { /// /// Returns errors on network or deserialization failures. pub async fn callback_cancel( - self, - remoteid: String, - ) -> Result { - let callback = self.core.callback_cancel(remoteid).url(); + &self, + remoteid: &str, + ) -> Result { + let callback = self.core.callback_cancel(remoteid); let response = self .client - .get(callback) + .get(callback.to_string()) .send() .await .map_err(|_| "request failed")?; @@ -95,15 +95,15 @@ impl Pay<'_> { /// /// Returns errors on network or deserialization failures. pub async fn callback( - self, + &self, millisatoshis: u64, - comment: String, - ) -> Result { - let callback = self.core.callback(millisatoshis, comment).url(); + comment: &str, + ) -> Result { + let callback = self.core.callback(millisatoshis, comment); let response = self .client - .get(callback) + .get(callback.to_string()) .send() .await .map_err(|_| "request failed")?; @@ -118,14 +118,14 @@ impl Withdraw<'_> { /// /// Returns errors on network or deserialization failures. pub async fn callback( - self, - pr: String, - ) -> Result { - let callback = self.core.callback(pr).url(); + &self, + pr: &str, + ) -> Result { + let callback = self.core.callback(pr); let response = self .client - .get(callback) + .get(callback.to_string()) .send() .await .map_err(|_| "request failed")?; diff --git a/src/core.rs b/src/core.rs index a67c8f7..bbce398 100644 --- a/src/core.rs +++ b/src/core.rs @@ -70,9 +70,9 @@ fn resolve_address(s: &str) -> Result { #[derive(Debug)] pub enum Query { - Channel(channel::Query), - Pay(pay::Query), - Withdraw(withdraw::Query), + Channel(channel::client::Query), + Pay(pay::client::Query), + Withdraw(withdraw::client::Query), } impl std::str::FromStr for Query { diff --git a/src/core/channel.rs b/src/core/channel.rs index fff4050..ebf2815 100644 --- a/src/core/channel.rs +++ b/src/core/channel.rs @@ -1,280 +1,3 @@ pub const TAG: &str = "channelRequest"; - -#[derive(Clone, Debug)] -pub struct Query { - pub callback: url::Url, - pub uri: String, - pub k1: String, -} - -impl std::str::FromStr for Query { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let d: de::Query = miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; - - Ok(Query { - callback: d.callback.0.into_owned(), - uri: d.uri, - k1: d.k1, - }) - } -} - -impl std::fmt::Display for Query { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&miniserde::json::to_string(&ser::Query { - tag: TAG, - callback: crate::serde::Url(std::borrow::Cow::Borrowed(&self.callback)), - uri: &self.uri, - k1: &self.k1, - })) - } -} - -impl Query { - #[must_use] - pub fn callback_accept(self, remoteid: String, private: bool) -> CallbackRequest { - CallbackRequest::Accept { - url: self.callback, - k1: self.k1, - remoteid, - private, - } - } - - #[must_use] - pub fn callback_cancel(self, remoteid: String) -> CallbackRequest { - CallbackRequest::Cancel { - url: self.callback, - k1: self.k1, - remoteid, - } - } -} - -pub enum CallbackRequest { - Cancel { - url: url::Url, - remoteid: String, - k1: String, - }, - Accept { - url: url::Url, - remoteid: String, - private: bool, - k1: String, - }, -} - -impl CallbackRequest { - #[must_use] - pub fn url(self) -> url::Url { - match self { - CallbackRequest::Cancel { - mut url, - remoteid, - k1, - } => { - let query = [ - ("k1", k1), - ("remoteid", remoteid), - ("cancel", String::from("1")), - ]; - - url.query_pairs_mut().extend_pairs(query); - url - } - CallbackRequest::Accept { - mut url, - remoteid, - private, - k1, - } => { - let query = [ - ("k1", k1), - ("remoteid", remoteid), - ("private", String::from(if private { "1" } else { "0" })), - ]; - - url.query_pairs_mut().extend_pairs(query); - url - } - } - } -} - -#[derive(Debug)] -pub enum CallbackResponse { - Error(String), - Ok, -} - -impl std::str::FromStr for CallbackResponse { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let map = miniserde::json::from_str::>(s) - .map_err(|_| "bad json")?; - - match map.get("status").map(|s| s as &str) { - Some("OK") => Ok(CallbackResponse::Ok), - Some("ERROR") => Ok(CallbackResponse::Error( - map.get("reason").cloned().unwrap_or_default(), - )), - _ => Err("bad status field"), - } - } -} - -impl std::fmt::Display for CallbackResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut map = std::collections::BTreeMap::new(); - - match self { - CallbackResponse::Error(reason) => { - map.insert("status", "ERROR"); - map.insert("reason", reason); - } - CallbackResponse::Ok => { - map.insert("status", "OK"); - } - } - - f.write_str(&miniserde::json::to_string(&map)) - } -} - -mod ser { - use crate::serde::Url; - use miniserde::Serialize; - - #[derive(Serialize)] - pub(super) struct Query<'a> { - pub tag: &'static str, - pub callback: Url<'a>, - pub uri: &'a str, - pub k1: &'a str, - } -} - -mod de { - use crate::serde::Url; - use miniserde::Deserialize; - - #[derive(Deserialize)] - pub(super) struct Query { - pub callback: Url<'static>, - pub uri: String, - pub k1: String, - } -} - -#[cfg(test)] -mod tests { - use std::assert_eq; - - #[test] - fn query_parse() { - let input = r#" - { - "uri": "noh@ipe:porta", - "callback": "https://yuri?o=callback", - "k1": "caum" - } - "#; - - let parsed = input.parse::().expect("parse"); - - assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); - assert_eq!(parsed.uri, "noh@ipe:porta"); - assert_eq!(parsed.k1, "caum"); - } - - #[test] - fn query_render() { - let query = super::Query { - callback: url::Url::parse("https://yuri/?o=callback").expect("url"), - uri: String::from("noh@ipe:porta"), - k1: String::from("caum"), - }; - - let json = r#"{"tag":"channelRequest","callback":"https://yuri/?o=callback","uri":"noh@ipe:porta","k1":"caum"}"#; - assert_eq!(query.to_string(), json); - } - - #[test] - fn callback_accept() { - let input = r#" - { - "uri": "noh@ipe:porta", - "callback": "https://yuri?o=callback", - "k1": "caum" - } - "#; - - let parsed = input.parse::().expect("parse"); - let url = parsed - .clone() - .callback_accept(String::from("idremoto"), true) - .url(); - - assert_eq!( - url.to_string(), - "https://yuri/?o=callback&k1=caum&remoteid=idremoto&private=1" - ); - - let url = parsed - .callback_accept(String::from("idremoto"), false) - .url(); - - assert_eq!( - url.to_string(), - "https://yuri/?o=callback&k1=caum&remoteid=idremoto&private=0" - ); - } - - #[test] - fn callback_cancel() { - let input = r#" - { - "uri": "noh@ipe:porta", - "callback": "https://yuri?o=callback", - "k1": "caum" - } - "#; - - let parsed = input.parse::().expect("parse"); - let url = parsed.callback_cancel(String::from("idremoto")).url(); - - assert_eq!( - url.to_string(), - "https://yuri/?o=callback&k1=caum&remoteid=idremoto&cancel=1" - ); - } - - #[test] - fn callback_response_parse() { - assert!(matches!( - r#"{ "status": "OK"}"#.parse().unwrap(), - super::CallbackResponse::Ok - )); - - assert!(matches!( - r#"{ "status": "ERROR", "reason": "razao" }"#.parse().unwrap(), - super::CallbackResponse::Error(r) if r == "razao" - )); - } - - #[test] - fn callback_response_render() { - assert_eq!( - super::CallbackResponse::Ok.to_string(), - r#"{"status":"OK"}"# - ); - assert_eq!( - super::CallbackResponse::Error(String::from("razao")).to_string(), - r#"{"reason":"razao","status":"ERROR"}"# - ); - } -} +pub mod client; +pub mod server; diff --git a/src/core/channel/client.rs b/src/core/channel/client.rs new file mode 100644 index 0000000..eec8bd2 --- /dev/null +++ b/src/core/channel/client.rs @@ -0,0 +1,193 @@ +#[derive(Clone, Debug)] +pub struct Query { + callback: url::Url, + pub uri: Box, + k1: Box, +} + +impl std::str::FromStr for Query { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let d: de::Query = miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; + + Ok(Query { + callback: d.callback.0.into_owned(), + uri: d.uri.into_boxed_str(), + k1: d.k1.into_boxed_str(), + }) + } +} + +impl Query { + #[must_use] + pub fn callback_accept<'a>(&'a self, remoteid: &'a str, private: bool) -> CallbackRequest<'a> { + CallbackRequest::Accept { + url: &self.callback, + k1: &self.k1, + remoteid, + private, + } + } + + #[must_use] + pub fn callback_cancel<'a>(&'a self, remoteid: &'a str) -> CallbackRequest<'a> { + CallbackRequest::Cancel { + url: &self.callback, + k1: &self.k1, + remoteid, + } + } +} + +#[derive(Clone, Debug)] +pub enum CallbackRequest<'a> { + Accept { + url: &'a url::Url, + remoteid: &'a str, + k1: &'a str, + private: bool, + }, + Cancel { + url: &'a url::Url, + remoteid: &'a str, + k1: &'a str, + }, +} + +impl std::fmt::Display for CallbackRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CallbackRequest::Cancel { url, remoteid, k1 } => { + let mut url = (*url).clone(); + let query = [("k1", *k1), ("remoteid", remoteid), ("cancel", "1")]; + url.query_pairs_mut().extend_pairs(query); + f.write_str(url.as_str()) + } + CallbackRequest::Accept { + url, + remoteid, + private, + k1, + } => { + let query = [ + ("k1", *k1), + ("remoteid", remoteid), + ("private", if *private { "1" } else { "0" }), + ]; + + let mut url = (*url).clone(); + url.query_pairs_mut().extend_pairs(query); + f.write_str(url.as_str()) + } + } + } +} + +#[derive(Clone, Debug)] +pub enum CallbackResponse { + Error { reason: Box }, + Ok, +} + +impl std::str::FromStr for CallbackResponse { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let map = miniserde::json::from_str::>(s) + .map_err(|_| "bad json")?; + + match map.get("status").map(|s| s as &str) { + Some("OK") => Ok(CallbackResponse::Ok), + Some("ERROR") => { + let reason = (map.get("reason").ok_or("error without reason")? as &str).into(); + Ok(CallbackResponse::Error { reason }) + } + _ => Err("bad status field"), + } + } +} + +mod de { + use crate::serde::Url; + use miniserde::Deserialize; + + #[derive(Deserialize)] + pub(super) struct Query { + pub callback: Url<'static>, + pub uri: String, + pub k1: String, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn query_parse() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "uri": "noh@ipe:porta", + "k1": "caum" + }"#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.callback.as_str(), "https://yuri/?o=callback"); + assert_eq!(&parsed.uri as &str, "noh@ipe:porta"); + assert_eq!(&parsed.k1 as &str, "caum"); + } + + #[test] + fn callback_request_accept_render() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "uri": "noh@ipe:porta", + "k1": "caum" + }"#; + + let parsed = input.parse::().expect("parse"); + let url = parsed.callback_accept("idremoto", true); + + assert_eq!( + url.to_string(), + "https://yuri/?o=callback&k1=caum&remoteid=idremoto&private=1" + ); + + let url = parsed.callback_accept("idremoto", false); + + assert_eq!( + url.to_string(), + "https://yuri/?o=callback&k1=caum&remoteid=idremoto&private=0" + ); + } + + #[test] + fn callback_request_cancel_render() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "uri": "noh@ipe:porta", + "k1": "caum" + }"#; + + let parsed = input.parse::().expect("parse"); + let url = parsed.callback_cancel("idremoto"); + + assert_eq!( + url.to_string(), + "https://yuri/?o=callback&k1=caum&remoteid=idremoto&cancel=1" + ); + } + + #[test] + fn callback_response_parse() { + assert!(matches!( + r#"{ "status": "OK" }"#.parse().unwrap(), + super::CallbackResponse::Ok + )); + + assert!(matches!( + r#"{ "status": "ERROR", "reason": "razao" }"#.parse().unwrap(), + super::CallbackResponse::Error { reason } if &reason as &str == "razao" + )); + } +} diff --git a/src/core/channel/server.rs b/src/core/channel/server.rs new file mode 100644 index 0000000..5c8052b --- /dev/null +++ b/src/core/channel/server.rs @@ -0,0 +1,190 @@ +#[derive(Clone, Debug)] +pub struct Query { + pub callback: url::Url, + pub uri: String, + pub k1: String, +} + +impl std::fmt::Display for Query { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&miniserde::json::to_string(&ser::Query { + tag: super::TAG, + callback: crate::serde::Url(std::borrow::Cow::Borrowed(&self.callback)), + uri: &self.uri, + k1: &self.k1, + })) + } +} + +#[derive(Clone, Debug)] +pub enum CallbackRequest { + Accept { + remoteid: Box, + k1: Box, + private: bool, + }, + Cancel { + remoteid: Box, + k1: Box, + }, +} + +impl std::str::FromStr for CallbackRequest { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let qs = s + .split('&') + .filter_map(|s| s.split_once('=')) + .collect::>(); + + let k1 = qs.get("k1").ok_or("missing k1")?; + let remoteid = qs.get("remoteid").ok_or("missing remoteid")?; + + if qs.get("cancel").copied() == Some("1") { + Some(CallbackRequest::Cancel { + remoteid: (*remoteid).into(), + k1: (*k1).into(), + }) + } else { + match qs.get("private").copied() { + Some("0") => Some(CallbackRequest::Accept { + remoteid: (*remoteid).into(), + k1: (*k1).into(), + private: false, + }), + Some("1") => Some(CallbackRequest::Accept { + remoteid: (*remoteid).into(), + k1: (*k1).into(), + private: true, + }), + _ => None, + } + } + .ok_or("missing cancel/private") + } +} + +#[derive(Clone, Debug)] +pub enum CallbackResponse { + Error { reason: String }, + Ok, +} + +impl std::fmt::Display for CallbackResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut map = std::collections::BTreeMap::new(); + + match self { + CallbackResponse::Error { reason } => { + map.insert("status", "ERROR"); + map.insert("reason", reason); + } + CallbackResponse::Ok => { + map.insert("status", "OK"); + } + } + + f.write_str(&miniserde::json::to_string(&map)) + } +} + +mod ser { + use crate::serde::Url; + use miniserde::Serialize; + + #[derive(Serialize)] + pub(super) struct Query<'a> { + pub tag: &'static str, + pub callback: Url<'a>, + pub uri: &'a str, + pub k1: &'a str, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn query_render() { + let query = super::Query { + callback: url::Url::parse("https://yuri/?o=callback").expect("url"), + uri: String::from("noh@ipe:porta"), + k1: String::from("caum"), + }; + + let json = r#"{"tag":"channelRequest","callback":"https://yuri/?o=callback","uri":"noh@ipe:porta","k1":"caum"}"#; + assert_eq!(query.to_string(), json); + } + + #[test] + fn callback_accept() { + let input = "remoteid=idremoto&k1=caum&private=1"; + let parsed = input.parse::().expect("parse"); + + let super::CallbackRequest::Accept { + remoteid, + private, + k1, + } = parsed + else { + panic!("wrong parsed"); + }; + + assert_eq!(&remoteid as &str, "idremoto"); + assert_eq!(&k1 as &str, "caum"); + assert!(private); + + let input = "remoteid=idremoto&k1=caum&private=0"; + let parsed = input.parse::().expect("parse"); + + let super::CallbackRequest::Accept { + remoteid, + private, + k1, + } = parsed + else { + panic!("wrong parsed"); + }; + + assert_eq!(&remoteid as &str, "idremoto"); + assert_eq!(&k1 as &str, "caum"); + assert!(!private); + + let input = "remoteid=idremoto&k1=caum&private=2"; + let parsed = input.parse::(); + assert!(parsed.is_err()); + } + + #[test] + fn callback_request_cancel_parse() { + let input = "remoteid=idremoto&k1=caum&cancel=1"; + let parsed = input.parse::().expect("parse"); + + let super::CallbackRequest::Cancel { remoteid, k1 } = parsed else { + panic!("wrong parsed"); + }; + + assert_eq!(&remoteid as &str, "idremoto"); + assert_eq!(&k1 as &str, "caum"); + + let input = "remoteid=idremoto&k1=caum&cancel=0"; + let parsed = input.parse::(); + assert!(parsed.is_err()); + } + + #[test] + fn callback_response_render() { + assert_eq!( + super::CallbackResponse::Ok.to_string(), + r#"{"status":"OK"}"# + ); + + assert_eq!( + super::CallbackResponse::Error { + reason: String::from("razao") + } + .to_string(), + r#"{"reason":"razao","status":"ERROR"}"# + ); + } +} diff --git a/src/core/pay.rs b/src/core/pay.rs index 677f7dc..84d7607 100644 --- a/src/core/pay.rs +++ b/src/core/pay.rs @@ -1,615 +1,3 @@ pub const TAG: &str = "payRequest"; - -#[derive(Clone, Debug)] -pub struct Query { - pub callback: url::Url, - pub short_description: String, - pub long_description: Option, - pub identifier: Option, - pub email: Option, - pub jpeg: Option>, - pub png: Option>, - pub comment_size: Option, - pub min: u64, - pub max: u64, - pub metadata_raw: Option, -} - -impl std::str::FromStr for Query { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - use base64::{prelude::BASE64_STANDARD, Engine}; - use miniserde::json::Value; - - let p: de::Query = miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; - - let metadata = miniserde::json::from_str::>(&p.metadata) - .map_err(|_| "deserialize metadata failed")?; - - let short_description = metadata - .iter() - .find_map(|(k, v)| (k == "text/plain").then_some(v)) - .and_then(|v| match v { - Value::String(s) => Some(String::from(s)), - _ => None, - }) - .ok_or("short description failed")?; - - let long_description = metadata - .iter() - .find_map(|(k, v)| (k == "text/long-desc").then_some(v)) - .and_then(|v| match v { - Value::String(s) => Some(String::from(s)), - _ => None, - }); - - let jpeg = metadata - .iter() - .find_map(|(k, v)| (k == "image/jpeg;base64").then_some(v)) - .and_then(|v| match v { - Value::String(s) => BASE64_STANDARD.decode(s).ok(), - _ => None, - }); - - let png = metadata - .iter() - .find_map(|(k, v)| (k == "image/png;base64").then_some(v)) - .and_then(|v| match v { - Value::String(s) => BASE64_STANDARD.decode(s).ok(), - _ => None, - }); - - let identifier = metadata - .iter() - .find_map(|(k, v)| (k == "text/identifier").then_some(v)) - .and_then(|v| match v { - Value::String(s) => Some(String::from(s)), - _ => None, - }); - - let email = metadata - .iter() - .find_map(|(k, v)| (k == "text/email").then_some(v)) - .and_then(|v| match v { - Value::String(s) => Some(String::from(s)), - _ => None, - }); - - Ok(Query { - metadata_raw: Some(p.metadata), - callback: p.callback.0.into_owned(), - comment_size: p.comment_allowed, - min: p.min_sendable, - max: p.max_sendable, - short_description, - long_description, - identifier, - email, - jpeg, - png, - }) - } -} - -impl std::fmt::Display for Query { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use base64::{prelude::BASE64_STANDARD, Engine}; - - let metadata = miniserde::json::to_string( - &[ - Some(("text/plain", self.short_description.clone())), - self.long_description - .as_ref() - .map(|s| ("text/long-desc", s.clone())), - self.jpeg - .as_ref() - .map(|s| ("image/jpeg;base64", BASE64_STANDARD.encode(s))), - self.png - .as_ref() - .map(|s| ("image/png;base64", BASE64_STANDARD.encode(s))), - self.identifier - .as_ref() - .map(|s| ("text/identifier", s.clone())), - self.email.as_ref().map(|s| ("text/email", s.clone())), - ] - .into_iter() - .flatten() - .collect::>(), - ); - - f.write_str(&miniserde::json::to_string(&ser::Query { - tag: TAG, - metadata, - callback: crate::serde::Url(std::borrow::Cow::Borrowed(&self.callback)), - min_sendable: self.min, - max_sendable: self.max, - comment_allowed: self.comment_size.unwrap_or(0), - })) - } -} - -impl Query { - #[must_use] - pub fn callback(self, millisatoshis: u64, comment: String) -> CallbackRequest { - CallbackRequest { - url: self.callback, - millisatoshis, - comment, - } - } -} - -pub struct CallbackRequest { - pub url: url::Url, - pub comment: String, - pub millisatoshis: u64, -} - -impl CallbackRequest { - #[must_use] - pub fn url(mut self) -> url::Url { - let query = [ - (!self.comment.is_empty()).then_some(("comment", self.comment)), - Some(("amount", self.millisatoshis.to_string())), - ]; - - self.url - .query_pairs_mut() - .extend_pairs(query.into_iter().flatten()); - - self.url - } -} - -#[derive(Debug)] -pub struct CallbackResponse { - pub pr: String, - pub disposable: bool, - pub success_action: Option, -} - -#[derive(Clone, Debug)] -pub enum SuccessAction { - Url(url::Url, String), - Message(String), -} - -impl std::str::FromStr for CallbackResponse { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let a: de::CallbackResponse = - miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; - - let success_action = a - .success_action - .and_then(|sa| match sa.get("tag")? as &str { - "message" => Some(SuccessAction::Message(sa.get("message")?.to_owned())), - "url" => { - let url = url::Url::parse(sa.get("url")?).ok()?; - Some(SuccessAction::Url(url, sa.get("description")?.to_owned())) - } - _ => None, - }); - - Ok(Self { - pr: a.pr, - disposable: a.disposable.unwrap_or(true), - success_action, - }) - } -} - -impl std::fmt::Display for CallbackResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let success_action = self.success_action.as_ref().map(|sa| { - let mut map = std::collections::BTreeMap::new(); - - match sa { - SuccessAction::Message(m) => { - map.insert("tag", "message".into()); - map.insert("message", m.into()); - } - SuccessAction::Url(u, d) => { - map.insert("tag", "url".into()); - map.insert("description", d.into()); - map.insert("url", u.to_string().into()); - } - } - - map - }); - - let cr = ser::CallbackResponse { - success_action, - disposable: self.disposable, - pr: &self.pr, - }; - - f.write_str(&miniserde::json::to_string(&cr)) - } -} - -mod ser { - use crate::serde::Url; - use miniserde::Serialize; - use std::collections::BTreeMap; - - #[derive(Serialize)] - pub(super) struct Query<'a> { - pub tag: &'static str, - pub metadata: String, - pub callback: Url<'a>, - #[serde(rename = "minSendable")] - pub min_sendable: u64, - #[serde(rename = "maxSendable")] - pub max_sendable: u64, - #[serde(rename = "commentAllowed")] - pub comment_allowed: u64, - } - - #[derive(Serialize)] - pub(super) struct CallbackResponse<'a> { - pub pr: &'a str, - pub disposable: bool, - #[serde(rename = "successAction")] - pub success_action: Option>>, - } -} - -mod de { - use crate::serde::Url; - use miniserde::Deserialize; - use std::collections::BTreeMap; - - #[derive(Deserialize)] - pub(super) struct Query { - pub metadata: String, - pub callback: Url<'static>, - #[serde(rename = "minSendable")] - pub min_sendable: u64, - #[serde(rename = "maxSendable")] - pub max_sendable: u64, - #[serde(rename = "commentAllowed")] - pub comment_allowed: Option, - } - - #[derive(Deserialize)] - pub(super) struct CallbackResponse { - pub pr: String, - pub disposable: Option, - #[serde(rename = "successAction")] - pub success_action: Option>, - } -} - -#[cfg(test)] -mod tests { - #[test] - fn query_parse_base() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/crazy\", \"👋🇧🇴💾\"]]", - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - - assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); - assert_eq!(parsed.short_description, "boneco do steve magal"); - assert_eq!( - parsed.metadata_raw.unwrap(), - "[[\"text/plain\", \"boneco do steve magal\"],[\"text/crazy\", \"👋🇧🇴💾\"]]" - ); - assert_eq!(parsed.min, 314); - assert_eq!(parsed.max, 315); - - assert!(parsed.comment_size.is_none()); - assert!(parsed.long_description.is_none()); - assert!(parsed.jpeg.is_none()); - assert!(parsed.png.is_none()); - assert!(parsed.identifier.is_none()); - assert!(parsed.email.is_none()); - } - - #[test] - fn query_parse_comment_size() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", - "commentAllowed": 140, - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - assert_eq!(parsed.comment_size.unwrap(), 140); - } - - #[test] - fn query_parse_long_description() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/long-desc\", \"mochila a jato brutal incluida\"]]", - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - assert_eq!( - parsed.long_description.unwrap(), - "mochila a jato brutal incluida" - ); - } - - #[test] - fn query_parse_images() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"image/png;base64\", \"Zm90b2JydXRhbA==\"],[\"image/jpeg;base64\", \"aW1hZ2VtYnJ1dGFs\"]]", - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - assert_eq!(parsed.jpeg.unwrap(), b"imagembrutal"); - assert_eq!(parsed.png.unwrap(), b"fotobrutal"); - } - - #[test] - fn query_parse_identifier() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/identifier\", \"steve@magal.brutal\"]]", - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - assert_eq!(parsed.identifier.unwrap(), "steve@magal.brutal"); - } - - #[test] - fn query_parse_email() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/email\", \"steve@magal.brutal\"]]", - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - assert_eq!(parsed.email.unwrap(), "steve@magal.brutal"); - } - - #[test] - fn query_render_base() { - let query = super::Query { - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - short_description: String::from("boneco do steve magal"), - long_description: None, - jpeg: None, - png: None, - comment_size: None, - min: 314, - max: 315, - identifier: None, - email: None, - metadata_raw: None, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# - ); - } - - #[test] - fn query_render_comment_size() { - let query = super::Query { - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - short_description: String::from("boneco do steve magal"), - long_description: None, - jpeg: None, - png: None, - comment_size: Some(140), - min: 314, - max: 315, - identifier: None, - email: None, - metadata_raw: None, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":140}"# - ); - } - - #[test] - fn query_render_long_description() { - let query = super::Query { - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - short_description: String::from("boneco do steve magal"), - long_description: Some(String::from("mochila a jato brutal incluida")), - jpeg: None, - png: None, - comment_size: None, - min: 314, - max: 315, - identifier: None, - email: None, - metadata_raw: None, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"text/long-desc\",\"mochila a jato brutal incluida\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# - ); - } - - #[test] - fn query_render_images() { - let query = super::Query { - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - short_description: String::from("boneco do steve magal"), - long_description: None, - jpeg: Some(b"imagembrutal".to_vec()), - png: Some(b"fotobrutal".to_vec()), - comment_size: None, - min: 314, - max: 315, - identifier: None, - email: None, - metadata_raw: None, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"image/jpeg;base64\",\"aW1hZ2VtYnJ1dGFs\"],[\"image/png;base64\",\"Zm90b2JydXRhbA==\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# - ); - } - - #[test] - fn query_render_identifier() { - let query = super::Query { - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - short_description: String::from("boneco do steve magal"), - long_description: None, - jpeg: Some(b"imagembrutal".to_vec()), - png: Some(b"fotobrutal".to_vec()), - comment_size: None, - min: 314, - max: 315, - identifier: Some(String::from("steve@magal.brutal")), - email: None, - metadata_raw: None, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"image/jpeg;base64\",\"aW1hZ2VtYnJ1dGFs\"],[\"image/png;base64\",\"Zm90b2JydXRhbA==\"],[\"text/identifier\",\"steve@magal.brutal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# - ); - } - - #[test] - fn query_render_email() { - let query = super::Query { - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - short_description: String::from("boneco do steve magal"), - long_description: None, - jpeg: Some(b"imagembrutal".to_vec()), - png: Some(b"fotobrutal".to_vec()), - comment_size: None, - min: 314, - max: 315, - identifier: None, - email: Some(String::from("steve@magal.brutal")), - metadata_raw: None, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"image/jpeg;base64\",\"aW1hZ2VtYnJ1dGFs\"],[\"image/png;base64\",\"Zm90b2JydXRhbA==\"],[\"text/email\",\"steve@magal.brutal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# - ); - } - - #[test] - fn callback() { - let input = r#" - { - "callback": "https://yuri?o=callback", - "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", - "maxSendable": 315, - "minSendable": 314 - } - "#; - - let parsed = input.parse::().expect("parse"); - - assert_eq!( - parsed - .clone() - .callback(314, String::from("comentario")) - .url() - .to_string(), - "https://yuri/?o=callback&comment=comentario&amount=314" - ); - - assert_eq!( - parsed.callback(314, String::new()).url().to_string(), - "https://yuri/?o=callback&amount=314" - ); - } - - #[test] - fn callback_response_parse_base() { - let input = r#" - { "pr": "pierre" } - "#; - - let parsed = input.parse::().expect("parse"); - assert!(parsed.success_action.is_none()); - assert_eq!(parsed.pr, "pierre"); - assert!(parsed.disposable); - } - - #[test] - fn callback_response_parse_disposable() { - let input = r#" - { "pr": "", "disposable": true } - "#; - - let parsed = input.parse::().expect("parse"); - assert!(parsed.disposable); - - let input = r#" - { "pr": "", "disposable": false } - "#; - - let parsed = input.parse::().expect("parse"); - assert!(!parsed.disposable); - } - - #[test] - fn callback_response_parse_success_actions() { - let input = r#" - { "pr": "", "successAction": { "tag": "message", "message": "obrigado!" } } - "#; - - let parsed = input.parse::().expect("parse"); - - let Some(super::SuccessAction::Message(m)) = parsed.success_action else { - panic!("bad success action"); - }; - - assert_eq!(m, "obrigado!"); - - let input = r#" - { "pr": "", "successAction": { "tag": "url", "description": "valeu demais", "url": "http://eh.nois" } } - "#; - - let parsed = input.parse::().expect("parse"); - - let Some(super::SuccessAction::Url(u, d)) = parsed.success_action else { - panic!("bad success action"); - }; - - assert_eq!(u.to_string(), "http://eh.nois/"); - assert_eq!(d, "valeu demais"); - } -} +pub mod client; +pub mod server; diff --git a/src/core/pay/client.rs b/src/core/pay/client.rs new file mode 100644 index 0000000..0ff48e0 --- /dev/null +++ b/src/core/pay/client.rs @@ -0,0 +1,379 @@ +#[derive(Clone, Debug)] +pub struct Query { + callback: url::Url, + pub metadata_raw: Box, + pub short_description: String, + pub long_description: Option>, + pub identifier: Option>, + pub email: Option>, + pub jpeg: Option>, + pub png: Option>, + pub comment_size: Option, + pub min: u64, + pub max: u64, +} + +impl std::str::FromStr for Query { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + use base64::{prelude::BASE64_STANDARD, Engine}; + use miniserde::json::Value; + + let p: de::Query = miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; + + let metadata = miniserde::json::from_str::>(&p.metadata) + .map_err(|_| "deserialize metadata failed")?; + + let short_description = metadata + .iter() + .find_map(|(k, v)| (k == "text/plain").then_some(v)) + .and_then(|v| match v { + Value::String(s) => Some(String::from(s)), + _ => None, + }) + .ok_or("short description failed")?; + + let long_description = metadata + .iter() + .find_map(|(k, v)| (k == "text/long-desc").then_some(v)) + .and_then(|v| match v { + Value::String(s) => Some(String::from(s)), + _ => None, + }) + .map(String::into_boxed_str); + + let jpeg = metadata + .iter() + .find_map(|(k, v)| (k == "image/jpeg;base64").then_some(v)) + .and_then(|v| match v { + Value::String(s) => BASE64_STANDARD.decode(s).ok(), + _ => None, + }) + .map(Vec::into_boxed_slice); + + let png = metadata + .iter() + .find_map(|(k, v)| (k == "image/png;base64").then_some(v)) + .and_then(|v| match v { + Value::String(s) => BASE64_STANDARD.decode(s).ok(), + _ => None, + }) + .map(Vec::into_boxed_slice); + + let identifier = metadata + .iter() + .find_map(|(k, v)| (k == "text/identifier").then_some(v)) + .and_then(|v| match v { + Value::String(s) => Some(String::from(s)), + _ => None, + }) + .map(String::into_boxed_str); + + let email = metadata + .iter() + .find_map(|(k, v)| (k == "text/email").then_some(v)) + .and_then(|v| match v { + Value::String(s) => Some(String::from(s)), + _ => None, + }) + .map(String::into_boxed_str); + + Ok(Query { + metadata_raw: p.metadata.into_boxed_str(), + callback: p.callback.0.into_owned(), + comment_size: p.comment_allowed, + min: p.min_sendable, + max: p.max_sendable, + short_description, + long_description, + identifier, + email, + jpeg, + png, + }) + } +} + +impl Query { + #[must_use] + pub fn callback<'a>(&'a self, millisatoshis: u64, comment: &'a str) -> CallbackRequest<'a> { + CallbackRequest { + url: &self.callback, + millisatoshis, + comment, + } + } +} + +pub struct CallbackRequest<'a> { + pub url: &'a url::Url, + pub comment: &'a str, + pub millisatoshis: u64, +} + +impl std::fmt::Display for CallbackRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let millisatoshis = self.millisatoshis.to_string(); + + let query = [ + (!self.comment.is_empty()).then_some(("comment", self.comment)), + Some(("amount", &millisatoshis)), + ]; + + let mut url = self.url.clone(); + url.query_pairs_mut() + .extend_pairs(query.into_iter().flatten()); + + f.write_str(url.as_str()) + } +} + +#[derive(Clone, Debug)] +pub struct CallbackResponse { + pub pr: Box, + pub disposable: bool, + pub success_action: Option, +} + +#[derive(Clone, Debug)] +pub enum SuccessAction { + Url(url::Url, Box), + Message(Box), +} + +impl std::str::FromStr for CallbackResponse { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let a: de::CallbackResponse = + miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; + + let success_action = a + .success_action + .and_then(|sa| match sa.get("tag")? as &str { + "message" => Some(SuccessAction::Message((sa.get("message")? as &str).into())), + "url" => { + let url = url::Url::parse(sa.get("url")?).ok()?; + let description = (sa.get("description")? as &str).into(); + Some(SuccessAction::Url(url, description)) + } + _ => None, + }); + + Ok(Self { + pr: a.pr.into(), + disposable: a.disposable.unwrap_or(true), + success_action, + }) + } +} + +mod de { + use crate::serde::Url; + use miniserde::Deserialize; + use std::collections::BTreeMap; + + #[derive(Deserialize)] + pub(super) struct Query { + pub metadata: String, + pub callback: Url<'static>, + #[serde(rename = "minSendable")] + pub min_sendable: u64, + #[serde(rename = "maxSendable")] + pub max_sendable: u64, + #[serde(rename = "commentAllowed")] + pub comment_allowed: Option, + } + + #[derive(Deserialize)] + pub(super) struct CallbackResponse { + pub pr: String, + pub disposable: Option, + #[serde(rename = "successAction")] + pub success_action: Option>, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn query_parse_base() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/crazy\", \"👋🇧🇴💾\"]]", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); + assert_eq!(parsed.short_description, "boneco do steve magal"); + assert_eq!( + &parsed.metadata_raw as &str, + "[[\"text/plain\", \"boneco do steve magal\"],[\"text/crazy\", \"👋🇧🇴💾\"]]" + ); + assert_eq!(parsed.min, 314); + assert_eq!(parsed.max, 315); + + assert!(parsed.comment_size.is_none()); + assert!(parsed.long_description.is_none()); + assert!(parsed.jpeg.is_none()); + assert!(parsed.png.is_none()); + assert!(parsed.identifier.is_none()); + assert!(parsed.email.is_none()); + } + + #[test] + fn query_parse_comment_size() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "commentAllowed": 140, + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + assert_eq!(parsed.comment_size.unwrap(), 140); + } + + #[test] + fn query_parse_long_description() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/long-desc\", \"mochila a jato brutal incluida\"]]", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + assert_eq!( + &parsed.long_description.unwrap() as &str, + "mochila a jato brutal incluida" + ); + } + + #[test] + fn query_parse_images() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"image/png;base64\", \"Zm90b2JydXRhbA==\"],[\"image/jpeg;base64\", \"aW1hZ2VtYnJ1dGFs\"]]", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + assert_eq!(&parsed.jpeg.unwrap() as &[u8], b"imagembrutal"); + assert_eq!(&parsed.png.unwrap() as &[u8], b"fotobrutal"); + } + + #[test] + fn query_parse_identifier() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/identifier\", \"steve@magal.brutal\"]]", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + assert_eq!(&parsed.identifier.unwrap() as &str, "steve@magal.brutal"); + } + + #[test] + fn query_parse_email() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/email\", \"steve@magal.brutal\"]]", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + assert_eq!(&parsed.email.unwrap() as &str, "steve@magal.brutal"); + } + + #[test] + fn callback_request_render_base() { + let input = r#"{ + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "callback": "https://yuri?o=callback", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!( + parsed.callback(314, "").to_string(), + "https://yuri/?o=callback&amount=314" + ); + } + + #[test] + fn callback_request_render_comment() { + let input = r#"{ + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "callback": "https://yuri?o=callback", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!( + parsed.callback(314, "comentario").to_string(), + "https://yuri/?o=callback&comment=comentario&amount=314" + ); + } + + #[test] + fn callback_response_parse_base() { + let input = r#"{ "pr": "pierre" }"#; + + let parsed = input.parse::().expect("parse"); + assert!(parsed.success_action.is_none()); + assert_eq!(&parsed.pr as &str, "pierre"); + assert!(parsed.disposable); + } + + #[test] + fn callback_response_parse_disposable() { + let input = r#"{ "pr": "", "disposable": true }"#; + let parsed = input.parse::().expect("parse"); + assert!(parsed.disposable); + + let input = r#"{ "pr": "", "disposable": false }"#; + let parsed = input.parse::().expect("parse"); + assert!(!parsed.disposable); + } + + #[test] + fn callback_response_parse_success_actions() { + let input = + r#"{ "pr": "", "successAction": { "tag": "message", "message": "obrigado!" } }"#; + + let parsed = input.parse::().expect("parse"); + + let Some(super::SuccessAction::Message(m)) = parsed.success_action else { + panic!("bad success action"); + }; + + assert_eq!(&m as &str, "obrigado!"); + + let input = r#" + { "pr": "", "successAction": { "tag": "url", "description": "valeu demais", "url": "http://eh.nois" } } + "#; + + let parsed = input.parse::().expect("parse"); + + let Some(super::SuccessAction::Url(u, d)) = parsed.success_action else { + panic!("bad success action"); + }; + + assert_eq!(u.to_string(), "http://eh.nois/"); + assert_eq!(&d as &str, "valeu demais"); + } +} diff --git a/src/core/pay/server.rs b/src/core/pay/server.rs new file mode 100644 index 0000000..9fca973 --- /dev/null +++ b/src/core/pay/server.rs @@ -0,0 +1,352 @@ +#[derive(Clone, Debug)] +pub struct Query { + pub callback: url::Url, + pub short_description: String, + pub long_description: Option, + pub identifier: Option, + pub email: Option, + pub jpeg: Option>, + pub png: Option>, + pub comment_size: Option, + pub min: u64, + pub max: u64, +} + +impl std::fmt::Display for Query { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use base64::{prelude::BASE64_STANDARD, Engine}; + + let metadata = miniserde::json::to_string( + &[ + Some(("text/plain", self.short_description.clone())), + self.long_description + .as_ref() + .map(|s| ("text/long-desc", s.clone())), + self.jpeg + .as_ref() + .map(|s| ("image/jpeg;base64", BASE64_STANDARD.encode(s))), + self.png + .as_ref() + .map(|s| ("image/png;base64", BASE64_STANDARD.encode(s))), + self.identifier + .as_ref() + .map(|s| ("text/identifier", s.clone())), + self.email.as_ref().map(|s| ("text/email", s.clone())), + ] + .into_iter() + .flatten() + .collect::>(), + ); + + f.write_str(&miniserde::json::to_string(&ser::Query { + tag: super::TAG, + metadata, + callback: crate::serde::Url(std::borrow::Cow::Borrowed(&self.callback)), + min_sendable: self.min, + max_sendable: self.max, + comment_allowed: self.comment_size.unwrap_or(0), + })) + } +} + +pub struct CallbackRequest { + pub millisatoshis: u64, + pub comment: Option>, +} + +impl std::str::FromStr for CallbackRequest { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let qs = s + .split('&') + .filter_map(|s| s.split_once('=')) + .collect::>(); + + let millisatoshis = qs + .get("amount") + .ok_or("missing amount")? + .parse() + .map_err(|_| "invalid amount")?; + + let comment = qs.get("comment").map(|c| (*c).into()); + + Ok(CallbackRequest { + millisatoshis, + comment, + }) + } +} + +#[derive(Clone, Debug)] +pub struct CallbackResponse { + pub pr: String, + pub disposable: bool, + pub success_action: Option, +} + +#[derive(Clone, Debug)] +pub enum SuccessAction { + Url(url::Url, String), + Message(String), +} + +impl std::fmt::Display for CallbackResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let success_action = self.success_action.as_ref().map(|sa| { + let mut map = std::collections::BTreeMap::new(); + + match sa { + SuccessAction::Message(m) => { + map.insert("tag", "message"); + map.insert("message", m); + } + SuccessAction::Url(u, d) => { + map.insert("tag", "url"); + map.insert("description", d); + map.insert("url", u.as_str()); + } + } + + map + }); + + let cr = ser::CallbackResponse { + success_action, + disposable: self.disposable, + pr: &self.pr, + }; + + f.write_str(&miniserde::json::to_string(&cr)) + } +} + +mod ser { + use crate::serde::Url; + use miniserde::Serialize; + use std::collections::BTreeMap; + + #[derive(Serialize)] + pub(super) struct Query<'a> { + pub tag: &'static str, + pub metadata: String, + pub callback: Url<'a>, + #[serde(rename = "minSendable")] + pub min_sendable: u64, + #[serde(rename = "maxSendable")] + pub max_sendable: u64, + #[serde(rename = "commentAllowed")] + pub comment_allowed: u64, + } + + #[derive(Serialize)] + pub(super) struct CallbackResponse<'a> { + pub pr: &'a str, + pub disposable: bool, + #[serde(rename = "successAction")] + pub success_action: Option>, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn query_render_base() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + short_description: String::from("boneco do steve magal"), + long_description: None, + jpeg: None, + png: None, + comment_size: None, + min: 314, + max: 315, + identifier: None, + email: None, + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# + ); + } + + #[test] + fn query_render_comment_size() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + short_description: String::from("boneco do steve magal"), + long_description: None, + jpeg: None, + png: None, + comment_size: Some(140), + min: 314, + max: 315, + identifier: None, + email: None, + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":140}"# + ); + } + + #[test] + fn query_render_long_description() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + short_description: String::from("boneco do steve magal"), + long_description: Some(String::from("mochila a jato brutal incluida")), + jpeg: None, + png: None, + comment_size: None, + min: 314, + max: 315, + identifier: None, + email: None, + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"text/long-desc\",\"mochila a jato brutal incluida\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# + ); + } + + #[test] + fn query_render_images() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + short_description: String::from("boneco do steve magal"), + long_description: None, + jpeg: Some(b"imagembrutal".to_vec()), + png: Some(b"fotobrutal".to_vec()), + comment_size: None, + min: 314, + max: 315, + identifier: None, + email: None, + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"image/jpeg;base64\",\"aW1hZ2VtYnJ1dGFs\"],[\"image/png;base64\",\"Zm90b2JydXRhbA==\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# + ); + } + + #[test] + fn query_render_identifier() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + short_description: String::from("boneco do steve magal"), + long_description: None, + jpeg: Some(b"imagembrutal".to_vec()), + png: Some(b"fotobrutal".to_vec()), + comment_size: None, + min: 314, + max: 315, + identifier: Some(String::from("steve@magal.brutal")), + email: None, + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"image/jpeg;base64\",\"aW1hZ2VtYnJ1dGFs\"],[\"image/png;base64\",\"Zm90b2JydXRhbA==\"],[\"text/identifier\",\"steve@magal.brutal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# + ); + } + + #[test] + fn query_render_email() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + short_description: String::from("boneco do steve magal"), + long_description: None, + jpeg: Some(b"imagembrutal".to_vec()), + png: Some(b"fotobrutal".to_vec()), + comment_size: None, + min: 314, + max: 315, + identifier: None, + email: Some(String::from("steve@magal.brutal")), + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"],[\"image/jpeg;base64\",\"aW1hZ2VtYnJ1dGFs\"],[\"image/png;base64\",\"Zm90b2JydXRhbA==\"],[\"text/email\",\"steve@magal.brutal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0}"# + ); + } + + #[test] + fn callback_request_parse_base() { + let input = "amount=314"; + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.millisatoshis, 314); + assert!(parsed.comment.is_none()); + } + + #[test] + fn callback_request_parse_comment() { + let input = "amount=314&comment=comentario"; + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.millisatoshis, 314); + assert_eq!(&parsed.comment.unwrap() as &str, "comentario"); + } + + #[test] + fn callback_response_render_base() { + let input = super::CallbackResponse { + pr: String::from("pierre"), + success_action: None, + disposable: true, + }; + + assert_eq!( + input.to_string(), + r#"{"pr":"pierre","disposable":true,"successAction":null}"# + ); + } + + #[test] + fn callback_response_render_disposable() { + let input = super::CallbackResponse { + pr: String::from("pierre"), + success_action: None, + disposable: false, + }; + + assert_eq!( + input.to_string(), + r#"{"pr":"pierre","disposable":false,"successAction":null}"# + ); + } + + #[test] + fn callback_response_render_success_actions() { + let input = super::CallbackResponse { + pr: String::from("pierre"), + success_action: Some(super::SuccessAction::Message(String::from("obrigado!"))), + disposable: false, + }; + + assert_eq!( + input.to_string(), + r#"{"pr":"pierre","disposable":false,"successAction":{"message":"obrigado!","tag":"message"}}"# + ); + + let input = super::CallbackResponse { + pr: String::from("pierre"), + success_action: Some(super::SuccessAction::Url( + url::Url::parse("http://recibo").expect("url"), + String::from("segue recibo"), + )), + disposable: false, + }; + + assert_eq!( + input.to_string(), + r#"{"pr":"pierre","disposable":false,"successAction":{"description":"segue recibo","tag":"url","url":"http://recibo/"}}"# + ); + } +} diff --git a/src/core/withdraw.rs b/src/core/withdraw.rs index 0b5eacc..30e691e 100644 --- a/src/core/withdraw.rs +++ b/src/core/withdraw.rs @@ -1,226 +1,3 @@ pub const TAG: &str = "withdrawRequest"; - -#[derive(Clone, Debug)] -pub struct Query { - pub k1: String, - pub callback: url::Url, - pub description: String, - pub min: u64, - pub max: u64, -} - -impl std::str::FromStr for Query { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let d: de::Query = miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; - - Ok(Query { - k1: d.k1, - callback: d.callback.0.into_owned(), - description: d.default_description, - min: d.min_withdrawable, - max: d.max_withdrawable, - }) - } -} - -impl std::fmt::Display for Query { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&miniserde::json::to_string(&ser::Query { - tag: TAG, - callback: crate::serde::Url(std::borrow::Cow::Borrowed(&self.callback)), - default_description: &self.description, - min_withdrawable: self.min, - max_withdrawable: self.max, - k1: &self.k1, - })) - } -} - -impl Query { - #[must_use] - pub fn callback(self, pr: String) -> CallbackRequest { - CallbackRequest { - url: self.callback, - k1: self.k1, - pr, - } - } -} - -pub struct CallbackRequest { - pub url: url::Url, - pub k1: String, - pub pr: String, -} - -impl CallbackRequest { - #[must_use] - pub fn url(mut self) -> url::Url { - let query = [("k1", self.k1), ("pr", self.pr)]; - self.url.query_pairs_mut().extend_pairs(query); - self.url - } -} - -#[derive(Debug)] -pub enum CallbackResponse { - Error(String), - Ok, -} - -impl std::str::FromStr for CallbackResponse { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let map = miniserde::json::from_str::>(s) - .map_err(|_| "bad json")?; - - match map.get("status").map(|s| s as &str) { - Some("OK") => Ok(CallbackResponse::Ok), - Some("ERROR") => Ok(CallbackResponse::Error( - map.get("reason").cloned().unwrap_or_default(), - )), - _ => Err("bad status field"), - } - } -} - -impl std::fmt::Display for CallbackResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut map = std::collections::BTreeMap::new(); - - match self { - CallbackResponse::Error(reason) => { - map.insert("status", "ERROR"); - map.insert("reason", reason); - } - CallbackResponse::Ok => { - map.insert("status", "OK"); - } - } - - f.write_str(&miniserde::json::to_string(&map)) - } -} - -mod ser { - use crate::serde::Url; - use miniserde::Serialize; - - #[derive(Serialize)] - pub(super) struct Query<'a> { - pub tag: &'static str, - pub k1: &'a str, - pub callback: Url<'a>, - #[serde(rename = "defaultDescription")] - pub default_description: &'a str, - #[serde(rename = "minWithdrawable")] - pub min_withdrawable: u64, - #[serde(rename = "maxWithdrawable")] - pub max_withdrawable: u64, - } -} - -mod de { - use crate::serde::Url; - use miniserde::Deserialize; - - #[derive(Deserialize)] - pub(super) struct Query { - pub k1: String, - pub callback: Url<'static>, - #[serde(rename = "defaultDescription")] - pub default_description: String, - #[serde(rename = "minWithdrawable")] - pub min_withdrawable: u64, - #[serde(rename = "maxWithdrawable")] - pub max_withdrawable: u64, - } -} - -#[cfg(test)] -mod tests { - #[test] - fn query_parse() { - let input = r#" - { - "k1": "caum", - "callback": "https://yuri?o=callback", - "defaultDescription": "verde com bolinhas", - "minWithdrawable": 314, - "maxWithdrawable": 315 - } - "#; - - let parsed = input.parse::().expect("parse"); - - assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); - assert_eq!(parsed.description, "verde com bolinhas"); - assert_eq!(parsed.k1, "caum"); - assert_eq!(parsed.max, 315); - assert_eq!(parsed.min, 314); - } - - #[test] - fn query_render() { - let query = super::Query { - k1: String::from("caum"), - callback: url::Url::parse("https://yuri?o=callback").expect("url"), - description: String::from("verde com bolinhas"), - min: 314, - max: 315, - }; - - assert_eq!( - query.to_string(), - r#"{"tag":"withdrawRequest","k1":"caum","callback":"https://yuri/?o=callback","defaultDescription":"verde com bolinhas","minWithdrawable":314,"maxWithdrawable":315}"# - ); - } - - #[test] - fn callback() { - let input = r#" - { - "k1": "caum", - "callback": "https://yuri?o=callback", - "defaultDescription": "verde com bolinhas", - "minWithdrawable": 314, - "maxWithdrawable": 315 - } - "#; - - let parsed = input.parse::().expect("parse"); - - assert_eq!( - parsed.callback(String::from("pierre")).url().to_string(), - "https://yuri/?o=callback&k1=caum&pr=pierre" - ); - } - - #[test] - fn callback_response_parse() { - assert!(matches!( - r#"{ "status": "OK"}"#.parse().unwrap(), - super::CallbackResponse::Ok - )); - - assert!(matches!( - r#"{ "status": "ERROR", "reason": "razao" }"#.parse().unwrap(), - super::CallbackResponse::Error(r) if r == "razao" - )); - } - - #[test] - fn callback_response_render() { - assert_eq!( - super::CallbackResponse::Ok.to_string(), - r#"{"status":"OK"}"# - ); - assert_eq!( - super::CallbackResponse::Error(String::from("razao")).to_string(), - r#"{"reason":"razao","status":"ERROR"}"# - ); - } -} +pub mod client; +pub mod server; diff --git a/src/core/withdraw/client.rs b/src/core/withdraw/client.rs new file mode 100644 index 0000000..eb59067 --- /dev/null +++ b/src/core/withdraw/client.rs @@ -0,0 +1,145 @@ +#[derive(Clone, Debug)] +pub struct Query { + k1: Box, + callback: url::Url, + pub description: Box, + pub min: u64, + pub max: u64, +} + +impl std::str::FromStr for Query { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let d: de::Query = miniserde::json::from_str(s).map_err(|_| "deserialize failed")?; + + Ok(Query { + k1: d.k1.into_boxed_str(), + callback: d.callback.0.into_owned(), + description: d.default_description.into_boxed_str(), + min: d.min_withdrawable, + max: d.max_withdrawable, + }) + } +} + +impl Query { + #[must_use] + pub fn callback<'a>(&'a self, pr: &'a str) -> CallbackRequest { + CallbackRequest { + url: &self.callback, + k1: &self.k1, + pr, + } + } +} + +#[derive(Clone, Debug)] +pub struct CallbackRequest<'a> { + pub url: &'a url::Url, + pub k1: &'a str, + pub pr: &'a str, +} + +impl std::fmt::Display for CallbackRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut url = self.url.clone(); + let query = [("k1", self.k1), ("pr", self.pr)]; + url.query_pairs_mut().extend_pairs(query); + f.write_str(url.as_str()) + } +} + +#[derive(Clone, Debug)] +pub enum CallbackResponse { + Error { reason: Box }, + Ok, +} + +impl std::str::FromStr for CallbackResponse { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let map = miniserde::json::from_str::>(s) + .map_err(|_| "bad json")?; + + match map.get("status").map(|s| s as &str) { + Some("OK") => Ok(CallbackResponse::Ok), + Some("ERROR") => { + let reason = (map.get("reason").ok_or("error without reason")? as &str).into(); + Ok(CallbackResponse::Error { reason }) + } + _ => Err("bad status field"), + } + } +} + +mod de { + use crate::serde::Url; + use miniserde::Deserialize; + + #[derive(Deserialize)] + pub(super) struct Query { + pub k1: String, + pub callback: Url<'static>, + #[serde(rename = "defaultDescription")] + pub default_description: String, + #[serde(rename = "minWithdrawable")] + pub min_withdrawable: u64, + #[serde(rename = "maxWithdrawable")] + pub max_withdrawable: u64, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn query_parse() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "defaultDescription": "verde com bolinhas", + "minWithdrawable": 314, + "maxWithdrawable": 315, + "k1": "caum" + }"#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!(parsed.callback.to_string(), "https://yuri/?o=callback"); + assert_eq!(&parsed.description as &str, "verde com bolinhas"); + assert_eq!(&parsed.k1 as &str, "caum"); + assert_eq!(parsed.max, 315); + assert_eq!(parsed.min, 314); + } + + #[test] + fn callback_request_render() { + let input = r#"{ + "callback": "https://yuri?o=callback", + "defaultDescription": "verde com bolinhas", + "minWithdrawable": 314, + "maxWithdrawable": 315, + "k1": "caum" + }"#; + + let parsed = input.parse::().expect("parse"); + + assert_eq!( + parsed.callback("pierre").to_string(), + "https://yuri/?o=callback&k1=caum&pr=pierre" + ); + } + + #[test] + fn callback_response_parse() { + assert!(matches!( + r#"{ "status": "OK" }"#.parse().unwrap(), + super::CallbackResponse::Ok + )); + + assert!(matches!( + r#"{ "status": "ERROR", "reason": "razao" }"#.parse().unwrap(), + super::CallbackResponse::Error { reason } if &reason as &str == "razao" + )); + } +} diff --git a/src/core/withdraw/server.rs b/src/core/withdraw/server.rs new file mode 100644 index 0000000..68fadf5 --- /dev/null +++ b/src/core/withdraw/server.rs @@ -0,0 +1,127 @@ +pub struct Query { + pub k1: String, + pub callback: url::Url, + pub description: String, + pub min: u64, + pub max: u64, +} + +impl std::fmt::Display for Query { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&miniserde::json::to_string(&ser::Query { + tag: super::TAG, + callback: crate::serde::Url(std::borrow::Cow::Borrowed(&self.callback)), + default_description: &self.description, + min_withdrawable: self.min, + max_withdrawable: self.max, + k1: &self.k1, + })) + } +} + +pub struct CallbackRequest { + pub k1: Box, + pub pr: Box, +} + +impl std::str::FromStr for CallbackRequest { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let qs = s + .split('&') + .filter_map(|s| s.split_once('=')) + .collect::>(); + + let k1 = (*qs.get("k1").ok_or("missing k1")?).into(); + let pr = (*qs.get("pr").ok_or("missing pr")?).into(); + + Ok(CallbackRequest { k1, pr }) + } +} + +#[derive(Clone, Debug)] +pub enum CallbackResponse { + Error { reason: String }, + Ok, +} + +impl std::fmt::Display for CallbackResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut map = std::collections::BTreeMap::new(); + + match self { + CallbackResponse::Error { reason } => { + map.insert("status", "ERROR"); + map.insert("reason", reason); + } + CallbackResponse::Ok => { + map.insert("status", "OK"); + } + } + + f.write_str(&miniserde::json::to_string(&map)) + } +} + +mod ser { + use crate::serde::Url; + use miniserde::Serialize; + + #[derive(Serialize)] + pub(super) struct Query<'a> { + pub tag: &'static str, + pub k1: &'a str, + pub callback: Url<'a>, + #[serde(rename = "defaultDescription")] + pub default_description: &'a str, + #[serde(rename = "minWithdrawable")] + pub min_withdrawable: u64, + #[serde(rename = "maxWithdrawable")] + pub max_withdrawable: u64, + } +} + +#[cfg(test)] +mod tests { + #[test] + fn query_render() { + let query = super::Query { + callback: url::Url::parse("https://yuri?o=callback").expect("url"), + description: String::from("verde com bolinhas"), + k1: String::from("caum"), + min: 314, + max: 315, + }; + + assert_eq!( + query.to_string(), + r#"{"tag":"withdrawRequest","k1":"caum","callback":"https://yuri/?o=callback","defaultDescription":"verde com bolinhas","minWithdrawable":314,"maxWithdrawable":315}"# + ); + } + + #[test] + fn callback_request_parse() { + let input = "k1=caum&pr=pierre"; + let parsed = input.parse::().expect("parse"); + + assert_eq!(&parsed.pr as &str, "pierre"); + assert_eq!(&parsed.k1 as &str, "caum"); + } + + #[test] + fn callback_response_render() { + assert_eq!( + super::CallbackResponse::Ok.to_string(), + r#"{"status":"OK"}"# + ); + + assert_eq!( + super::CallbackResponse::Error { + reason: String::from("razao") + } + .to_string(), + r#"{"reason":"razao","status":"ERROR"}"# + ); + } +} diff --git a/src/server.rs b/src/server.rs index f1a116a..d3dc8c6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,14 +1,12 @@ use axum::{ extract::{Path, RawQuery}, http::StatusCode, - http::Uri, routing::get, Router, }; use std::future::Future; pub struct Server { - base: String, channel_query: CQ, channel_callback: CC, pay_query: PQ, @@ -17,24 +15,30 @@ pub struct Server { withdraw_callback: WC, } -impl - Server< +impl Default + for Server< // Channel Request - unimplemented::Handler<(), crate::channel::Query>, - unimplemented::Handler, + unimplemented::Handler<(), crate::channel::server::Query>, + unimplemented::Handler< + crate::channel::server::CallbackRequest, + crate::channel::server::CallbackResponse, + >, // Pay Request - unimplemented::Handler, crate::pay::Query>, - unimplemented::Handler, + unimplemented::Handler, crate::pay::server::Query>, + unimplemented::Handler< + crate::pay::server::CallbackRequest, + crate::pay::server::CallbackResponse, + >, // Withdraw Request - unimplemented::Handler<(), crate::withdraw::Query>, - unimplemented::Handler, + unimplemented::Handler<(), crate::withdraw::server::Query>, + unimplemented::Handler< + crate::withdraw::server::CallbackRequest, + crate::withdraw::server::CallbackResponse, + >, > { - #[must_use] - pub fn new(base: String) -> Self { + fn default() -> Self { Server { - base, - channel_query: unimplemented::handler, channel_callback: unimplemented::handler, @@ -54,7 +58,6 @@ impl Server { channel_callback: CC2, ) -> Server { Server { - base: self.base, channel_query, channel_callback, pay_query: self.pay_query, @@ -70,7 +73,6 @@ impl Server { pay_callback: PC2, ) -> Server { Server { - base: self.base, channel_query: self.channel_query, channel_callback: self.channel_callback, pay_query, @@ -86,7 +88,6 @@ impl Server { withdraw_callback: WC2, ) -> Server { Server { - base: self.base, channel_query: self.channel_query, channel_callback: self.channel_callback, pay_query: self.pay_query, @@ -101,29 +102,25 @@ impl Server where CQ: 'static + Send + Clone + Fn(()) -> CQFut, - CQFut: Send + Future>, + CQFut: Send + Future>, - CC: 'static + Send + Clone + Fn(crate::channel::CallbackRequest) -> CCFut, - CCFut: Send + Future>, + CC: 'static + Send + Clone + Fn(crate::channel::server::CallbackRequest) -> CCFut, + CCFut: Send + Future>, PQ: 'static + Send + Clone + Fn(Option) -> PQFut, - PQFut: Send + Future>, + PQFut: Send + Future>, - PC: 'static + Send + Clone + Fn(crate::pay::CallbackRequest) -> PCFut, - PCFut: Send + Future>, + PC: 'static + Send + Clone + Fn(crate::pay::server::CallbackRequest) -> PCFut, + PCFut: Send + Future>, WQ: 'static + Send + Clone + Fn(()) -> WQFut, - WQFut: Send + Future>, + WQFut: Send + Future>, - WC: 'static + Send + Clone + Fn(crate::withdraw::CallbackRequest) -> WCFut, - WCFut: Send + Future>, + WC: 'static + Send + Clone + Fn(crate::withdraw::server::CallbackRequest) -> WCFut, + WCFut: Send + Future>, { #[allow(clippy::too_many_lines)] pub fn build(self) -> Router<()> { - let base_c = self.base.clone(); - let base_p = self.base.clone(); - let base_w = self.base.clone(); - Router::new() .route( "/lnurlc", @@ -134,43 +131,11 @@ where ) .route( "/lnurlc/callback", - get(move |uri: Uri, RawQuery(q): RawQuery| { + get(move |RawQuery(q): RawQuery| { let cc = self.channel_callback.clone(); async move { - let url = url::Url::parse(&format!("https://{base_c}{}", uri.path())) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let q = q.ok_or(StatusCode::BAD_REQUEST)?; - let qs = q - .split('&') - .filter_map(|s| s.split_once('=')) - .collect::>(); - - let k1 = String::from(*qs.get("k1").ok_or(StatusCode::BAD_REQUEST)?); - let remoteid = - String::from(*qs.get("remoteid").ok_or(StatusCode::BAD_REQUEST)?); - - let req = if qs.get("cancel").copied() == Some("1") { - Some(crate::channel::CallbackRequest::Cancel { url, remoteid, k1 }) - } else { - match qs.get("private").copied() { - Some("0") => Some(crate::channel::CallbackRequest::Accept { - url, - remoteid, - k1, - private: false, - }), - Some("1") => Some(crate::channel::CallbackRequest::Accept { - url, - remoteid, - k1, - private: true, - }), - _ => None, - } - } - .ok_or(StatusCode::BAD_REQUEST)?; - + let req = q.parse().map_err(|_| StatusCode::BAD_REQUEST)?; cc(req).await.map(|a| a.to_string()) } }), @@ -194,35 +159,12 @@ where ) .route( "/lnurlp/callback", - get(move |uri: Uri, RawQuery(q): RawQuery| { + get(move |RawQuery(q): RawQuery| { let pc = self.pay_callback.clone(); async move { - let url = url::Url::parse(&format!("https://{base_p}{}", uri.path())) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let q = q.ok_or(StatusCode::BAD_REQUEST)?; - let qs = q - .split('&') - .filter_map(|s| s.split_once('=')) - .collect::>(); - - let millisatoshis = qs - .get("amount") - .and_then(|s| s.parse().ok()) - .ok_or(StatusCode::BAD_REQUEST)?; - - let comment = qs - .get("comment") - .map(|c| String::from(*c)) - .unwrap_or_default(); - - pc(crate::pay::CallbackRequest { - url, - comment, - millisatoshis, - }) - .await - .map(|a| a.to_string()) + let req = q.parse().map_err(|_| StatusCode::BAD_REQUEST)?; + pc(req).await.map(|a| a.to_string()) } }), ) @@ -235,24 +177,12 @@ where ) .route( "/lnurlw/callback", - get(move |uri: Uri, RawQuery(q): RawQuery| { + get(move |RawQuery(q): RawQuery| { let wc = self.withdraw_callback.clone(); async move { - let url = url::Url::parse(&format!("https://{base_w}{}", uri.path())) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let q = q.ok_or(StatusCode::BAD_REQUEST)?; - let qs = q - .split('&') - .filter_map(|s| s.split_once('=')) - .collect::>(); - - let k1 = String::from(*qs.get("k1").ok_or(StatusCode::BAD_REQUEST)?); - let pr = String::from(*qs.get("pr").ok_or(StatusCode::BAD_REQUEST)?); - - wc(crate::withdraw::CallbackRequest { url, k1, pr }) - .await - .map(|a| a.to_string()) + let req = q.parse().map_err(|_| StatusCode::BAD_REQUEST)?; + wc(req).await.map(|a| a.to_string()) } }), ) @@ -288,6 +218,6 @@ mod unimplemented { mod tests { #[test] fn default_builds() { - drop(super::Server::new(String::from("base:31415")).build()); + drop(super::Server::default().build()); } } diff --git a/tests/lud02.rs b/tests/lud02.rs index 809feee..e50a96f 100644 --- a/tests/lud02.rs +++ b/tests/lud02.rs @@ -9,29 +9,35 @@ async fn test() { let query_url = format!("http://{addr}/lnurlc"); let callback_url = url::Url::parse(&format!("http://{addr}/lnurlc/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .channel_request( move |()| { let callback = callback_url.clone(); async { - Ok(lnurlkit::channel::Query { + Ok(lnurlkit::channel::server::Query { uri: String::from("u@r:i"), k1: String::from("caum"), callback, }) } }, - |req: lnurlkit::channel::CallbackRequest| async move { + |req: lnurlkit::channel::server::CallbackRequest| async move { Ok(match req { - lnurlkit::channel::CallbackRequest::Cancel { remoteid, k1, .. } => { - if remoteid == "idremoto" { - lnurlkit::channel::CallbackResponse::Ok + lnurlkit::channel::server::CallbackRequest::Cancel { remoteid, k1 } => { + if &remoteid as &str == "idremoto" { + lnurlkit::channel::server::CallbackResponse::Ok } else { - lnurlkit::channel::CallbackResponse::Error(format!("{k1}/Cancel")) + let reason = format!("Cancel/{k1}/{remoteid}"); + lnurlkit::channel::server::CallbackResponse::Error { reason } } } - lnurlkit::channel::CallbackRequest::Accept { private, k1, .. } => { - lnurlkit::channel::CallbackResponse::Error(format!("{k1}/Accept/{private}")) + lnurlkit::channel::server::CallbackRequest::Accept { + remoteid, + private, + k1, + } => { + let reason = format!("Accept/{k1}/{remoteid}/{private}"); + lnurlkit::channel::server::CallbackResponse::Error { reason } } }) }, @@ -56,45 +62,39 @@ async fn test() { panic!("not pay request"); }; - assert_eq!(cr.core.uri, "u@r:i"); + assert_eq!(&cr.core.uri as &str, "u@r:i"); - let response = cr - .clone() - .callback_cancel(String::from("idremoto")) - .await - .expect("callback"); + let response = cr.callback_cancel("idremoto").await.expect("callback"); - assert!(matches!(response, lnurlkit::channel::CallbackResponse::Ok)); + assert!(matches!( + response, + lnurlkit::channel::client::CallbackResponse::Ok + )); - let response = cr - .clone() - .callback_cancel(String::from("iderrado")) - .await - .expect("callback"); + let response = cr.callback_cancel("iderrado").await.expect("callback"); assert!(matches!( response, - lnurlkit::channel::CallbackResponse::Error(r) if r == "caum/Cancel" + lnurlkit::channel::client::CallbackResponse::Error { reason } if &reason as &str == "Cancel/caum/iderrado" )); let response = cr - .clone() - .callback_accept(String::from("iderrado"), true) + .callback_accept("iderrado", true) .await .expect("callback"); assert!(matches!( response, - lnurlkit::channel::CallbackResponse::Error(r) if r == "caum/Accept/true" + lnurlkit::channel::client::CallbackResponse::Error { reason } if &reason as &str == "Accept/caum/iderrado/true" )); let response = cr - .callback_accept(String::from("iderrado"), false) + .callback_accept("iderrado", false) .await .expect("callback"); assert!(matches!( response, - lnurlkit::channel::CallbackResponse::Error(r) if r == "caum/Accept/false" + lnurlkit::channel::client::CallbackResponse::Error { reason } if &reason as &str == "Accept/caum/iderrado/false" )); } diff --git a/tests/lud03.rs b/tests/lud03.rs index 109b20f..7c71039 100644 --- a/tests/lud03.rs +++ b/tests/lud03.rs @@ -9,12 +9,12 @@ async fn test() { let query_url = format!("http://{addr}/lnurlw"); let callback_url = url::Url::parse(&format!("http://{addr}/lnurlw/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .withdraw_request( move |()| { let callback = callback_url.clone(); async { - Ok(lnurlkit::withdraw::Query { + Ok(lnurlkit::withdraw::server::Query { description: String::from("descricao"), k1: String::from("caum"), callback, @@ -23,11 +23,13 @@ async fn test() { }) } }, - |req: lnurlkit::withdraw::CallbackRequest| async move { - Ok(if req.pr == "pierre" { - lnurlkit::withdraw::CallbackResponse::Ok + |req: lnurlkit::withdraw::server::CallbackRequest| async move { + Ok(if &req.pr as &str == "pierre" { + lnurlkit::withdraw::server::CallbackResponse::Ok } else { - lnurlkit::withdraw::CallbackResponse::Error(req.k1) + lnurlkit::withdraw::server::CallbackResponse::Error { + reason: req.k1.to_string(), + } }) }, ) @@ -53,23 +55,19 @@ async fn test() { assert_eq!(wr.core.min, 314); assert_eq!(wr.core.max, 315); - assert_eq!(wr.core.description, "descricao"); + assert_eq!(&wr.core.description as &str, "descricao"); - let response = wr - .clone() - .callback(String::from("pierre")) - .await - .expect("callback"); + let response = wr.callback("pierre").await.expect("callback"); - assert!(matches!(response, lnurlkit::withdraw::CallbackResponse::Ok)); + assert!(matches!( + response, + lnurlkit::withdraw::client::CallbackResponse::Ok + )); - let response = wr - .callback(String::from("pierrado")) - .await - .expect("callback"); + let response = wr.callback("pierrado").await.expect("callback"); assert!(matches!( response, - lnurlkit::withdraw::CallbackResponse::Error(r) if r == "caum" + lnurlkit::withdraw::client::CallbackResponse::Error { reason } if &reason as &str == "caum" )); } diff --git a/tests/lud06.rs b/tests/lud06.rs index db079b1..d65a1c9 100644 --- a/tests/lud06.rs +++ b/tests/lud06.rs @@ -9,12 +9,12 @@ async fn test() { let query_url = format!("http://{addr}/lnurlp"); let callback_url = url::Url::parse(&format!("http://{addr}/lnurlp/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .pay_request( move |_| { let callback = callback_url.clone(); async { - Ok(lnurlkit::pay::Query { + Ok(lnurlkit::pay::server::Query { callback, short_description: String::from("today i become death"), long_description: Some(String::from("the destroyer of worlds")), @@ -25,12 +25,11 @@ async fn test() { max: 315, identifier: None, email: None, - metadata_raw: None, }) } }, - |req: lnurlkit::pay::CallbackRequest| async move { - Ok(lnurlkit::pay::CallbackResponse { + |req: lnurlkit::pay::server::CallbackRequest| async move { + Ok(lnurlkit::pay::server::CallbackResponse { pr: format!("pierre:{}", req.millisatoshis), disposable: false, success_action: None, @@ -61,14 +60,11 @@ async fn test() { assert_eq!(pr.core.max, 315); assert_eq!(pr.core.short_description, "today i become death"); assert_eq!( - pr.core.long_description.as_ref().unwrap(), + &pr.core.long_description.as_ref().unwrap() as &str, "the destroyer of worlds" ); - let invoice = pr - .callback(314, String::from("comment")) - .await - .expect("callback"); + let invoice = pr.callback(314, "comment").await.expect("callback"); - assert_eq!(invoice.pr, "pierre:314"); + assert_eq!(&invoice.pr as &str, "pierre:314"); } diff --git a/tests/lud09.rs b/tests/lud09.rs index 08730ab..6670f65 100644 --- a/tests/lud09.rs +++ b/tests/lud09.rs @@ -9,12 +9,12 @@ async fn test() { let query_url = format!("http://{addr}/lnurlp"); let callback_url = url::Url::parse(&format!("http://{addr}/lnurlp/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .pay_request( move |_| { let callback = callback_url.clone(); async { - Ok(lnurlkit::pay::Query { + Ok(lnurlkit::pay::server::Query { callback, short_description: String::new(), long_description: None, @@ -25,22 +25,23 @@ async fn test() { max: 315, identifier: None, email: None, - metadata_raw: None, }) } }, - |req: lnurlkit::pay::CallbackRequest| async move { - Ok(lnurlkit::pay::CallbackResponse { + |req: lnurlkit::pay::server::CallbackRequest| async move { + Ok(lnurlkit::pay::server::CallbackResponse { pr: String::new(), disposable: false, success_action: if req.millisatoshis == 0 { None } else if req.millisatoshis == 1 { - Some(lnurlkit::pay::SuccessAction::Message(req.comment)) + Some(lnurlkit::pay::server::SuccessAction::Message( + req.comment.map(|a| a.to_string()).unwrap_or_default(), + )) } else { - Some(lnurlkit::pay::SuccessAction::Url( + Some(lnurlkit::pay::server::SuccessAction::Url( url::Url::parse("http://u.rl").expect("url"), - req.comment, + req.comment.map(|a| a.to_string()).unwrap_or_default(), )) }, }) @@ -66,35 +67,24 @@ async fn test() { panic!("not pay request"); }; - let invoice = pr - .clone() - .callback(0, String::new()) - .await - .expect("callback"); + let invoice = pr.callback(0, "").await.expect("callback"); assert!(invoice.success_action.is_none()); - let invoice = pr - .clone() - .callback(1, String::from("mensagem")) - .await - .expect("callback"); + let invoice = pr.callback(1, "mensagem").await.expect("callback"); - let Some(lnurlkit::pay::SuccessAction::Message(m)) = invoice.success_action else { + let Some(lnurlkit::pay::client::SuccessAction::Message(m)) = invoice.success_action else { panic!("bad success action"); }; - assert_eq!(m, "mensagem"); + assert_eq!(&m as &str, "mensagem"); - let invoice = pr - .callback(2, String::from("descricao")) - .await - .expect("callback"); + let invoice = pr.callback(2, "descricao").await.expect("callback"); - let Some(lnurlkit::pay::SuccessAction::Url(u, d)) = invoice.success_action else { + let Some(lnurlkit::pay::client::SuccessAction::Url(u, d)) = invoice.success_action else { panic!("bad success action"); }; assert_eq!(u.to_string(), "http://u.rl/"); - assert_eq!(d, "descricao"); + assert_eq!(&d as &str, "descricao"); } diff --git a/tests/lud11.rs b/tests/lud11.rs index 69dd63d..4114c7f 100644 --- a/tests/lud11.rs +++ b/tests/lud11.rs @@ -9,12 +9,12 @@ async fn test() { let query_url = format!("http://{addr}/lnurlp"); let callback_url = url::Url::parse(&format!("http://{addr}/lnurlp/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .pay_request( move |_| { let callback = callback_url.clone(); async { - Ok(lnurlkit::pay::Query { + Ok(lnurlkit::pay::server::Query { callback, short_description: String::new(), long_description: None, @@ -25,12 +25,11 @@ async fn test() { max: 315, identifier: None, email: None, - metadata_raw: None, }) } }, - |req: lnurlkit::pay::CallbackRequest| async move { - Ok(lnurlkit::pay::CallbackResponse { + |req: lnurlkit::pay::server::CallbackRequest| async move { + Ok(lnurlkit::pay::server::CallbackResponse { pr: String::new(), disposable: req.millisatoshis % 2 == 0, success_action: None, @@ -57,14 +56,10 @@ async fn test() { panic!("not pay request"); }; - let invoice = pr - .clone() - .callback(314, String::new()) - .await - .expect("callback"); + let invoice = pr.callback(314, "").await.expect("callback"); assert!(invoice.disposable); - let invoice = pr.callback(315, String::new()).await.expect("callback"); + let invoice = pr.callback(315, "").await.expect("callback"); assert!(!invoice.disposable); } diff --git a/tests/lud12.rs b/tests/lud12.rs index 78f87f9..e016553 100644 --- a/tests/lud12.rs +++ b/tests/lud12.rs @@ -9,12 +9,12 @@ async fn test() { let query_url = format!("http://{addr}/lnurlp"); let callback_url = url::Url::parse(&format!("http://{addr}/lnurlp/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .pay_request( move |_| { let callback = callback_url.clone(); async { - Ok(lnurlkit::pay::Query { + Ok(lnurlkit::pay::server::Query { callback, short_description: String::new(), long_description: None, @@ -25,13 +25,12 @@ async fn test() { max: 315, identifier: None, email: None, - metadata_raw: None, }) } }, - |req: lnurlkit::pay::CallbackRequest| async move { - Ok(lnurlkit::pay::CallbackResponse { - pr: format!("pierre:{}", req.comment), + |req: lnurlkit::pay::server::CallbackRequest| async move { + Ok(lnurlkit::pay::server::CallbackResponse { + pr: format!("pierre:{:?}", req.comment), disposable: false, success_action: None, }) @@ -59,18 +58,11 @@ async fn test() { assert_eq!(pr.core.comment_size.unwrap(), 140); - let invoice = pr - .clone() - .callback(314, String::new()) - .await - .expect("callback"); + let invoice = pr.callback(314, "").await.expect("callback"); - assert_eq!(invoice.pr, "pierre:"); + assert_eq!(&invoice.pr as &str, "pierre:None"); - let invoice = pr - .callback(314, String::from("comentario")) - .await - .expect("callback"); + let invoice = pr.callback(314, "comentario").await.expect("callback"); - assert_eq!(invoice.pr, "pierre:comentario"); + assert_eq!(&invoice.pr as &str, "pierre:Some(\"comentario\")"); } diff --git a/tests/lud16.rs b/tests/lud16.rs index 943a31a..ff97c6a 100644 --- a/tests/lud16.rs +++ b/tests/lud16.rs @@ -8,12 +8,12 @@ async fn test() { let callback_url = url::Url::parse(&format!("http://{addr}/lnurlp/callback")).expect("url"); - let router = lnurlkit::Server::new(addr.to_string()) + let router = lnurlkit::Server::default() .pay_request( move |identifier: Option| { let callback = callback_url.clone(); async { - Ok(lnurlkit::pay::Query { + Ok(lnurlkit::pay::server::Query { callback, short_description: String::from("today i become death"), long_description: Some(String::from("the destroyer of worlds")), @@ -24,12 +24,11 @@ async fn test() { max: 315, identifier: identifier.clone().filter(|i| i.starts_with('n')), email: identifier.filter(|i| i.starts_with('j')), - metadata_raw: None, }) } }, - |req: lnurlkit::pay::CallbackRequest| async move { - Ok(lnurlkit::pay::CallbackResponse { + |req: lnurlkit::pay::server::CallbackRequest| async move { + Ok(lnurlkit::pay::server::CallbackResponse { pr: format!("pierre:{}", req.millisatoshis), disposable: false, success_action: None, @@ -60,7 +59,7 @@ async fn test() { panic!("not pay request"); }; - assert_eq!(pr.core.identifier.unwrap(), "nico"); + assert_eq!(&pr.core.identifier.unwrap() as &str, "nico"); let lnaddr = format!("jorel@{addr}"); let mut lnurl = lnurlkit::resolve(&lnaddr).expect("resolve"); @@ -78,5 +77,5 @@ async fn test() { panic!("not pay request"); }; - assert_eq!(pr.core.email.unwrap(), "jorel"); + assert_eq!(&pr.core.email.unwrap() as &str, "jorel"); } diff --git a/tests/real.rs b/tests/real.rs index 8f571ef..61f6d0c 100644 --- a/tests/real.rs +++ b/tests/real.rs @@ -7,10 +7,6 @@ mod pay_request { panic!("parse"); }; - assert_eq!( - pr.callback.to_string(), - "https://getalby.com/lnurlp/lorenzo/callback" - ); assert_eq!(pr.short_description, "Sats for lorenzo"); assert!(pr.long_description.is_none()); @@ -20,6 +16,11 @@ mod pay_request { assert!(pr.jpeg.is_none()); assert!(pr.png.is_none()); + + assert_eq!( + pr.callback(314, "").to_string(), + "https://getalby.com/lnurlp/lorenzo/callback?amount=314" + ); } #[test] @@ -30,10 +31,6 @@ mod pay_request { panic!("parse"); }; - assert_eq!( - pr.callback.to_string(), - "https://pay.mainnet.galoy.io/lnurlp/lorenzo/callback" - ); assert_eq!(pr.short_description, "Payment to lorenzo"); assert!(pr.long_description.is_none()); @@ -43,6 +40,11 @@ mod pay_request { assert!(pr.jpeg.is_none()); assert!(pr.png.is_none()); + + assert_eq!( + pr.callback(314, "").to_string(), + "https://pay.mainnet.galoy.io/lnurlp/lorenzo/callback?amount=314" + ); } #[test] @@ -53,7 +55,6 @@ mod pay_request { panic!("parse"); }; - assert_eq!(pr.callback.to_string(), "https://api.bipa.app/ln/request/invoice/kenu/1701784379/50n3BjOSWb1ZrxE9WmRcqlk2ylDzUJ1Q_GHN0pk_Q7Q/P6IMTO82jj6W21mUvXNgIlGmqGibx8MiaWfSjQ2wI88"); assert_eq!(pr.short_description, "$kenu ⚡ bipa.app"); assert!(pr.long_description.is_none()); @@ -62,7 +63,12 @@ mod pay_request { assert_eq!(pr.min, 1000); assert!(pr.jpeg.is_none()); - assert_eq!(pr.png.unwrap().len(), 54697); + assert_eq!(pr.png.as_ref().unwrap().len(), 54697); + + assert_eq!( + pr.callback(314, "").to_string(), + "https://api.bipa.app/ln/request/invoice/kenu/1701784379/50n3BjOSWb1ZrxE9WmRcqlk2ylDzUJ1Q_GHN0pk_Q7Q/P6IMTO82jj6W21mUvXNgIlGmqGibx8MiaWfSjQ2wI88?amount=314" + ); } #[test] @@ -73,10 +79,6 @@ mod pay_request { panic!("parse"); }; - assert_eq!( - pr.callback.to_string(), - "https://app.pouch.ph/api/v2/lnurl/pay/ethan" - ); assert_eq!(pr.short_description, "Lightning payment to ethan@pouch.ph"); assert!(pr.long_description.is_none()); @@ -86,6 +88,11 @@ mod pay_request { assert!(pr.jpeg.is_none()); assert!(pr.png.is_none()); + + assert_eq!( + pr.callback(314, "").to_string(), + "https://app.pouch.ph/api/v2/lnurl/pay/ethan?amount=314" + ); } #[test] @@ -96,7 +103,6 @@ mod pay_request { panic!("parse"); }; - assert_eq!(pr.callback.to_string(), "https://livingroomofsatoshi.com/api/v1/lnurl/payreq/0e7f30e3-e74d-410d-bf86-50d101715e81"); assert_eq!( pr.short_description, "Pay to Wallet of Satoshi user: wailingcity51" @@ -109,6 +115,11 @@ mod pay_request { assert!(pr.jpeg.is_none()); assert!(pr.png.is_none()); + + assert_eq!( + pr.callback(314, "").to_string(), + "https://livingroomofsatoshi.com/api/v1/lnurl/payreq/0e7f30e3-e74d-410d-bf86-50d101715e81?amount=314" + ); } #[test] @@ -119,10 +130,6 @@ mod pay_request { panic!("parse"); }; - assert_eq!( - pr.callback.to_string(), - "https://api.zebedee.io/v0/process-static-charges/8d648ac7-09f6-400c-8479-d05ac4d9d61d" - ); assert_eq!(pr.short_description, "luhack - Welcome to my zbd.gg page!"); assert!(pr.long_description.is_none()); @@ -131,6 +138,11 @@ mod pay_request { assert_eq!(pr.min, 1000); assert!(pr.jpeg.is_none()); - assert_eq!(pr.png.unwrap().len(), 3993); + assert_eq!(pr.png.as_ref().unwrap().len(), 3993); + + assert_eq!( + pr.callback(314, "").to_string(), + "https://api.zebedee.io/v0/process-static-charges/8d648ac7-09f6-400c-8479-d05ac4d9d61d?amount=314" + ); } }