|
1 | 1 | #[cfg(feature = "_manual-tls")] |
2 | 2 | mod e2e { |
3 | 3 | use std::process::{ExitStatus, Stdio}; |
| 4 | + use std::str::FromStr; |
4 | 5 |
|
5 | 6 | use nix::sys::signal::{kill, Signal}; |
6 | 7 | use nix::unistd::Pid; |
| 8 | + use payjoin::bitcoin::Txid; |
7 | 9 | use payjoin_test_utils::{init_bitcoind_sender_receiver, BoxError}; |
8 | 10 | use tempfile::tempdir; |
9 | 11 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; |
10 | | - use tokio::process::Command; |
| 12 | + use tokio::process::{Child, Command}; |
11 | 13 |
|
12 | 14 | async fn terminate(mut child: tokio::process::Child) -> tokio::io::Result<ExitStatus> { |
13 | 15 | let pid = child.id().expect("Failed to get child PID"); |
@@ -64,6 +66,20 @@ mod e2e { |
64 | 66 | res |
65 | 67 | } |
66 | 68 |
|
| 69 | + async fn send_until_request_timeout(mut cli_sender: Child) -> Result<(), BoxError> { |
| 70 | + let mut stdout = cli_sender.stdout.take().expect("failed to take stdout of child process"); |
| 71 | + let timeout = tokio::time::Duration::from_secs(35); |
| 72 | + let res = tokio::time::timeout( |
| 73 | + timeout, |
| 74 | + wait_for_stdout_match(&mut stdout, |line| line.contains("No response yet.")), |
| 75 | + ) |
| 76 | + .await?; |
| 77 | + |
| 78 | + terminate(cli_sender).await.expect("Failed to kill payjoin-cli initial sender"); |
| 79 | + assert!(res.is_some(), "Fallback send was not detected"); |
| 80 | + Ok(()) |
| 81 | + } |
| 82 | + |
67 | 83 | #[cfg(feature = "v1")] |
68 | 84 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
69 | 85 | async fn send_receive_payjoin_v1() -> Result<(), BoxError> { |
@@ -201,7 +217,6 @@ mod e2e { |
201 | 217 | async fn send_receive_payjoin_v2() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
202 | 218 | use payjoin_test_utils::{init_tracing, TestServices}; |
203 | 219 | use tempfile::TempDir; |
204 | | - use tokio::process::Child; |
205 | 220 |
|
206 | 221 | type Result<T> = std::result::Result<T, BoxError>; |
207 | 222 |
|
@@ -385,21 +400,6 @@ mod e2e { |
385 | 400 | Ok(()) |
386 | 401 | } |
387 | 402 |
|
388 | | - async fn send_until_request_timeout(mut cli_sender: Child) -> Result<()> { |
389 | | - let mut stdout = |
390 | | - cli_sender.stdout.take().expect("failed to take stdout of child process"); |
391 | | - let timeout = tokio::time::Duration::from_secs(35); |
392 | | - let res = tokio::time::timeout( |
393 | | - timeout, |
394 | | - wait_for_stdout_match(&mut stdout, |line| line.contains("No response yet.")), |
395 | | - ) |
396 | | - .await?; |
397 | | - |
398 | | - terminate(cli_sender).await.expect("Failed to kill payjoin-cli initial sender"); |
399 | | - assert!(res.is_some(), "Fallback send was not detected"); |
400 | | - Ok(()) |
401 | | - } |
402 | | - |
403 | 403 | async fn respond_with_payjoin(mut cli_receive_resumer: Child) -> Result<()> { |
404 | 404 | let mut stdout = |
405 | 405 | cli_receive_resumer.stdout.take().expect("Failed to take stdout of child process"); |
@@ -621,4 +621,142 @@ mod e2e { |
621 | 621 |
|
622 | 622 | Ok(()) |
623 | 623 | } |
| 624 | + |
| 625 | + #[cfg(feature = "v2")] |
| 626 | + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
| 627 | + async fn sender_fallback_v2() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 628 | + use payjoin_test_utils::{init_tracing, TestServices}; |
| 629 | + use tempfile::TempDir; |
| 630 | + |
| 631 | + type Result<T> = std::result::Result<T, BoxError>; |
| 632 | + |
| 633 | + init_tracing(); |
| 634 | + let mut services = TestServices::initialize().await?; |
| 635 | + let temp_dir = tempdir()?; |
| 636 | + |
| 637 | + let result = tokio::select! { |
| 638 | + res = services.take_ohttp_relay_handle() => Err(format!("Ohttp relay is long running: {res:?}").into()), |
| 639 | + res = services.take_directory_handle() => Err(format!("Directory server is long running: {res:?}").into()), |
| 640 | + res = fallback_cli_async(&services, &temp_dir) => res, |
| 641 | + }; |
| 642 | + |
| 643 | + assert!(result.is_ok(), "sender_fallback_v2 failed: {:#?}", result.unwrap_err()); |
| 644 | + |
| 645 | + async fn fallback_cli_async(services: &TestServices, temp_dir: &TempDir) -> Result<()> { |
| 646 | + let sender_db_path = temp_dir.path().join("sender_db"); |
| 647 | + let (bitcoind, sender, _receiver) = init_bitcoind_sender_receiver(None, None)?; |
| 648 | + let cert_path = &temp_dir.path().join("localhost.der"); |
| 649 | + tokio::fs::write(cert_path, services.cert()).await?; |
| 650 | + services.wait_for_services_ready().await?; |
| 651 | + let ohttp_keys = services.fetch_ohttp_keys().await?; |
| 652 | + let ohttp_keys_path = temp_dir.path().join("ohttp_keys"); |
| 653 | + tokio::fs::write(&ohttp_keys_path, ohttp_keys.encode()?).await?; |
| 654 | + |
| 655 | + let receiver_db_path = temp_dir.path().join("receiver_db"); |
| 656 | + let receiver_rpchost = format!("http://{}/wallet/receiver", bitcoind.params.rpc_socket); |
| 657 | + let sender_rpchost = format!("http://{}/wallet/sender", bitcoind.params.rpc_socket); |
| 658 | + let cookie_file = &bitcoind.params.cookie_file; |
| 659 | + let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli"); |
| 660 | + let directory = &services.directory_url(); |
| 661 | + let ohttp_relay = &services.ohttp_relay_url(); |
| 662 | + |
| 663 | + // Get a BIP21 from a receiver then kill it so the sender can never complete payjoin |
| 664 | + let cli_receiver = Command::new(payjoin_cli) |
| 665 | + .arg("--root-certificate") |
| 666 | + .arg(cert_path) |
| 667 | + .arg("--rpchost") |
| 668 | + .arg(&receiver_rpchost) |
| 669 | + .arg("--cookie-file") |
| 670 | + .arg(cookie_file) |
| 671 | + .arg("--db-path") |
| 672 | + .arg(&receiver_db_path) |
| 673 | + .arg("--ohttp-relays") |
| 674 | + .arg(ohttp_relay) |
| 675 | + .arg("receive") |
| 676 | + .arg(RECEIVE_SATS) |
| 677 | + .arg("--pj-directory") |
| 678 | + .arg(directory) |
| 679 | + .arg("--ohttp-keys") |
| 680 | + .arg(&ohttp_keys_path) |
| 681 | + .stdout(Stdio::piped()) |
| 682 | + .stderr(Stdio::inherit()) |
| 683 | + .spawn() |
| 684 | + .expect("Failed to execute payjoin-cli receiver"); |
| 685 | + let bip21 = get_bip21_from_receiver(cli_receiver).await; |
| 686 | + |
| 687 | + // Start sender and capture the session-id from the hint line, then interrupt |
| 688 | + let cli_sender = Command::new(payjoin_cli) |
| 689 | + .arg("--root-certificate") |
| 690 | + .arg(cert_path) |
| 691 | + .arg("--rpchost") |
| 692 | + .arg(&sender_rpchost) |
| 693 | + .arg("--cookie-file") |
| 694 | + .arg(cookie_file) |
| 695 | + .arg("--db-path") |
| 696 | + .arg(&sender_db_path) |
| 697 | + .arg("--ohttp-relays") |
| 698 | + .arg(ohttp_relay) |
| 699 | + .arg("send") |
| 700 | + .arg(&bip21) |
| 701 | + .arg("--fee-rate") |
| 702 | + .arg("1") |
| 703 | + .stdout(Stdio::piped()) |
| 704 | + .stderr(Stdio::inherit()) |
| 705 | + .spawn() |
| 706 | + .expect("Failed to execute payjoin-cli sender"); |
| 707 | + |
| 708 | + send_until_request_timeout(cli_sender).await?; |
| 709 | + |
| 710 | + // There is only one sender session in progress. |
| 711 | + let session_id = 1i64; |
| 712 | + // Ensure the fallback was not broadcast yet |
| 713 | + let mempool_size = |
| 714 | + sender.get_mempool_info().expect("should be able to get mempool").unbroadcast_count; |
| 715 | + assert_eq!(mempool_size, 0, "fallback should not be in mempool"); |
| 716 | + |
| 717 | + // Run `payjoin-cli fallback <session-id>` and assert broadcast |
| 718 | + let mut cli_fallback = Command::new(payjoin_cli) |
| 719 | + .arg("--root-certificate") |
| 720 | + .arg(cert_path) |
| 721 | + .arg("--rpchost") |
| 722 | + .arg(&sender_rpchost) |
| 723 | + .arg("--cookie-file") |
| 724 | + .arg(cookie_file) |
| 725 | + .arg("--db-path") |
| 726 | + .arg(&sender_db_path) |
| 727 | + .arg("--ohttp-relays") |
| 728 | + .arg(ohttp_relay) |
| 729 | + .arg("fallback") |
| 730 | + .arg(session_id.to_string()) |
| 731 | + .stdout(Stdio::piped()) |
| 732 | + .stderr(Stdio::inherit()) |
| 733 | + .spawn() |
| 734 | + .expect("Failed to execute payjoin-cli fallback"); |
| 735 | + |
| 736 | + let mut fallback_stdout = |
| 737 | + cli_fallback.stdout.take().expect("failed to take stdout of fallback"); |
| 738 | + let timeout = tokio::time::Duration::from_secs(10); |
| 739 | + let broadcast_line = tokio::time::timeout( |
| 740 | + timeout, |
| 741 | + wait_for_stdout_match(&mut fallback_stdout, |l| { |
| 742 | + l.contains("Broadcasted fallback transaction txid") |
| 743 | + }), |
| 744 | + ) |
| 745 | + .await?; |
| 746 | + |
| 747 | + terminate(cli_fallback).await.expect("Failed to kill payjoin-cli fallback"); |
| 748 | + let subcommand_output = broadcast_line.expect("fallback should broadcast"); |
| 749 | + let fallback_txid = subcommand_output.split_whitespace().nth(4).unwrap_or(""); |
| 750 | + let fallback_txid = Txid::from_str(fallback_txid).expect("valid txid"); |
| 751 | + |
| 752 | + assert!( |
| 753 | + sender.get_raw_transaction(fallback_txid).is_ok(), |
| 754 | + "fallback tx should be in the mempool" |
| 755 | + ); |
| 756 | + |
| 757 | + Ok(()) |
| 758 | + } |
| 759 | + |
| 760 | + Ok(()) |
| 761 | + } |
624 | 762 | } |
0 commit comments