diff --git a/payjoin-cli/src/app/v1.rs b/payjoin-cli/src/app/v1.rs index 9ab6c9ba7..ad4421316 100644 --- a/payjoin-cli/src/app/v1.rs +++ b/payjoin-cli/src/app/v1.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result}; use bitcoincore_rpc::bitcoin::Amount; use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Full}; -use hyper::body::{Buf, Bytes, Incoming}; +use hyper::body::{Bytes, Incoming}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Method, Request, Response, StatusCode}; @@ -88,12 +88,10 @@ impl AppTrait for App { "Sent fallback transaction hex: {:#}", payjoin::bitcoin::consensus::encode::serialize_hex(&fallback_tx) ); - let psbt = ctx.process_response(&mut response.bytes().await?.to_vec().as_slice()).map_err( - |e| { - log::debug!("Error processing response: {e:?}"); - anyhow!("Failed to process response {e}") - }, - )?; + let psbt = ctx.process_response(&response.bytes().await?).map_err(|e| { + log::debug!("Error processing response: {e:?}"); + anyhow!("Failed to process response {e}") + })?; self.process_pj_response(psbt)?; Ok(()) @@ -279,8 +277,8 @@ impl App { let (parts, body) = req.into_parts(); let headers = Headers(&parts.headers); let query_string = parts.uri.query().unwrap_or(""); - let body = body.collect().await.map_err(|e| Implementation(e.into()))?.aggregate().reader(); - let proposal = UncheckedProposal::from_request(body, query_string, headers)?; + let body = body.collect().await.map_err(|e| Implementation(e.into()))?.to_bytes(); + let proposal = UncheckedProposal::from_request(&body, query_string, headers)?; let payjoin_proposal = self.process_v1_proposal(proposal)?; let psbt = payjoin_proposal.psbt(); diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index 2fbe1b7a8..75173e875 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -234,7 +234,7 @@ impl App { println!("Posting Original PSBT Payload request..."); let response = post_request(req).await?; println!("Sent fallback transaction"); - match v1_ctx.process_response(&mut response.bytes().await?.to_vec().as_slice()) { + match v1_ctx.process_response(&response.bytes().await?) { Ok(psbt) => Ok(psbt), Err(re) => { println!("{re}"); diff --git a/payjoin-ffi/src/send/mod.rs b/payjoin-ffi/src/send/mod.rs index ae2717a02..355c4233a 100644 --- a/payjoin-ffi/src/send/mod.rs +++ b/payjoin-ffi/src/send/mod.rs @@ -1,4 +1,3 @@ -use std::io::Cursor; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -185,10 +184,9 @@ impl From for V1Context { impl V1Context { ///Decodes and validates the response. /// Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast. - pub fn process_response(&self, response: Vec) -> Result { - let mut decoder = Cursor::new(response); + pub fn process_response(&self, response: &[u8]) -> Result { ::clone(&self.0.clone()) - .process_response(&mut decoder) + .process_response(response) .map(|e| e.to_string()) .map_err(Into::into) } diff --git a/payjoin-ffi/src/send/uni.rs b/payjoin-ffi/src/send/uni.rs index 1c1c8b4a8..0c36183fa 100644 --- a/payjoin-ffi/src/send/uni.rs +++ b/payjoin-ffi/src/send/uni.rs @@ -185,7 +185,7 @@ impl From for V1Context { impl V1Context { /// Decodes and validates the response. /// Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast. - pub fn process_response(&self, response: Vec) -> Result { + pub fn process_response(&self, response: &[u8]) -> Result { self.0.process_response(response) } } diff --git a/payjoin/src/receive/error.rs b/payjoin/src/receive/error.rs index b8542f4ed..9cfdc23be 100644 --- a/payjoin/src/receive/error.rs +++ b/payjoin/src/receive/error.rs @@ -164,7 +164,7 @@ impl From for PayloadError { #[derive(Debug)] pub(crate) enum InternalPayloadError { /// The payload is not valid utf-8 - Utf8(std::string::FromUtf8Error), + Utf8(std::str::Utf8Error), /// The payload is not a valid PSBT ParsePsbt(bitcoin::psbt::PsbtParseError), /// Invalid sender parameters diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index 2de2cd536..c6619997b 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -212,11 +212,11 @@ impl<'a> From<&'a InputPair> for InternalInputPair<'a> { /// Validate the payload of a Payjoin request for PSBT and Params sanity pub(crate) fn parse_payload( - base64: String, + base64: &str, query: &str, supported_versions: &'static [Version], ) -> Result<(Psbt, Params), PayloadError> { - let unchecked_psbt = Psbt::from_str(&base64).map_err(InternalPayloadError::ParsePsbt)?; + let unchecked_psbt = Psbt::from_str(base64).map_err(InternalPayloadError::ParsePsbt)?; let psbt = unchecked_psbt.validate().map_err(InternalPayloadError::InconsistentPsbt)?; log::debug!("Received original psbt: {psbt:?}"); diff --git a/payjoin/src/receive/v1/exclusive/error.rs b/payjoin/src/receive/v1/exclusive/error.rs index 66fcdde1d..9ed684658 100644 --- a/payjoin/src/receive/v1/exclusive/error.rs +++ b/payjoin/src/receive/v1/exclusive/error.rs @@ -18,8 +18,6 @@ pub struct RequestError(InternalRequestError); #[derive(Debug)] pub(crate) enum InternalRequestError { - /// I/O error while reading the request body - Io(std::io::Error), /// A required HTTP header is missing from the request MissingHeader(&'static str), /// The Content-Type header has an invalid value @@ -43,8 +41,7 @@ impl From for JsonReply { use InternalRequestError::*; match &e.0 { - Io(_) - | MissingHeader(_) + MissingHeader(_) | InvalidContentType(_) | InvalidContentLength(_) | ContentLengthTooLarge(_) => @@ -56,7 +53,6 @@ impl From for JsonReply { impl fmt::Display for RequestError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.0 { - InternalRequestError::Io(e) => write!(f, "{e}"), InternalRequestError::MissingHeader(header) => write!(f, "Missing header: {header}"), InternalRequestError::InvalidContentType(content_type) => write!(f, "Invalid content type: {content_type}"), @@ -70,7 +66,6 @@ impl fmt::Display for RequestError { impl error::Error for RequestError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self.0 { - InternalRequestError::Io(e) => Some(e), InternalRequestError::InvalidContentLength(e) => Some(e), InternalRequestError::MissingHeader(_) => None, InternalRequestError::InvalidContentType(_) => None, diff --git a/payjoin/src/receive/v1/exclusive/mod.rs b/payjoin/src/receive/v1/exclusive/mod.rs index 469c298db..927017228 100644 --- a/payjoin/src/receive/v1/exclusive/mod.rs +++ b/payjoin/src/receive/v1/exclusive/mod.rs @@ -23,13 +23,13 @@ pub fn build_v1_pj_uri<'a>( impl UncheckedProposal { pub fn from_request( - body: impl std::io::Read, + body: &[u8], query: &str, headers: impl Headers, ) -> Result { - let parsed_body = parse_body(headers, body).map_err(ReplyableError::V1)?; + let validated_body = validate_body(headers, body).map_err(ReplyableError::V1)?; - let base64 = String::from_utf8(parsed_body).map_err(InternalPayloadError::Utf8)?; + let base64 = std::str::from_utf8(validated_body).map_err(InternalPayloadError::Utf8)?; let (psbt, params) = crate::receive::parse_payload(base64, query, SUPPORTED_VERSIONS) .map_err(ReplyableError::Payload)?; @@ -41,10 +41,7 @@ impl UncheckedProposal { /// Validate the request headers for a Payjoin request /// /// [`RequestError`] should only be produced here. -fn parse_body( - headers: impl Headers, - mut body: impl std::io::Read, -) -> Result, RequestError> { +fn validate_body(headers: impl Headers, body: &[u8]) -> Result<&[u8], RequestError> { let content_type = headers .get_header("content-type") .ok_or(InternalRequestError::MissingHeader("Content-Type"))?; @@ -61,9 +58,7 @@ fn parse_body( return Err(InternalRequestError::ContentLengthTooLarge(content_length).into()); } - let mut buf = vec![0; content_length]; - body.read_exact(&mut buf).map_err(InternalRequestError::Io)?; - Ok(buf) + Ok(&body[..content_length]) } #[cfg(test)] @@ -99,9 +94,9 @@ mod tests { padded_body.resize(MAX_CONTENT_LENGTH + 1, 0); let headers = MockHeaders::new(padded_body.len() as u64); - let parsed_request = parse_body(headers.clone(), padded_body.as_slice()); - assert!(parsed_request.is_err()); - match parsed_request { + let validated_request = validate_body(headers.clone(), padded_body.as_slice()); + assert!(validated_request.is_err()); + match validated_request { Ok(_) => panic!("Expected error, got success"), Err(error) => { assert_eq!( @@ -119,8 +114,8 @@ mod tests { fn test_from_request() -> Result<(), Box> { let body = ORIGINAL_PSBT.as_bytes(); let headers = MockHeaders::new(body.len() as u64); - let parsed_request = parse_body(headers.clone(), body); - assert!(parsed_request.is_ok()); + let validated_request = validate_body(headers.clone(), body); + assert!(validated_request.is_ok()); let proposal = UncheckedProposal::from_request(body, QUERY_PARAMS, headers)?; diff --git a/payjoin/src/receive/v2/mod.rs b/payjoin/src/receive/v2/mod.rs index d8bc142e7..83e5d4438 100644 --- a/payjoin/src/receive/v2/mod.rs +++ b/payjoin/src/receive/v2/mod.rs @@ -188,7 +188,7 @@ impl Receiver { Some(body) => body, None => return Ok(None), }; - match String::from_utf8(body.clone()) { + match std::str::from_utf8(&body) { // V1 response bodies are utf8 plaintext Ok(response) => Ok(Some(Receiver { state: self.extract_proposal_from_v1(response)? })), // V2 response bodies are encrypted binary @@ -208,7 +208,7 @@ impl Receiver { fn extract_proposal_from_v1( &mut self, - response: String, + response: &str, ) -> Result { self.unchecked_from_payload(response) } @@ -216,20 +216,20 @@ impl Receiver { fn extract_proposal_from_v2(&mut self, response: Vec) -> Result { let (payload_bytes, e) = decrypt_message_a(&response, self.context.s.secret_key().clone())?; self.context.e = Some(e); - let payload = String::from_utf8(payload_bytes) + let payload = std::str::from_utf8(&payload_bytes) .map_err(|e| Error::ReplyToSender(InternalPayloadError::Utf8(e).into()))?; self.unchecked_from_payload(payload).map_err(Error::ReplyToSender) } fn unchecked_from_payload( &mut self, - payload: String, + payload: &str, ) -> Result { let (base64, padded_query) = payload.split_once('\n').unwrap_or_default(); let query = padded_query.trim_matches('\0'); log::trace!("Received query: {query}, base64: {base64}"); // my guess is no \n so default is wrong - let (psbt, mut params) = parse_payload(base64.to_string(), query, SUPPORTED_VERSIONS) - .map_err(ReplyableError::Payload)?; + let (psbt, mut params) = + parse_payload(base64, query, SUPPORTED_VERSIONS).map_err(ReplyableError::Payload)?; // Output substitution must be disabled for V1 sessions in V2 contexts. // diff --git a/payjoin/src/send/error.rs b/payjoin/src/send/error.rs index 5a8a82323..2dabd858e 100644 --- a/payjoin/src/send/error.rs +++ b/payjoin/src/send/error.rs @@ -95,7 +95,6 @@ pub struct ValidationError(InternalValidationError); #[derive(Debug)] pub(crate) enum InternalValidationError { Parse, - Io(std::io::Error), ContentTooLarge, Proposal(InternalProposalError), #[cfg(feature = "v2")] @@ -120,7 +119,6 @@ impl fmt::Display for ValidationError { match &self.0 { Parse => write!(f, "couldn't decode as PSBT or JSON",), - Io(e) => write!(f, "couldn't read PSBT: {e}"), ContentTooLarge => write!(f, "content is larger than {MAX_CONTENT_LENGTH} bytes"), Proposal(e) => write!(f, "proposal PSBT error: {e}"), #[cfg(feature = "v2")] @@ -135,7 +133,6 @@ impl std::error::Error for ValidationError { match &self.0 { Parse => None, - Io(error) => Some(error), ContentTooLarge => None, Proposal(e) => Some(e), #[cfg(feature = "v2")] diff --git a/payjoin/src/send/v1.rs b/payjoin/src/send/v1.rs index ed6319777..6024bcdd5 100644 --- a/payjoin/src/send/v1.rs +++ b/payjoin/src/send/v1.rs @@ -21,8 +21,6 @@ //! [`bitmask-core`](https://github.com/diba-io/bitmask-core) BDK integration. Bring your own //! wallet and http client. -use std::io::{BufRead, BufReader}; - use bitcoin::psbt::Psbt; use bitcoin::{FeeRate, ScriptBuf, Weight}; use error::{BuildSenderError, InternalBuildSenderError}; @@ -275,18 +273,12 @@ impl V1Context { /// Call this method with response from receiver to continue BIP78 flow. If the response is /// valid you will get appropriate PSBT that you should sign and broadcast. #[inline] - pub fn process_response( - self, - response: &mut impl std::io::Read, - ) -> Result { - let mut buf_reader = BufReader::with_capacity(MAX_CONTENT_LENGTH + 1, response); - let buffer = buf_reader.fill_buf().map_err(InternalValidationError::Io)?; - - if buffer.len() > MAX_CONTENT_LENGTH { + pub fn process_response(self, response: &[u8]) -> Result { + if response.len() > MAX_CONTENT_LENGTH { return Err(ResponseError::from(InternalValidationError::ContentTooLarge)); } - let res_str = std::str::from_utf8(buffer).map_err(|_| InternalValidationError::Parse)?; + let res_str = std::str::from_utf8(response).map_err(|_| InternalValidationError::Parse)?; let proposal = Psbt::from_str(res_str).map_err(|_| ResponseError::parse(res_str))?; self.psbt_context.process_proposal(proposal).map_err(Into::into) } @@ -334,7 +326,7 @@ mod test { "message": "This version of payjoin is not supported." }) .to_string(); - match ctx.process_response(&mut known_json_error.as_bytes()) { + match ctx.process_response(known_json_error.as_bytes()) { Err(ResponseError::WellKnown(WellKnownError { code: ErrorCode::VersionUnsupported, .. @@ -348,7 +340,7 @@ mod test { "message": "This version of payjoin is not supported." }) .to_string(); - match ctx.process_response(&mut invalid_json_error.as_bytes()) { + match ctx.process_response(invalid_json_error.as_bytes()) { Err(ResponseError::Validation(_)) => (), _ => panic!("Expected unrecognized JSON error"), } @@ -356,19 +348,15 @@ mod test { #[test] fn process_response_valid() { - let mut cursor = std::io::Cursor::new(PAYJOIN_PROPOSAL.as_bytes()); - let ctx = create_v1_context(); - let response = ctx.process_response(&mut cursor); + let response = ctx.process_response(PAYJOIN_PROPOSAL.as_bytes()); assert!(response.is_ok()) } #[test] fn process_response_invalid_psbt() { - let mut cursor = std::io::Cursor::new(INVALID_PSBT.as_bytes()); - let ctx = create_v1_context(); - let response = ctx.process_response(&mut cursor); + let response = ctx.process_response(INVALID_PSBT.as_bytes()); match response { Ok(_) => panic!("Invalid PSBT should have caused an error"), Err(error) => match error { @@ -386,11 +374,10 @@ mod test { #[test] fn process_response_invalid_utf8() { // In UTF-8, 0xF0 represents the start of a 4-byte sequence, so 0xF0 by itself is invalid - let invalid_utf8 = [0xF0]; - let mut cursor = std::io::Cursor::new(invalid_utf8); + let invalid_utf8 = &[0xF0]; let ctx = create_v1_context(); - let response = ctx.process_response(&mut cursor); + let response = ctx.process_response(invalid_utf8); match response { Ok(_) => panic!("Invalid UTF-8 should have caused an error"), Err(error) => match error { diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index fc9510fcc..0d11dda4a 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -100,7 +100,7 @@ mod integration { // ********************** // Inside the Sender: // Sender checks, signs, finalizes, extracts, and broadcasts - let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes())?; + let checked_payjoin_proposal_psbt = ctx.process_response(response.as_bytes())?; let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt)?; sender.send_raw_transaction(&payjoin_tx)?; @@ -565,7 +565,7 @@ mod integration { // ********************** // Inside the Sender: // Sender checks, signs, finalizes, extracts, and broadcasts - let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes())?; + let checked_payjoin_proposal_psbt = ctx.process_response(response.as_bytes())?; let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt)?; sender.send_raw_transaction(&payjoin_tx)?; @@ -694,9 +694,8 @@ mod integration { log::info!("Response: {:#?}", &response); assert!(response.status().is_success(), "error response: {}", response.status()); - let res = response.bytes().await?.to_vec(); let checked_payjoin_proposal_psbt = - send_ctx.process_response(&mut res.as_slice())?; + send_ctx.process_response(&response.bytes().await?)?; let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt)?; sender.send_raw_transaction(&payjoin_tx)?; log::info!("sent"); @@ -984,10 +983,7 @@ mod integration { .await?; assert!(response.status().is_success()); - finalize_ctx.process_response( - response.bytes().await?.to_vec().as_slice(), - ohttp_response_ctx, - )?; + finalize_ctx.process_response(&response.bytes().await?, ohttp_response_ctx)?; } //********************** @@ -1183,7 +1179,7 @@ mod integration { // ********************** // Inside the Sender: // Sender checks, signs, finalizes, extracts, and broadcasts - let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes())?; + let checked_payjoin_proposal_psbt = ctx.process_response(response.as_bytes())?; let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt)?; sender.send_raw_transaction(&payjoin_tx)?; @@ -1269,7 +1265,7 @@ mod integration { // ********************** // Inside the Sender: // Sender checks, signs, finalizes, extracts, and broadcasts - let checked_payjoin_proposal_psbt = ctx.process_response(&mut response.as_bytes())?; + let checked_payjoin_proposal_psbt = ctx.process_response(response.as_bytes())?; let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt)?; sender.send_raw_transaction(&payjoin_tx)?;