@@ -2,7 +2,7 @@ use crate::auth::MutinyAuthClient;
2
2
use crate :: labels:: LabelStorage ;
3
3
use crate :: ldkstorage:: CHANNEL_CLOSURE_PREFIX ;
4
4
use crate :: logging:: LOGGING_KEY ;
5
- use crate :: payjoin:: { Error as PayjoinError , PayjoinStorage , RecvSession } ;
5
+ use crate :: payjoin:: { random_ohttp_relay , Error as PayjoinError , PayjoinStorage , RecvSession } ;
6
6
use crate :: utils:: { sleep, spawn} ;
7
7
use crate :: MutinyInvoice ;
8
8
use crate :: MutinyWalletConfig ;
@@ -56,7 +56,6 @@ use reqwest::Client;
56
56
use serde:: { Deserialize , Serialize } ;
57
57
use serde_json:: Value ;
58
58
use std:: cmp:: max;
59
- use std:: io:: Cursor ;
60
59
use std:: str:: FromStr ;
61
60
use std:: sync:: atomic:: { AtomicBool , Ordering } ;
62
61
#[ cfg( not( target_arch = "wasm32" ) ) ]
@@ -580,10 +579,14 @@ impl<S: MutinyStorage> NodeManager<S> {
580
579
581
580
/// Starts a background task to poll payjoin sessions to attempt receiving.
582
581
pub ( crate ) fn resume_payjoins ( nm : Arc < NodeManager < S > > ) {
583
- let all = nm. storage . list_recv_sessions ( ) . unwrap_or_default ( ) ;
584
- for payjoin in all {
582
+ let receives = nm. storage . list_recv_sessions ( ) . unwrap_or_default ( ) ;
583
+ for payjoin in receives {
585
584
nm. clone ( ) . spawn_payjoin_receiver ( payjoin) ;
586
585
}
586
+ let sends = nm. storage . list_send_sessions ( ) . unwrap_or_default ( ) ;
587
+ for payjoin in sends {
588
+ nm. clone ( ) . spawn_payjoin_sender ( payjoin) ;
589
+ }
587
590
}
588
591
589
592
/// Creates a background process that will sync the wallet with the blockchain.
@@ -678,7 +681,7 @@ impl<S: MutinyStorage> NodeManager<S> {
678
681
pub async fn start_payjoin_session (
679
682
& self ,
680
683
) -> Result < ( Enrolled , payjoin:: OhttpKeys ) , PayjoinError > {
681
- use crate :: payjoin:: { fetch_ohttp_keys, random_ohttp_relay , PAYJOIN_DIR } ;
684
+ use crate :: payjoin:: { fetch_ohttp_keys, PAYJOIN_DIR } ;
682
685
683
686
log_info ! ( self . logger, "Starting payjoin session" ) ;
684
687
@@ -704,7 +707,7 @@ impl<S: MutinyStorage> NodeManager<S> {
704
707
) )
705
708
}
706
709
707
- // Send v1 payjoin request
710
+ // Send v2 payjoin request
708
711
pub async fn send_payjoin (
709
712
& self ,
710
713
uri : Uri < ' _ , NetworkUnchecked > ,
@@ -717,64 +720,151 @@ impl<S: MutinyStorage> NodeManager<S> {
717
720
. map_err ( |_| MutinyError :: IncorrectNetwork ) ?;
718
721
let address = uri. address . clone ( ) ;
719
722
let original_psbt = self . wallet . create_signed_psbt ( address, amount, fee_rate) ?;
723
+ // Track this transaction in the wallet so it shows as an ActivityItem in UI.
724
+ // We'll cancel it if and when this original_psbt fallback is replaced with a received payjoin.
725
+ self . wallet
726
+ . insert_tx (
727
+ original_psbt. clone ( ) . extract_tx ( ) ,
728
+ ConfirmationTime :: unconfirmed ( crate :: utils:: now ( ) . as_secs ( ) ) ,
729
+ None ,
730
+ )
731
+ . await ?;
732
+
720
733
let fee_rate = if let Some ( rate) = fee_rate {
721
734
FeeRate :: from_sat_per_vb ( rate)
722
735
} else {
723
736
let sat_per_kwu = self . fee_estimator . get_normal_fee_rate ( ) ;
724
737
FeeRate :: from_sat_per_kwu ( sat_per_kwu as f32 )
725
738
} ;
726
739
let fee_rate = payjoin:: bitcoin:: FeeRate :: from_sat_per_kwu ( fee_rate. sat_per_kwu ( ) as u64 ) ;
727
- let original_psbt = payjoin:: bitcoin:: psbt:: PartiallySignedTransaction :: from_str (
728
- & original_psbt. to_string ( ) ,
729
- )
730
- . map_err ( |_| MutinyError :: WalletOperationFailed ) ?;
731
740
log_debug ! ( self . logger, "Creating payjoin request" ) ;
732
- let ( req, ctx) =
733
- payjoin:: send:: RequestBuilder :: from_psbt_and_uri ( original_psbt. clone ( ) , uri)
734
- . unwrap ( )
735
- . build_recommended ( fee_rate)
736
- . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?
737
- . extract_v1 ( ) ?;
738
-
739
- let client = Client :: builder ( )
740
- . build ( )
741
- . map_err ( |e| MutinyError :: Other ( e. into ( ) ) ) ?;
741
+ let req_ctx = payjoin:: send:: RequestBuilder :: from_psbt_and_uri ( original_psbt. clone ( ) , uri)
742
+ . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?
743
+ . build_recommended ( fee_rate)
744
+ . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
745
+ let session = self . storage . store_new_send_session (
746
+ labels. clone ( ) ,
747
+ original_psbt. clone ( ) ,
748
+ req_ctx. clone ( ) ,
749
+ ) ?;
750
+ self . spawn_payjoin_sender ( session) ;
751
+ Ok ( original_psbt. extract_tx ( ) . txid ( ) )
752
+ }
742
753
743
- log_debug ! ( self . logger, "Sending payjoin request" ) ;
744
- let res = client
745
- . post ( req. url )
746
- . body ( req. body )
747
- . header ( "Content-Type" , "text/plain" )
748
- . send ( )
754
+ fn spawn_payjoin_sender ( & self , session : crate :: payjoin:: SendSession ) {
755
+ let wallet = self . wallet . clone ( ) ;
756
+ let logger = self . logger . clone ( ) ;
757
+ let stop = self . stop . clone ( ) ;
758
+ let storage = Arc :: new ( self . storage . clone ( ) ) ;
759
+ utils:: spawn ( async move {
760
+ let proposal_psbt = match Self :: poll_payjoin_sender (
761
+ stop,
762
+ wallet. clone ( ) ,
763
+ storage. clone ( ) ,
764
+ session. clone ( ) ,
765
+ )
749
766
. await
750
- . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?
751
- . bytes ( )
767
+ {
768
+ Ok ( psbt) => psbt,
769
+ Err ( e) => {
770
+ // self.wallet cancel_tx
771
+ log_error ! ( logger, "Error polling payjoin sender: {e}" ) ;
772
+ return ;
773
+ }
774
+ } ;
775
+
776
+ let session_clone = session. clone ( ) ;
777
+ match Self :: handle_proposal_psbt (
778
+ logger. clone ( ) ,
779
+ wallet,
780
+ session_clone. original_psbt ,
781
+ proposal_psbt,
782
+ session_clone. labels ,
783
+ )
752
784
. await
753
- . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?;
785
+ {
786
+ // Ensure ResponseError is logged with debug formatting
787
+ Err ( e) => log_error ! ( logger, "Error handling payjoin proposal: {:?}" , e) ,
788
+ Ok ( txid) => log_info ! ( logger, "Payjoin proposal handled: {}" , txid) ,
789
+ }
790
+ let o_txid = session. clone ( ) . original_psbt . clone ( ) . extract_tx ( ) . txid ( ) ;
791
+ match storage. delete_send_session ( session) {
792
+ Ok ( _) => log_info ! ( logger, "Deleted payjoin send session: {}" , o_txid) ,
793
+ Err ( e) => log_error ! ( logger, "Error deleting payjoin send session: {e}" ) ,
794
+ }
795
+ } ) ;
796
+ }
754
797
755
- let mut cursor = Cursor :: new ( res. to_vec ( ) ) ;
798
+ async fn poll_payjoin_sender (
799
+ stop : Arc < AtomicBool > ,
800
+ wallet : Arc < OnChainWallet < S > > ,
801
+ storage : Arc < S > ,
802
+ mut session : crate :: payjoin:: SendSession ,
803
+ ) -> Result < bitcoin:: psbt:: Psbt , MutinyError > {
804
+ let http = Client :: builder ( )
805
+ . build ( )
806
+ . map_err ( |_| MutinyError :: Other ( anyhow ! ( "failed to build http client" ) ) ) ?;
807
+ loop {
808
+ if stop. load ( Ordering :: Relaxed ) {
809
+ return Err ( MutinyError :: NotRunning ) ;
810
+ }
756
811
757
- log_debug ! ( self . logger, "Processing payjoin response" ) ;
758
- let proposal_psbt = ctx. process_response ( & mut cursor) . map_err ( |e| {
759
- // unrecognized error contents may only appear in debug logs and will not Display
760
- log_debug ! ( self . logger, "Payjoin response error: {:?}" , e) ;
761
- e
762
- } ) ?;
812
+ if session. expiry < utils:: now ( ) {
813
+ wallet
814
+ . cancel_tx ( & session. clone ( ) . original_psbt . extract_tx ( ) )
815
+ . map_err ( |_| crate :: payjoin:: Error :: CancelPayjoinTx ) ?;
816
+ storage. delete_send_session ( session) ?;
817
+ return Err ( MutinyError :: Payjoin ( crate :: payjoin:: Error :: SessionExpired ) ) ;
818
+ }
763
819
764
- // convert to pdk types
765
- let original_psbt = PartiallySignedTransaction :: from_str ( & original_psbt. to_string ( ) )
766
- . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
767
- let proposal_psbt = PartiallySignedTransaction :: from_str ( & proposal_psbt. to_string ( ) )
768
- . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
820
+ let ( req, ctx) = session
821
+ . req_ctx
822
+ . extract_v2 ( random_ohttp_relay ( ) . to_owned ( ) )
823
+ . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
824
+ // extract_v2 mutates the session, so we need to update it in storage to not reuse keys
825
+ storage. update_send_session ( session. clone ( ) ) ?;
826
+ let response = http
827
+ . post ( req. url )
828
+ . header ( "Content-Type" , "message/ohttp-req" )
829
+ . body ( req. body )
830
+ . send ( )
831
+ . await
832
+ . map_err ( |_| MutinyError :: Other ( anyhow ! ( "failed to parse payjoin response" ) ) ) ?;
833
+ let mut reader =
834
+ std:: io:: Cursor :: new ( response. bytes ( ) . await . map_err ( |_| {
835
+ MutinyError :: Other ( anyhow ! ( "failed to parse payjoin response" ) )
836
+ } ) ?) ;
837
+
838
+ let psbt = ctx
839
+ . process_response ( & mut reader)
840
+ . map_err ( MutinyError :: PayjoinResponse ) ?;
841
+ if let Some ( psbt) = psbt {
842
+ let psbt = bitcoin:: psbt:: Psbt :: from_str ( & psbt. to_string ( ) )
843
+ . map_err ( |_| MutinyError :: Other ( anyhow ! ( "psbt conversion failed" ) ) ) ?;
844
+ return Ok ( psbt) ;
845
+ } else {
846
+ log:: info!( "No response yet for POST payjoin request, retrying some seconds" ) ;
847
+ std:: thread:: sleep ( std:: time:: Duration :: from_secs ( 5 ) ) ;
848
+ }
849
+ }
850
+ }
769
851
770
- log_debug ! ( self . logger, "Sending payjoin.." ) ;
771
- let tx = self
772
- . wallet
852
+ async fn handle_proposal_psbt (
853
+ logger : Arc < MutinyLogger > ,
854
+ wallet : Arc < OnChainWallet < S > > ,
855
+ original_psbt : PartiallySignedTransaction ,
856
+ proposal_psbt : PartiallySignedTransaction ,
857
+ labels : Vec < String > ,
858
+ ) -> Result < Txid , MutinyError > {
859
+ log_debug ! ( logger, "Sending payjoin.." ) ;
860
+ let original_tx = original_psbt. clone ( ) . extract_tx ( ) ;
861
+ let tx = wallet
773
862
. send_payjoin ( original_psbt, proposal_psbt, labels)
774
863
. await ?;
775
864
let txid = tx. txid ( ) ;
776
- self . broadcast_transaction ( tx) . await ?;
777
- log_debug ! ( self . logger, "Payjoin broadcast! TXID: {txid}" ) ;
865
+ wallet. broadcast_transaction ( tx) . await ?;
866
+ wallet. cancel_tx ( & original_tx) ?;
867
+ log_info ! ( logger, "Payjoin broadcast! TXID: {txid}" ) ;
778
868
Ok ( txid)
779
869
}
780
870
0 commit comments