@@ -55,7 +55,7 @@ use crate::ohttp::{
5555use crate :: output_substitution:: OutputSubstitution ;
5656use crate :: persist:: {
5757 MaybeFatalOrSuccessTransition , MaybeFatalTransition , MaybeFatalTransitionWithNoResults ,
58- MaybeSuccessTransition , MaybeTransientTransition , NextStateTransition ,
58+ MaybeSuccessTransition , MaybeTransientTransition , NextStateTransition , TerminalTransition ,
5959} ;
6060use crate :: receive:: { parse_payload, InputPair , OriginalPayload , PsbtContext } ;
6161use crate :: time:: Time ;
@@ -232,20 +232,73 @@ impl ReceiveSession {
232232}
233233
234234mod sealed {
235- pub trait State { }
235+ pub trait State {
236+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > { None }
237+ }
236238
237239 impl State for super :: Initialized { }
238- impl State for super :: UncheckedOriginalPayload { }
239- impl State for super :: MaybeInputsOwned { }
240- impl State for super :: MaybeInputsSeen { }
241- impl State for super :: OutputsUnknown { }
242- impl State for super :: WantsOutputs { }
243- impl State for super :: WantsInputs { }
244- impl State for super :: WantsFeeRange { }
245- impl State for super :: ProvisionalProposal { }
246- impl State for super :: PayjoinProposal { }
240+
241+ impl State for super :: UncheckedOriginalPayload {
242+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
243+ Some ( self . original . psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
244+ }
245+ }
246+
247+ impl State for super :: MaybeInputsOwned {
248+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
249+ Some ( self . original . psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
250+ }
251+ }
252+
253+ impl State for super :: MaybeInputsSeen {
254+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
255+ Some ( self . original . psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
256+ }
257+ }
258+
259+ impl State for super :: OutputsUnknown {
260+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
261+ Some ( self . original . psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
262+ }
263+ }
264+
265+ impl State for super :: WantsOutputs {
266+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
267+ Some ( self . inner . original_psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
268+ }
269+ }
270+
271+ impl State for super :: WantsInputs {
272+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
273+ Some ( self . inner . original_psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
274+ }
275+ }
276+
277+ impl State for super :: WantsFeeRange {
278+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
279+ Some ( self . inner . original_psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
280+ }
281+ }
282+
283+ impl State for super :: ProvisionalProposal {
284+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
285+ Some ( self . psbt_context . original_psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
286+ }
287+ }
288+
289+ impl State for super :: PayjoinProposal {
290+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
291+ Some ( self . psbt_context . original_psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
292+ }
293+ }
294+
247295 impl State for super :: HasReplyableError { }
248- impl State for super :: Monitor { }
296+
297+ impl State for super :: Monitor {
298+ fn fallback_tx ( & self ) -> Option < bitcoin:: Transaction > {
299+ Some ( self . psbt_context . original_psbt . clone ( ) . extract_tx_unchecked_fee_rate ( ) )
300+ }
301+ }
249302}
250303
251304/// Sealed trait for V2 receive session states.
@@ -255,6 +308,8 @@ mod sealed {
255308/// can implement this trait, ensuring type safety and protocol integrity.
256309pub trait State : sealed:: State { }
257310
311+ impl < S : sealed:: State > State for S { }
312+
258313/// A higher-level receiver construct which will be taken through different states through the
259314/// protocol workflow.
260315///
@@ -285,6 +340,20 @@ impl<State> core::ops::DerefMut for Receiver<State> {
285340 fn deref_mut ( & mut self ) -> & mut Self :: Target { & mut self . state }
286341}
287342
343+ impl < S : State > Receiver < S > {
344+ /// Cancel the Payjoin session immediately.
345+ ///
346+ /// Returns a [`TerminalTransition`] that, once persisted, yields the fallback
347+ /// transaction when applicable. The fallback transaction is the sender's original
348+ /// transaction that should be broadcast to complete the payment without Payjoin.
349+ ///
350+ /// This is a terminal transition — the session cannot be used after cancellation.
351+ pub fn cancel ( self ) -> TerminalTransition < SessionEvent , Option < bitcoin:: Transaction > > {
352+ let fallback = self . state . fallback_tx ( ) ;
353+ TerminalTransition :: new ( SessionEvent :: Closed ( SessionOutcome :: Cancel ) , fallback)
354+ }
355+ }
356+
288357#[ derive( Debug , Clone ) ]
289358pub struct ReceiverBuilder ( SessionContext ) ;
290359
@@ -1830,4 +1899,26 @@ pub mod test {
18301899 let psbt = receiver. psbt_to_sign ( ) ;
18311900 assert_eq ! ( psbt, PARSED_PAYJOIN_PROPOSAL . clone( ) ) ;
18321901 }
1902+
1903+ #[ test]
1904+ fn cancel_from_initialized_returns_no_fallback ( ) {
1905+ let persister = InMemoryTestPersister :: < SessionEvent > :: default ( ) ;
1906+ let receiver = Receiver { state : Initialized { } , session_context : SHARED_CONTEXT . clone ( ) } ;
1907+ let fallback = receiver. cancel ( ) . save ( & persister) . expect ( "save should succeed" ) ;
1908+ assert ! ( fallback. is_none( ) ) ;
1909+ assert ! ( persister. inner. read( ) . expect( "lock should not be poisoned" ) . is_closed) ;
1910+ }
1911+
1912+ #[ tokio:: test]
1913+ async fn cancel_from_maybe_inputs_owned_returns_fallback ( ) {
1914+ let persister = InMemoryTestPersister :: < SessionEvent > :: default ( ) ;
1915+ let receiver = Receiver {
1916+ state : maybe_inputs_owned_v2_from_test_vector ( ) ,
1917+ session_context : SHARED_CONTEXT . clone ( ) ,
1918+ } ;
1919+ let expected_fallback = receiver. extract_tx_to_schedule_broadcast ( ) ;
1920+ let fallback = receiver. cancel ( ) . save ( & persister) . expect ( "save should succeed" ) ;
1921+ assert_eq ! ( fallback, Some ( expected_fallback) ) ;
1922+ assert ! ( persister. inner. read( ) . expect( "lock should not be poisoned" ) . is_closed) ;
1923+ }
18331924}
0 commit comments