From 3d32dd2626541869c918b9edce14d39b6a545b74 Mon Sep 17 00:00:00 2001 From: DanGould Date: Tue, 17 Oct 2023 18:47:14 -0400 Subject: [PATCH] Parse receiver response errors --- Cargo.lock | 30 ++++---- payjoin/Cargo.toml | 8 +- payjoin/src/send/error.rs | 158 +++++++++++++++++++++++++++++++++++++- payjoin/src/send/mod.rs | 12 +-- 4 files changed, 181 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 049a99ce..87eaba76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -969,7 +969,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1767,9 +1767,9 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ "base64 0.21.5", "serde", @@ -1827,7 +1827,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2305,7 +2305,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2708,9 +2708,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e" dependencies = [ "proc-macro2", "quote", @@ -2799,7 +2799,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2883,7 +2883,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2956,7 +2956,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3175,7 +3175,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-shared", ] @@ -3197,7 +3197,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3462,7 +3462,7 @@ checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3482,7 +3482,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] diff --git a/payjoin/Cargo.toml b/payjoin/Cargo.toml index 9e73fa22..ee96589b 100644 --- a/payjoin/Cargo.toml +++ b/payjoin/Cargo.toml @@ -18,7 +18,7 @@ exclude = ["tests"] send = [] receive = ["rand"] base64 = ["bitcoin/base64"] -v2 = ["bitcoin/rand-std", "chacha20poly1305", "ohttp", "bhttp", "serde"] +v2 = ["bitcoin/rand-std", "chacha20poly1305", "ohttp", "bhttp"] [dependencies] bitcoin = { version = "0.30.0", features = ["base64"] } @@ -28,11 +28,11 @@ log = { version = "0.4.14"} ohttp = { version = "0.4.0", optional = true } bhttp = { version = "0.4.0", optional = true } rand = { version = "0.8.4", optional = true } -serde = { version = "1.0.186", default-features = false, optional = true } +serde = { version = "1.0.186", default-features = false } url = "2.2.2" +bitcoind = { version = "0.31.1", features = ["0_21_2"] } [dev-dependencies] -bitcoind = { version = "0.31.1", features = ["0_21_2"] } env_logger = "0.9.0" rustls = "0.21.9" testcontainers = "0.15.0" @@ -41,4 +41,4 @@ tokio = { version = "1.12.0", features = ["full"] } ureq = "2.8.0" [package.metadata.docs.rs] -features = ["send", "receive", "base64"] \ No newline at end of file +features = ["send", "receive", "base64"] diff --git a/payjoin/src/send/error.rs b/payjoin/src/send/error.rs index 6e70a822..266ca51a 100644 --- a/payjoin/src/send/error.rs +++ b/payjoin/src/send/error.rs @@ -1,7 +1,9 @@ use std::fmt; +use std::str::FromStr; use bitcoin::locktime::absolute::LockTime; use bitcoin::Sequence; +use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::Value; use crate::input_type::{InputType, InputTypeError}; @@ -16,7 +18,7 @@ pub struct ValidationError { #[derive(Debug)] pub(crate) enum InternalValidationError { - PsbtParse(bitcoin::psbt::PsbtParseError), + Parse, Io(std::io::Error), InvalidInputType(InputTypeError), InvalidProposedInput(crate::psbt::PrevTxOutError), @@ -74,7 +76,7 @@ impl fmt::Display for ValidationError { use InternalValidationError::*; match &self.internal { - PsbtParse(e) => write!(f, "couldn't decode PSBT: {}", e), + Parse => write!(f, "couldn't decode as PSBT or JSON",), Io(e) => write!(f, "couldn't read PSBT: {}", e), InvalidInputType(e) => write!(f, "invalid transaction input type: {}", e), InvalidProposedInput(e) => write!(f, "invalid proposed transaction input: {}", e), @@ -115,7 +117,7 @@ impl std::error::Error for ValidationError { use InternalValidationError::*; match &self.internal { - PsbtParse(error) => Some(error), + Parse => None, Io(error) => Some(error), InvalidInputType(error) => Some(error), InvalidProposedInput(error) => Some(error), @@ -235,3 +237,153 @@ impl std::error::Error for CreateRequestError { impl From for CreateRequestError { fn from(value: InternalCreateRequestError) -> Self { CreateRequestError(value) } } + +pub enum ResponseError { + /// `WellKnown` errors following the BIP78 spec + /// https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors + /// These errors are displayed to end users. + /// + /// The `WellKnownError` represents `errorCode` and `message`. + /// The `String` is a custom message that can be used for debug logs. + WellKnown(WellKnownError, String), + /// `Unrecognized` errors are errors that are not well known and are only displayed in debug logs. + /// They are not displayed to end users. + /// + /// The first `String` is `errorCode` + /// The second `String` is `message`. + Unrecognized(String, String), + /// `Validation` errors are errors that are caused by malformed responses. + /// They are only displayed in debug logs. + Validation(ValidationError), +} + +impl From for ResponseError { + fn from(value: InternalValidationError) -> Self { + Self::Validation(ValidationError { internal: value }) + } +} + +// It is imperative to carefully display pre-defined messages to end users and the details in debug. +impl fmt::Display for ResponseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::WellKnown(e, _) => e.fmt(f), + // Don't display unknowns to end users, only debug logs + Self::Unrecognized(_, _) => write!(f, "The receiver sent an unrecognized error."), + Self::Validation(e) => write!(f, "The receiver sent an invalid response: {}", e), + } + } +} + +impl fmt::Debug for ResponseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::WellKnown(e, msg) => write!( + f, + r#"Well known error: {{ "errorCode": "{}", + "message": "{}" }}"#, + e, msg + ), + Self::Unrecognized(code, msg) => write!( + f, + r#"Unrecognized error: {{ "errorCode": "{}", "message": "{}" }}"#, + code, msg + ), + Self::Validation(e) => write!(f, "Validation({:?})", e), + } + } +} + +pub enum WellKnownError { + Unavailable, + NotEnoughMoney, + VersionUnsupported, + OriginalPsbtRejected, +} + +impl std::error::Error for ResponseError {} + +impl FromStr for WellKnownError { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "unavailable" => Ok(WellKnownError::Unavailable), + "not-enough-money" => Ok(WellKnownError::NotEnoughMoney), + "version-unsupported" => Ok(WellKnownError::VersionUnsupported), + "original-psbt-rejected" => Ok(WellKnownError::OriginalPsbtRejected), + _ => Err(()), + } + } +} + +impl fmt::Display for WellKnownError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Unavailable => write!(f, "The payjoin endpoint is not available for now."), + Self::NotEnoughMoney => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."), + Self::VersionUnsupported => write!(f, "This version of payjoin is not supported."), + Self::OriginalPsbtRejected => write!(f, "The receiver rejected the original PSBT."), + } + } +} + +impl WellKnownError { + pub fn message(&self) -> &'static str { + match self { + Self::Unavailable => "The receiver is unavailable.", + Self::NotEnoughMoney => "The receiver doesn't have enough money.", + Self::VersionUnsupported => + "The receiver doesn't support this version of the protocol.", + Self::OriginalPsbtRejected => "The receiver rejected the original PSBT.", + } + } + pub fn error_code_from_json(json: Value) -> Result { + let error_code = json + .as_object() + .and_then(|v| v.get("errorCode")) + .and_then(|v| v.as_str()) + .ok_or(InternalValidationError::Parse)?; + WellKnownError::from_str(error_code).map_err(|_| { + let message = match json.get("message") { + Some(v) => v.to_string(), + None => { + log::debug!("Unrecognized Error detected, {}", json); + "Unrecognized Error detected".to_string() + } + }; + ResponseError::Unrecognized(error_code.to_string(), message) + }) + } + pub fn error_code_from_str(error_code: &str) -> Result { + Self::error_code_from_json(Value::from_str(&error_code).map_err(|e| { + log::debug!("Invalid json detected, {}", e); + InternalValidationError::Parse + })?) + } +} + +#[cfg(test)] +mod tests { + use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::json; + + use super::*; + + #[test] + fn test_parse_json() { + let json_error = json!({ + "errorCode": "version-unsupported", + "message": "This version of payjoin is not supported." + }); + assert_eq!( + WellKnownError::error_code_from_json(json_error).unwrap().to_string(), + WellKnownError::VersionUnsupported.to_string() + ); + let str_error = "{\"errorCode\":\"version-unsupported\", + \"message\":\"This version of payjoin is not supported.\"}"; + assert_eq!( + WellKnownError::error_code_from_str(str_error).unwrap().to_string(), + WellKnownError::VersionUnsupported.to_string() + ); + } +} diff --git a/payjoin/src/send/mod.rs b/payjoin/src/send/mod.rs index 735f4c02..aab17c1b 100644 --- a/payjoin/src/send/mod.rs +++ b/payjoin/src/send/mod.rs @@ -140,10 +140,11 @@ use std::str::FromStr; use bitcoin::address::NetworkChecked; use bitcoin::psbt::Psbt; use bitcoin::{FeeRate, Script, ScriptBuf, Sequence, TxOut, Weight}; -pub use error::{CreateRequestError, ValidationError}; +pub use error::{CreateRequestError, ResponseError, ValidationError}; pub(crate) use error::{InternalCreateRequestError, InternalValidationError}; use url::Url; +use self::error::WellKnownError; use crate::input_type::InputType; use crate::psbt::PsbtExt; use crate::uri::UriExt; @@ -529,12 +530,13 @@ impl ContextV1 { pub fn process_response( self, response: &mut impl std::io::Read, - ) -> Result { + ) -> Result { let mut res_str = String::new(); response.read_to_string(&mut res_str).map_err(InternalValidationError::Io)?; - let proposal = Psbt::from_str(&res_str).map_err(InternalValidationError::PsbtParse)?; - - // process in non-generic function + let proposal = Psbt::from_str(&res_str).or_else(|_| { + WellKnownError::error_code_from_str(&res_str) + .map(|well_known| Err(ResponseError::WellKnown(well_known, "".to_string())))? + })?; self.process_proposal(proposal).map(Into::into).map_err(Into::into) }