@@ -11,7 +11,10 @@ use clap::ArgMatches;
1111use config:: { Config , File , FileFormat } ;
1212use payjoin:: bitcoin:: psbt:: Psbt ;
1313use payjoin:: bitcoin:: { self , base64} ;
14- use payjoin:: receive:: { Error , PayjoinProposal , ProvisionalProposal , UncheckedProposal } ;
14+ use payjoin:: receive:: {
15+ EnrollContext , Error , PayjoinProposal , ProvisionalProposal , UncheckedProposal ,
16+ } ;
17+ use payjoin:: send:: RequestContext ;
1518#[ cfg( not( feature = "v2" ) ) ]
1619use rouille:: { Request , Response } ;
1720use serde:: { Deserialize , Serialize } ;
@@ -44,61 +47,61 @@ impl App {
4447
4548 #[ cfg( feature = "v2" ) ]
4649 pub async fn send_payjoin ( & self , bip21 : & str ) -> Result < ( ) > {
47- let ( req, ctx) = self . create_pj_request ( bip21) ?;
50+ // TODO extract requests inside poll loop for unique OHTTP payloads
51+ let req_ctx = self . create_pj_request ( bip21) ?;
4852
4953 let client = reqwest:: Client :: builder ( )
5054 . danger_accept_invalid_certs ( self . config . danger_accept_invalid_certs )
5155 . build ( )
5256 . with_context ( || "Failed to build reqwest http client" ) ?;
5357
5458 log:: debug!( "Awaiting response" ) ;
55- let res = Self :: long_poll_post ( & client, req) . await ?;
56- let mut res = std:: io:: Cursor :: new ( & res) ;
57- self . process_pj_response ( ctx, & mut res) ?;
59+ let res = self . long_poll_post ( & client, req_ctx) . await ?;
60+ self . process_pj_response ( res) ?;
5861 Ok ( ( ) )
5962 }
6063
6164 #[ cfg( feature = "v2" ) ]
6265 async fn long_poll_post (
66+ & self ,
6367 client : & reqwest:: Client ,
64- req : payjoin:: send:: Request ,
65- ) -> Result < Vec < u8 > , reqwest :: Error > {
68+ req_ctx : payjoin:: send:: RequestContext < ' _ > ,
69+ ) -> Result < Psbt > {
6670 loop {
71+ let ( req, ctx) = req_ctx. extract_v2 ( & self . config . ohttp_proxy ) ?;
6772 let response = client
68- . post ( req. url . as_str ( ) )
73+ . post ( req. url )
6974 . body ( req. body . clone ( ) )
7075 . header ( "Content-Type" , "text/plain" )
7176 . send ( )
7277 . await ?;
73-
74- if response. status ( ) == reqwest:: StatusCode :: OK {
75- let body = response. bytes ( ) . await ?. to_vec ( ) ;
76- return Ok ( body) ;
77- } else if response. status ( ) == reqwest:: StatusCode :: ACCEPTED {
78+ let bytes = response. bytes ( ) . await ?;
79+ let mut cursor = std:: io:: Cursor :: new ( bytes) ;
80+ let psbt = ctx. process_response ( & mut cursor) ?;
81+ if let Some ( psbt) = psbt {
82+ return Ok ( psbt) ;
83+ } else {
7884 log:: info!( "No response yet for POST payjoin request, retrying some seconds" ) ;
7985 tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 5 ) ) . await ;
80- } else {
81- log:: error!( "Unexpected response status: {}" , response. status( ) ) ;
82- // TODO handle error
83- panic ! ( "Unexpected response status: {}" , response. status( ) )
8486 }
8587 }
8688 }
8789
8890 #[ cfg( feature = "v2" ) ]
89- async fn long_poll_get ( client : & reqwest:: Client , url : & str ) -> Result < Vec < u8 > , reqwest:: Error > {
91+ async fn long_poll_get (
92+ & self ,
93+ client : & reqwest:: Client ,
94+ enroll_context : & mut EnrollContext ,
95+ ) -> Result < UncheckedProposal , reqwest:: Error > {
9096 loop {
91- let response = client. get ( url) . send ( ) . await ?;
92-
93- if response. status ( ) . is_success ( ) {
94- let body = response. bytes ( ) . await ?;
95- if !body. is_empty ( ) {
96- return Ok ( body. to_vec ( ) ) ;
97- } else {
98- log:: info!( "No response yet for GET payjoin request, retrying in 5 seconds" ) ;
99- }
100-
101- tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 5 ) ) . await ;
97+ let ( enroll_body, context) = enroll_context. enroll_body ( ) ;
98+ let ohttp_response =
99+ client. post ( & self . config . ohttp_proxy ) . body ( enroll_body) . send ( ) . await ?;
100+ let ohttp_response = ohttp_response. bytes ( ) . await ?;
101+ let proposal = enroll_context. parse_proposal ( ohttp_response. as_ref ( ) , context) . unwrap ( ) ;
102+ match proposal {
103+ Some ( proposal) => return Ok ( proposal) ,
104+ None => tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 5 ) ) . await ,
102105 }
103106 }
104107 }
@@ -122,10 +125,7 @@ impl App {
122125 Ok ( ( ) )
123126 }
124127
125- fn create_pj_request (
126- & self ,
127- bip21 : & str ,
128- ) -> Result < ( payjoin:: send:: Request , payjoin:: send:: Context ) > {
128+ fn create_pj_request < ' a > ( & self , bip21 : & ' a str ) -> Result < RequestContext < ' a > > {
129129 let uri = payjoin:: Uri :: try_from ( bip21)
130130 . map_err ( |e| anyhow ! ( "Failed to create URI from BIP21: {}" , e) ) ?;
131131
@@ -167,22 +167,16 @@ impl App {
167167 . psbt ;
168168 let psbt = Psbt :: from_str ( & psbt) . with_context ( || "Failed to load PSBT from base64" ) ?;
169169 log:: debug!( "Original psbt: {:#?}" , psbt) ;
170-
171- let ( req, ctx) = payjoin:: send:: RequestBuilder :: from_psbt_and_uri ( psbt, uri)
170+ let req_ctx = payjoin:: send:: RequestBuilder :: from_psbt_and_uri ( psbt, uri)
172171 . with_context ( || "Failed to build payjoin request" ) ?
173172 . build_recommended ( fee_rate)
174173 . with_context ( || "Failed to build payjoin request" ) ?;
175174
176- Ok ( ( req , ctx ) )
175+ Ok ( req_ctx )
177176 }
178177
179- fn process_pj_response (
180- & self ,
181- ctx : payjoin:: send:: Context ,
182- response : & mut impl std:: io:: Read ,
183- ) -> Result < bitcoin:: Txid > {
178+ fn process_pj_response ( & self , psbt : Psbt ) -> Result < bitcoin:: Txid > {
184179 // TODO display well-known errors and log::debug the rest
185- let psbt = ctx. process_response ( response) . with_context ( || "Failed to process response" ) ?;
186180 log:: debug!( "Proposed psbt: {:#?}" , psbt) ;
187181 let psbt = self
188182 . bitcoind
@@ -218,7 +212,11 @@ impl App {
218212
219213 #[ cfg( feature = "v2" ) ]
220214 pub async fn receive_payjoin ( self , amount_arg : & str ) -> Result < ( ) > {
221- let context = payjoin:: receive:: ProposalContext :: new ( ) ;
215+ let mut context = EnrollContext :: from_relay_config (
216+ & self . config . pj_endpoint ,
217+ & self . config . ohttp_config ,
218+ & self . config . ohttp_proxy ,
219+ ) ;
222220 let pj_uri_string =
223221 self . construct_payjoin_uri ( amount_arg, Some ( & context. subdirectory ( ) ) ) ?;
224222 println ! (
@@ -231,25 +229,26 @@ impl App {
231229 . danger_accept_invalid_certs ( self . config . danger_accept_invalid_certs )
232230 . build ( )
233231 . with_context ( || "Failed to build reqwest http client" ) ?;
234- log:: debug!( "Awaiting request" ) ;
235- let receive_endpoint = format ! ( "{}/{}" , self . config. pj_endpoint, context. receive_subdir( ) ) ;
236- let mut buffer = Self :: long_poll_get ( & client, & receive_endpoint) . await ?;
232+ log:: debug!( "Awaiting proposal" ) ;
233+ let proposal = self . long_poll_get ( & client, & mut context) . await ?;
237234
238- log:: debug!( "Received request" ) ;
239- let proposal = context
240- . parse_proposal ( & mut buffer)
241- . map_err ( |e| anyhow ! ( "Failed to parse into UncheckedProposal {}" , e) ) ?;
235+ log:: debug!( "Received proposal" ) ;
242236 let payjoin_proposal = self
243237 . process_proposal ( proposal)
244238 . map_err ( |e| anyhow ! ( "Failed to process UncheckedProposal {}" , e) ) ?;
245239
246- let body = payjoin_proposal. serialize_body ( ) ;
247- let _ = client
248- . post ( receive_endpoint)
240+ let receive_endpoint = format ! ( "{}/{}" , self . config. pj_endpoint, context. receive_subdir( ) ) ;
241+ let ( body, ohttp_ctx) =
242+ payjoin_proposal. extract_v2_req ( & self . config . ohttp_config , & receive_endpoint) ;
243+ let res = client
244+ . post ( & self . config . ohttp_proxy )
249245 . body ( body)
250246 . send ( )
251247 . await
252248 . with_context ( || "HTTP request failed" ) ?;
249+ let res = res. bytes ( ) . await ?;
250+ let res = payjoin_proposal. deserialize_res ( res. to_vec ( ) , ohttp_ctx) ;
251+ log:: debug!( "Received response {:?}" , res) ;
253252 Ok ( ( ) )
254253 }
255254
@@ -286,14 +285,15 @@ impl App {
286285 let amount = Amount :: from_sat ( amount_arg. parse ( ) ?) ;
287286 //let subdir = self.config.pj_endpoint + pubkey.map_or(&String::from(""), |s| &format!("/{}", s));
288287 let pj_uri_string = format ! (
289- "{}?amount={}&pj={}" ,
288+ "{}?amount={}&pj={}&ohttp={} " ,
290289 pj_receiver_address. to_qr_uri( ) ,
291290 amount. to_btc( ) ,
292291 format!(
293292 "{}{}" ,
294293 self . config. pj_endpoint,
295294 pubkey. map_or( String :: from( "" ) , |s| format!( "/{}" , s) )
296- )
295+ ) ,
296+ self . config. ohttp_config,
297297 ) ;
298298
299299 // check validity
@@ -472,6 +472,19 @@ impl App {
472472 }
473473}
474474
475+ fn serialize_request_to_bytes ( req : reqwest:: Request ) -> Vec < u8 > {
476+ let mut serialized_request =
477+ format ! ( "{} {} HTTP/1.1\r \n " , req. method( ) , req. url( ) ) . into_bytes ( ) ;
478+
479+ for ( name, value) in req. headers ( ) . iter ( ) {
480+ let header_line = format ! ( "{}: {}\r \n " , name. as_str( ) , value. to_str( ) . unwrap( ) ) ;
481+ serialized_request. extend ( header_line. as_bytes ( ) ) ;
482+ }
483+
484+ serialized_request. extend ( b"\r \n " ) ;
485+ serialized_request
486+ }
487+
475488struct SeenInputs {
476489 set : OutPointSet ,
477490 file : std:: fs:: File ,
@@ -511,6 +524,8 @@ pub(crate) struct AppConfig {
511524 pub bitcoind_cookie : Option < String > ,
512525 pub bitcoind_rpcuser : String ,
513526 pub bitcoind_rpcpass : String ,
527+ pub ohttp_config : String ,
528+ pub ohttp_proxy : String ,
514529
515530 // send-only
516531 pub danger_accept_invalid_certs : bool ,
@@ -544,6 +559,16 @@ impl AppConfig {
544559 "bitcoind_rpcpass" ,
545560 matches. get_one :: < String > ( "rpcpass" ) . map ( |s| s. as_str ( ) ) ,
546561 ) ?
562+ . set_default ( "ohttp_config" , "" ) ?
563+ . set_override_option (
564+ "ohttp_config" ,
565+ matches. get_one :: < String > ( "ohttp_config" ) . map ( |s| s. as_str ( ) ) ,
566+ ) ?
567+ . set_default ( "ohttp_proxy" , "" ) ?
568+ . set_override_option (
569+ "ohttp_proxy" ,
570+ matches. get_one :: < String > ( "ohttp_proxy" ) . map ( |s| s. as_str ( ) ) ,
571+ ) ?
547572 // Subcommand defaults without which file serialization fails.
548573 . set_default ( "danger_accept_invalid_certs" , false ) ?
549574 . set_default ( "pj_host" , "0.0.0.0:3000" ) ?
0 commit comments