@@ -62,7 +62,6 @@ use reqwest::Client;
62
62
use serde:: { Deserialize , Serialize } ;
63
63
use serde_json:: Value ;
64
64
use std:: cmp:: max;
65
- use std:: io:: Cursor ;
66
65
use std:: str:: FromStr ;
67
66
use std:: sync:: atomic:: { AtomicBool , Ordering } ;
68
67
#[ cfg( not( target_arch = "wasm32" ) ) ]
@@ -755,7 +754,7 @@ impl<S: MutinyStorage> NodeManager<S> {
755
754
) )
756
755
}
757
756
758
- // Send v1 payjoin request
757
+ // Send v2 payjoin request
759
758
pub async fn send_payjoin (
760
759
& self ,
761
760
uri : Uri < ' _ , NetworkUnchecked > ,
@@ -768,65 +767,112 @@ impl<S: MutinyStorage> NodeManager<S> {
768
767
. map_err ( |_| MutinyError :: IncorrectNetwork ) ?;
769
768
let address = uri. address . clone ( ) ;
770
769
let original_psbt = self . wallet . create_signed_psbt ( address, amount, fee_rate) ?;
771
-
770
+ // TODO ensure this creates a pending tx in the UI. Ensure locked UTXO.
772
771
let fee_rate = if let Some ( rate) = fee_rate {
773
772
FeeRate :: from_sat_per_vb ( rate)
774
773
} else {
775
774
let sat_per_kwu = self . fee_estimator . get_normal_fee_rate ( ) ;
776
775
FeeRate :: from_sat_per_kwu ( sat_per_kwu as f32 )
777
776
} ;
778
777
let fee_rate = payjoin:: bitcoin:: FeeRate :: from_sat_per_kwu ( fee_rate. sat_per_kwu ( ) as u64 ) ;
779
- let original_psbt = payjoin:: bitcoin:: psbt:: PartiallySignedTransaction :: from_str (
780
- & original_psbt. to_string ( ) ,
781
- )
782
- . map_err ( |_| MutinyError :: WalletOperationFailed ) ?;
783
778
log_debug ! ( self . logger, "Creating payjoin request" ) ;
784
- let ( req, ctx) =
785
- payjoin:: send:: RequestBuilder :: from_psbt_and_uri ( original_psbt. clone ( ) , uri)
786
- . unwrap ( )
787
- . build_recommended ( fee_rate)
788
- . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?
789
- . extract_v1 ( ) ?;
779
+ let req_ctx = payjoin:: send:: RequestBuilder :: from_psbt_and_uri ( original_psbt. clone ( ) , uri)
780
+ . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?
781
+ . build_recommended ( fee_rate)
782
+ . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
783
+ self . spawn_payjoin_sender ( labels, original_psbt. clone ( ) , req_ctx)
784
+ . await ;
785
+ Ok ( original_psbt. extract_tx ( ) . txid ( ) )
786
+ }
790
787
791
- let client = Client :: builder ( )
792
- . build ( )
793
- . map_err ( |e| MutinyError :: Other ( e. into ( ) ) ) ?;
788
+ async fn spawn_payjoin_sender (
789
+ & self ,
790
+ labels : Vec < String > ,
791
+ original_psbt : bitcoin:: psbt:: Psbt ,
792
+ req_ctx : payjoin:: send:: RequestContext ,
793
+ ) {
794
+ let wallet = self . wallet . clone ( ) ;
795
+ let logger = self . logger . clone ( ) ;
796
+ let stop = self . stop . clone ( ) ;
797
+ utils:: spawn ( async move {
798
+ let proposal_psbt = match Self :: poll_payjoin_sender ( stop, req_ctx) . await {
799
+ Ok ( psbt) => psbt,
800
+ Err ( e) => {
801
+ log_error ! ( logger, "Error polling payjoin sender: {e}" ) ;
802
+ return ;
803
+ }
804
+ } ;
794
805
795
- log_debug ! ( self . logger, "Sending payjoin request" ) ;
796
- let res = client
797
- . post ( req. url )
798
- . body ( req. body )
799
- . header ( "Content-Type" , "text/plain" )
800
- . send ( )
801
- . await
802
- . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?
803
- . bytes ( )
806
+ if let Err ( e) = Self :: handle_proposal_psbt (
807
+ logger. clone ( ) ,
808
+ wallet,
809
+ original_psbt,
810
+ proposal_psbt,
811
+ labels,
812
+ )
804
813
. await
805
- . map_err ( |_| MutinyError :: PayjoinCreateRequest ) ?;
806
-
807
- let mut cursor = Cursor :: new ( res. to_vec ( ) ) ;
814
+ {
815
+ // Ensure ResponseError is logged with debug formatting
816
+ log_error ! ( logger, "Error handling payjoin proposal: {:?}" , e) ;
817
+ }
818
+ } ) ;
819
+ }
808
820
809
- log_debug ! ( self . logger, "Processing payjoin response" ) ;
810
- let proposal_psbt = ctx. process_response ( & mut cursor) . map_err ( |e| {
811
- // unrecognized error contents may only appear in debug logs and will not Display
812
- log_debug ! ( self . logger, "Payjoin response error: {:?}" , e) ;
813
- e
814
- } ) ?;
821
+ async fn poll_payjoin_sender (
822
+ stop : Arc < AtomicBool > ,
823
+ mut req_ctx : payjoin:: send:: RequestContext ,
824
+ ) -> Result < bitcoin:: psbt:: Psbt , MutinyError > {
825
+ let http = Client :: builder ( )
826
+ . build ( )
827
+ . map_err ( |_| MutinyError :: Other ( anyhow ! ( "failed to build http client" ) ) ) ?;
828
+ loop {
829
+ if stop. load ( Ordering :: Relaxed ) {
830
+ return Err ( MutinyError :: NotRunning ) ;
831
+ }
815
832
816
- // convert to pdk types
817
- let original_psbt = PartiallySignedTransaction :: from_str ( & original_psbt. to_string ( ) )
818
- . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
819
- let proposal_psbt = PartiallySignedTransaction :: from_str ( & proposal_psbt. to_string ( ) )
820
- . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
833
+ let ( req, ctx) = req_ctx
834
+ . extract_v2 ( crate :: payjoin:: OHTTP_RELAYS [ 0 ] . to_owned ( ) )
835
+ . map_err ( |_| MutinyError :: PayjoinConfigError ) ?;
836
+ let response = http
837
+ . post ( req. url )
838
+ . header ( "Content-Type" , "message/ohttp-req" )
839
+ . body ( req. body )
840
+ . send ( )
841
+ . await
842
+ . map_err ( |_| MutinyError :: Other ( anyhow ! ( "failed to parse payjoin response" ) ) ) ?;
843
+ let mut reader =
844
+ std:: io:: Cursor :: new ( response. bytes ( ) . await . map_err ( |_| {
845
+ MutinyError :: Other ( anyhow ! ( "failed to parse payjoin response" ) )
846
+ } ) ?) ;
847
+
848
+ let psbt = ctx
849
+ . process_response ( & mut reader)
850
+ . map_err ( MutinyError :: PayjoinResponse ) ?;
851
+ if let Some ( psbt) = psbt {
852
+ let psbt = bitcoin:: psbt:: Psbt :: from_str ( & psbt. to_string ( ) )
853
+ . map_err ( |_| MutinyError :: Other ( anyhow ! ( "psbt conversion failed" ) ) ) ?;
854
+ return Ok ( psbt) ;
855
+ } else {
856
+ log:: info!( "No response yet for POST payjoin request, retrying some seconds" ) ;
857
+ std:: thread:: sleep ( std:: time:: Duration :: from_secs ( 5 ) ) ;
858
+ }
859
+ }
860
+ }
821
861
822
- log_debug ! ( self . logger, "Sending payjoin.." ) ;
823
- let tx = self
824
- . wallet
862
+ async fn handle_proposal_psbt (
863
+ logger : Arc < MutinyLogger > ,
864
+ wallet : Arc < OnChainWallet < S > > ,
865
+ original_psbt : PartiallySignedTransaction ,
866
+ proposal_psbt : PartiallySignedTransaction ,
867
+ labels : Vec < String > ,
868
+ ) -> Result < Txid , MutinyError > {
869
+ log_debug ! ( logger, "Sending payjoin.." ) ;
870
+ let tx = wallet
825
871
. send_payjoin ( original_psbt, proposal_psbt, labels)
826
872
. await ?;
827
873
let txid = tx. txid ( ) ;
828
- self . broadcast_transaction ( tx) . await ?;
829
- log_debug ! ( self . logger, "Payjoin broadcast! TXID: {txid}" ) ;
874
+ wallet . broadcast_transaction ( tx) . await ?;
875
+ log_info ! ( logger, "Payjoin broadcast! TXID: {txid}" ) ;
830
876
Ok ( txid)
831
877
}
832
878
0 commit comments