From 356f04cad6f35accfa45f54e3560ff77ff61a7e8 Mon Sep 17 00:00:00 2001 From: Benalleng Date: Fri, 5 Jun 2026 08:45:13 -0400 Subject: [PATCH 1/3] Merge Cancel Failure variants in receiver --- payjoin-cli/src/app/v2/mod.rs | 7 +- payjoin/src/core/receive/v2/mod.rs | 119 ++++++++++--------------- payjoin/src/core/receive/v2/session.rs | 22 ++--- 3 files changed, 56 insertions(+), 92 deletions(-) diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index 8a1ff785e..26684ab5d 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -82,9 +82,8 @@ impl StatusText for ReceiveSession { ReceiveSession::Monitor(_) => "Monitoring payjoin proposal", ReceiveSession::PendingFallback(_) => "Pending fallback handling", ReceiveSession::Closed(session_outcome) => match session_outcome { - ReceiverSessionOutcome::Failure => "Session failure", + ReceiverSessionOutcome::Aborted => "Session aborted", 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", @@ -430,7 +429,7 @@ impl AppTrait for App { let row = SessionHistoryRow { session_id, role: Role::Receiver, - status: ReceiveSession::Closed(ReceiverSessionOutcome::Failure), + status: ReceiveSession::Closed(ReceiverSessionOutcome::Aborted), completed_at: None, error_message: Some(e.to_string()), }; @@ -485,7 +484,7 @@ impl AppTrait for App { let row = SessionHistoryRow { session_id, role: Role::Receiver, - status: ReceiveSession::Closed(ReceiverSessionOutcome::Failure), + status: ReceiveSession::Closed(ReceiverSessionOutcome::Aborted), completed_at: Some(completed_at), error_message: Some(e.to_string()), }; diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index 978802369..863610749 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -202,7 +202,7 @@ impl ReceiveSession { Ok(state.apply_payjoin_posted()), (session, SessionEvent::Cancelled) => - try_pending_fallback(session, SessionOutcome::Cancel).map_err(|session| { + try_pending_fallback(session).map_err(|session| { InternalReplayError::InvalidEvent( Box::new(SessionEvent::Cancelled), Some(session), @@ -211,7 +211,7 @@ impl ReceiveSession { }), (session, SessionEvent::ProtocolFailed) => - try_pending_fallback(session, SessionOutcome::Failure).map_err(|session| { + try_pending_fallback(session).map_err(|session| { InternalReplayError::InvalidEvent( Box::new(SessionEvent::ProtocolFailed), Some(session), @@ -268,25 +268,21 @@ impl ReceiveSession { } } -fn pending_fallback_from( - r: Receiver, - outcome: SessionOutcome, -) -> ReceiveSession { +fn pending_fallback_from(r: Receiver) -> ReceiveSession { let fallback_tx = r.state.fallback_tx(); ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { fallback_tx, outcome }, + state: PendingFallback { fallback_tx }, session_context: r.session_context, }) } fn pending_fallback_from_replyable_error( r: Receiver, - outcome: SessionOutcome, ) -> Result> { let Receiver { state: HasReplyableError { error_reply, fallback_tx }, session_context } = r; match fallback_tx { Some(fallback_tx) => Ok(ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { fallback_tx, outcome }, + state: PendingFallback { fallback_tx }, session_context, })), None => Err(Box::new(ReceiveSession::HasReplyableError(Receiver { @@ -296,23 +292,19 @@ fn pending_fallback_from_replyable_error( } } -fn try_pending_fallback( - session: ReceiveSession, - outcome: SessionOutcome, -) -> Result> { +fn try_pending_fallback(session: ReceiveSession) -> Result> { match session { - ReceiveSession::MaybeInputsOwned(receiver) => Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::MaybeInputsSeen(receiver) => Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::OutputsUnknown(receiver) => Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::WantsOutputs(receiver) => Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::WantsInputs(receiver) => Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::WantsFeeRange(receiver) => Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::ProvisionalProposal(receiver) => - Ok(pending_fallback_from(receiver, outcome)), - ReceiveSession::PayjoinProposal(receiver) => Ok(pending_fallback_from(receiver, outcome)), + ReceiveSession::MaybeInputsOwned(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::MaybeInputsSeen(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::OutputsUnknown(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::WantsOutputs(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::WantsInputs(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::WantsFeeRange(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::ProvisionalProposal(receiver) => Ok(pending_fallback_from(receiver)), + ReceiveSession::PayjoinProposal(receiver) => Ok(pending_fallback_from(receiver)), ReceiveSession::HasReplyableError(receiver) => - pending_fallback_from_replyable_error(receiver, outcome), - ReceiveSession::Monitor(receiver) => Ok(pending_fallback_from(receiver, outcome)), + pending_fallback_from_replyable_error(receiver), + ReceiveSession::Monitor(receiver) => Ok(pending_fallback_from(receiver)), session => Err(Box::new(session)), } } @@ -442,14 +434,13 @@ impl core::ops::DerefMut for Receiver { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PendingFallback { fallback_tx: bitcoin::Transaction, - outcome: SessionOutcome, } impl Receiver { pub fn fallback_tx(&self) -> &bitcoin::Transaction { &self.state.fallback_tx } pub fn close(self) -> TerminalTransition { - TerminalTransition::new(SessionEvent::Closed(self.state.outcome), ()) + TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Aborted), ()) } } @@ -460,7 +451,7 @@ impl Receiver { NextStateTransition::success( SessionEvent::Cancelled, Receiver { - state: PendingFallback { fallback_tx, outcome: SessionOutcome::Cancel }, + state: PendingFallback { fallback_tx }, session_context: self.session_context, }, ) @@ -470,7 +461,7 @@ impl Receiver { impl Receiver { /// Cancel before any fallback transaction exists. pub fn cancel(self) -> TerminalTransition { - TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Cancel), ()) + TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Aborted), ()) } } @@ -587,7 +578,7 @@ impl Receiver { ))) => if directory_error.is_fatal() { return MaybeFatalTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), e, ); } else { @@ -595,7 +586,7 @@ impl Receiver { }, _ => return MaybeFatalTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), e, ), }, @@ -724,7 +715,7 @@ pub struct UncheckedOriginalPayload { impl Receiver { /// Cancel before broadcast suitability has been checked. pub fn cancel(self) -> TerminalTransition { - TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Cancel), ()) + TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Aborted), ()) } } @@ -1349,10 +1340,7 @@ impl Receiver { MaybeFatalTransition::replyable_error( SessionEvent::ProtocolFailed, Receiver { - state: PendingFallback { - fallback_tx: self.state.fallback_tx(), - outcome: SessionOutcome::Failure, - }, + state: PendingFallback { fallback_tx: self.state.fallback_tx() }, session_context: self.session_context.clone(), }, ProtocolError::V2(InternalSessionError::DirectoryResponse(e).into()), @@ -1386,13 +1374,10 @@ impl Receiver { match fallback_tx { Some(fallback_tx) => MaybeTerminalTransition::advance( SessionEvent::Cancelled, - Receiver { - state: PendingFallback { fallback_tx, outcome: SessionOutcome::Cancel }, - session_context, - }, + Receiver { state: PendingFallback { fallback_tx }, session_context }, ), None => - MaybeTerminalTransition::terminate(SessionEvent::Closed(SessionOutcome::Cancel)), + MaybeTerminalTransition::terminate(SessionEvent::Closed(SessionOutcome::Aborted)), } } @@ -1441,7 +1426,7 @@ impl Receiver { let pending = self.pending_fallback_after_protocol_failure(); let event = match &pending { Some(_) => SessionEvent::ProtocolFailed, - None => SessionEvent::Closed(SessionOutcome::Failure), + None => SessionEvent::Closed(SessionOutcome::Aborted), }; let protocol_error = |e| ProtocolError::V2(InternalSessionError::DirectoryResponse(e).into()); @@ -1464,7 +1449,7 @@ impl Receiver { fn pending_fallback_after_protocol_failure(&self) -> Option> { self.state.fallback_tx.clone().map(|fallback_tx| Receiver { - state: PendingFallback { fallback_tx, outcome: SessionOutcome::Failure }, + state: PendingFallback { fallback_tx }, session_context: self.session_context.clone(), }) } @@ -2046,7 +2031,7 @@ pub mod test { let pending_fallback = receiver.process_error_response(&response, ctx).save(&persister)?; assert!(pending_fallback.is_none()); - assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Failure)], true); + assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Aborted)], true); Ok(()) } @@ -2087,7 +2072,7 @@ pub mod test { .expect_err("fatal response should error"); assert!(err.api_error_ref().is_some()); - assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Failure)], true); + assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Aborted)], true); Ok(()) } @@ -2240,7 +2225,7 @@ pub mod test { let persister = InMemoryPersister::::default(); receiver(Initialized {}).cancel().save(&persister).expect("save should succeed"); - assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Cancel)], true); + assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Aborted)], true); } #[test] @@ -2253,7 +2238,7 @@ pub mod test { .save(&persister) .expect("save should succeed"); - assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Cancel)], true); + assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Aborted)], true); } #[test] @@ -2298,7 +2283,7 @@ pub mod test { .expect("save should succeed"); assert!(pending_fallback.is_none()); - assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Cancel)], true); + assert_events(&persister, &[SessionEvent::Closed(SessionOutcome::Aborted)], true); } #[test] @@ -2312,9 +2297,9 @@ pub mod test { ( vec![ SessionEvent::Created(SHARED_CONTEXT.clone()), - SessionEvent::Closed(SessionOutcome::Cancel), + SessionEvent::Closed(SessionOutcome::Aborted), ], - ReceiveSession::Closed(SessionOutcome::Cancel), + ReceiveSession::Closed(SessionOutcome::Aborted), ), ( vec![ @@ -2323,9 +2308,9 @@ pub mod test { original: original.clone(), reply_key: None, }, - SessionEvent::Closed(SessionOutcome::Cancel), + SessionEvent::Closed(SessionOutcome::Aborted), ], - ReceiveSession::Closed(SessionOutcome::Cancel), + ReceiveSession::Closed(SessionOutcome::Aborted), ), ( vec![ @@ -2338,10 +2323,7 @@ pub mod test { SessionEvent::Cancelled, ], ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { - fallback_tx: expected_tx.clone(), - outcome: SessionOutcome::Cancel, - }, + state: PendingFallback { fallback_tx: expected_tx.clone() }, session_context: SHARED_CONTEXT.clone(), }), ), @@ -2357,10 +2339,7 @@ pub mod test { SessionEvent::Cancelled, ], ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { - fallback_tx: expected_tx.clone(), - outcome: SessionOutcome::Cancel, - }, + state: PendingFallback { fallback_tx: expected_tx.clone() }, session_context: SHARED_CONTEXT.clone(), }), ), @@ -2375,10 +2354,7 @@ pub mod test { SessionEvent::ProtocolFailed, ], ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { - fallback_tx: expected_tx.clone(), - outcome: SessionOutcome::Failure, - }, + state: PendingFallback { fallback_tx: expected_tx.clone() }, session_context: SHARED_CONTEXT.clone(), }), ), @@ -2394,10 +2370,7 @@ pub mod test { SessionEvent::ProtocolFailed, ], ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { - fallback_tx: expected_tx, - outcome: SessionOutcome::Failure, - }, + state: PendingFallback { fallback_tx: expected_tx }, session_context: SHARED_CONTEXT.clone(), }), ), @@ -2409,9 +2382,9 @@ pub mod test { reply_key: None, }, SessionEvent::GotReplyableError(replyable_error.clone()), - SessionEvent::Closed(SessionOutcome::Cancel), + SessionEvent::Closed(SessionOutcome::Aborted), ], - ReceiveSession::Closed(SessionOutcome::Cancel), + ReceiveSession::Closed(SessionOutcome::Aborted), ), ( vec![ @@ -2422,9 +2395,9 @@ pub mod test { }, SessionEvent::CheckedBroadcastSuitability(), SessionEvent::Cancelled, - SessionEvent::Closed(SessionOutcome::Cancel), + SessionEvent::Closed(SessionOutcome::Aborted), ], - ReceiveSession::Closed(SessionOutcome::Cancel), + ReceiveSession::Closed(SessionOutcome::Aborted), ), ( vec![ @@ -2432,9 +2405,9 @@ pub mod test { SessionEvent::RetrievedOriginalPayload { original, reply_key: None }, SessionEvent::CheckedBroadcastSuitability(), SessionEvent::ProtocolFailed, - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), ], - ReceiveSession::Closed(SessionOutcome::Failure), + ReceiveSession::Closed(SessionOutcome::Aborted), ), ]; diff --git a/payjoin/src/core/receive/v2/session.rs b/payjoin/src/core/receive/v2/session.rs index 749a84a4d..250b3a914 100644 --- a/payjoin/src/core/receive/v2/session.rs +++ b/payjoin/src/core/receive/v2/session.rs @@ -163,7 +163,7 @@ impl SessionHistory { Some(SessionEvent::Closed(outcome)) => match outcome { SessionOutcome::Success(_) | SessionOutcome::PayjoinProposalSent => SessionStatus::Completed, - SessionOutcome::Failure | SessionOutcome::Cancel => SessionStatus::Failed, + SessionOutcome::Aborted => SessionStatus::Failed, SessionOutcome::FallbackBroadcasted => SessionStatus::FallbackBroadcasted, }, Some(SessionEvent::Cancelled | SessionEvent::ProtocolFailed) => @@ -211,10 +211,8 @@ pub enum SessionEvent { pub enum SessionOutcome { /// Payjoin completed successfully Success(Vec<(bitcoin::ScriptBuf, bitcoin::Witness)>), - /// Payjoin failed to complete due to a counterparty deviation from the protocol - Failure, - /// Payjoin was cancelled by the user - Cancel, + /// Payjoin was not successful + Aborted, /// Fallback transaction was broadcasted FallbackBroadcasted, /// Payjoin proposal was sent, but its broadcast status cannot be tracked because @@ -553,10 +551,7 @@ mod tests { expected_status: SessionStatus::PendingFallback, }, expected_receiver_state: ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { - fallback_tx: expected_fallback, - outcome: SessionOutcome::Cancel, - }, + state: PendingFallback { fallback_tx: expected_fallback }, session_context: SessionContext { reply_key, ..session_context }, }), }; @@ -586,10 +581,7 @@ mod tests { expected_status: SessionStatus::PendingFallback, }, expected_receiver_state: ReceiveSession::PendingFallback(Receiver { - state: PendingFallback { - fallback_tx: expected_fallback, - outcome: SessionOutcome::Failure, - }, + state: PendingFallback { fallback_tx: expected_fallback }, session_context: SessionContext { reply_key, ..session_context }, }), }; @@ -769,7 +761,7 @@ mod tests { reply_key: reply_key.clone(), }); events.push(SessionEvent::GotReplyableError((&expected_error).into())); - events.push(SessionEvent::Closed(SessionOutcome::Failure)); + events.push(SessionEvent::Closed(SessionOutcome::Aborted)); let test = SessionHistoryTest { events, @@ -777,7 +769,7 @@ mod tests { fallback_tx: None, expected_status: SessionStatus::Failed, }, - expected_receiver_state: ReceiveSession::Closed(SessionOutcome::Failure), + expected_receiver_state: ReceiveSession::Closed(SessionOutcome::Aborted), }; run_session_history_test(&test); run_session_history_test_async(&test).await; From 83e67f96f3879a0aa036d64df3fb6de8c695889a Mon Sep 17 00:00:00 2001 From: Benalleng Date: Fri, 5 Jun 2026 08:46:10 -0400 Subject: [PATCH 2/3] Merge Cancel Failure variants in sender --- payjoin-cli/src/app/v2/mod.rs | 10 ++++------ payjoin-ffi/src/send/mod.rs | 8 ++------ payjoin/src/core/send/v2/mod.rs | 14 +++++++------- payjoin/src/core/send/v2/session.rs | 15 ++++++--------- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index 26684ab5d..f18ec3e99 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -55,9 +55,8 @@ impl StatusText for SendSession { SendSession::WithReplyKey(_) | SendSession::PollingForProposal(_) => "Waiting for proposal", SendSession::Closed(session_outcome) => match session_outcome { - SenderSessionOutcome::Failure => "Session failure", + SenderSessionOutcome::Aborted => "Session aborted", SenderSessionOutcome::Success(_) => "Session success", - SenderSessionOutcome::Cancel => "Session cancelled", }, SendSession::PendingFallback(_) => "Session awaiting fallback", } @@ -403,7 +402,7 @@ impl AppTrait for App { let row = SessionHistoryRow { session_id, role: Role::Sender, - status: SendSession::Closed(SenderSessionOutcome::Failure), + status: SendSession::Closed(SenderSessionOutcome::Aborted), completed_at: None, error_message: Some(e.to_string()), }; @@ -456,7 +455,7 @@ impl AppTrait for App { let row = SessionHistoryRow { session_id, role: Role::Sender, - status: SendSession::Closed(SenderSessionOutcome::Failure), + status: SendSession::Closed(SenderSessionOutcome::Aborted), completed_at: Some(completed_at), error_message: Some(e.to_string()), }; @@ -664,8 +663,7 @@ impl App { self.process_pj_response(proposal)?; return Ok(()); } - SendSession::Closed(SenderSessionOutcome::Failure) - | SendSession::Closed(SenderSessionOutcome::Cancel) => { + SendSession::Closed(SenderSessionOutcome::Aborted) => { println!("Session is closed. Nothing left to do"); return Ok(()); } diff --git a/payjoin-ffi/src/send/mod.rs b/payjoin-ffi/src/send/mod.rs index 546cb4cf1..d62e28897 100644 --- a/payjoin-ffi/src/send/mod.rs +++ b/payjoin-ffi/src/send/mod.rs @@ -166,12 +166,8 @@ impl SenderSessionOutcome { } } - pub fn is_failure(&self) -> bool { - matches!(self.0, payjoin::send::v2::SessionOutcome::Failure) - } - - pub fn is_cancelled(&self) -> bool { - matches!(self.0, payjoin::send::v2::SessionOutcome::Cancel) + pub fn is_aborted(&self) -> bool { + matches!(self.0, payjoin::send::v2::SessionOutcome::Aborted) } } diff --git a/payjoin/src/core/send/v2/mod.rs b/payjoin/src/core/send/v2/mod.rs index e7aaa4fde..7550689d4 100644 --- a/payjoin/src/core/send/v2/mod.rs +++ b/payjoin/src/core/send/v2/mod.rs @@ -398,7 +398,7 @@ impl Sender { Err(e) => if e.is_fatal() { return MaybeFatalTransition::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), InternalDecapsulationError::DirectoryResponse(e).into(), ); } else { @@ -532,7 +532,7 @@ impl Sender { Err(e) => if e.is_fatal() { return MaybeSuccessTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), InternalDecapsulationError::DirectoryResponse(e).into(), ); } else { @@ -550,14 +550,14 @@ impl Sender { Ok(body) => body, Err(e) => return MaybeSuccessTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), InternalDecapsulationError::Hpke(e).into(), ), }; if let Ok(resp_err) = ResponseError::from_slice(&body) { return MaybeSuccessTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), resp_err, ); } @@ -566,7 +566,7 @@ impl Sender { Ok(proposal) => proposal, Err(e) => return MaybeSuccessTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), InternalProposalError::Psbt(e).into(), ), }; @@ -575,7 +575,7 @@ impl Sender { Ok(processed_proposal) => processed_proposal, Err(e) => return MaybeSuccessTransitionWithNoResults::fatal( - SessionEvent::Closed(SessionOutcome::Failure), + SessionEvent::Closed(SessionOutcome::Aborted), e.into(), ), }; @@ -599,7 +599,7 @@ impl Sender { /// Mark the session as complete, signaling that the fallback transaction /// has been broadcast or its control has been transferred. pub fn close(&self) -> TerminalTransition { - TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Cancel), ()) + TerminalTransition::new(SessionEvent::Closed(SessionOutcome::Aborted), ()) } } diff --git a/payjoin/src/core/send/v2/session.rs b/payjoin/src/core/send/v2/session.rs index b0d4b9c63..ecea10d69 100644 --- a/payjoin/src/core/send/v2/session.rs +++ b/payjoin/src/core/send/v2/session.rs @@ -130,7 +130,7 @@ impl SessionHistory { match self.events.last() { Some(SessionEvent::Closed(outcome)) => match outcome { SessionOutcome::Success(_) => SessionStatus::Completed, - SessionOutcome::Failure | SessionOutcome::Cancel => SessionStatus::Failed, + SessionOutcome::Aborted => SessionStatus::Failed, }, _ => SessionStatus::Active, } @@ -164,10 +164,8 @@ pub enum SessionEvent { pub enum SessionOutcome { /// Successful payjoin Success(bitcoin::Psbt), - /// Payjoin failed to complete due to a counterparty deviation from the protocol - Failure, - /// Payjoin was cancelled by the user - Cancel, + /// Payjoin was not successful + Aborted, } #[cfg(test)] @@ -222,8 +220,7 @@ mod tests { SessionEvent::Created(Box::new(sender_with_reply_key.session_context.clone())), SessionEvent::PostedOriginalPsbt(), SessionEvent::Closed(SessionOutcome::Success(PARSED_ORIGINAL_PSBT.clone())), - SessionEvent::Closed(SessionOutcome::Failure), - SessionEvent::Closed(SessionOutcome::Cancel), + SessionEvent::Closed(SessionOutcome::Aborted), SessionEvent::Cancelled(), ]; @@ -279,7 +276,7 @@ mod tests { panic!("Unexpected error: {err_str}"); } assert_eq!( - SendSession::Closed(SessionOutcome::Failure), + SendSession::Closed(SessionOutcome::Aborted), test.expected_sender_state ); assert_eq!(test.expected_session_history.expected_status, SessionStatus::Expired); @@ -330,7 +327,7 @@ mod tests { pj_param: sender.session_context.pj_param.clone(), expected_status: SessionStatus::Expired, }, - expected_sender_state: SendSession::Closed(SessionOutcome::Failure), + expected_sender_state: SendSession::Closed(SessionOutcome::Aborted), expected_error: Some("Session expired at".to_string()), }; run_session_history_test(&test); From cc3e0463d857d56a7d8819a2194242da50f88acf Mon Sep 17 00:00:00 2001 From: Benalleng Date: Fri, 5 Jun 2026 08:56:53 -0400 Subject: [PATCH 3/3] Add test to demonstrate event log holds outcome info --- payjoin/src/core/receive/v2/session.rs | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/payjoin/src/core/receive/v2/session.rs b/payjoin/src/core/receive/v2/session.rs index 250b3a914..101e48fae 100644 --- a/payjoin/src/core/receive/v2/session.rs +++ b/payjoin/src/core/receive/v2/session.rs @@ -589,6 +589,44 @@ mod tests { run_session_history_test_async(&test).await; } + #[test] + fn event_log_distinguishes_abort_outcome() { + let session_context = SHARED_CONTEXT.clone(); + let original = original_from_test_vector(); + + // Cancel scenario: log contains Cancelled event + let cancel_events = vec![ + SessionEvent::Created(session_context.clone()), + SessionEvent::RetrievedOriginalPayload { original: original.clone(), reply_key: None }, + SessionEvent::CheckedBroadcastSuitability(), + SessionEvent::Cancelled, + SessionEvent::Closed(SessionOutcome::Aborted), + ]; + let cancel_history = SessionHistory { events: cancel_events }; + let cancel_is_cancel = + cancel_history.events.iter().any(|e| matches!(e, SessionEvent::Cancelled)); + let cancel_is_failure = + cancel_history.events.iter().any(|e| matches!(e, SessionEvent::ProtocolFailed)); + assert!(cancel_is_cancel); + assert!(!cancel_is_failure); + + // Failure scenario: log contains ProtocolFailed event + let fail_events = vec![ + SessionEvent::Created(session_context.clone()), + SessionEvent::RetrievedOriginalPayload { original: original.clone(), reply_key: None }, + SessionEvent::CheckedBroadcastSuitability(), + SessionEvent::ProtocolFailed, + SessionEvent::Closed(SessionOutcome::Aborted), + ]; + let fail_history = SessionHistory { events: fail_events }; + let fail_is_cancel = + fail_history.events.iter().any(|e| matches!(e, SessionEvent::Cancelled)); + let fail_is_failure = + fail_history.events.iter().any(|e| matches!(e, SessionEvent::ProtocolFailed)); + assert!(!fail_is_cancel); + assert!(fail_is_failure); + } + #[tokio::test] async fn test_contributed_inputs() { let persister = InMemoryPersister::::default();