1use std::str::FromStr;
2
3use anyhow::{anyhow, Result};
4use bech32::{FromBase32, ToBase32};
5
6use crate::lightning_invoice::Bolt11Invoice;
7
8pub 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
20pub 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
30pub 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
44pub 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 let decoded = parse_lnurl(&encoded).unwrap();
69 assert_eq!(decoded, url);
70 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}