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