lnurl-rs 0.10.0

A basic LNURL implementation in Rust
Documentation
use crate::lightning_address::LightningAddress;
use crate::Error;
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;

#[derive(Debug, PartialEq, Clone, Ord, PartialOrd, Eq, Hash)]
pub struct LnUrl {
    pub url: String,
}

impl LnUrl {
    #[inline]
    pub fn encode(&self) -> String {
        bech32::encode::<bech32::Bech32>(bech32::Hrp::parse("lnurl").unwrap(), self.url.as_bytes())
            .unwrap()
    }

    pub fn is_lnurl_auth(&self) -> bool {
        self.url.contains("tag=login") && self.url.contains("k1=")
    }

    pub fn lightning_address(&self) -> Option<LightningAddress> {
        let url = url::Url::from_str(&self.url).ok()?;
        let local_part = url.path().strip_prefix("/.well-known/lnurlp/")?;
        LightningAddress::from_domain_and_local_part(url.host_str()?, local_part).ok()
    }

    #[inline]
    pub fn decode(lnurl: String) -> Result<LnUrl, Error> {
        LnUrl::from_str(&lnurl)
    }

    #[inline]
    pub fn from_url(url: String) -> LnUrl {
        LnUrl { url }
    }
}

impl Display for LnUrl {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.encode())
    }
}

impl Serialize for LnUrl {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.encode())
    }
}

impl<'de> Deserialize<'de> for LnUrl {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        LnUrl::from_str(&s).map_err(serde::de::Error::custom)
    }
}

impl FromStr for LnUrl {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Error> {
        if s.to_lowercase().starts_with("lnurl") {
            let (_, bytes) = bech32::decode(s).map_err(|_| Error::InvalidLnUrl)?;
            let url = String::from_utf8(bytes).map_err(|_| Error::InvalidLnUrl)?;
            Ok(LnUrl { url })
        } else {
            Err(Error::InvalidLnUrl)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn encode_test() {
        let url = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";
        let expected =
            "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";

        let lnurl = LnUrl::from_url(url.to_string());
        assert_eq!(lnurl.to_string().to_uppercase(), expected);
    }

    #[test]
    fn decode_tests() {
        let str =
            "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";
        let expected = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";

        let lnurl = LnUrl::decode(str.to_string()).unwrap();
        assert_eq!(lnurl.url, expected);
    }

    #[test]
    fn lnurl_auth_test() {
        let str = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df&tag=login&k1=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";
        let lnurl = LnUrl::from_url(str.to_string());
        assert!(lnurl.is_lnurl_auth());

        let str = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df&tag=login";
        let lnurl = LnUrl::from_url(str.to_string());
        assert!(!lnurl.is_lnurl_auth());

        let str = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df&k1=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df";
        let lnurl = LnUrl::from_url(str.to_string());
        assert!(!lnurl.is_lnurl_auth());
    }

    #[test]
    fn lnurl_to_lightning_address() {
        let lightning_address = LightningAddress::from_str("me@benthecarman.com").unwrap();
        let lnurl = lightning_address.lnurl();

        assert_eq!(lnurl.lightning_address(), Some(lightning_address));
    }
}