@@ -2,6 +2,7 @@ use crate::auth::MutinyAuthClient;
22use crate :: labels:: LabelStorage ;
33use crate :: ldkstorage:: CHANNEL_CLOSURE_PREFIX ;
44use crate :: logging:: LOGGING_KEY ;
5+ use crate :: payjoin:: Error as PayjoinError ;
56use crate :: utils:: { sleep, spawn} ;
67use crate :: MutinyInvoice ;
78use crate :: MutinyWalletConfig ;
@@ -50,6 +51,7 @@ use lightning::{log_debug, log_error, log_info, log_trace, log_warn};
5051use lightning_invoice:: Bolt11Invoice ;
5152use lightning_transaction_sync:: EsploraSyncClient ;
5253use payjoin:: Uri ;
54+ use pj:: receive:: v2:: Enrolled ;
5355use reqwest:: Client ;
5456use serde:: { Deserialize , Serialize } ;
5557use serde_json:: Value ;
@@ -99,6 +101,8 @@ pub struct MutinyBip21RawMaterials {
99101 pub invoice : Option < Bolt11Invoice > ,
100102 pub btc_amount : Option < String > ,
101103 pub labels : Vec < String > ,
104+ pub pj : Option < String > ,
105+ pub ohttp : Option < String > ,
102106}
103107
104108#[ derive( Serialize , Deserialize , Clone , Eq , PartialEq ) ]
@@ -666,6 +670,34 @@ impl<S: MutinyStorage> NodeManager<S> {
666670 Err ( MutinyError :: WalletOperationFailed )
667671 }
668672
673+ pub async fn start_payjoin_session (
674+ & self ,
675+ ) -> Result < ( Enrolled , payjoin:: OhttpKeys ) , PayjoinError > {
676+ use crate :: payjoin:: { fetch_ohttp_keys, random_ohttp_relay, PAYJOIN_DIR } ;
677+
678+ let ohttp_keys = fetch_ohttp_keys ( PAYJOIN_DIR . to_owned ( ) ) . await ?;
679+ let http_client = reqwest:: Client :: builder ( ) . build ( ) ?;
680+
681+ let mut enroller = payjoin:: receive:: v2:: Enroller :: from_directory_config (
682+ PAYJOIN_DIR . to_owned ( ) ,
683+ ohttp_keys. clone ( ) ,
684+ random_ohttp_relay ( ) . to_owned ( ) ,
685+ ) ;
686+ let ( req, context) = enroller. extract_req ( ) ?;
687+ let ohttp_response = http_client
688+ . post ( req. url )
689+ . header ( "Content-Type" , "message/ohttp-req" )
690+ . body ( req. body )
691+ . send ( )
692+ . await ?;
693+ let ohttp_response = ohttp_response. bytes ( ) . await ?;
694+ Ok ( (
695+ enroller. process_res ( ohttp_response. as_ref ( ) , context) ?,
696+ ohttp_keys,
697+ ) )
698+ }
699+
700+ // Send v1 payjoin request
669701 pub async fn send_payjoin (
670702 & self ,
671703 uri : Uri < ' _ , NetworkUnchecked > ,
@@ -740,6 +772,78 @@ impl<S: MutinyStorage> NodeManager<S> {
740772 Ok ( txid)
741773 }
742774
775+ pub fn spawn_payjoin_receiver ( & self , enrolled : Enrolled ) {
776+ let logger = self . logger . clone ( ) ;
777+ let wallet = self . wallet . clone ( ) ;
778+ utils:: spawn ( async move {
779+ match Self :: receive_payjoin ( wallet, enrolled) . await {
780+ Ok ( txid) => log_info ! ( logger, "Received payjoin txid: {txid}" ) ,
781+ Err ( e) => log_error ! ( logger, "Error receiving payjoin: {e}" ) ,
782+ } ;
783+ } ) ;
784+ }
785+
786+ /// Poll the payjoin relay to maintain a payjoin session and create a payjoin proposal.
787+ async fn receive_payjoin (
788+ wallet : Arc < OnChainWallet < S > > ,
789+ mut enrolled : payjoin:: receive:: v2:: Enrolled ,
790+ ) -> Result < Txid , MutinyError > {
791+ let http_client = reqwest:: Client :: builder ( )
792+ . build ( )
793+ . map_err ( PayjoinError :: Reqwest ) ?;
794+ let proposal: payjoin:: receive:: v2:: UncheckedProposal =
795+ Self :: poll_for_fallback_psbt ( & http_client, & mut enrolled) . await ?;
796+ let original_tx = proposal. extract_tx_to_schedule_broadcast ( ) ;
797+ let mut payjoin_proposal = match wallet
798+ . process_payjoin_proposal ( proposal)
799+ . map_err ( |e| PayjoinError :: ReceiverStateMachine ( e. to_string ( ) ) )
800+ {
801+ Ok ( p) => p,
802+ Err ( e) => {
803+ wallet. broadcast_transaction ( original_tx) . await ?;
804+ return Err ( e. into ( ) ) ;
805+ }
806+ } ;
807+
808+ let ( req, ohttp_ctx) = payjoin_proposal
809+ . extract_v2_req ( )
810+ . map_err ( |e| PayjoinError :: ReceiverStateMachine ( e. to_string ( ) ) ) ?;
811+ let res = http_client
812+ . post ( req. url )
813+ . header ( "Content-Type" , "message/ohttp-req" )
814+ . body ( req. body )
815+ . send ( )
816+ . await
817+ . map_err ( PayjoinError :: Reqwest ) ?;
818+ let res = res. bytes ( ) . await . map_err ( PayjoinError :: Reqwest ) ?;
819+ // enroll must succeed
820+ let _res = payjoin_proposal
821+ . deserialize_res ( res. to_vec ( ) , ohttp_ctx)
822+ . map_err ( |e| PayjoinError :: ReceiverStateMachine ( e. to_string ( ) ) ) ?;
823+ Ok ( payjoin_proposal. psbt ( ) . clone ( ) . extract_tx ( ) . txid ( ) )
824+ }
825+
826+ async fn poll_for_fallback_psbt (
827+ client : & reqwest:: Client ,
828+ enroller : & mut payjoin:: receive:: v2:: Enrolled ,
829+ ) -> Result < payjoin:: receive:: v2:: UncheckedProposal , PayjoinError > {
830+ loop {
831+ let ( req, context) = enroller. extract_req ( ) ?;
832+ let ohttp_response = client
833+ . post ( req. url )
834+ . header ( "Content-Type" , "message/ohttp-req" )
835+ . body ( req. body )
836+ . send ( )
837+ . await ?;
838+ let ohttp_response = ohttp_response. bytes ( ) . await ?;
839+ let proposal = enroller. process_res ( ohttp_response. as_ref ( ) , context) ?;
840+ match proposal {
841+ Some ( proposal) => return Ok ( proposal) ,
842+ None => utils:: sleep ( 5000 ) . await ,
843+ }
844+ }
845+ }
846+
743847 /// Sends an on-chain transaction to the given address.
744848 /// The amount is in satoshis and the fee rate is in sat/vbyte.
745849 ///
0 commit comments