Skip to content

Commit 069fa34

Browse files
committed
Broadcast fallback transaction V2 test case
1 parent d8151a3 commit 069fa34

File tree

2 files changed

+130
-1
lines changed

2 files changed

+130
-1
lines changed

payjoin-cli/src/app/v2/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ impl App {
507507
return Ok(());
508508
}
509509
Err(e) => {
510+
// Check if it's a PersistedError with an API error
510511
if let Some(persisted_error) = e.downcast_ref::<PersistedError<
511512
EncapsulationError,
512513
db_error::Error,
@@ -521,6 +522,12 @@ impl App {
521522
));
522523
}
523524
}
525+
// Also handle HTTP/network errors (e.g., invalid directory URL)
526+
// These indicate the payjoin cannot proceed, so broadcast fallback
527+
println!("Error posting original proposal: {e}");
528+
let txid = self.wallet().broadcast_tx(fallback_tx)?;
529+
println!("Fallback transaction broadcasted. TXID: {txid}");
530+
return Err(anyhow!("Fallback transaction broadcasted due to: {e}"));
524531
}
525532
}
526533
}
@@ -531,6 +538,7 @@ impl App {
531538
return Ok(());
532539
}
533540
Err(e) => {
541+
// Check if it's a PersistedError with an API error
534542
if let Some(persisted_error) = e.downcast_ref::<PersistedError<
535543
EncapsulationError,
536544
db_error::Error,
@@ -545,6 +553,11 @@ impl App {
545553
));
546554
}
547555
}
556+
// Also handle HTTP/network errors
557+
println!("Error getting proposed payjoin psbt: {e}");
558+
let txid = self.wallet().broadcast_tx(fallback_tx)?;
559+
println!("Fallback transaction broadcasted. TXID: {txid}");
560+
return Err(anyhow!("Fallback transaction broadcasted due to: {e}"));
548561
}
549562
}
550563
}
@@ -554,7 +567,6 @@ impl App {
554567
}
555568
_ => return Err(anyhow!("Unexpected sender state")),
556569
}
557-
Ok(())
558570
}
559571

560572
async fn post_original_proposal(

payjoin-cli/tests/e2e.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,123 @@ mod e2e {
196196
Ok(())
197197
}
198198

199+
#[cfg(feature = "v2")]
200+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
201+
async fn send_receive_payjoin_v2_fallback_transaction_handling(
202+
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
203+
use payjoin_test_utils::{init_tracing, TestServices};
204+
use tempfile::TempDir;
205+
use tokio::process::Child;
206+
207+
type Result<T> = std::result::Result<T, BoxError>;
208+
209+
init_tracing();
210+
let mut services = TestServices::initialize().await?;
211+
let temp_dir = tempdir()?;
212+
213+
let result = tokio::select! {
214+
res = services.take_ohttp_relay_handle() => Err(format!("Ohttp relay is long running: {res:?}").into()),
215+
res = services.take_directory_handle() => Err(format!("Directory server is long running: {res:?}").into()),
216+
res = send_receive_fallback_async(&services, &temp_dir) => res,
217+
};
218+
219+
assert!(result.is_ok(), "v2 fallback test failed: {:#?}", result.unwrap_err());
220+
221+
async fn send_receive_fallback_async(
222+
services: &TestServices,
223+
temp_dir: &TempDir,
224+
) -> Result<()> {
225+
let receiver_db_path = temp_dir.path().join("receiver_db");
226+
let sender_db_path = temp_dir.path().join("sender_db");
227+
let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None)?;
228+
let cert_path = &temp_dir.path().join("localhost.der");
229+
tokio::fs::write(cert_path, services.cert()).await?;
230+
services.wait_for_services_ready().await?;
231+
let ohttp_keys = services.fetch_ohttp_keys().await?;
232+
let ohttp_keys_path = temp_dir.path().join("ohttp_keys");
233+
tokio::fs::write(&ohttp_keys_path, ohttp_keys.encode()?).await?;
234+
235+
let receiver_rpchost = format!("http://{}/wallet/receiver", bitcoind.params.rpc_socket);
236+
let sender_rpchost = format!("http://{}/wallet/sender", bitcoind.params.rpc_socket);
237+
let cookie_file = &bitcoind.params.cookie_file;
238+
239+
let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli");
240+
241+
// Use an invalid directory URL to trigger fallback behavior
242+
let invalid_directory = "https://random-does-not-exist.com";
243+
let ohttp_relay = &services.ohttp_relay_url();
244+
245+
// Start receiver with invalid directory to simulate failure scenario
246+
let cli_receive_initiator = Command::new(payjoin_cli)
247+
.arg("--root-certificate")
248+
.arg(cert_path)
249+
.arg("--rpchost")
250+
.arg(&receiver_rpchost)
251+
.arg("--cookie-file")
252+
.arg(cookie_file)
253+
.arg("--db-path")
254+
.arg(&receiver_db_path)
255+
.arg("--ohttp-relays")
256+
.arg(ohttp_relay)
257+
.arg("receive")
258+
.arg(RECEIVE_SATS)
259+
.arg("--pj-directory")
260+
.arg(invalid_directory)
261+
.arg("--ohttp-keys")
262+
.arg(&ohttp_keys_path)
263+
.stdout(Stdio::piped())
264+
.stderr(Stdio::inherit())
265+
.spawn()
266+
.expect("Failed to execute payjoin-cli");
267+
268+
let bip21 = get_bip21_from_receiver(cli_receive_initiator).await;
269+
270+
// Send with the BIP21 URI - should broadcast fallback transaction
271+
let cli_send_initiator = Command::new(payjoin_cli)
272+
.arg("--root-certificate")
273+
.arg(cert_path)
274+
.arg("--rpchost")
275+
.arg(&sender_rpchost)
276+
.arg("--cookie-file")
277+
.arg(cookie_file)
278+
.arg("--db-path")
279+
.arg(&sender_db_path)
280+
.arg("--ohttp-relays")
281+
.arg(ohttp_relay)
282+
.arg("send")
283+
.arg(&bip21)
284+
.arg("--fee-rate")
285+
.arg("1")
286+
.stdout(Stdio::piped())
287+
.stderr(Stdio::inherit())
288+
.spawn()
289+
.expect("Failed to execute payjoin-cli");
290+
291+
check_fallback_broadcasted(cli_send_initiator).await?;
292+
293+
Ok(())
294+
}
295+
296+
async fn check_fallback_broadcasted(mut cli_sender: Child) -> Result<()> {
297+
let mut stdout =
298+
cli_sender.stdout.take().expect("failed to take stdout of child process");
299+
let timeout = tokio::time::Duration::from_secs(35);
300+
let res = tokio::time::timeout(
301+
timeout,
302+
wait_for_stdout_match(&mut stdout, |line| {
303+
line.contains("Fallback transaction broadcasted")
304+
}),
305+
)
306+
.await?;
307+
308+
terminate(cli_sender).await.expect("Failed to kill payjoin-cli sender");
309+
assert!(res.is_some(), "Fallback transaction broadcast was not detected");
310+
Ok(())
311+
}
312+
313+
Ok(())
314+
}
315+
199316
#[cfg(feature = "v1")]
200317
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
201318
async fn send_receive_payjoin_v1_fallback_transaction_handling() -> Result<(), BoxError> {

0 commit comments

Comments
 (0)