1use bech32::{Bech32, Hrp};
2use lightning_invoice::Bolt11Invoice;
3use serde::{Deserialize, Serialize};
4use serde_with::hex::Hex;
5use serde_with::serde_as;
6use url::Url;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "status")]
12pub enum LnurlResponse<T> {
13 #[serde(rename = "OK")]
14 Ok(T),
15 #[serde(rename = "ERROR")]
16 Error { reason: String },
17}
18
19impl<T> LnurlResponse<T> {
20 pub fn into_result(self) -> Result<T, String> {
21 match self {
22 Self::Ok(data) => Ok(data),
23 Self::Error { reason } => Err(reason),
24 }
25 }
26}
27
28pub fn parse_lnurl(s: &str) -> Option<Url> {
30 let (hrp, data) = bech32::decode(s).ok()?;
31
32 if hrp.as_str() != "lnurl" {
33 return None;
34 }
35
36 String::from_utf8(data)
37 .ok()
38 .and_then(|s| Url::parse(&s).ok())
39}
40
41pub fn encode_lnurl(url: &Url) -> String {
43 bech32::encode::<Bech32>(
44 Hrp::parse("lnurl").expect("valid hrp"),
45 url.as_str().as_bytes(),
46 )
47 .expect("encoding succeeds")
48}
49
50pub fn parse_address(s: &str) -> Option<Url> {
52 let (user, domain) = s.split_once('@')?;
53
54 if user.is_empty() || domain.is_empty() {
55 return None;
56 }
57
58 Url::parse(&format!("https://{domain}/.well-known/lnurlp/{user}")).ok()
59}
60
61pub fn pay_request_tag() -> String {
62 "payRequest".to_string()
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct PayResponse {
69 pub tag: String,
70 pub callback: String,
71 pub metadata: String,
72 pub min_sendable: u64,
73 pub max_sendable: u64,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct InvoiceResponse {
79 pub pr: Bolt11Invoice,
81 pub verify: Option<String>,
83}
84
85#[serde_as]
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct VerifyResponse {
89 pub settled: bool,
90 #[serde_as(as = "Option<Hex>")]
91 pub preimage: Option<[u8; 32]>,
92}
93
94pub async fn request(url: &Url) -> Result<PayResponse, String> {
96 reqwest::get(url.clone())
97 .await
98 .map_err(|e| e.to_string())?
99 .json::<LnurlResponse<PayResponse>>()
100 .await
101 .map_err(|e| e.to_string())?
102 .into_result()
103}
104
105pub async fn get_invoice(
107 response: &PayResponse,
108 amount_msat: u64,
109) -> Result<InvoiceResponse, String> {
110 if amount_msat < response.min_sendable {
111 return Err("Amount too low".to_string());
112 }
113
114 if amount_msat > response.max_sendable {
115 return Err("Amount too high".to_string());
116 }
117
118 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
119
120 reqwest::get(callback_url)
121 .await
122 .map_err(|e| e.to_string())?
123 .json::<LnurlResponse<InvoiceResponse>>()
124 .await
125 .map_err(|e| e.to_string())?
126 .into_result()
127}
128
129pub async fn verify_invoice(url: &str) -> Result<VerifyResponse, String> {
131 reqwest::get(url)
132 .await
133 .map_err(|e| e.to_string())?
134 .json::<LnurlResponse<VerifyResponse>>()
135 .await
136 .map_err(|e| e.to_string())?
137 .into_result()
138}