lnurl_pay/
lud06.rs

1// Copyright (c) 2024 Yuki Kishimoto
2// Distributed under the MIT software license
3
4use 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}