From 2598a98a6e529e71cc4420dd87c14ee562ddb4ef Mon Sep 17 00:00:00 2001 From: Lucas Sunsi Abreu Date: Sun, 17 Dec 2023 11:07:55 -0300 Subject: [PATCH] feat(pay): add support for basic serde of payer data --- src/client.rs | 2 +- src/core.rs | 4 +- src/core/pay.rs | 50 +++++++++++++++++++ src/core/pay/client.rs | 93 +++++++++++++++++++++++++++++++++- src/core/pay/server.rs | 110 ++++++++++++++++++++++++++++++++++++++++- tests/lud06.rs | 1 + tests/lud09.rs | 1 + tests/lud11.rs | 1 + tests/lud12.rs | 1 + tests/lud16.rs | 1 + tests/lud21.rs | 1 + 11 files changed, 260 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 92146db..d342dd8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -55,7 +55,7 @@ pub struct Channel<'a> { #[derive(Clone, Debug)] pub struct Pay<'a> { client: &'a reqwest::Client, - pub core: crate::pay::client::Entrypoint, + pub core: Box, } #[derive(Clone, Debug)] diff --git a/src/core.rs b/src/core.rs index 87d3469..6144240 100644 --- a/src/core.rs +++ b/src/core.rs @@ -94,7 +94,7 @@ fn resolve_address(s: &str) -> Result { #[derive(Debug)] pub enum Entrypoint { Channel(channel::client::Entrypoint), - Pay(pay::client::Entrypoint), + Pay(Box), Withdraw(withdraw::client::Entrypoint), } @@ -114,7 +114,7 @@ impl TryFrom<&[u8]> for Entrypoint { Ok(Entrypoint::Channel(cr)) } else if tag.tag == pay::TAG { let pr = s.try_into().map_err(|_| "deserialize data failed")?; - Ok(Entrypoint::Pay(pr)) + Ok(Entrypoint::Pay(Box::new(pr))) } else if tag.tag == withdraw::TAG { let wr = s.try_into().map_err(|_| "deserialize data failed")?; Ok(Entrypoint::Withdraw(wr)) diff --git a/src/core/pay.rs b/src/core/pay.rs index 2073aae..68ddf1b 100644 --- a/src/core/pay.rs +++ b/src/core/pay.rs @@ -18,8 +18,30 @@ pub struct Currency { pub convertible: bool, } +#[derive(Clone, Debug)] +pub struct Payer { + pub name: Option, + pub pubkey: Option, + pub identifier: Option, + pub email: Option, + pub auth: Option, + pub others: std::collections::HashMap, +} + +#[derive(Clone, Debug)] +pub struct PayerRequirement { + pub mandatory: bool, +} + +#[derive(Clone, Debug)] +pub struct PayerRequirementAuth { + pub mandatory: bool, + pub k1: [u8; 32], +} + mod serde { use serde::{Deserialize, Serialize}; + use std::collections::HashMap; #[derive(Deserialize, Serialize)] pub(super) struct Currency<'a> { @@ -32,6 +54,34 @@ mod serde { pub convertible: bool, } + #[derive(Deserialize, Serialize)] + pub(super) struct Payer<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + #[serde(borrow, flatten)] + pub others: HashMap<&'a str, PayerRequirement>, + } + + #[derive(Deserialize, Serialize)] + pub struct PayerRequirement { + pub mandatory: bool, + } + + #[derive(Deserialize, Serialize)] + pub struct PayerRequirementAuth { + pub mandatory: bool, + #[serde(with = "hex::serde")] + pub k1: [u8; 32], + } + pub(super) mod amount { use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/src/core/pay/client.rs b/src/core/pay/client.rs index 6749d08..af1d01f 100644 --- a/src/core/pay/client.rs +++ b/src/core/pay/client.rs @@ -12,8 +12,10 @@ pub struct Entrypoint { pub min: u64, pub max: u64, pub currencies: Option>, + pub payer: Option, } +#[allow(clippy::too_many_lines)] impl TryFrom<&[u8]> for Entrypoint { type Error = &'static str; @@ -36,6 +38,37 @@ impl TryFrom<&[u8]> for Entrypoint { .collect() }); + let payer = p.payer_data.map(|p| super::Payer { + name: p.name.map(|p| super::PayerRequirement { + mandatory: p.mandatory, + }), + pubkey: p.pubkey.map(|p| super::PayerRequirement { + mandatory: p.mandatory, + }), + identifier: p.identifier.map(|p| super::PayerRequirement { + mandatory: p.mandatory, + }), + email: p.email.map(|p| super::PayerRequirement { + mandatory: p.mandatory, + }), + auth: p.auth.map(|p| super::PayerRequirementAuth { + mandatory: p.mandatory, + k1: p.k1, + }), + others: p + .others + .into_iter() + .map(|(k, v)| { + ( + String::from(k), + super::PayerRequirement { + mandatory: v.mandatory, + }, + ) + }) + .collect(), + }); + let metadata = serde_json::from_str::>(&p.metadata) .map_err(|_| "deserialize metadata failed")?; @@ -101,6 +134,7 @@ impl TryFrom<&[u8]> for Entrypoint { jpeg, png, currencies, + payer, }) } } @@ -195,7 +229,7 @@ mod ser { } mod de { - use super::super::serde::Currency; + use super::super::serde::{Currency, Payer}; use serde::Deserialize; use std::collections::BTreeMap; use url::Url; @@ -212,6 +246,8 @@ mod de { pub comment_allowed: Option, #[serde(borrow)] pub currencies: Option>>, + #[serde(rename = "payerData")] + pub payer_data: Option>, } #[derive(Deserialize)] @@ -252,6 +288,7 @@ mod tests { assert!(parsed.identifier.is_none()); assert!(parsed.email.is_none()); assert!(parsed.currencies.is_none()); + assert!(parsed.payer.is_none()); } #[test] @@ -368,6 +405,60 @@ mod tests { assert!(!currencies[1].convertible); } + #[test] + fn entrypoint_parse_payer() { + use super::super::{PayerRequirement, PayerRequirementAuth}; + + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/crazy\", \"👋🇧🇴💾\"]]", + "maxSendable": 315, + "minSendable": 314, + "payerData": { + "name": { "mandatory": true }, + "pubkey": { "mandatory": true }, + "identifier": { "mandatory": false }, + "email": { "mandatory": true }, + "auth": { "mandatory": true, "k1": "3132333132333231333132333132333132333132333132333331323132333132" }, + "outro": { "mandatory": false } + } + }"#; + + let parsed: super::Entrypoint = input.as_bytes().try_into().expect("parse"); + let payer = parsed.payer.unwrap(); + + assert!(matches!(payer.name.unwrap(), PayerRequirement { mandatory } if mandatory)); + assert!(matches!(payer.pubkey.unwrap(), PayerRequirement { mandatory } if mandatory)); + assert!(matches!(payer.identifier.unwrap(), PayerRequirement { mandatory } if !mandatory)); + assert!(matches!(payer.email.unwrap(), PayerRequirement { mandatory } if mandatory)); + assert!( + matches!(payer.auth.unwrap(), PayerRequirementAuth { mandatory, k1 } if mandatory && &k1 == b"12312321312312312312312331212312") + ); + + assert_eq!(payer.others.len(), 1); + assert!( + matches!(payer.others.get("outro").unwrap(), PayerRequirement { mandatory } if !mandatory) + ); + + let input = r#"{ + "callback": "https://yuri?o=callback", + "metadata": "[[\"text/plain\", \"boneco do steve magal\"],[\"text/crazy\", \"👋🇧🇴💾\"]]", + "maxSendable": 315, + "minSendable": 314, + "payerData": {} + }"#; + + let parsed: super::Entrypoint = input.as_bytes().try_into().expect("parse"); + let payer = parsed.payer.unwrap(); + + assert!(payer.name.is_none()); + assert!(payer.pubkey.is_none()); + assert!(payer.identifier.is_none()); + assert!(payer.email.is_none()); + assert!(payer.auth.is_none()); + assert_eq!(payer.others.len(), 0); + } + #[test] fn callback_render_base() { let input = r#"{ diff --git a/src/core/pay/server.rs b/src/core/pay/server.rs index 929d6ea..76f7e57 100644 --- a/src/core/pay/server.rs +++ b/src/core/pay/server.rs @@ -11,6 +11,7 @@ pub struct Entrypoint { pub min: u64, pub max: u64, pub currencies: Option>, + pub payer: Option, } impl TryFrom for Vec { @@ -61,6 +62,39 @@ impl TryFrom for Vec { }) .collect() }), + payer: r.payer.as_ref().map(|p| super::serde::Payer { + name: p.name.as_ref().map(|p| super::serde::PayerRequirement { + mandatory: p.mandatory, + }), + pubkey: p.pubkey.as_ref().map(|p| super::serde::PayerRequirement { + mandatory: p.mandatory, + }), + identifier: p + .identifier + .as_ref() + .map(|p| super::serde::PayerRequirement { + mandatory: p.mandatory, + }), + email: p.email.as_ref().map(|p| super::serde::PayerRequirement { + mandatory: p.mandatory, + }), + auth: p.auth.as_ref().map(|p| super::serde::PayerRequirementAuth { + mandatory: p.mandatory, + k1: p.k1, + }), + others: p + .others + .iter() + .map(|(k, v)| { + ( + k as &str, + super::serde::PayerRequirement { + mandatory: v.mandatory, + }, + ) + }) + .collect(), + }), }) .map_err(|_| "serialize failed") } @@ -130,7 +164,7 @@ impl std::fmt::Display for CallbackResponse { } mod ser { - use super::super::serde::Currency; + use super::super::serde::{Currency, Payer}; use serde::Serialize; use std::collections::BTreeMap; use url::Url; @@ -148,6 +182,8 @@ mod ser { pub comment_allowed: u64, #[serde(skip_serializing_if = "Option::is_none")] pub currencies: Option>>, + #[serde(rename = "payerData", skip_serializing_if = "Option::is_none")] + pub payer: Option>, } #[derive(Serialize)] @@ -187,6 +223,7 @@ mod tests { identifier: None, email: None, currencies: None, + payer: None, }; assert_eq!( @@ -209,6 +246,7 @@ mod tests { identifier: None, email: None, currencies: None, + payer: None, }; assert_eq!( @@ -231,6 +269,7 @@ mod tests { identifier: None, email: None, currencies: None, + payer: None, }; assert_eq!( @@ -253,6 +292,7 @@ mod tests { identifier: None, email: None, currencies: None, + payer: None, }; assert_eq!( @@ -275,6 +315,7 @@ mod tests { identifier: Some(String::from("steve@magal.brutal")), email: None, currencies: None, + payer: None, }; assert_eq!( @@ -297,6 +338,7 @@ mod tests { identifier: None, email: Some(String::from("steve@magal.brutal")), currencies: None, + payer: None, }; assert_eq!( @@ -336,6 +378,7 @@ mod tests { convertible: false, }, ]), + payer: None, }; assert_eq!( @@ -344,6 +387,71 @@ mod tests { ); } + #[test] + fn entrypoint_render_payer() { + let query = super::Entrypoint { + 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, + currencies: None, + payer: Some(super::super::Payer { + name: Some(super::super::PayerRequirement { mandatory: false }), + pubkey: Some(super::super::PayerRequirement { mandatory: true }), + identifier: Some(super::super::PayerRequirement { mandatory: false }), + email: Some(super::super::PayerRequirement { mandatory: true }), + auth: Some(super::super::PayerRequirementAuth { + mandatory: false, + k1: *b"12312321312312312312312331212312", + }), + others: [( + String::from("outro"), + super::super::PayerRequirement { mandatory: false }, + )] + .into_iter() + .collect(), + }), + }; + + assert_eq!( + Vec::::try_from(query.clone()).unwrap(), + br#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0,"payerData":{"name":{"mandatory":false},"pubkey":{"mandatory":true},"identifier":{"mandatory":false},"email":{"mandatory":true},"auth":{"mandatory":false,"k1":"3132333132333231333132333132333132333132333132333331323132333132"},"outro":{"mandatory":false}}}"# + ); + + let query = super::Entrypoint { + 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, + currencies: None, + payer: Some(super::super::Payer { + name: None, + pubkey: None, + identifier: None, + email: None, + auth: None, + others: std::collections::HashMap::new(), + }), + }; + + assert_eq!( + Vec::::try_from(query).unwrap(), + br#"{"tag":"payRequest","metadata":"[[\"text/plain\",\"boneco do steve magal\"]]","callback":"https://yuri/?o=callback","minSendable":314,"maxSendable":315,"commentAllowed":0,"payerData":{}}"# + ); + } + #[test] fn callback_parse_base() { let input = "amount=314"; diff --git a/tests/lud06.rs b/tests/lud06.rs index 4838dca..775af10 100644 --- a/tests/lud06.rs +++ b/tests/lud06.rs @@ -26,6 +26,7 @@ async fn test() { identifier: None, email: None, currencies: None, + payer: None, }) } }, diff --git a/tests/lud09.rs b/tests/lud09.rs index 8c33075..20f111c 100644 --- a/tests/lud09.rs +++ b/tests/lud09.rs @@ -26,6 +26,7 @@ async fn test() { identifier: None, email: None, currencies: None, + payer: None, }) } }, diff --git a/tests/lud11.rs b/tests/lud11.rs index 4dee76e..34c5822 100644 --- a/tests/lud11.rs +++ b/tests/lud11.rs @@ -26,6 +26,7 @@ async fn test() { identifier: None, email: None, currencies: None, + payer: None, }) } }, diff --git a/tests/lud12.rs b/tests/lud12.rs index fafc069..e6fc54c 100644 --- a/tests/lud12.rs +++ b/tests/lud12.rs @@ -26,6 +26,7 @@ async fn test() { identifier: None, email: None, currencies: None, + payer: None, }) } }, diff --git a/tests/lud16.rs b/tests/lud16.rs index 2bbb660..e31ace7 100644 --- a/tests/lud16.rs +++ b/tests/lud16.rs @@ -25,6 +25,7 @@ async fn test() { identifier: identifier.clone().filter(|i| i.starts_with('n')), email: identifier.filter(|i| i.starts_with('j')), currencies: None, + payer: None, }) } }, diff --git a/tests/lud21.rs b/tests/lud21.rs index 681d1a9..af15e66 100644 --- a/tests/lud21.rs +++ b/tests/lud21.rs @@ -43,6 +43,7 @@ async fn test() { convertible: false, }, ]), + payer: None, }) } },