Skip to content

Commit

Permalink
errorCodes without serde
Browse files Browse the repository at this point in the history
  • Loading branch information
jbesraa committed Dec 7, 2023
1 parent c76ad71 commit 4db3f8b
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 83 deletions.
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ bitcoin = { version = "0.30.0", features = ["base64"] }
bip21 = "0.3.1"
log = { version = "0.4.14"}
rand = { version = "0.8.4", optional = true }
serde = { version = "1.0.107", features = ["derive"] }
serde_json = "1.0"
url = "2.2.2"

[dev-dependencies]
env_logger = "0.9.0"
bitcoind = { version = "0.31.1", features = ["0_21_2"] }

[package.metadata.docs.rs]
features = ["send", "receive", "base64"]
features = ["send", "receive", "base64"]
49 changes: 49 additions & 0 deletions payjoin/src/des.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::collections::HashMap;

pub fn parse_json(json: &str) -> Option<HashMap<String, String>> {
let mut map = HashMap::new();

// check if json have more than two properties
// return None if it does

let (first_key_value, second_key_value) = json.split_at(json.find(',').unwrap());
let (first_key, first_value) = first_key_value.split_at(first_key_value.find(':').unwrap());
let (second_key, second_value) = second_key_value.split_at(second_key_value.find(':').unwrap());
let first_key =
first_key.chars().filter(|c| c.is_alphabetic()).collect::<String>().trim().to_string();
let first_value = first_value
.chars()
.filter(|c| c.is_alphabetic() || c == &'-' || c == &' ' || c == &'.')
.collect::<String>()
.trim()
.to_string();
let second_key =
second_key.chars().filter(|c| c.is_alphabetic()).collect::<String>().trim().to_string();
let second_value = second_value
.chars()
.filter(|c| c.is_alphabetic() || c == &'-' || c == &' ' || c == &'.')
.collect::<String>()
.trim()
.to_string();

map.insert(second_key, second_value);
map.insert(first_key, first_value);
Some(map)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_json() {
let json = r"errorCode: 'unsupported-version', message: 'This version of payjoin is not supported.'";
let map = parse_json(json).unwrap();
dbg!(&map);
assert_eq!(map.get("errorCode"), Some(&"unsupported-version".to_string()));
assert_eq!(
map.get("message"),
Some(&"This version of payjoin is not supported.".to_string())
);
}
}
2 changes: 2 additions & 0 deletions payjoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ pub(crate) mod weight;
#[cfg(feature = "base64")]
pub use bitcoin::base64;
pub use uri::{PjParseError, PjUri, Uri};

pub mod des;
2 changes: 1 addition & 1 deletion payjoin/src/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::fmt::{self, Display};
pub enum Error {
/// To be returned as HTTP 400
BadRequest(RequestError),
// To be returned as HTTP 500
/// To be returned as HTTP 500
Server(Box<dyn error::Error>),
}

Expand Down
82 changes: 48 additions & 34 deletions payjoin/src/send/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;

Expand Down Expand Up @@ -207,22 +208,21 @@ impl From<InternalCreateRequestError> for CreateRequestError {
fn from(value: InternalCreateRequestError) -> Self { CreateRequestError(value) }
}

use serde::Deserialize;

/// Error returned when response could not be parsed.
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
/// `WellKnown` errors following the BIP78 spec.
///
/// 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.
/// `WellKnownError` represents `errorCode` and `message`.
/// `String` is a custom message that can be used for debug logs.
WellKnown(WellKnownError, Option<String>),
/// `Unrecognized` errors are errors that are not well known and are only displayed in debug logs.
/// `Unrecognized` errors are errors that are not
/// mentioned in the spec and are only displayed in debug logs.
/// They are not displayed to end users.
///
/// The first `String` is `errorCode`
/// The second `String` is `message`.
/// First `String` is `errorCode`
/// Second `String` is `message`.
Unrecognized(String, String),
/// `Validation` errors are errors that are caused by malformed responses.
/// They are only displayed in debug logs.
Expand All @@ -248,7 +248,7 @@ impl Display for ResponseError {
write!(f, "The receiver doesn't support this version of the protocol.")
}
WellKnownError::OriginalPsbtRejected => {
write!(f, "The receiver rejected the original PSBT.")
write!(f, "{}", WellKnownError::OriginalPsbtRejected.message())
}
},
// Don't display unknowns to end users, only debug logs
Expand Down Expand Up @@ -297,32 +297,9 @@ impl Debug for ResponseError {
}
}

impl<'de> Deserialize<'de> for ResponseError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let res = RawResponseError::deserialize(deserializer)?;
let well_known_error = WellKnownError::from_str(&res.error_code);

if let Ok(wk_error) = well_known_error {
Ok(ResponseError::WellKnown(wk_error, Some(res.message)))
} else {
Ok(ResponseError::Unrecognized(res.error_code, res.message))
}
}
}

impl std::error::Error for ResponseError {}

#[derive(Debug, Deserialize)]
struct RawResponseError {
#[serde(rename = "errorCode")]
error_code: String,
message: String,
}

#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum WellKnownError {
Unavailable,
NotEnoughMoney,
Expand Down Expand Up @@ -365,4 +342,41 @@ impl WellKnownError {
Self::OriginalPsbtRejected => "The receiver rejected the original PSBT.",
}
}

pub fn from_json(json: HashMap<String, String>) -> Option<Self> {
match json.get("errorCode") {
Some(error_code) => match WellKnownError::from_str(&error_code.to_lowercase()) {
Ok(known_error) => Some(known_error),
Err(_) => None,
},
None => None,
}
}
}
// impl ResponseError {
// pub fn from_json(json: HashMap<String, String>) -> Option<Self> {
// match json.get("errorCode") {
// Some(error_code) => match WellKnownError::from_str(&error_code.to_lowercase()) {
// Ok(known_error) => Some(Self::WellKnown(known_error, json.get("message").cloned())),
// Err(_) => Some(Self::Unrecognized(
// *error_code,
// json.get("message").cloned().unwrap_or_default(),
// )),
// },
// None => None,
// }
// }
// }

#[cfg(test)]
mod tests {
use super::*;
use crate::des;

#[test]
fn test_parse_json() {
let json = r#"errorCode: 'version-unsupported', message: 'This version of payjoin is not supported.'"#;
let map = des::parse_json(json).unwrap();
assert_eq!(WellKnownError::from_json(map), WellKnownError::VersionUnsupported);
}
}
21 changes: 13 additions & 8 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,18 @@ 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;
use crate::weight::{varint_size, ComputeWeight};
use crate::PjUri;
use crate::{des, PjUri};

// See usize casts
#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
compile_error!("This crate currently only supports 32 bit and 64 bit architectures");

mod error;
pub mod error;

type InternalResult<T> = Result<T, InternalValidationError>;

Expand Down Expand Up @@ -379,6 +380,7 @@ pub struct Request {
///
/// This type is used to process the response. Get it from [`RequestBuilder`](crate::send::RequestBuilder)'s build methods.
/// Then you only need to call [`.process_response()`](crate::send::Context::process_response()) on it to continue BIP78 flow.
#[derive(Clone)]
pub struct Context {
original_psbt: Psbt,
disable_output_substitution: bool,
Expand Down Expand Up @@ -421,11 +423,14 @@ impl Context {
response.read_to_string(&mut res_str).map_err(InternalValidationError::Io)?;
let proposal = Psbt::from_str(&res_str).or_else(|_| {
// That wasn't a valid PSBT. Maybe it's a valid error response?
serde_json::from_str::<ResponseError>(&res_str)
// which isn't the Ok result, it's actually an error.
.map(|e| Err(e))
// if not, we have an invalid response
.unwrap_or_else(|_| Err(InternalValidationError::Parse.into()))
match des::parse_json(&res_str) {
Some(map) => match WellKnownError::from_json(map) {
Some(well_known) =>
return Err(ResponseError::WellKnown(well_known, Some(res_str))),
None => return Err(ResponseError::Unrecognized(res_str, "".to_string())),
},
None => Err(InternalValidationError::Parse.into()),
}
})?;
self.process_proposal(proposal).map(Into::into).map_err(Into::into)
}
Expand Down Expand Up @@ -806,7 +811,7 @@ fn serialize_url(
Ok(url)
}

fn serialize_psbt(psbt: &Psbt) -> Vec<u8> {
pub fn serialize_psbt(psbt: &Psbt) -> Vec<u8> {
let bytes = psbt.serialize();
bitcoin::base64::encode(bytes).into_bytes()
}
Expand Down
Loading

0 comments on commit 4db3f8b

Please sign in to comment.