1use sdk_common::prelude::*;
2use serde::Serialize;
3
4use crate::Payment;
5
6#[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 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 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 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 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 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 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 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 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 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 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 let sa = SuccessActionProcessed::Aes {
781 result: AesSuccessActionDataResult::ErrorStatus {
782 reason: "Unpad Error".into(),
783 },
784 };
785
786 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 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}