From d4c1053f8eee5a32443bf63f3f3f9a8b2ba99caa Mon Sep 17 00:00:00 2001 From: Mehmet Efe Umit Date: Mon, 8 Dec 2025 21:52:11 -0800 Subject: [PATCH 1/2] Check for exact events in the monitor unit test for Payjoin broadcast detection --- payjoin/src/core/receive/v2/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index 245cb17c8..7e2f36e23 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -1270,7 +1270,7 @@ impl Receiver { tx.input.get(i).expect("sender_input_indexes should return valid indices"); sender_witnesses.push((input.script_sig.clone(), input.witness.clone())); } - // Payjoin transaction with segwit inputs was detected. Log the signatures and complete the session + // Payjoin transaction with SegWit inputs was detected. Log the signatures and complete the session. return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( SessionOutcome::Success(sender_witnesses), )); @@ -1461,9 +1461,15 @@ pub mod test { assert!(matches!(res, OptionalTransitionOutcome::Progress(_))); assert!(persister.inner.read().expect("Shouldn't be poisoned").is_closed); assert_eq!(persister.inner.read().expect("Shouldn't be poisoned").events.len(), 1); - // TODO: check for exact events + assert_eq!( + persister.inner.read().expect("Shouldn't be poisoned").events.last(), + Some(&SessionEvent::Closed(SessionOutcome::Success(vec![( + ScriptBuf::default(), + Witness::default() + )]))) + ); - // fallback was broadcasted, should progress to success + // Fallback was broadcasted, should progress to success let persister = InMemoryTestPersister::default(); let res = monitor .check_payment( From fb368c0de80aa3a24e733e2ed519a70a2cc49056 Mon Sep 17 00:00:00 2001 From: Mehmet Efe Umit Date: Mon, 8 Dec 2025 21:58:46 -0800 Subject: [PATCH 2/2] Skip check_payment in Receiver if the sender is using non-segwit address The previous implementation of the check_payment function assumed that if the outpoints of the Payjoin transaction have been removed from the UTXO set, it is an indication of the Payjoin being broadcasted by the sender. Moreover, it relied on the same closure to detect whether there is a double-spend attempt from the sender, if only a subset of the outpoints have been spent. Both of the usages of the outpoint closure is incorrect. If the sender does RBF on the fallback transaction, this would change the transaction ID and cause the previous implementation to incorrectly detect double-spend. Moreover, the sender can use some of the UTXOs in the Payjoin session if they wish without necessarily "attacking" the receiver. This change removes the outpoint closure, and instead skips the check if the fallback transaction has any inputs which does not have witness data. This assumes that the fallback transaction has been signed which is a certainty at this point in the Payjoin session. --- payjoin-cli/src/app/v2/mod.rs | 21 ++-- payjoin-cli/src/app/wallet.rs | 20 ---- payjoin-ffi/src/receive/mod.rs | 25 ++-- payjoin/src/core/receive/v2/mod.rs | 155 ++++++++++--------------- payjoin/src/core/receive/v2/session.rs | 7 +- payjoin/tests/integration.rs | 132 +++++++++++++-------- 6 files changed, 167 insertions(+), 193 deletions(-) diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index d91b729fa..dd6f2fc7a 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -79,9 +79,11 @@ impl StatusText for ReceiveSession { ReceiveSession::Monitor(_) => "Monitoring payjoin proposal", ReceiveSession::Closed(session_outcome) => match session_outcome { ReceiverSessionOutcome::Failure => "Session failure", - ReceiverSessionOutcome::Success(_) => "Session success", + ReceiverSessionOutcome::Success(_) => "Session success, Payjoin proposal was broadcasted", ReceiverSessionOutcome::Cancel => "Session cancelled", ReceiverSessionOutcome::FallbackBroadcasted => "Fallback broadcasted", + ReceiverSessionOutcome::PayjoinProposalSent => + "Payjoin proposal sent, skipping monitoring as the sender is spending non-SegWit inputs", }, } } @@ -778,18 +780,11 @@ impl App { loop { interval.tick().await; let check_result = proposal - .check_payment( - |txid| { - self.wallet() - .get_raw_transaction(&txid) - .map_err(|e| ImplementationError::from(e.into_boxed_dyn_error())) - }, - |outpoint| { - self.wallet() - .is_outpoint_spent(&outpoint) - .map_err(|e| ImplementationError::from(e.into_boxed_dyn_error())) - }, - ) + .check_payment(|txid| { + self.wallet() + .get_raw_transaction(&txid) + .map_err(|e| ImplementationError::from(e.into_boxed_dyn_error())) + }) .save(persister); match check_result { diff --git a/payjoin-cli/src/app/wallet.rs b/payjoin-cli/src/app/wallet.rs index 83efe0d1f..ce9e746b8 100644 --- a/payjoin-cli/src/app/wallet.rs +++ b/payjoin-cli/src/app/wallet.rs @@ -134,26 +134,6 @@ impl BitcoindWallet { } } - #[cfg(feature = "v2")] - pub fn is_outpoint_spent(&self, outpoint: &OutPoint) -> Result { - let _ = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current() - // Note: explicitly ignore txouts in the mempool. Those should be considered spent for our purposes - .block_on(async { - match self.rpc.get_tx_out(&outpoint.txid, outpoint.vout, false).await { - Ok(_) => Ok(true), - Err(e) => - if e.is_missing_or_invalid_input() { - Ok(false) - } else { - Err(e) - }, - } - }) - })?; - Ok(true) - } - #[cfg(feature = "v2")] pub fn get_raw_transaction( &self, diff --git a/payjoin-ffi/src/receive/mod.rs b/payjoin-ffi/src/receive/mod.rs index b3b32aa09..16e631a87 100644 --- a/payjoin-ffi/src/receive/mod.rs +++ b/payjoin-ffi/src/receive/mod.rs @@ -1136,24 +1136,13 @@ fn try_deserialize_tx( #[uniffi::export] impl Monitor { - pub fn monitor( - &self, - transaction_exists: Arc, - outpoint_spent: Arc, - ) -> MonitorTransition { - MonitorTransition(Arc::new(RwLock::new(Some(self.0.clone().check_payment( - |txid| { - transaction_exists - .callback(txid.to_string()) - .and_then(|buf| buf.map(try_deserialize_tx).transpose()) - .map_err(|e| ImplementationError::new(e).into()) - }, - |outpoint| { - outpoint_spent - .callback(outpoint.into()) - .map_err(|e| ImplementationError::new(e).into()) - }, - ))))) + pub fn monitor(&self, transaction_exists: Arc) -> MonitorTransition { + MonitorTransition(Arc::new(RwLock::new(Some(self.0.clone().check_payment(|txid| { + transaction_exists + .callback(txid.to_string()) + .and_then(|buf| buf.map(try_deserialize_tx).transpose()) + .map_err(|e| ImplementationError::new(e).into()) + }))))) } } diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index 7e2f36e23..52d4f0da4 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -1228,32 +1228,41 @@ pub struct Monitor { /// Call [`Receiver::check_payment`] to confirm the broadcast and conclude the Payjoin /// session. impl Receiver { - /// Checks if the Payjoin proposal, the fallback transaction, or a double-spend attempt - /// has been broadcasted by the sender. If the sender broadcasted either the Payjoin proposal - /// or the fallback transaction, concludes the Payjoin session with a success. If there was a - /// double-spend attempt, concludes with a failure. + /// Checks if the Payjoin proposal or the fallback transaction, has been broadcasted by the sender. + /// If the sender broadcasted either the Payjoin proposal or the fallback transaction, concludes + /// the Payjoin session with a success. /// - /// After the receiver has finalized the Payjoin proposal and sent it to the sender for the - /// final signature and broadcast, what the sender does changes how the receiver should track - /// the network and confirm that Payjoin session has concluded: - /// - /// 1. The sender may contribute segwit inputs, which would keep the transaction ID the same as - /// what it was when the receiver sent the Payjoin proposal. In this case, the - /// `transaction_exists` function will be used to confirm the broadcast. - /// 2. The sender may contribute non-segwit inputs, which would change the - /// transaction ID. In this case, `outpoint_spent` will be used to confirm that the UTXOs - /// the receiver contributed with have been spent. This function will fail if UTXOs have - /// been spent but not in the Payjoin proposal, signalling a double-spend. - /// 3. The sender might not broadcast the Payjoin transaction and instead broadcast the original - /// proposal which paid to the receiver but did not have any receiver contributions. + /// If the receiver input address type in the fallback transaction is non-SegWit, then this + /// function will directly conclude the Payjoin session with a Success without running the + /// provided `transaction_exists` closure. `transaction_exists` uses the transaction ID to + /// search for the transaction in the network. Since a non-SegWit input signature is going to + /// change the TXID of the Payjoin proposal, it cannot be monitored. pub fn check_payment( &self, transaction_exists: impl Fn(Txid) -> Result, ImplementationError>, - outpoint_spent: impl Fn(OutPoint) -> Result, ) -> MaybeFatalOrSuccessTransition { + let fallback_tx = self + .state + .psbt_context + .original_psbt + .clone() + .extract_tx_fee_rate_limit() + .expect("fallback transaction should be in the receiver context"); + + // If the fallback transaction included any non-SegWit inputs, then the transaction ID of + // the Payjoin proposal is going to change when the sender signs their non-SegWit address + // one more time. The receiver cannot monitor the broadcast, and should conclude the session. + if fallback_tx.input.iter().any(|txin| txin.witness.is_empty()) { + return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( + SessionOutcome::PayjoinProposalSent, + )); + } + let payjoin_proposal = &self.state.psbt_context.payjoin_psbt; let payjoin_txid = payjoin_proposal.unsigned_tx.compute_txid(); - // If we have a payjoin transaction with segwit inputs, we can check for the txid + // If the sender is spending SegWit-only inputs, then the transaction ID of the Payjoin proposal + // is not going to change when the sender signs it. So we can use the TXID to determine if + // the Payjoin proposal has been broadcasted. match transaction_exists(payjoin_txid) { Ok(Some(tx)) => { let tx_id = tx.compute_txid(); @@ -1279,14 +1288,8 @@ impl Receiver { Err(e) => return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e)), } - // Check for fallback being broadcasted - let fallback_tx = self - .state - .psbt_context - .original_psbt - .clone() - .extract_tx_fee_rate_limit() - .expect("Checked in earlier typestates"); + // If the Payjoin proposal was not found, check the fallback transaction, at it is + // the second of two transactions whose IDs the receiver is aware of. match transaction_exists(fallback_tx.compute_txid()) { Ok(Some(_)) => return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( @@ -1296,29 +1299,6 @@ impl Receiver { Err(e) => return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e)), } - let mut outpoints_spend = 0; - for ot in payjoin_proposal.unsigned_tx.input.iter() { - match outpoint_spent(ot.previous_output) { - Ok(false) => {} - Ok(true) => outpoints_spend += 1, - Err(e) => - return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e)), - } - } - - if outpoints_spend == payjoin_proposal.unsigned_tx.input.len() { - // All the payjoin proposal outpoints were spent. This means our payjoin proposal has non-segwit inputs and is broadcasted. - return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( - // TODO: there seems to be not great way to get the tx of the tx that spent these outpoints. - SessionOutcome::Success(vec![]), - )); - } else if outpoints_spend > 0 { - // Some outpoints were spent but not in the payjoin proposal. This is a double spend. - return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( - SessionOutcome::Failure, - )); - } - MaybeFatalOrSuccessTransition::no_results(self.clone()) } } @@ -1359,7 +1339,7 @@ pub(crate) fn pj_uri<'a>( pub mod test { use std::str::FromStr; - use bitcoin::FeeRate; + use bitcoin::{FeeRate, ScriptBuf, Witness}; use once_cell::sync::Lazy; use payjoin_test_utils::{ BoxError, EXAMPLE_URL, KEM, KEY_ID, ORIGINAL_PSBT, PARSED_ORIGINAL_PSBT, @@ -1444,7 +1424,7 @@ pub mod test { // Nothing was spent, should be in the same state let persister = InMemoryTestPersister::default(); let res = monitor - .check_payment(|_| Ok(None), |_| Ok(false)) + .check_payment(|_| Ok(None)) .save(&persister) .expect("InMemoryTestPersister shouldn't fail"); assert!(matches!(res, OptionalTransitionOutcome::Stasis(_))); @@ -1454,7 +1434,7 @@ pub mod test { // Payjoin was broadcasted, should progress to success let persister = InMemoryTestPersister::default(); let res = monitor - .check_payment(|_| Ok(Some(payjoin_tx.clone())), |_| Ok(false)) + .check_payment(|_| Ok(Some(payjoin_tx.clone()))) .save(&persister) .expect("InMemoryTestPersister shouldn't fail"); @@ -1472,70 +1452,55 @@ pub mod test { // Fallback was broadcasted, should progress to success let persister = InMemoryTestPersister::default(); let res = monitor - .check_payment( - |txid| { - // Emulate if one of the fallback outpoints was double spent - if txid == original_tx.compute_txid() { - Ok(Some(original_tx.clone())) - } else { - Ok(None) - } - }, - |_| Ok(false), - ) + .check_payment(|txid| { + // Emulate if one of the fallback outpoints was double spent + if txid == original_tx.compute_txid() { + Ok(Some(original_tx.clone())) + } else { + Ok(None) + } + }) .save(&persister) .expect("InMemoryTestPersister shouldn't fail"); assert!(matches!(res, OptionalTransitionOutcome::Progress(_))); assert!(persister.inner.read().expect("Shouldn't be poisoned").is_closed); + assert_eq!(persister.inner.read().expect("Shouldn't be poisoned").events.len(), 1); assert_eq!( persister.inner.read().expect("Shouldn't be poisoned").events.last(), Some(&SessionEvent::Closed(SessionOutcome::FallbackBroadcasted)) ); - let persister = InMemoryTestPersister::default(); - let res = monitor - .check_payment(|_| Ok(None), |_| Ok(true)) - .save(&persister) - .expect("InMemoryTestPersister shouldn't fail"); + // Fallback transaction is non-SegWit address type, should end the session without checking + // the network for broadcasts. + // Not using the test-utils vectors here as they are SegWit. + let parsed_original_psbt_p2pkh = Psbt::from_str("cHNidP8BAFICAAAAAd5tU7sqAGa46oUVdEfV1HTeVVPYqvSvxy8/dvF3dwpZAQAAAAD9////AUTxBSoBAAAAFgAUhV1NWa6seBB5g6VZC2lnduxfEaUAAAAAAAEA/QoBAgAAAAIT2eO393FPqJ4fw6NH0rXALebtTCderecX0y6DumtjNgAAAAAA/f///5hrwcRiTXqXScbvk3APDdzy162Yj+6JD/iSEO9KYQl+AQAAAGpHMEQCIGcFm57xH5tQvJMipWfzxS7OGRi7+JfTT6WA27kOt8fVAiAp2I3WGdLk3/dVhoVxN6Jl9Wp/xeCIZZ1OTukSs8jszgEhAjjEq9kNnhvQbdVlWsE9QTIe4h39UPQ8flvU5Ivq6DFm/f///wIo3gUqAQAAABl2qRTWng6zTFWPZX1k12UqqBI6kLz8z4isAPIFKgEAAAAZdqkUIz2wzl605b3cg3j72nXReQuXXaWIrGcAAAABB2pHMEQCIEP33+9X/ecNmaiydM54HS+HoHfZygAQ/vMlc5r1IWkeAiA9oKjOVmp+RnrDF4zzHHGtoG1yy1+UWXBNaDiwd0LokgEhAmfCwbIv1mi5psiB3HFqXN1bFAo+goNUPWIso60J1matAAA=").expect("known psbt should parse"); + let parsed_payjoin_proposal_p2pkh: Psbt = + Psbt::from_str("cHNidP8BAHsCAAAAAphrwcRiTXqXScbvk3APDdzy162Yj+6JD/iSEO9KYQl+AAAAAAD9////3m1TuyoAZrjqhRV0R9XUdN5VU9iq9K/HLz928Xd3ClkBAAAAAP3///8BsOILVAIAAAAWABSFXU1Zrqx4EHmDpVkLaWd27F8RpQAAAAAAAQCgAgAAAAJgEjBIihNzFXar4wIYepzXJwQVpbqZep9GCY8pQCqh3wAAAAAA/f///x8caN/onT7AOPRWJz7vnT6yiNxcsAIs/U3RcgU4kiq4AAAAAAD9////AgDyBSoBAAAAGXapFDGh2kOIa5aNVHT2bHSoFfcawEMiiKyk6QUqAQAAABl2qRQY8AsQvx+jg9NdGUwCuShS3qk2KYisZwAAAAEBIgDyBSoBAAAAGXapFDGh2kOIa5aNVHT2bHSoFfcawEMiiKwBB2pHMEQCICQEE2dMDzlyH3ojsc0l98Da0yd2ARuy5AcWQjlgHHjkAiA70WPB+yQhW5zhsOBTg6qLsi0KzoofRAj1BZFpKT2QwAEhA68L99Q+xdIIp0rinuVDs+4qmqMZwg4E+aqbTQ8RClXLAAEA/QoBAgAAAAIT2eO393FPqJ4fw6NH0rXALebtTCderecX0y6DumtjNgAAAAAA/f///5hrwcRiTXqXScbvk3APDdzy162Yj+6JD/iSEO9KYQl+AQAAAGpHMEQCIGcFm57xH5tQvJMipWfzxS7OGRi7+JfTT6WA27kOt8fVAiAp2I3WGdLk3/dVhoVxN6Jl9Wp/xeCIZZ1OTukSs8jszgEhAjjEq9kNnhvQbdVlWsE9QTIe4h39UPQ8flvU5Ivq6DFm/f///wIo3gUqAQAAABl2qRTWng6zTFWPZX1k12UqqBI6kLz8z4isAPIFKgEAAAAZdqkUIz2wzl605b3cg3j72nXReQuXXaWIrGcAAAAAAA==").expect("known psbt should parse"); - assert!(matches!(res, OptionalTransitionOutcome::Progress(_))); - assert!(persister.inner.read().expect("Shouldn't be poisoned").is_closed); - assert_eq!(persister.inner.read().expect("Shouldn't be poisoned").events.len(), 1); + let psbt_ctx_p2pkh = PsbtContext { + original_psbt: parsed_original_psbt_p2pkh.clone(), + payjoin_psbt: parsed_payjoin_proposal_p2pkh.clone(), + }; + let monitor = Receiver { + state: Monitor { psbt_context: psbt_ctx_p2pkh }, + session_context: SHARED_CONTEXT.clone(), + }; let persister = InMemoryTestPersister::default(); - monitor - .check_payment( - |_| Ok(None), - |outpoint| { - if outpoint == payjoin_tx.input[0].previous_output { - Ok(true) - } else { - Ok(false) - } - }, - ) + let res = monitor + .check_payment(|_| panic!("check_payment should return before this closure is called")) .save(&persister) .expect("InMemoryTestPersister shouldn't fail"); + assert!(matches!(res, OptionalTransitionOutcome::Progress(_))); assert!(persister.inner.read().expect("Shouldn't be poisoned").is_closed); assert_eq!(persister.inner.read().expect("Shouldn't be poisoned").events.len(), 1); assert_eq!( persister.inner.read().expect("Shouldn't be poisoned").events.last(), - Some(&SessionEvent::Closed(SessionOutcome::Failure)) + Some(&SessionEvent::Closed(SessionOutcome::PayjoinProposalSent)) ); - // assert_eq!( - // err.to_string(), - // Error::Protocol(ProtocolError::V2( - // InternalSessionError::FallbackOutpointsSpent(vec![ - // payjoin_tx.input[0].previous_output - // ],) - // .into() - // )) - // .to_string() - // ); - Ok(()) } diff --git a/payjoin/src/core/receive/v2/session.rs b/payjoin/src/core/receive/v2/session.rs index ca9feba7d..c9703233f 100644 --- a/payjoin/src/core/receive/v2/session.rs +++ b/payjoin/src/core/receive/v2/session.rs @@ -119,7 +119,8 @@ impl SessionHistory { } match self.events.last() { Some(SessionEvent::Closed(outcome)) => match outcome { - SessionOutcome::Success(_) => SessionStatus::Completed, + SessionOutcome::Success(_) | SessionOutcome::PayjoinProposalSent => + SessionStatus::Completed, SessionOutcome::Failure | SessionOutcome::Cancel => SessionStatus::Failed, SessionOutcome::FallbackBroadcasted => SessionStatus::FallbackBroadcasted, }, @@ -169,6 +170,10 @@ pub enum SessionOutcome { Cancel, /// Fallback transaction was broadcasted FallbackBroadcasted, + /// Payjoin proposal was sent, but its broadcast status cannot be tracked because + /// the sender is using non-SegWit inputs which will change the transaction ID + /// of the proposal + PayjoinProposalSent, } #[cfg(test)] diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index da6356c97..d5daffc72 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -491,6 +491,66 @@ mod integration { Ok(()) } + #[tokio::test] + async fn v2_to_v2_p2pkh() -> Result<(), BoxSendSyncError> { + init_tracing(); + let mut services = TestServices::initialize().await?; + let expected_weight = Weight::from_wu( + TX_HEADER_LEGACY_WEIGHT + (P2PKH_INPUT_WEIGHT * 2) + P2WPKH_OUTPUT_WEIGHT, + ) + // bitcoin-cli wallet uses signature grinding to save one vbyte on the original PSBT. + // subtract it here + - Weight::from_vb_unchecked(1); + let expected_fee = expected_weight * FeeRate::BROADCAST_MIN; + + let (_bitcoind, sender, receiver) = + init_bitcoind_sender_receiver(Some(AddressType::Legacy), Some(AddressType::Legacy)) + .expect("should be able to initialize the sender and the receiver"); + let recv_persister = InMemoryTestPersister::default(); + let send_persister = InMemoryTestPersister::default(); + + let result = tokio::select!( + err = services.take_ohttp_relay_handle() => panic!("Ohttp relay exited early: {:?}", err), + err = services.take_directory_handle() => panic!("Directory server exited early: {:?}", err), + res = do_v2_to_v2(&services, &receiver, &sender, &recv_persister, &send_persister, SenderFinalAction::SignAndBroadcastPayjoinProposal) => res + ); + + assert!(result.is_ok(), "v2 p2pkh send receive failed: {:#?}", result.unwrap_err()); + + let (broadcasted_transaction, monitoring_payment) = result.unwrap(); + + // Sender should have sent the entire value of their UTXO to receiver (minus fees). + assert_eq!(broadcasted_transaction.input.len(), 2); + assert_eq!(broadcasted_transaction.output.len(), 1); + assert_eq!( + receiver.get_balances()?.into_model()?.mine.untrusted_pending, + Amount::from_btc(100.0)? - expected_fee + ); + assert_eq!( + sender.get_balances()?.into_model()?.mine.untrusted_pending, + Amount::from_btc(0.0)? + ); + + // Receiver cannot validate that the sender has broadcasted the Payjoin proposal or the fallback transaction. + // The sender is using a non-SegWit address, so their signature is going to change the TXID. So we test whether the + // function exists early and does not call the closure. + monitoring_payment + .check_payment(|_| { + panic!("when the sender is using a non-SegWit address type, the check_payment function should skip the check and return success") + }) + .save(&recv_persister) + .expect("receiver should successfully monitor for the payment"); + + let (_session, session_history) = replay_receiver_event_log(&recv_persister)?; + assert_eq!( + recv_persister.load().unwrap().last(), + Some(payjoin::receive::v2::SessionEvent::Closed(payjoin::receive::v2::SessionOutcome::PayjoinProposalSent)), + "The last event of the persister should be a SessionOutcome::PayjoinProposalSent since the sender is going to change the TXID when they sign the Payjoin proposal", + ); + assert_eq!(session_history.status(), SessionStatus::Completed); + Ok(()) + } + #[tokio::test] async fn v2_to_v2_p2wpkh() -> Result<(), BoxSendSyncError> { init_tracing(); @@ -530,22 +590,16 @@ mod integration { // Receiver should be able to validate that the sender has broadcasted the Payjoin proposal. monitoring_payment - .check_payment( - |txid| { - let get_tx_result = receiver.get_raw_transaction(txid); - match get_tx_result { - Ok(tx) => { - Ok(Some(tx.transaction().expect("transaction should be decodable"))) - }, - Err(_) => { - panic!("should be able to find the payjoin proposal broadcasted") - } + .check_payment(|txid| { + let get_tx_result = receiver.get_raw_transaction(txid); + match get_tx_result { + Ok(tx) => + Ok(Some(tx.transaction().expect("transaction should be decodable"))), + Err(_) => { + panic!("should be able to find the payjoin proposal broadcasted") } - }, - |_| { - panic!("should not even check outpoints for a segwit payjoin proposal or a fallback transaction") - }, - ) + } + }) .save(&recv_persister) .expect("receiver should successfully monitor for the payment"); @@ -618,22 +672,16 @@ mod integration { // Receiver should be able to validate that the sender has broadcasted the Payjoin proposal. monitoring_payment - .check_payment( - |txid| { - let get_tx_result = receiver.get_raw_transaction(txid); - match get_tx_result { - Ok(tx) => { - Ok(Some(tx.transaction().expect("transaction should be decodable"))) - }, - Err(_) => { - panic!("should be able to find the payjoin proposal broadcasted") - } + .check_payment(|txid| { + let get_tx_result = receiver.get_raw_transaction(txid); + match get_tx_result { + Ok(tx) => + Ok(Some(tx.transaction().expect("transaction should be decodable"))), + Err(_) => { + panic!("should be able to find the payjoin proposal broadcasted") } - }, - |_| { - panic!("should not even check outpoints for a segwit payjoin proposal or a fallback transaction") - }, - ) + } + }) .save(&recv_persister) .expect("receiver should successfully monitor for the payment"); @@ -703,22 +751,14 @@ mod integration { // The check_payment closure should be called twice: first for the Payjoin proposal, which will not be found, // and then for the fallback transaction, which will be found.. monitoring_payment - .check_payment( - |txid| { - let get_tx_result = receiver.get_raw_transaction(txid); - match get_tx_result { - Ok(tx) => { - Ok(Some(tx.transaction().expect("transaction should be decodable"))) - }, - Err(_) => { - Ok(None) - } - } - }, - |_| { - panic!("should not even check outpoints for a segwit payjoin proposal or a fallback transaction") - }, - ) + .check_payment(|txid| { + let get_tx_result = receiver.get_raw_transaction(txid); + match get_tx_result { + Ok(tx) => + Ok(Some(tx.transaction().expect("transaction should be decodable"))), + Err(_) => Ok(None), + } + }) .save(&recv_persister) .expect("receiver should successfully monitor for the payment");