breez_sdk_core/lnurl/
pay.rs

1use sdk_common::prelude::*;
2use serde::Serialize;
3
4use crate::Payment;
5
6/// Contains the result of the entire LNURL-pay interaction, as reported by the LNURL endpoint.
7///
8/// * `EndpointSuccess` indicates the payment is complete. The endpoint may return a `SuccessActionProcessed`,
9///   in which case, the wallet has to present it to the user as described in
10///   <https://github.com/lnurl/luds/blob/luds/09.md>
11///
12/// * `EndpointError` indicates a generic issue the LNURL endpoint encountered, including a freetext
13///   field with the reason.
14///
15/// * `PayError` indicates that an error occurred while trying to pay the invoice from the LNURL endpoint.
16///   This includes the payment hash of the failed invoice and the failure reason.
17#[derive(Serialize)]
18#[allow(clippy::large_enum_variant)]
19pub enum LnUrlPayResult {
20    EndpointSuccess { data: LnUrlPaySuccessData },
21    EndpointError { data: LnUrlErrorData },
22    PayError { data: LnUrlPayErrorData },
23}
24
25#[derive(Serialize)]
26pub struct LnUrlPaySuccessData {
27    pub payment: Payment,
28    pub success_action: Option<SuccessActionProcessed>,
29}
30
31#[cfg(test)]
32pub(crate) mod tests {
33    use std::sync::Arc;
34
35    use anyhow::{anyhow, Result};
36    use gl_client::bitcoin::hashes::hex::ToHex;
37    use gl_client::pb::cln::pay_response::PayStatus;
38    use rand::random;
39    use serde_json::json;
40
41    use crate::bitcoin::hashes::{sha256, Hash};
42    use crate::breez_services::tests::{breez_services_with, get_dummy_node_state};
43    use crate::lnurl::pay::*;
44    use crate::{test_utils::*, LnUrlPayRequest};
45
46    struct LnurlPayCallbackParams {
47        error: Option<String>,
48        pr: Option<String>,
49    }
50
51    struct AesPayCallbackParams {
52        error: Option<String>,
53        pr: Option<String>,
54        sa_data: AesSuccessActionDataDecrypted,
55        iv_bytes: [u8; 16],
56        key_bytes: [u8; 32],
57    }
58
59    /// Mock an LNURL-pay endpoint that responds with no Success Action
60    fn mock_lnurl_pay_callback_endpoint_no_success_action(
61        mock_rest_client: &MockRestClient,
62        callback_params: LnurlPayCallbackParams,
63    ) {
64        let LnurlPayCallbackParams { error, pr } = callback_params;
65
66        let response_body = match error {
67            None => json!({
68                "pr": pr.unwrap_or_else(|| "token-invoice".to_string()),
69                "routes": []
70            })
71            .to_string(),
72            Some(err_reason) => json!({
73                "status": "ERROR",
74                "reason": err_reason
75            })
76            .to_string(),
77        };
78
79        mock_rest_client.add_response(MockResponse::new(200, response_body));
80    }
81
82    /// Mock an LNURL-pay endpoint that responds with an unsupported Success Action
83    fn mock_lnurl_pay_callback_endpoint_unsupported_success_action(
84        mock_rest_client: &MockRestClient,
85        callback_params: LnurlPayCallbackParams,
86    ) {
87        let LnurlPayCallbackParams { error, pr } = callback_params;
88
89        let response_body = match error {
90            None => json!({
91                "pr": pr.unwrap_or_else(|| "token-invoice".to_string()),
92                "routes": [],
93                "successAction": {
94                    "tag": "random-type-that-is-not-supported",
95                    "message": "test msg"
96                }
97            })
98            .to_string(),
99            Some(err_reason) => json!({
100                "status": "ERROR",
101                "reason": err_reason
102            })
103            .to_string(),
104        };
105
106        mock_rest_client.add_response(MockResponse::new(200, response_body));
107    }
108
109    /// Mock an LNURL-pay endpoint that responds with a Success Action of type message
110    fn mock_lnurl_pay_callback_endpoint_msg_success_action(
111        mock_rest_client: &MockRestClient,
112        callback_params: LnurlPayCallbackParams,
113    ) {
114        let LnurlPayCallbackParams { error, pr } = callback_params;
115
116        let response_body = match error {
117            None => json!({
118                "pr": pr.unwrap_or_else(|| "token-invoice".to_string()),
119                "routes":[],
120                "successAction": {
121                    "tag": "message",
122                    "message": "test msg"
123                }
124            })
125            .to_string(),
126            Some(err_reason) => json!({
127                "status": "ERROR",
128                "reason": err_reason
129            })
130            .to_string(),
131        };
132
133        mock_rest_client.add_response(MockResponse::new(200, response_body));
134    }
135
136    /// Mock an LNURL-pay endpoint that responds with a Success Action of type URL
137    fn mock_lnurl_pay_callback_endpoint_url_success_action(
138        mock_rest_client: &MockRestClient,
139        callback_params: LnurlPayCallbackParams,
140        success_action_url: Option<&str>,
141    ) {
142        let LnurlPayCallbackParams { error, pr } = callback_params;
143
144        let response_body = match error {
145            None => json!({
146                "pr": pr.unwrap_or_else(|| "token-invoice".to_string()),
147                "routes":[],
148                "successAction": {
149                    "tag": "url",
150                    "description": "test description",
151                    "url": success_action_url.unwrap_or("http://localhost:8080/test-url"),
152                }
153            })
154            .to_string(),
155            Some(err_reason) => json!({
156                "status": "ERROR",
157                "reason": err_reason
158            })
159            .to_string(),
160        };
161
162        mock_rest_client.add_response(MockResponse::new(200, response_body));
163    }
164
165    /// Mock an LNURL-pay endpoint that responds with a Success Action of type AES
166    fn mock_lnurl_pay_callback_endpoint_aes_success_action(
167        mock_rest_client: &MockRestClient,
168        aes_callback_params: AesPayCallbackParams,
169    ) {
170        let AesPayCallbackParams {
171            error,
172            pr,
173            sa_data,
174            iv_bytes,
175            key_bytes,
176        } = aes_callback_params;
177
178        let iv_base64 = base64::encode(iv_bytes);
179        let cipertext =
180            AesSuccessActionData::encrypt(&key_bytes, &iv_bytes, sa_data.plaintext).unwrap();
181
182        let response_body = match error {
183            None => json!({
184                "pr": pr.unwrap_or_else(|| "token-invoice".to_string()),
185                "routes": [],
186                "successAction": {
187                    "tag": "aes",
188                    "description": sa_data.description,
189                    "iv": iv_base64,
190                    "ciphertext": cipertext
191                }
192            })
193            .to_string(),
194            Some(err_reason) => json!({
195                "status": "ERROR",
196                "reason": err_reason
197            })
198            .to_string(),
199        };
200
201        mock_rest_client.add_response(MockResponse::new(200, response_body));
202    }
203
204    fn get_test_pay_req_data(
205        min_sendable: u64,
206        max_sendable: u64,
207        comment_len: u16,
208    ) -> LnUrlPayRequestData {
209        LnUrlPayRequestData {
210            min_sendable,
211            max_sendable,
212            comment_allowed: comment_len,
213            metadata_str: "".into(),
214            callback: "http://localhost:8080/callback".into(),
215            domain: "localhost".into(),
216            allows_nostr: false,
217            nostr_pubkey: None,
218            ln_address: None,
219        }
220    }
221
222    #[test]
223    fn test_lnurl_pay_validate_invoice() -> Result<()> {
224        let req = get_test_pay_req_data(0, 100_000, 0);
225        let temp_desc = req.metadata_str.clone();
226        let inv = rand_invoice_with_description_hash(temp_desc.clone())?;
227        let payreq: String = rand_invoice_with_description_hash(temp_desc)?.to_string();
228
229        assert!(validate_invoice(
230            inv.amount_milli_satoshis().unwrap(),
231            &payreq,
232            Network::Bitcoin
233        )
234        .is_ok());
235        assert!(validate_invoice(
236            inv.amount_milli_satoshis().unwrap() + 1000,
237            &payreq,
238            Network::Bitcoin,
239        )
240        .is_err());
241
242        Ok(())
243    }
244
245    #[test]
246    fn test_lnurl_pay_validate_invoice_network() -> Result<()> {
247        let req = get_test_pay_req_data(0, 50_000, 0);
248        let temp_desc = req.metadata_str.clone();
249        let inv = rand_invoice_with_description_hash(temp_desc.clone())?;
250        let payreq: String = rand_invoice_with_description_hash(temp_desc)?.to_string();
251
252        assert!(validate_invoice(
253            inv.amount_milli_satoshis().unwrap(),
254            &payreq,
255            Network::Bitcoin,
256        )
257        .is_ok());
258        assert!(validate_invoice(
259            inv.amount_milli_satoshis().unwrap() + 1000,
260            &payreq,
261            Network::Bitcoin,
262        )
263        .is_err());
264
265        Ok(())
266    }
267
268    #[test]
269    fn test_lnurl_pay_validate_invoice_wrong_network() -> Result<()> {
270        let req = get_test_pay_req_data(0, 25_000, 0);
271        let temp_desc = req.metadata_str.clone();
272        let inv = rand_invoice_with_description_hash(temp_desc.clone())?;
273        let payreq: String = rand_invoice_with_description_hash(temp_desc)?.to_string();
274
275        assert!(validate_invoice(
276            inv.amount_milli_satoshis().unwrap(),
277            &payreq,
278            Network::Testnet,
279        )
280        .is_err());
281
282        Ok(())
283    }
284
285    #[tokio::test]
286    async fn test_lnurl_pay_no_success_action() -> Result<()> {
287        let mock_rest_client = MockRestClient::new();
288        let comment = rand_string(COMMENT_LENGTH as usize);
289        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
290        let temp_desc = pay_req.metadata_str.clone();
291        let inv = rand_invoice_with_description_hash(temp_desc)?;
292        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
293
294        mock_lnurl_pay_callback_endpoint_no_success_action(
295            &mock_rest_client,
296            LnurlPayCallbackParams {
297                error: None,
298                pr: Some(inv.to_string()),
299            },
300        );
301
302        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
303        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
304        match mock_breez_services
305            .lnurl_pay(LnUrlPayRequest {
306                data: pay_req,
307                amount_msat: user_amount_msat,
308                use_trampoline: false,
309                comment: Some(comment),
310                payment_label: None,
311                validate_success_action_url: None,
312            })
313            .await?
314        {
315            LnUrlPayResult::EndpointSuccess {
316                data:
317                    LnUrlPaySuccessData {
318                        success_action: None,
319                        ..
320                    },
321            } => Ok(()),
322            LnUrlPayResult::EndpointSuccess {
323                data:
324                    LnUrlPaySuccessData {
325                        success_action: Some(_),
326                        ..
327                    },
328            } => Err(anyhow!("Unexpected success action")),
329            _ => Err(anyhow!("Unexpected success action type")),
330        }
331    }
332
333    static COMMENT_LENGTH: u16 = 10;
334
335    #[tokio::test]
336    async fn test_lnurl_pay_unsupported_success_action() -> Result<()> {
337        let mock_rest_client = MockRestClient::new();
338        let user_amount_msat = 11000;
339        let comment = rand_string(COMMENT_LENGTH as usize);
340        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
341
342        mock_lnurl_pay_callback_endpoint_unsupported_success_action(&mock_rest_client, LnurlPayCallbackParams {
343                error: None,
344                pr: Some("lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz".to_string()),
345            });
346
347        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
348        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
349        let r = mock_breez_services
350            .lnurl_pay(LnUrlPayRequest {
351                data: pay_req,
352                amount_msat: user_amount_msat,
353                use_trampoline: false,
354                comment: Some(comment),
355                payment_label: None,
356                validate_success_action_url: None,
357            })
358            .await;
359        // An unsupported Success Action results in an error
360        assert!(r.is_err());
361
362        Ok(())
363    }
364
365    #[tokio::test]
366    async fn test_lnurl_pay_success_payment_hash() -> Result<()> {
367        let mock_rest_client = MockRestClient::new();
368        let comment = rand_string(COMMENT_LENGTH as usize);
369        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
370        let temp_desc = pay_req.metadata_str.clone();
371        let inv = rand_invoice_with_description_hash(temp_desc)?;
372        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
373
374        mock_lnurl_pay_callback_endpoint_msg_success_action(
375            &mock_rest_client,
376            LnurlPayCallbackParams {
377                error: None,
378                pr: Some(inv.to_string()),
379            },
380        );
381
382        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
383        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
384        match mock_breez_services
385            .lnurl_pay(LnUrlPayRequest {
386                data: pay_req,
387                amount_msat: user_amount_msat,
388                use_trampoline: false,
389                comment: Some(comment),
390                payment_label: None,
391                validate_success_action_url: None,
392            })
393            .await?
394        {
395            LnUrlPayResult::EndpointSuccess { data } => match data.payment.id {
396                s if s == inv.payment_hash().to_hex() => Ok(()),
397                _ => Err(anyhow!("Unexpected payment hash")),
398            },
399            _ => Err(anyhow!("Unexpected result")),
400        }
401    }
402
403    #[tokio::test]
404    async fn test_lnurl_pay_msg_success_action() -> Result<()> {
405        let mock_rest_client = MockRestClient::new();
406        let comment = rand_string(COMMENT_LENGTH as usize);
407        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
408        let temp_desc = pay_req.metadata_str.clone();
409        let inv = rand_invoice_with_description_hash(temp_desc)?;
410        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
411
412        mock_lnurl_pay_callback_endpoint_msg_success_action(
413            &mock_rest_client,
414            LnurlPayCallbackParams {
415                error: None,
416                pr: Some(inv.to_string()),
417            },
418        );
419
420        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
421        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
422        match mock_breez_services
423            .lnurl_pay(LnUrlPayRequest {
424                data: pay_req,
425                amount_msat: user_amount_msat,
426                use_trampoline: false,
427                comment: Some(comment),
428                payment_label: None,
429                validate_success_action_url: None,
430            })
431            .await?
432        {
433            LnUrlPayResult::EndpointSuccess {
434                data:
435                    LnUrlPaySuccessData {
436                        success_action: None,
437                        ..
438                    },
439            } => Err(anyhow!(
440                "Expected success action in callback, but none provided"
441            )),
442            LnUrlPayResult::EndpointSuccess {
443                data:
444                    LnUrlPaySuccessData {
445                        success_action: Some(SuccessActionProcessed::Message { data: msg }),
446                        ..
447                    },
448            } => match msg.message {
449                s if s == "test msg" => Ok(()),
450                _ => Err(anyhow!("Unexpected success action message content")),
451            },
452            _ => Err(anyhow!("Unexpected success action type")),
453        }
454    }
455
456    #[tokio::test]
457    async fn test_lnurl_pay_msg_success_action_incorrect_amount() -> Result<()> {
458        let mock_rest_client = MockRestClient::new();
459        let comment = rand_string(COMMENT_LENGTH as usize);
460        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
461        let temp_desc = pay_req.metadata_str.clone();
462        let inv = rand_invoice_with_description_hash(temp_desc)?;
463        let user_amount_msat = inv.amount_milli_satoshis().unwrap() + 1000;
464
465        mock_lnurl_pay_callback_endpoint_msg_success_action(
466            &mock_rest_client,
467            LnurlPayCallbackParams {
468                error: None,
469                pr: Some(inv.to_string()),
470            },
471        );
472
473        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
474        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
475        assert!(mock_breez_services
476            .lnurl_pay(LnUrlPayRequest {
477                data: pay_req,
478                amount_msat: user_amount_msat,
479                use_trampoline: false,
480                comment: Some(comment),
481                payment_label: None,
482                validate_success_action_url: None,
483            })
484            .await
485            .is_err());
486
487        Ok(())
488    }
489
490    #[tokio::test]
491    async fn test_lnurl_pay_msg_success_action_error_from_endpoint() -> Result<()> {
492        let mock_rest_client = MockRestClient::new();
493        let comment = rand_string(COMMENT_LENGTH as usize);
494        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
495        let temp_desc = pay_req.metadata_str.clone();
496        let inv = rand_invoice_with_description_hash(temp_desc)?;
497        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
498        let expected_error_msg = "Error message from LNURL endpoint";
499
500        mock_lnurl_pay_callback_endpoint_msg_success_action(
501            &mock_rest_client,
502            LnurlPayCallbackParams {
503                error: Some(expected_error_msg.to_string()),
504                pr: Some(inv.to_string()),
505            },
506        );
507
508        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
509        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
510        let res = mock_breez_services
511            .lnurl_pay(LnUrlPayRequest {
512                data: pay_req,
513                amount_msat: user_amount_msat,
514                use_trampoline: false,
515                comment: Some(comment),
516                payment_label: None,
517                validate_success_action_url: None,
518            })
519            .await;
520        assert!(matches!(res, Ok(LnUrlPayResult::EndpointError { data: _ })));
521
522        if let Ok(LnUrlPayResult::EndpointError { data: err_msg }) = res {
523            assert_eq!(expected_error_msg, err_msg.reason);
524        } else {
525            return Err(anyhow!(
526                "Expected error type but received another Success Action type"
527            ));
528        }
529
530        Ok(())
531    }
532
533    #[tokio::test]
534    async fn test_lnurl_pay_url_success_action() -> Result<()> {
535        let mock_rest_client = MockRestClient::new();
536        let comment = rand_string(COMMENT_LENGTH as usize);
537        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
538        let temp_desc = pay_req.metadata_str.clone();
539        let inv = rand_invoice_with_description_hash(temp_desc)?;
540        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
541
542        mock_lnurl_pay_callback_endpoint_url_success_action(
543            &mock_rest_client,
544            LnurlPayCallbackParams {
545                error: None,
546                pr: Some(inv.to_string()),
547            },
548            None,
549        );
550
551        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
552        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
553        match mock_breez_services
554            .lnurl_pay(LnUrlPayRequest {
555                data: pay_req,
556                amount_msat: user_amount_msat,
557                use_trampoline: false,
558                comment: Some(comment),
559                payment_label: None,
560                validate_success_action_url: None,
561            })
562            .await?
563        {
564            LnUrlPayResult::EndpointSuccess {
565                data:
566                    LnUrlPaySuccessData {
567                        success_action: Some(SuccessActionProcessed::Url { data: url }),
568                        ..
569                    },
570            } => {
571                if url.url == "http://localhost:8080/test-url"
572                    && url.description == "test description"
573                {
574                    Ok(())
575                } else {
576                    Err(anyhow!("Unexpected success action content"))
577                }
578            }
579            LnUrlPayResult::EndpointSuccess {
580                data:
581                    LnUrlPaySuccessData {
582                        success_action: None,
583                        ..
584                    },
585            } => Err(anyhow!(
586                "Expected success action in callback, but none provided"
587            )),
588            _ => Err(anyhow!("Unexpected success action type")),
589        }
590    }
591
592    #[tokio::test]
593    async fn test_lnurl_pay_url_success_action_validate_url_invalid() -> Result<()> {
594        let mock_rest_client = MockRestClient::new();
595        let comment = rand_string(COMMENT_LENGTH as usize);
596        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
597        let temp_desc = pay_req.metadata_str.clone();
598        let inv = rand_invoice_with_description_hash(temp_desc)?;
599        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
600
601        mock_lnurl_pay_callback_endpoint_url_success_action(
602            &mock_rest_client,
603            LnurlPayCallbackParams {
604                error: None,
605                pr: Some(inv.to_string()),
606            },
607            Some("http://different.localhost:8080/test-url"),
608        );
609
610        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
611        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
612        let r = mock_breez_services
613            .lnurl_pay(LnUrlPayRequest {
614                data: pay_req,
615                amount_msat: user_amount_msat,
616                comment: Some(comment),
617                payment_label: None,
618                validate_success_action_url: Some(true),
619                use_trampoline: false,
620            })
621            .await;
622        // An invalid Success Action URL results in an error
623        assert!(r.is_err());
624
625        Ok(())
626    }
627
628    #[tokio::test]
629    async fn test_lnurl_pay_url_success_action_validate_url_valid() -> Result<()> {
630        let mock_rest_client = MockRestClient::new();
631        let comment = rand_string(COMMENT_LENGTH as usize);
632        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
633        let temp_desc = pay_req.metadata_str.clone();
634        let inv = rand_invoice_with_description_hash(temp_desc)?;
635        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
636
637        mock_lnurl_pay_callback_endpoint_url_success_action(
638            &mock_rest_client,
639            LnurlPayCallbackParams {
640                error: None,
641                pr: Some(inv.to_string()),
642            },
643            Some("http://different.localhost:8080/test-url"),
644        );
645
646        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
647        let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?;
648        match mock_breez_services
649            .lnurl_pay(LnUrlPayRequest {
650                data: pay_req,
651                amount_msat: user_amount_msat,
652                comment: Some(comment),
653                payment_label: None,
654                validate_success_action_url: Some(false),
655                use_trampoline: false,
656            })
657            .await?
658        {
659            LnUrlPayResult::EndpointSuccess {
660                data:
661                    LnUrlPaySuccessData {
662                        success_action: Some(SuccessActionProcessed::Url { data: url }),
663                        ..
664                    },
665            } => {
666                if url.url == "http://different.localhost:8080/test-url"
667                    && url.description == "test description"
668                {
669                    Ok(())
670                } else {
671                    Err(anyhow!("Unexpected success action content"))
672                }
673            }
674            LnUrlPayResult::EndpointSuccess {
675                data:
676                    LnUrlPaySuccessData {
677                        success_action: None,
678                        ..
679                    },
680            } => Err(anyhow!(
681                "Expected success action in callback, but none provided"
682            )),
683            _ => Err(anyhow!("Unexpected success action type")),
684        }
685    }
686
687    #[tokio::test]
688    async fn test_lnurl_pay_aes_success_action() -> Result<()> {
689        let mock_rest_client = MockRestClient::new();
690        // Expected fields in the AES payload
691        let description = "test description in AES payload".to_string();
692        let plaintext = "Hello, test plaintext".to_string();
693        let sa_data = AesSuccessActionDataDecrypted {
694            description: description.clone(),
695            plaintext: plaintext.clone(),
696        };
697        let sa = SuccessActionProcessed::Aes {
698            result: AesSuccessActionDataResult::Decrypted {
699                data: sa_data.clone(),
700            },
701        };
702
703        // Generate preimage
704        let preimage = sha256::Hash::hash(&rand_vec_u8(10));
705
706        let comment = rand_string(COMMENT_LENGTH as usize);
707        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
708        let temp_desc = pay_req.metadata_str.clone();
709
710        // The invoice (served by LNURL-pay endpoint, matching preimage and description hash)
711        let inv = rand_invoice_with_description_hash_and_preimage(temp_desc, preimage)?;
712
713        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
714        let bolt11 = inv.to_string();
715
716        mock_lnurl_pay_callback_endpoint_aes_success_action(
717            &mock_rest_client,
718            AesPayCallbackParams {
719                error: None,
720                pr: Some(bolt11.clone()),
721                sa_data: sa_data.clone(),
722                iv_bytes: random::<[u8; 16]>(),
723                key_bytes: preimage.into_inner(),
724            },
725        );
726
727        let mock_node_api = MockNodeAPI::new(get_dummy_node_state());
728        let model_payment = mock_node_api
729            .add_dummy_payment_for(bolt11, Some(preimage), Some(PayStatus::Pending))
730            .await?;
731
732        let known_payments: Vec<crate::models::Payment> = vec![model_payment];
733        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
734        let mock_breez_services = breez_services_with(
735            Some(Arc::new(mock_node_api)),
736            Some(rest_client),
737            known_payments,
738        )
739        .await?;
740        match mock_breez_services
741            .lnurl_pay(LnUrlPayRequest {
742                data: pay_req,
743                amount_msat: user_amount_msat,
744                use_trampoline: false,
745                comment: Some(comment),
746                payment_label: None,
747                validate_success_action_url: None,
748            })
749            .await?
750        {
751            LnUrlPayResult::EndpointSuccess {
752                data:
753                    LnUrlPaySuccessData {
754                        success_action: Some(received_sa),
755                        ..
756                    },
757            } => match received_sa == sa {
758                true => Ok(()),
759                false => Err(anyhow!(
760                    "Decrypted payload and description doesn't match expected success action"
761                )),
762            },
763            LnUrlPayResult::EndpointSuccess {
764                data:
765                    LnUrlPaySuccessData {
766                        success_action: None,
767                        ..
768                    },
769            } => Err(anyhow!(
770                "Expected success action in callback, but none provided"
771            )),
772            _ => Err(anyhow!("Unexpected success action type")),
773        }
774    }
775
776    #[tokio::test]
777    async fn test_lnurl_pay_aes_success_action_fail_to_decrypt() -> Result<()> {
778        let mock_rest_client = MockRestClient::new();
779        // Expected error in the AES payload
780        let sa = SuccessActionProcessed::Aes {
781            result: AesSuccessActionDataResult::ErrorStatus {
782                reason: "Unpad Error".into(),
783            },
784        };
785
786        // Generate preimage
787        let preimage = sha256::Hash::hash(&rand_vec_u8(10));
788
789        let comment = rand_string(COMMENT_LENGTH as usize);
790        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
791        let temp_desc = pay_req.metadata_str.clone();
792
793        // The invoice (served by LNURL-pay endpoint, matching preimage and description hash)
794        let inv = rand_invoice_with_description_hash_and_preimage(temp_desc, preimage)?;
795
796        let user_amount_msat = inv.amount_milli_satoshis().unwrap();
797        let bolt11 = inv.to_string();
798        let description = "test description in AES payload".to_string();
799        let plaintext = "Hello, test plaintext".to_string();
800        let sa_data = AesSuccessActionDataDecrypted {
801            description,
802            plaintext,
803        };
804        let wrong_key = vec![0u8; 32];
805
806        mock_lnurl_pay_callback_endpoint_aes_success_action(
807            &mock_rest_client,
808            AesPayCallbackParams {
809                error: None,
810                pr: Some(bolt11.clone()),
811                sa_data: sa_data.clone(),
812                iv_bytes: random::<[u8; 16]>(),
813                key_bytes: wrong_key.try_into().unwrap(),
814            },
815        );
816
817        let mock_node_api = MockNodeAPI::new(get_dummy_node_state());
818        let model_payment = mock_node_api
819            .add_dummy_payment_for(bolt11, Some(preimage), Some(PayStatus::Pending))
820            .await?;
821
822        let known_payments: Vec<crate::models::Payment> = vec![model_payment];
823        let rest_client: Arc<dyn RestClient> = Arc::new(mock_rest_client);
824        let mock_breez_services = breez_services_with(
825            Some(Arc::new(mock_node_api)),
826            Some(rest_client),
827            known_payments,
828        )
829        .await?;
830        match mock_breez_services
831            .lnurl_pay(LnUrlPayRequest {
832                data: pay_req,
833                amount_msat: user_amount_msat,
834                use_trampoline: false,
835                comment: Some(comment),
836                payment_label: None,
837                validate_success_action_url: None,
838            })
839            .await?
840        {
841            LnUrlPayResult::EndpointSuccess {
842                data:
843                LnUrlPaySuccessData {
844                    success_action: Some(received_sa),
845                    ..
846                },
847            } => match received_sa == sa {
848                true => Ok(()),
849                false => Err(anyhow!(
850                    "Decrypted payload and description doesn't match expected success action: {received_sa:?}"
851                )),
852            },
853            LnUrlPayResult::EndpointSuccess {
854                data:
855                LnUrlPaySuccessData {
856                    success_action: None,
857                    ..
858                },
859            } => Err(anyhow!(
860                "Expected success action in callback, but none provided"
861            )),
862            _ => Err(anyhow!("Unexpected success action type")),
863        }
864    }
865
866    #[test]
867    fn test_lnurl_pay_build_pay_callback_url() -> Result<()> {
868        let pay_req = get_test_pay_req_data(0, 100_000, 0);
869        let user_amount_msat = 50_000;
870
871        let amount_arg = format!("amount={}", user_amount_msat);
872        let user_comment = "test comment".to_string();
873        let comment_arg = format!("comment={user_comment}");
874
875        let url_amount_no_comment = build_pay_callback_url(user_amount_msat, &None, &pay_req)?;
876        assert!(url_amount_no_comment.contains(&amount_arg));
877        assert!(!url_amount_no_comment.contains(&comment_arg));
878
879        let url_amount_with_comment =
880            build_pay_callback_url(user_amount_msat, &Some(user_comment), &pay_req)?;
881        assert!(url_amount_with_comment.contains(&amount_arg));
882        assert!(url_amount_with_comment.contains("comment=test+comment"));
883
884        Ok(())
885    }
886}