@@ -337,6 +337,12 @@ impl AppTrait for App {
337337 Err ( e) => {
338338 if e. is_expired ( ) {
339339 println ! ( "Session {session_id} receiver expired." ) ;
340+ match e. fallback_tx ( ) {
341+ Some ( tx) => self . broadcast_fallback_tx ( tx, & session_id, "receiver" ) ,
342+ None => println ! (
343+ "No fallback transaction available for expired receiver session {session_id}."
344+ ) ,
345+ }
340346 } else {
341347 tracing:: error!(
342348 "An error {:?} occurred while replaying receiver session" ,
@@ -365,6 +371,12 @@ impl AppTrait for App {
365371 Err ( e) => {
366372 if e. is_expired ( ) {
367373 println ! ( "Session {session_id} sender expired." ) ;
374+ match e. fallback_tx ( ) {
375+ Some ( tx) => self . broadcast_fallback_tx ( tx, & session_id, "sender" ) ,
376+ None => println ! (
377+ "No fallback transaction available for expired sender session {session_id}."
378+ ) ,
379+ }
368380 } else {
369381 tracing:: error!( "An error {:?} occurred while replaying Sender session" , e) ;
370382 println ! ( "Session {session_id} sender failed to replay - {e}" ) ;
@@ -688,10 +700,52 @@ impl App {
688700 if let Err ( close_err) = SessionPersister :: close ( persister) {
689701 tracing:: error!( "Failed to close {} session {}: {:?}" , role, session_id, close_err) ;
690702 } else {
691- tracing:: error!( "Closed failed {} session: {}" , role, session_id) ;
703+ tracing:: debug!( "Closed failed {} session: {}" , role, session_id) ;
704+ }
705+ }
706+
707+ /// Broadcast a fallback transaction with graceful error handling. Logs
708+ /// success/failure, never propagates the broadcast error.
709+ fn broadcast_fallback_tx (
710+ & self ,
711+ tx : & payjoin:: bitcoin:: Transaction ,
712+ session_id : & SessionId ,
713+ role : & str ,
714+ ) {
715+ match self . wallet ( ) . broadcast_tx ( tx) {
716+ Ok ( txid) =>
717+ println ! ( "Broadcasted fallback transaction for {role} session {session_id}: {txid}" ) ,
718+ Err ( err) => {
719+ println ! (
720+ "Expired {role} session {session_id} has a fallback transaction \
721+ but it failed to broadcast: {err}\n \
722+ Recover it manually with `payjoin-cli history` / `cancel`."
723+ ) ;
724+ tracing:: error!(
725+ "Fallback broadcast failed for {role} session {session_id}: {err:?}"
726+ ) ;
727+ }
692728 }
693729 }
694730
731+ // / Best-effort broadcast of a fallback transaction recovered from an expired
732+ // / session's `ReplayError`. A sender always has a fallback (its original
733+ // / PSBT); a receiver only has one if it had already received and validated
734+ // / the sender's original proposal.
735+ // fn broadcast_fallback_on_expiry<SessionState, SessionEvent>(
736+ // &self,
737+ // e: &payjoin::error::ReplayError<SessionState, SessionEvent>,
738+ // session_id: &SessionId,
739+ // role: &str,
740+ // ) {
741+ // match e.fallback_tx() {
742+ // Some(tx) => self.broadcast_fallback_tx(tx, session_id, role),
743+ // None => println!(
744+ // "No fallback transaction available for expired {role} session {session_id}."
745+ // ),
746+ // }
747+ // }
748+
695749 async fn process_sender_session (
696750 & self ,
697751 session : SendSession ,
@@ -739,26 +793,53 @@ impl App {
739793 persister : & SenderPersister ,
740794 ) -> Result < ( ) > {
741795 let mut session = sender. clone ( ) ;
742- // Long poll until we get a response
796+ // Long poll until we get a response. The session's expiration is enforced
797+ // here: once `create_poll_request` reports expiry, the session is driven to
798+ // a terminal `Closed(Aborted)` state and the fallback transaction is surfaced
799+ // so it can be broadcast manually. The persisted terminal outcome prevents
800+ // the session from being mistakenly resumed.
743801 loop {
744- let ( response, ctx) =
745- self . post_via_relay ( |relay| session. create_poll_request ( relay) ) . await ?;
746- let res = session. process_response ( & response. bytes ( ) . await ?, ctx) . save ( persister) ;
747- match res {
748- Ok ( OptionalTransitionOutcome :: Progress ( psbt) ) => {
749- println ! ( "Proposal received. Processing..." ) ;
750- self . process_pj_response ( psbt) ?;
751- return Ok ( ( ) ) ;
802+ let relay = self . mailroom_manager . choose_relay ( ) ?;
803+ let ( req, ctx) = match session. create_poll_request ( relay. as_str ( ) ) {
804+ Ok ( r) => r,
805+ Err ( e) if e. is_expired ( ) => {
806+ let pending = session. cancel ( ) . save ( persister) ?;
807+ self . broadcast_fallback_tx (
808+ pending. fallback_tx ( ) ,
809+ & persister. session_id ( ) ,
810+ "sender" ,
811+ ) ;
812+ pending. close ( ) . save ( persister) ?;
813+ return Err ( anyhow ! ( "Sender session expired" ) ) ;
752814 }
753- Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
754- println ! ( "No response yet." ) ;
755- session = current_state;
756- continue ;
815+ Err ( e) => return Err ( e. into ( ) ) ,
816+ } ;
817+ match self . post_request ( req) . await {
818+ Ok ( response) => {
819+ let bytes = response. bytes ( ) . await ?;
820+ let res = session. process_response ( & bytes, ctx) . save ( persister) ;
821+ match res {
822+ Ok ( OptionalTransitionOutcome :: Progress ( psbt) ) => {
823+ println ! ( "Proposal received. Processing..." ) ;
824+ self . process_pj_response ( psbt) ?;
825+ return Ok ( ( ) ) ;
826+ }
827+ Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
828+ println ! ( "No response yet." ) ;
829+ session = current_state;
830+ continue ;
831+ }
832+ Err ( re) => {
833+ println ! ( "{re}" ) ;
834+ tracing:: debug!( "{re:?}" ) ;
835+ return Err ( anyhow ! ( "Response error" ) . context ( re) ) ;
836+ }
837+ }
757838 }
758- Err ( re ) => {
759- println ! ( "{re }" ) ;
760- tracing :: debug! ( "{re:?}" ) ;
761- return Err ( anyhow ! ( "Response error" ) . context ( re ) ) ;
839+ Err ( e ) => {
840+ tracing :: debug !( "Request to relay {relay} failed: {e:? }" ) ;
841+ self . mailroom_manager . add_failed_relay ( relay ) ;
842+ continue ;
762843 }
763844 }
764845 }
@@ -771,22 +852,44 @@ impl App {
771852 ) -> Result < Receiver < UncheckedOriginalPayload > > {
772853 let mut session = session;
773854 loop {
855+ let relay = self . mailroom_manager . choose_relay ( ) ?;
856+ let ( req, context) = match session. create_poll_request ( relay. as_str ( ) ) {
857+ Ok ( r) => r,
858+ Err ( e) if e. is_expired ( ) => {
859+ session. cancel ( ) . save ( persister) ?;
860+ println ! (
861+ "Receiver session expired before any proposal was received; \
862+ no fallback transaction to broadcast."
863+ ) ;
864+ return Err ( anyhow ! ( "Receiver session expired" ) ) ;
865+ }
866+ Err ( e) => return Err ( e. into ( ) ) ,
867+ } ;
774868 println ! ( "Polling receive request..." ) ;
775- let ( ohttp_response, context) =
776- self . post_via_relay ( |relay| session. create_poll_request ( relay) ) . await ?;
777- let state_transition = session
778- . process_response ( ohttp_response. bytes ( ) . await ?. to_vec ( ) . as_slice ( ) , context)
779- . save ( persister) ;
780- match state_transition {
781- Ok ( OptionalTransitionOutcome :: Progress ( next_state) ) => {
782- println ! ( "Got a request from the sender. Responding with a Payjoin proposal." ) ;
783- return Ok ( next_state) ;
869+ match self . post_request ( req) . await {
870+ Ok ( ohttp_response) => {
871+ let bytes = ohttp_response. bytes ( ) . await ?. to_vec ( ) ;
872+ let state_transition =
873+ session. process_response ( bytes. as_slice ( ) , context) . save ( persister) ;
874+ match state_transition {
875+ Ok ( OptionalTransitionOutcome :: Progress ( next_state) ) => {
876+ println ! (
877+ "Got a request from the sender. Responding with a Payjoin proposal."
878+ ) ;
879+ return Ok ( next_state) ;
880+ }
881+ Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
882+ session = current_state;
883+ continue ;
884+ }
885+ Err ( e) => return Err ( e. into ( ) ) ,
886+ }
784887 }
785- Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
786- session = current_state;
888+ Err ( e) => {
889+ tracing:: debug!( "Request to relay {relay} failed: {e:?}" ) ;
890+ self . mailroom_manager . add_failed_relay ( relay) ;
787891 continue ;
788892 }
789- Err ( e) => return Err ( e. into ( ) ) ,
790893 }
791894 }
792895 }
0 commit comments