@@ -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,7 +700,31 @@ 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
@@ -739,26 +775,53 @@ impl App {
739775 persister : & SenderPersister ,
740776 ) -> Result < ( ) > {
741777 let mut session = sender. clone ( ) ;
742- // Long poll until we get a response
778+ // Long poll until we get a response. The session's expiration is enforced
779+ // here: once `create_poll_request` reports expiry, the session is driven to
780+ // a terminal `Closed(Aborted)` state and the fallback transaction is surfaced
781+ // so it can be broadcast manually. The persisted terminal outcome prevents
782+ // the session from being mistakenly resumed.
743783 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 ( ( ) ) ;
784+ let relay = self . mailroom_manager . choose_relay ( ) ?;
785+ let ( req, ctx) = match session. create_poll_request ( relay. as_str ( ) ) {
786+ Ok ( r) => r,
787+ Err ( e) if e. is_expired ( ) => {
788+ let pending = session. cancel ( ) . save ( persister) ?;
789+ self . broadcast_fallback_tx (
790+ pending. fallback_tx ( ) ,
791+ & persister. session_id ( ) ,
792+ "sender" ,
793+ ) ;
794+ pending. close ( ) . save ( persister) ?;
795+ return Err ( anyhow ! ( "Sender session expired" ) ) ;
752796 }
753- Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
754- println ! ( "No response yet." ) ;
755- session = current_state;
756- continue ;
797+ Err ( e) => return Err ( e. into ( ) ) ,
798+ } ;
799+ match self . post_request ( req) . await {
800+ Ok ( response) => {
801+ let bytes = response. bytes ( ) . await ?;
802+ let res = session. process_response ( & bytes, ctx) . save ( persister) ;
803+ match res {
804+ Ok ( OptionalTransitionOutcome :: Progress ( psbt) ) => {
805+ println ! ( "Proposal received. Processing..." ) ;
806+ self . process_pj_response ( psbt) ?;
807+ return Ok ( ( ) ) ;
808+ }
809+ Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
810+ println ! ( "No response yet." ) ;
811+ session = current_state;
812+ continue ;
813+ }
814+ Err ( re) => {
815+ println ! ( "{re}" ) ;
816+ tracing:: debug!( "{re:?}" ) ;
817+ return Err ( anyhow ! ( "Response error" ) . context ( re) ) ;
818+ }
819+ }
757820 }
758- Err ( re ) => {
759- println ! ( "{re }" ) ;
760- tracing :: debug! ( "{re:?}" ) ;
761- return Err ( anyhow ! ( "Response error" ) . context ( re ) ) ;
821+ Err ( e ) => {
822+ tracing :: debug !( "Request to relay {relay} failed: {e:? }" ) ;
823+ self . mailroom_manager . add_failed_relay ( relay ) ;
824+ continue ;
762825 }
763826 }
764827 }
@@ -771,22 +834,44 @@ impl App {
771834 ) -> Result < Receiver < UncheckedOriginalPayload > > {
772835 let mut session = session;
773836 loop {
837+ let relay = self . mailroom_manager . choose_relay ( ) ?;
838+ let ( req, context) = match session. create_poll_request ( relay. as_str ( ) ) {
839+ Ok ( r) => r,
840+ Err ( e) if e. is_expired ( ) => {
841+ session. cancel ( ) . save ( persister) ?;
842+ println ! (
843+ "Receiver session expired before any proposal was received; \
844+ no fallback transaction to broadcast."
845+ ) ;
846+ return Err ( anyhow ! ( "Receiver session expired" ) ) ;
847+ }
848+ Err ( e) => return Err ( e. into ( ) ) ,
849+ } ;
774850 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) ;
851+ match self . post_request ( req) . await {
852+ Ok ( ohttp_response) => {
853+ let bytes = ohttp_response. bytes ( ) . await ?. to_vec ( ) ;
854+ let state_transition =
855+ session. process_response ( bytes. as_slice ( ) , context) . save ( persister) ;
856+ match state_transition {
857+ Ok ( OptionalTransitionOutcome :: Progress ( next_state) ) => {
858+ println ! (
859+ "Got a request from the sender. Responding with a Payjoin proposal."
860+ ) ;
861+ return Ok ( next_state) ;
862+ }
863+ Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
864+ session = current_state;
865+ continue ;
866+ }
867+ Err ( e) => return Err ( e. into ( ) ) ,
868+ }
784869 }
785- Ok ( OptionalTransitionOutcome :: Stasis ( current_state) ) => {
786- session = current_state;
870+ Err ( e) => {
871+ tracing:: debug!( "Request to relay {relay} failed: {e:?}" ) ;
872+ self . mailroom_manager . add_failed_relay ( relay) ;
787873 continue ;
788874 }
789- Err ( e) => return Err ( e. into ( ) ) ,
790875 }
791876 }
792877 }
0 commit comments