Skip to main content

gl_client/lnurl/
utils.rs

1use std::str::FromStr;
2
3use anyhow::{anyhow, Result};
4use bech32::{FromBase32, ToBase32};
5
6use crate::lightning_invoice::Bolt11Invoice;
7
8/// Decode an LNURL bech32 string into the underlying URL (LUD-01).
9pub fn parse_lnurl(lnurl: &str) -> Result<String> {
10    let (_hrp, data, _variant) =
11        bech32::decode(lnurl).map_err(|e| anyhow!("Failed to decode lnurl: {}", e))?;
12
13    let vec = Vec::<u8>::from_base32(&data)
14        .map_err(|e| anyhow!("Failed to base32 decode data: {}", e))?;
15
16    let url = String::from_utf8(vec).map_err(|e| anyhow!("Failed to convert to utf-8: {}", e))?;
17    Ok(url)
18}
19
20/// Encode a URL as an LNURL bech32 string (LUD-01).
21///
22/// Returns uppercase by convention (for QR code compatibility).
23pub fn lnurl_encode(url: &str) -> Result<String> {
24    let data = url.as_bytes().to_base32();
25    bech32::encode("lnurl", data, bech32::Variant::Bech32)
26        .map(|s| s.to_uppercase())
27        .map_err(|e| anyhow!("Failed to encode lnurl: {}", e))
28}
29
30/// Extract the "text/plain" description from LNURL metadata JSON.
31///
32/// Metadata is a JSON array of `["mime", "content"]` pairs.
33/// Returns the content of the first "text/plain" entry, or None.
34pub fn extract_description_from_metadata(metadata: &str) -> Option<String> {
35    let entries: Vec<Vec<String>> = serde_json::from_str(metadata).ok()?;
36    for entry in entries {
37        if entry.len() >= 2 && entry[0] == "text/plain" {
38            return Some(entry[1].clone());
39        }
40    }
41    None
42}
43
44/// Parse a BOLT11 invoice string.
45pub fn parse_invoice(invoice_str: &str) -> Result<Bolt11Invoice> {
46    Bolt11Invoice::from_str(invoice_str)
47        .map_err(|e| anyhow!(format!("Failed to parse invoice: {}", e)))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_lnurl_encode_decode_roundtrip() {
56        let url = "https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b7e86a0850";
57        let encoded = lnurl_encode(url).unwrap();
58        assert!(encoded.starts_with("LNURL1"));
59        let decoded = parse_lnurl(&encoded).unwrap();
60        assert_eq!(decoded, url);
61    }
62
63    #[test]
64    fn test_lnurl_decode_is_case_insensitive() {
65        let url = "https://example.com/lnurl";
66        let encoded = lnurl_encode(url).unwrap();
67        // Uppercase (default) should work
68        let decoded = parse_lnurl(&encoded).unwrap();
69        assert_eq!(decoded, url);
70        // Lowercase should also work
71        let decoded = parse_lnurl(&encoded.to_lowercase()).unwrap();
72        assert_eq!(decoded, url);
73    }
74
75    #[test]
76    fn test_extract_description_from_metadata() {
77        let metadata = r#"[["text/plain", "Pay to example"]]"#;
78        assert_eq!(
79            extract_description_from_metadata(metadata),
80            Some("Pay to example".to_string())
81        );
82    }
83
84    #[test]
85    fn test_extract_description_from_metadata_with_multiple_entries() {
86        let metadata =
87            r#"[["text/identifier", "user@example.com"], ["text/plain", "Pay user"]]"#;
88        assert_eq!(
89            extract_description_from_metadata(metadata),
90            Some("Pay user".to_string())
91        );
92    }
93
94    #[test]
95    fn test_extract_description_from_metadata_missing() {
96        let metadata = r#"[["text/identifier", "user@example.com"]]"#;
97        assert_eq!(extract_description_from_metadata(metadata), None);
98    }
99
100    #[test]
101    fn test_extract_description_from_metadata_invalid_json() {
102        assert_eq!(extract_description_from_metadata("not json"), None);
103    }
104}