diff --git a/README.md b/README.md index df0e3fc..51052d5 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ This library works as a toolkit so you can serve and make your LNURL requests wi - [LUD-18](https://github.com/lnurl/luds/blob/luds/18.md): 🆘 core 🆘 client 🆘 server 🆘 tests - [LUD-19](https://github.com/lnurl/luds/blob/luds/19.md): 🆘 core 🆘 client 🆘 server 🆘 tests - [LUD-20](https://github.com/lnurl/luds/blob/luds/20.md): ✅ core ✅ client ✅ server ⚠️ tests -- [LUD-21 *proposal*](https://github.com/lnurl/luds/blob/8580e3c8cbfd8fc95a6c0e5f7fcb5b048a0d5b61/21.md): ⚠️ core 🆘 client 🆘 server 🆘 tests +- [LUD-21 *proposal*](https://github.com/lnurl/luds/blob/8580e3c8cbfd8fc95a6c0e5f7fcb5b048a0d5b61/21.md): ⚠️ core ⚠️ client ⚠️ server 🆘 tests -- ###### Soon. ™ +###### Soon. ™ ## Future work - Remove SOS signs from above list (by just working on it) diff --git a/src/client.rs b/src/client.rs index 7851323..4417618 100644 --- a/src/client.rs +++ b/src/client.rs @@ -133,10 +133,10 @@ impl Pay<'_> { /// Returns errors on network or deserialization failures. pub async fn invoice( &self, - millisatoshis: u64, + amount: &crate::pay::Amount, comment: Option<&str>, ) -> Result { - let callback = self.core.invoice(millisatoshis, comment); + let callback = self.core.invoice(amount, comment); let response = self .client diff --git a/src/core/pay.rs b/src/core/pay.rs index 8939902..2073aae 100644 --- a/src/core/pay.rs +++ b/src/core/pay.rs @@ -2,6 +2,12 @@ pub const TAG: &str = "payRequest"; pub mod client; pub mod server; +#[derive(Clone, Debug)] +pub enum Amount { + Millisatoshis(u64), + Currency(String, u64), +} + #[derive(Clone, Debug)] pub struct Currency { pub code: String, @@ -26,9 +32,31 @@ mod serde { pub convertible: bool, } - #[derive(Deserialize, Serialize)] - pub(super) struct Callback<'a> { - pub comment: Option<&'a str>, - pub amount: u64, + pub(super) mod amount { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize( + a: &super::super::Amount, + serializer: S, + ) -> Result { + match a { + crate::pay::Amount::Millisatoshis(a) => (*a).serialize(serializer), + crate::pay::Amount::Currency(c, a) => format!("{a}.{c}").serialize(serializer), + } + } + + pub fn deserialize<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result { + let s = <&str>::deserialize(deserializer)?; + + let (amount, currency) = s.split_once('.').map_or((s, None), |(a, c)| (a, Some(c))); + let amount = amount.parse::().map_err(serde::de::Error::custom)?; + + Ok(match currency { + Some(currency) => super::super::Amount::Currency(String::from(currency), amount), + None => super::super::Amount::Millisatoshis(amount), + }) + } } } diff --git a/src/core/pay/client.rs b/src/core/pay/client.rs index 4926e37..53454f0 100644 --- a/src/core/pay/client.rs +++ b/src/core/pay/client.rs @@ -107,10 +107,14 @@ impl TryFrom<&[u8]> for Entrypoint { impl Entrypoint { #[must_use] - pub fn invoice<'a>(&'a self, millisatoshis: u64, comment: Option<&'a str>) -> Callback<'a> { + pub fn invoice<'a>( + &'a self, + amount: &'a super::Amount, + comment: Option<&'a str>, + ) -> Callback<'a> { Callback { url: &self.callback, - millisatoshis, + amount, comment, } } @@ -119,14 +123,14 @@ impl Entrypoint { pub struct Callback<'a> { pub url: &'a url::Url, pub comment: Option<&'a str>, - pub millisatoshis: u64, + pub amount: &'a super::Amount, } impl std::fmt::Display for Callback<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let query = super::serde::Callback { + let query = ser::Callback { comment: self.comment, - amount: self.millisatoshis, + amount: self.amount, }; let querystr = serde_urlencoded::to_string(query).map_err(|_| std::fmt::Error)?; @@ -174,6 +178,17 @@ impl std::str::FromStr for CallbackResponse { } } +mod ser { + use serde::Serialize; + + #[derive(Serialize)] + pub(super) struct Callback<'a> { + pub comment: Option<&'a str>, + #[serde(with = "super::super::serde::amount")] + pub amount: &'a super::super::Amount, + } +} + mod de { use super::super::serde::Currency; use serde::Deserialize; @@ -360,7 +375,9 @@ mod tests { let parsed: super::Entrypoint = input.as_bytes().try_into().expect("parse"); assert_eq!( - parsed.invoice(314, None).to_string(), + parsed + .invoice(&super::super::Amount::Millisatoshis(314), None) + .to_string(), "https://yuri/?o=callback&amount=314" ); } @@ -377,11 +394,38 @@ mod tests { let parsed: super::Entrypoint = input.as_bytes().try_into().expect("parse"); assert_eq!( - parsed.invoice(314, Some("comentario")).to_string(), + parsed + .invoice( + &super::super::Amount::Millisatoshis(314), + Some("comentario") + ) + .to_string(), "https://yuri/?o=callback&comment=comentario&amount=314" ); } + #[test] + fn callback_render_currency() { + let input = r#"{ + "metadata": "[[\"text/plain\", \"boneco do steve magal\"]]", + "callback": "https://yuri?o=callback", + "maxSendable": 315, + "minSendable": 314 + }"#; + + let parsed: super::Entrypoint = input.as_bytes().try_into().expect("parse"); + + assert_eq!( + parsed + .invoice( + &super::super::Amount::Currency(String::from("BRL"), 314), + None + ) + .to_string(), + "https://yuri/?o=callback&amount=314.BRL" + ); + } + #[test] fn callback_response_parse_base() { let input = r#"{ "pr": "pierre" }"#; diff --git a/src/core/pay/server.rs b/src/core/pay/server.rs index 16dade5..e1d65c9 100644 --- a/src/core/pay/server.rs +++ b/src/core/pay/server.rs @@ -67,7 +67,7 @@ impl TryFrom for Vec { } pub struct Callback { - pub millisatoshis: u64, + pub amount: super::Amount, pub comment: Option, } @@ -75,10 +75,10 @@ impl<'a> TryFrom<&'a str> for Callback { type Error = &'static str; fn try_from(s: &'a str) -> Result { - serde_urlencoded::from_str::(s) + serde_urlencoded::from_str::(s) .map_err(|_| "deserialize failed") .map(|query| Callback { - millisatoshis: query.amount, + amount: query.amount, comment: query.comment.map(String::from), }) } @@ -157,6 +157,17 @@ mod ser { } } +mod de { + use serde::Deserialize; + + #[derive(Deserialize)] + pub(super) struct Callback<'a> { + pub comment: Option<&'a str>, + #[serde(with = "super::super::serde::amount")] + pub amount: super::super::Amount, + } +} + #[cfg(test)] mod tests { #[test] @@ -335,7 +346,10 @@ mod tests { let input = "amount=314"; let parsed: super::Callback = input.try_into().expect("parse"); - assert_eq!(parsed.millisatoshis, 314); + assert!(matches!( + parsed.amount, + super::super::Amount::Millisatoshis(314) + )); assert!(parsed.comment.is_none()); } @@ -344,10 +358,25 @@ mod tests { let input = "amount=314&comment=comentario"; let parsed: super::Callback = input.try_into().expect("parse"); - assert_eq!(parsed.millisatoshis, 314); + assert!(matches!( + parsed.amount, + super::super::Amount::Millisatoshis(314) + )); assert_eq!(parsed.comment.unwrap(), "comentario"); } + #[test] + fn callback_parse_currency() { + let input = "amount=314.BRL"; + let parsed: super::Callback = input.try_into().expect("parse"); + + assert!(matches!( + parsed.amount, + super::super::Amount::Currency(c, 314) if c == "BRL" + )); + assert!(parsed.comment.is_none()); + } + #[test] fn callback_response_render_base() { let input = super::CallbackResponse { diff --git a/tests/lud06.rs b/tests/lud06.rs index 9574870..91cbd94 100644 --- a/tests/lud06.rs +++ b/tests/lud06.rs @@ -31,7 +31,7 @@ async fn test() { }, |req: lnurlkit::pay::server::Callback| async move { Ok(lnurlkit::pay::server::CallbackResponse { - pr: format!("pierre:{}", req.millisatoshis), + pr: format!("pierre:{:?}", req.amount), disposable: false, success_action: None, }) @@ -65,7 +65,10 @@ async fn test() { "the destroyer of worlds" ); - let invoice = pr.invoice(314, Some("comment")).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(314), Some("comment")) + .await + .expect("callback"); - assert_eq!(&invoice.pr as &str, "pierre:314"); + assert_eq!(&invoice.pr as &str, "pierre:Millisatoshis(314)"); } diff --git a/tests/lud09.rs b/tests/lud09.rs index 97a6cbc..8464449 100644 --- a/tests/lud09.rs +++ b/tests/lud09.rs @@ -33,9 +33,10 @@ async fn test() { Ok(lnurlkit::pay::server::CallbackResponse { pr: String::new(), disposable: false, - success_action: if req.millisatoshis == 0 { + success_action: if matches!(req.amount, lnurlkit::pay::Amount::Millisatoshis(0)) + { None - } else if req.millisatoshis == 1 { + } else if matches!(req.amount, lnurlkit::pay::Amount::Millisatoshis(1)) { Some(lnurlkit::pay::server::SuccessAction::Message( req.comment.map(|a| a.to_string()).unwrap_or_default(), )) @@ -68,11 +69,17 @@ async fn test() { panic!("not pay request"); }; - let invoice = pr.invoice(0, None).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(0), None) + .await + .expect("callback"); assert!(invoice.success_action.is_none()); - let invoice = pr.invoice(1, Some("mensagem")).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(1), Some("mensagem")) + .await + .expect("callback"); let Some(lnurlkit::pay::client::SuccessAction::Message(m)) = invoice.success_action else { panic!("bad success action"); @@ -80,7 +87,10 @@ async fn test() { assert_eq!(&m as &str, "mensagem"); - let invoice = pr.invoice(2, Some("descricao")).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(2), Some("descricao")) + .await + .expect("callback"); let Some(lnurlkit::pay::client::SuccessAction::Url(u, d)) = invoice.success_action else { panic!("bad success action"); diff --git a/tests/lud11.rs b/tests/lud11.rs index 8f92163..49e698a 100644 --- a/tests/lud11.rs +++ b/tests/lud11.rs @@ -32,7 +32,7 @@ async fn test() { |req: lnurlkit::pay::server::Callback| async move { Ok(lnurlkit::pay::server::CallbackResponse { pr: String::new(), - disposable: req.millisatoshis % 2 == 0, + disposable: matches!(req.amount, lnurlkit::pay::Amount::Millisatoshis(a) if a % 2 == 0), success_action: None, }) }, @@ -57,10 +57,16 @@ async fn test() { panic!("not pay request"); }; - let invoice = pr.invoice(314, None).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(314), None) + .await + .expect("callback"); assert!(invoice.disposable); - let invoice = pr.invoice(315, None).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(315), None) + .await + .expect("callback"); assert!(!invoice.disposable); } diff --git a/tests/lud12.rs b/tests/lud12.rs index 78755d2..ac57dcc 100644 --- a/tests/lud12.rs +++ b/tests/lud12.rs @@ -59,11 +59,20 @@ async fn test() { assert_eq!(pr.core.comment_size.unwrap(), 140); - let invoice = pr.invoice(314, None).await.expect("callback"); + let invoice = pr + .invoice(&lnurlkit::pay::Amount::Millisatoshis(314), None) + .await + .expect("callback"); assert_eq!(&invoice.pr as &str, "pierre:None"); - let invoice = pr.invoice(314, Some("comentario")).await.expect("callback"); + let invoice = pr + .invoice( + &lnurlkit::pay::Amount::Millisatoshis(314), + Some("comentario"), + ) + .await + .expect("callback"); assert_eq!(&invoice.pr as &str, "pierre:Some(\"comentario\")"); } diff --git a/tests/lud16.rs b/tests/lud16.rs index cff5773..2bbb660 100644 --- a/tests/lud16.rs +++ b/tests/lud16.rs @@ -28,9 +28,9 @@ async fn test() { }) } }, - |req: lnurlkit::pay::server::Callback| async move { + |_: lnurlkit::pay::server::Callback| async move { Ok(lnurlkit::pay::server::CallbackResponse { - pr: format!("pierre:{}", req.millisatoshis), + pr: String::from("pierre"), disposable: false, success_action: None, })