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 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 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 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 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 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 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 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 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 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 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 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 let sa = SuccessActionProcessed::Aes {
780 result: AesSuccessActionDataResult::ErrorStatus {
781 reason: "Unpad Error".into(),
782 },
783 };
784
785 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 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}