1use alloc::string::String;
5use core::str::FromStr;
6
7use bech32::{Bech32, Hrp};
8use serde::{Deserialize, Deserializer, Serialize};
9
10use crate::error::Error;
11
12const PREFIX: &str = "lnurl";
13const HRP_PREFIX: Hrp = Hrp::parse_unchecked(PREFIX);
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct LnUrl {
17 url: String,
18}
19
20impl LnUrl {
21 pub fn new<S>(url: S) -> Self
22 where
23 S: Into<String>,
24 {
25 Self { url: url.into() }
26 }
27
28 #[inline]
29 pub fn decode<S>(lnurl: S) -> Result<Self, Error>
30 where
31 S: AsRef<str>,
32 {
33 Self::from_str(lnurl.as_ref())
34 }
35
36 #[inline]
37 pub fn encode(&self) -> Result<String, Error> {
38 let bytes = self.url.as_bytes();
39 Ok(bech32::encode::<Bech32>(HRP_PREFIX, bytes)?)
40 }
41
42 #[inline]
43 pub fn endpoint(&self) -> String {
44 self.url.clone()
45 }
46}
47
48impl FromStr for LnUrl {
49 type Err = Error;
50
51 fn from_str(s: &str) -> Result<Self, Error> {
52 if s.to_lowercase().starts_with(PREFIX) {
53 let (hrp, bytes) = bech32::decode(s).map_err(|_| Error::InvalidLnUrl)?;
54
55 if hrp != HRP_PREFIX {
56 return Err(Error::InvalidLnUrl);
57 }
58
59 let url = String::from_utf8(bytes).map_err(|_| Error::InvalidLnUrl)?;
60 Ok(Self { url })
61 } else {
62 Err(Error::InvalidLnUrl)
63 }
64 }
65}
66
67impl Serialize for LnUrl {
68 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
69 where
70 S: serde::Serializer,
71 {
72 serializer.serialize_str(&self.encode().map_err(serde::ser::Error::custom)?)
73 }
74}
75
76impl<'de> Deserialize<'de> for LnUrl {
77 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78 where
79 D: Deserializer<'de>,
80 {
81 let lnurl = String::deserialize(deserializer)?;
82 LnUrl::from_str(&lnurl).map_err(serde::de::Error::custom)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use alloc::string::ToString;
89
90 use super::*;
91
92 #[test]
93 fn encode_test() {
94 let url = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";
95 let expected =
96 "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";
97
98 let lnurl = LnUrl::new(url.to_string());
99 assert_eq!(lnurl.encode().unwrap().to_uppercase(), expected);
100 }
101
102 #[test]
103 fn decode_tests() {
104 let str =
105 "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";
106 let expected = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";
107
108 let lnurl = LnUrl::decode(str.to_string()).unwrap();
109 assert_eq!(lnurl.url, expected);
110 }
111}