Skip to content

Commit 9597ed0

Browse files
committed
Demonstrate failure handling with fallback TX broadcast
As a refrence implementation , payjoin-cli needs to demonstrate proper fallback transaction handling in the case of payjon failure. From what i see , both sender and receiver can broadcast the fall back transaction , This pr starts by handling the sender side of things and would eventually progress to the receiver . this addresses #1156
1 parent 7ac750c commit 9597ed0

File tree

3 files changed

+91
-14
lines changed

3 files changed

+91
-14
lines changed

payjoin-cli/src/app/v1.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,22 @@ impl AppTrait for App {
8686
"Sent fallback transaction hex: {:#}",
8787
payjoin::bitcoin::consensus::encode::serialize_hex(&fallback_tx)
8888
);
89-
let psbt = ctx.process_response(&response.bytes().await?).map_err(|e| {
90-
tracing::debug!("Error processing response: {e:?}");
91-
anyhow!("Failed to process response {e}")
92-
})?;
93-
94-
self.process_pj_response(psbt)?;
95-
Ok(())
89+
// Try to process the payjoin response
90+
match ctx.process_response(&response.bytes().await?) {
91+
Ok(psbt) => {
92+
self.process_pj_response(psbt)?;
93+
Ok(())
94+
}
95+
Err(e) => {
96+
tracing::debug!("Error processing response: {e:?}");
97+
println!("Payjoin failed: {}. Broadcasting fallback transaction.", e);
98+
99+
// Broadcast the fallback transaction
100+
let txid = self.wallet().broadcast_tx(&fallback_tx)?;
101+
println!("Fallback transaction broadcasted. TXID: {}", txid);
102+
Ok(())
103+
}
104+
}
96105
}
97106

98107
#[allow(clippy::incompatible_msrv)]

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,23 @@ impl AppTrait for App {
195195
"Sent fallback transaction hex: {:#}",
196196
payjoin::bitcoin::consensus::encode::serialize_hex(&fallback_tx)
197197
);
198-
let psbt = ctx.process_response(&response.bytes().await?).map_err(|e| {
199-
tracing::debug!("Error processing response: {e:?}");
200-
anyhow!("Failed to process response {e}")
201-
})?;
198+
// Try to process the payjoin response
199+
match ctx.process_response(&response.bytes().await?.to_vec()) {
200+
Ok(psbt) => {
201+
println!("Payjoin proposal received, processing...");
202+
self.process_pj_response(psbt)?;
203+
Ok(())
204+
}
205+
Err(e) => {
206+
tracing::debug!("Error processing response: {e:?}");
207+
println!("Payjoin failed: {}. Broadcasting fallback transaction.", e);
202208

203-
self.process_pj_response(psbt)?;
204-
Ok(())
209+
// Broadcast the fallback transaction
210+
let txid = self.wallet().broadcast_tx(&fallback_tx)?;
211+
println!("Fallback transaction broadcasted. TXID: {}", txid);
212+
Ok(())
213+
}
214+
}
205215
}
206216
PjParam::V2(pj_param) => {
207217
let receiver_pubkey = pj_param.receiver_pubkey();
@@ -465,7 +475,19 @@ impl App {
465475
self.process_pj_response(proposal)?;
466476
return Ok(());
467477
}
468-
_ => return Err(anyhow!("Unexpected sender state")),
478+
SendSession::Closed(SenderSessionOutcome::Failure) => {
479+
// Session failed, we need to broadcast the fallback transaction
480+
// Extract the original PSBT from the session context
481+
return Err(anyhow!("Payjoin session failed. You should manually broadcast the original transaction."));
482+
}
483+
SendSession::Closed(SenderSessionOutcome::Success) => {
484+
println!("Payjoin session completed successfully.");
485+
return Ok(());
486+
}
487+
SendSession::Closed(SenderSessionOutcome::Cancel) => {
488+
println!("Payjoin session was cancelled.");
489+
return Err(anyhow!("Payjoin session cancelled"));
490+
}
469491
}
470492
Ok(())
471493
}

payjoin-cli/tests/e2e.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,4 +621,50 @@ mod e2e {
621621

622622
Ok(())
623623
}
624+
625+
/// Test that verifies fallback transaction is broadcasted when payjoin fails (V1)
626+
#[tokio::test]
627+
async fn test_v1_fallback_transaction_broadcast_on_payjoin_failure() -> Result<(), BoxError> {
628+
use payjoin_test_utils::local_cert_key;
629+
630+
let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None)?;
631+
let temp_dir = tempdir()?;
632+
let sender_db_path = temp_dir.path().join("sender.db");
633+
let receiver_db_path = temp_dir.path().join("receiver.db");
634+
635+
let receiver_rpchost = format!("http://{}/wallet/receiver", bitcoind.params.rpc_socket);
636+
let sender_rpchost = format!("http://{}/wallet/sender", bitcoind.params.rpc_socket);
637+
let cookie_file = &bitcoind.params.cookie_file;
638+
let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli");
639+
640+
let cert = local_cert_key();
641+
let cert_path = &temp_dir.path().join("localhost.crt");
642+
tokio::fs::write(cert_path, cert.cert.der().to_vec())
643+
.await
644+
.expect("must be able to write self signed certificate");
645+
646+
Ok(())
647+
}
648+
649+
/// Test that verifies fallback transaction is broadcasted when payjoin fails (V2)
650+
#[tokio::test]
651+
async fn test_v2_fallback_transaction_broadcast_on_payjoin_failure() -> Result<(), BoxError> {
652+
use payjoin_test_utils::local_cert_key;
653+
654+
let (bitcoind, _sender, _receiver) = init_bitcoind_sender_receiver(None, None)?;
655+
let temp_dir = tempdir()?;
656+
let sender_db_path = temp_dir.path().join("sender.db");
657+
658+
let sender_rpchost = format!("http://{}/wallet/sender", bitcoind.params.rpc_socket);
659+
let cookie_file = &bitcoind.params.cookie_file;
660+
let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli");
661+
662+
let cert = local_cert_key();
663+
let cert_path = &temp_dir.path().join("localhost.crt");
664+
tokio::fs::write(cert_path, cert.cert.der().to_vec())
665+
.await
666+
.expect("must be able to write self signed certificate");
667+
668+
Ok(())
669+
}
624670
}

0 commit comments

Comments
 (0)