lnurl/
pay.rs

1use aes::cipher::block_padding::Pkcs7;
2use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
3use aes::Aes256;
4use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
5use base64::Engine;
6use bitcoin::hashes::sha256::Hash as Sha256;
7use bitcoin::hashes::Hash;
8use bitcoin::key::XOnlyPublicKey;
9use cbc::{Decryptor, Encryptor};
10use serde::{Deserialize, Serialize};
11use std::convert::TryInto;
12use url::Url;
13
14type Aes256CbcEnc = Encryptor<Aes256>;
15type Aes256CbcDec = Decryptor<Aes256>;
16
17use crate::Tag;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct PayResponse {
21    /// a second-level url which give you an invoice with a GET request
22    /// and an amount
23    pub callback: String,
24    /// max sendable amount for a given user on a given service
25    #[serde(rename = "maxSendable")]
26    pub max_sendable: u64,
27    /// min sendable amount for a given user on a given service,
28    /// can not be less than 1 or more than `max_sendable`
29    #[serde(rename = "minSendable")]
30    pub min_sendable: u64,
31    /// tag of the request
32    pub tag: Tag,
33    /// Metadata json which must be presented as raw string here,
34    /// this is required to pass signature verification at a later step
35    pub metadata: String,
36
37    /// Optional, if true, the service allows comments
38    /// the number is the max length of the comment
39    #[serde(rename = "commentAllowed")]
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub comment_allowed: Option<u32>,
42
43    /// Optional, if true, the service allows nostr zaps
44    #[serde(rename = "allowsNostr")]
45    pub allows_nostr: Option<bool>,
46
47    /// Optional, if true, the nostr pubkey that will be used to sign zap events
48    #[serde(rename = "nostrPubkey")]
49    pub nostr_pubkey: Option<XOnlyPublicKey>,
50}
51
52impl PayResponse {
53    pub fn metadata_json(&self) -> serde_json::Value {
54        serde_json::from_str(&self.metadata).unwrap()
55    }
56
57    pub fn metadata_hash(&self) -> [u8; 32] {
58        Sha256::hash(self.metadata.as_bytes()).to_byte_array()
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct LnURLPayInvoice {
64    /// Encoded bolt 11 invoice
65    pub pr: String,
66    /// If this invoice is a hodl invoice
67    pub hodl_invoice: Option<bool>,
68    /// Optional, if present, can be used to display a message to the user
69    /// after the payment has been completed
70    #[serde(rename = "successAction")]
71    #[serde(skip_serializing_if = "Option::is_none")]
72    success_action: Option<SuccessActionParams>,
73}
74
75impl LnURLPayInvoice {
76    pub fn new(invoice: String) -> Self {
77        Self {
78            pr: invoice,
79            hodl_invoice: None,
80            success_action: None,
81        }
82    }
83
84    pub fn invoice(&self) -> &str {
85        self.pr.as_str()
86    }
87
88    pub fn success_action(&self) -> Option<SuccessAction> {
89        self.success_action.clone().map(SuccessAction::from_params)
90    }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub enum SuccessAction {
95    Message(String),
96    Url { url: Url, description: String },
97    AES(AesParams),
98    Unknown(SuccessActionParams),
99}
100
101impl SuccessAction {
102    pub fn tag(&self) -> &str {
103        match self {
104            SuccessAction::Message(_) => "message",
105            SuccessAction::Url { .. } => "url",
106            SuccessAction::AES(_) => "aes",
107            SuccessAction::Unknown(params) => params.tag.as_str(),
108        }
109    }
110
111    pub fn into_params(self) -> SuccessActionParams {
112        match self {
113            SuccessAction::Message(message) => SuccessActionParams {
114                tag: "message".to_string(),
115                message: Some(message),
116                url: None,
117                description: None,
118                ciphertext: None,
119                iv: None,
120            },
121            SuccessAction::Url { url, description } => SuccessActionParams {
122                tag: "url".to_string(),
123                message: None,
124                url: Some(url),
125                description: Some(description),
126                ciphertext: None,
127                iv: None,
128            },
129            SuccessAction::AES(params) => SuccessActionParams {
130                tag: "aes".to_string(),
131                message: None,
132                url: None,
133                description: Some(params.description),
134                ciphertext: Some(params.ciphertext),
135                iv: Some(params.iv),
136            },
137            SuccessAction::Unknown(params) => params,
138        }
139    }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
143pub struct AesParams {
144    pub description: String,
145    pub ciphertext: String,
146    pub iv: String,
147}
148
149impl AesParams {
150    pub fn new(description: String, text: &str, preimage: &[u8; 32]) -> anyhow::Result<AesParams> {
151        let iv = bitcoin::secp256k1::rand::random::<[u8; 16]>();
152        let cipher = Aes256CbcEnc::new(preimage.into(), &iv.into());
153        let encrypted: Vec<u8> = cipher.encrypt_padded_vec_mut::<Pkcs7>(text.as_bytes());
154        let ciphertext = BASE64_STANDARD.encode(encrypted);
155
156        let iv = BASE64_STANDARD.encode(iv);
157        Ok(AesParams {
158            description,
159            ciphertext,
160            iv,
161        })
162    }
163
164    pub fn decrypt(&self, preimage: &[u8; 32]) -> anyhow::Result<String> {
165        // decode base64
166        let iv = BASE64_STANDARD.decode(&self.iv)?;
167        let ciphertext = BASE64_STANDARD.decode(&self.ciphertext)?;
168
169        // check iv length
170        if iv.len() != 16 {
171            return Err(anyhow::anyhow!("iv length is not 16"));
172        }
173        // turn into generic array
174        let iv: [u8; 16] = iv.try_into().unwrap();
175
176        // decrypt
177        let cipher = Aes256CbcDec::new(preimage.into(), &iv.into());
178        let decrypted: Vec<u8> = cipher
179            .decrypt_padded_vec_mut::<Pkcs7>(&ciphertext)
180            .map_err(|_| anyhow::anyhow!("decryption failed"))?;
181
182        Ok(String::from_utf8(decrypted)?)
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
187pub struct SuccessActionParams {
188    pub tag: String,
189    pub message: Option<String>,
190    pub url: Option<Url>,
191    pub description: Option<String>,
192    pub ciphertext: Option<String>,
193    pub iv: Option<String>,
194}
195
196impl SuccessAction {
197    pub fn from_params(params: SuccessActionParams) -> Self {
198        match params.tag.as_str() {
199            "message" => {
200                if params.message.is_none() {
201                    return SuccessAction::Unknown(params);
202                }
203                SuccessAction::Message(params.message.unwrap())
204            }
205            "url" => {
206                if params.url.is_none() || params.description.is_none() {
207                    return SuccessAction::Unknown(params);
208                }
209                SuccessAction::Url {
210                    url: params.url.unwrap(),
211                    description: params.description.unwrap(),
212                }
213            }
214            "aes" => {
215                if params.description.is_none()
216                    || params.ciphertext.is_none()
217                    || params.iv.is_none()
218                {
219                    return SuccessAction::Unknown(params);
220                }
221
222                SuccessAction::AES(AesParams {
223                    description: params.description.unwrap(),
224                    ciphertext: params.ciphertext.unwrap(),
225                    iv: params.iv.unwrap(),
226                })
227            }
228            _ => SuccessAction::Unknown(params),
229        }
230    }
231}
232
233#[cfg(test)]
234mod test {
235    use super::*;
236
237    #[test]
238    fn test_encrypt_decrypt() {
239        let description = "test_description".to_string();
240        let text = "hello world".to_string();
241        let preimage = [1u8; 32];
242
243        let params = AesParams::new(description.clone(), &text, &preimage).unwrap();
244
245        let decrypted = params.decrypt(&preimage).unwrap();
246        assert_eq!(decrypted, text);
247    }
248}