1use anyhow::{anyhow, ensure, Result};
2use async_trait::async_trait;
3use log::debug;
4use mockall::automock;
5use reqwest::Response;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Debug, Clone)]
10pub struct PayRequestResponse {
11 pub callback: String,
12 #[serde(rename = "maxSendable")]
13 pub max_sendable: u64,
14 #[serde(rename = "minSendable")]
15 pub min_sendable: u64,
16 pub tag: String,
17 pub metadata: String,
18 #[serde(rename = "commentAllowed")]
21 #[serde(default)]
22 pub comment_allowed: Option<u64>,
23}
24
25#[derive(Deserialize, Clone, Debug)]
26pub struct PayRequestCallbackResponse {
27 pub pr: String,
28 pub routes: Vec<String>,
29 #[serde(rename = "successAction")]
31 #[serde(default)]
32 pub success_action: Option<SuccessAction>,
33}
34
35#[derive(Debug, Deserialize, Serialize)]
36pub struct OkResponse {
37 pub status: String,
38}
39
40#[derive(Debug, Deserialize, Serialize)]
41pub struct ErrorResponse {
42 pub status: String,
43 pub reason: String,
44}
45
46#[derive(Serialize, Deserialize, Debug, Clone)]
47pub struct WithdrawRequestResponse {
48 pub tag: String,
49 pub callback: String,
50 pub k1: String,
51 #[serde(rename = "defaultDescription")]
52 pub default_description: String,
53 #[serde(rename = "minWithdrawable")]
54 pub min_withdrawable: u64,
55 #[serde(rename = "maxWithdrawable")]
56 pub max_withdrawable: u64,
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
66#[serde(tag = "tag")]
67pub enum SuccessAction {
68 #[serde(rename = "message")]
69 Message { message: String },
70 #[serde(rename = "url")]
71 Url { description: String, url: String },
72 #[serde(rename = "aes")]
73 Aes {
74 description: String,
75 ciphertext: String,
77 iv: String,
79 },
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
88pub enum ProcessedSuccessAction {
89 Message { message: String },
90 Url { description: String, url: String },
91 Aes { description: String, plaintext: String },
92}
93
94impl SuccessAction {
95 pub fn process(self, preimage: &[u8]) -> Result<ProcessedSuccessAction> {
108 match self {
109 SuccessAction::Message { message } => {
110 ensure!(
111 message.len() <= 144,
112 "Message success action exceeds 144 chars"
113 );
114 Ok(ProcessedSuccessAction::Message { message })
115 }
116 SuccessAction::Url { description, url } => {
117 ensure!(
118 description.len() <= 144,
119 "Url success action description exceeds 144 chars"
120 );
121 Ok(ProcessedSuccessAction::Url { description, url })
122 }
123 SuccessAction::Aes {
124 description,
125 ciphertext,
126 iv,
127 } => {
128 ensure!(
129 description.len() <= 144,
130 "AES success action description exceeds 144 chars"
131 );
132 ensure!(
133 ciphertext.len() <= 4096,
134 "AES success action ciphertext exceeds 4096 chars"
135 );
136 ensure!(
137 iv.len() == 24,
138 "AES success action IV must be exactly 24 base64 chars"
139 );
140 let plaintext =
141 super::pay::decrypt_aes_success_action(preimage, &ciphertext, &iv)?;
142 Ok(ProcessedSuccessAction::Aes {
143 description,
144 plaintext,
145 })
146 }
147 }
148 }
149}
150
151#[async_trait]
152#[automock]
153pub trait LnUrlHttpClient {
154 async fn get_pay_request_response(&self, lnurl: &str) -> Result<PayRequestResponse>;
155 async fn get_pay_request_callback_response(
156 &self,
157 callback_url: &str,
158 ) -> Result<PayRequestCallbackResponse>;
159 async fn get_withdrawal_request_response(&self, url: &str) -> Result<WithdrawRequestResponse>;
160 async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result<OkResponse>;
161 async fn get_json(&self, url: &str) -> Result<serde_json::Value>;
162}
163
164pub struct LnUrlHttpClearnetClient {
165 client: reqwest::Client,
166}
167
168impl LnUrlHttpClearnetClient {
169 pub fn new() -> LnUrlHttpClearnetClient {
170 LnUrlHttpClearnetClient {
171 client: reqwest::Client::new(),
172 }
173 }
174
175 async fn get<T: DeserializeOwned + 'static>(&self, url: &str) -> Result<T> {
176 let response: Response = self.client.get(url).send().await?;
177 match response.json::<T>().await {
178 Ok(body) => Ok(body),
179 Err(e) => {
180 debug!("{}", e);
181 Err(anyhow!("Unable to parse http response body as json"))
182 }
183 }
184 }
185}
186
187#[async_trait]
188impl LnUrlHttpClient for LnUrlHttpClearnetClient {
189 async fn get_pay_request_response(&self, lnurl: &str) -> Result<PayRequestResponse> {
190 self.get::<PayRequestResponse>(lnurl).await
191 }
192
193 async fn get_pay_request_callback_response(
194 &self,
195 callback_url: &str,
196 ) -> Result<PayRequestCallbackResponse> {
197 self.get::<PayRequestCallbackResponse>(callback_url).await
198 }
199
200 async fn get_withdrawal_request_response(&self, url: &str) -> Result<WithdrawRequestResponse> {
201 self.get::<WithdrawRequestResponse>(url).await
202 }
203
204 async fn send_invoice_for_withdraw_request(&self, url: &str) -> Result<OkResponse> {
205 self.get::<OkResponse>(url).await
206 }
207
208 async fn get_json(&self, url: &str) -> Result<serde_json::Value> {
209 self.get::<serde_json::Value>(url).await
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_success_action_message_serde() {
219 let json = r#"{"tag":"message","message":"Thank you!"}"#;
220 let action: SuccessAction = serde_json::from_str(json).unwrap();
221 match action {
222 SuccessAction::Message { message } => assert_eq!(message, "Thank you!"),
223 _ => panic!("Expected Message variant"),
224 }
225 }
226
227 #[test]
228 fn test_success_action_url_serde() {
229 let json = r#"{"tag":"url","description":"View order","url":"https://example.com/order/123"}"#;
230 let action: SuccessAction = serde_json::from_str(json).unwrap();
231 match action {
232 SuccessAction::Url { description, url } => {
233 assert_eq!(description, "View order");
234 assert_eq!(url, "https://example.com/order/123");
235 }
236 _ => panic!("Expected Url variant"),
237 }
238 }
239
240 #[test]
241 fn test_success_action_aes_serde() {
242 let json = r#"{"tag":"aes","description":"Secret","ciphertext":"YWJj","iv":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0"}"#;
243 let action: SuccessAction = serde_json::from_str(json).unwrap();
244 match action {
245 SuccessAction::Aes {
246 description,
247 ciphertext,
248 iv,
249 } => {
250 assert_eq!(description, "Secret");
251 assert_eq!(ciphertext, "YWJj");
252 assert_eq!(iv, "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0");
253 }
254 _ => panic!("Expected Aes variant"),
255 }
256 }
257
258 #[test]
259 fn test_callback_response_without_success_action() {
260 let json = r#"{"pr":"lnbc1...","routes":[]}"#;
261 let resp: PayRequestCallbackResponse = serde_json::from_str(json).unwrap();
262 assert!(resp.success_action.is_none());
263 }
264
265 #[test]
266 fn test_callback_response_with_success_action() {
267 let json =
268 r#"{"pr":"lnbc1...","routes":[],"successAction":{"tag":"message","message":"Done"}}"#;
269 let resp: PayRequestCallbackResponse = serde_json::from_str(json).unwrap();
270 assert!(resp.success_action.is_some());
271 }
272
273 #[test]
274 fn test_pay_request_response_with_comment_allowed() {
275 let json = r#"{"callback":"https://example.com/cb","maxSendable":100000,"minSendable":1000,"tag":"payRequest","metadata":"[]","commentAllowed":140}"#;
276 let resp: PayRequestResponse = serde_json::from_str(json).unwrap();
277 assert_eq!(resp.comment_allowed, Some(140));
278 }
279
280 #[test]
281 fn test_pay_request_response_without_comment_allowed() {
282 let json = r#"{"callback":"https://example.com/cb","maxSendable":100000,"minSendable":1000,"tag":"payRequest","metadata":"[]"}"#;
283 let resp: PayRequestResponse = serde_json::from_str(json).unwrap();
284 assert_eq!(resp.comment_allowed, None);
285 }
286}