Skip to content

Commit

Permalink
feat(pay): add preliminary support for currencies on callback
Browse files Browse the repository at this point in the history
  • Loading branch information
lsunsi committed Dec 16, 2023
1 parent d187345 commit abed4a0
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::pay::client::CallbackResponse, &'static str> {
let callback = self.core.invoice(millisatoshis, comment);
let callback = self.core.invoice(amount, comment);

let response = self
.client
Expand Down
36 changes: 32 additions & 4 deletions src/core/pay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<S: Serializer>(
a: &super::super::Amount,
serializer: S,
) -> Result<S::Ok, S::Error> {
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<super::super::Amount, D::Error> {
let s = <&str>::deserialize(deserializer)?;

let (amount, currency) = s.split_once('.').map_or((s, None), |(a, c)| (a, Some(c)));
let amount = amount.parse::<u64>().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),
})
}
}
}
58 changes: 51 additions & 7 deletions src/core/pay/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand All @@ -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)?;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"
);
}
Expand All @@ -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" }"#;
Expand Down
39 changes: 34 additions & 5 deletions src/core/pay/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,18 @@ impl TryFrom<Entrypoint> for Vec<u8> {
}

pub struct Callback {
pub millisatoshis: u64,
pub amount: super::Amount,
pub comment: Option<String>,
}

impl<'a> TryFrom<&'a str> for Callback {
type Error = &'static str;

fn try_from(s: &'a str) -> Result<Self, Self::Error> {
serde_urlencoded::from_str::<super::serde::Callback>(s)
serde_urlencoded::from_str::<de::Callback>(s)
.map_err(|_| "deserialize failed")
.map(|query| Callback {
millisatoshis: query.amount,
amount: query.amount,
comment: query.comment.map(String::from),
})
}
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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());
}

Expand All @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions tests/lud06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -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)");
}
20 changes: 15 additions & 5 deletions tests/lud09.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
))
Expand Down Expand Up @@ -68,19 +69,28 @@ 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");
};

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");
Expand Down
12 changes: 9 additions & 3 deletions tests/lud11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
},
Expand All @@ -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);
}
13 changes: 11 additions & 2 deletions tests/lud12.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\")");
}
4 changes: 2 additions & 2 deletions tests/lud16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down

0 comments on commit abed4a0

Please sign in to comment.