Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supply & parse receiver response errors #110

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 33 additions & 38 deletions payjoin-cli/Cargo.lock

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

9 changes: 6 additions & 3 deletions payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@ impl App {
.header("Content-Type", "text/plain")
.send()
.with_context(|| "HTTP request failed")?;
// TODO display well-known errors and log::debug the rest
let psbt =
ctx.process_response(&mut response).with_context(|| "Failed to process response")?;
let psbt = ctx.process_response(&mut response).or_else(|e| {
// It is imperative to carefully display pre-defined messages to end users and the details in debug, otherwise receiver could craft a message to phish the sender.
log::error!("Response contains error: {}", e);
log::debug!("Response contains error: {:?}", e);
Comment on lines +112 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could have the definition of debug execute all the logs of error as well and then we wont have to add double log lines.
for example, if we have these levels of logs:

  • Debug <- show everything Trace is showing + some more logs
  • Trace <- show everything Error is showing + some more logs
  • Error <- show only errors

Err(e)
})?;
log::debug!("Proposed psbt: {:#?}", psbt);
let psbt = self
.bitcoind
Expand Down
1 change: 1 addition & 0 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bitcoin = { version = "0.30.0", features = ["base64"] }
bip21 = "0.3.1"
log = { version = "0.4.14"}
rand = { version = "0.8.4", optional = true }
tinyjson = "2"
url = "2.2.2"

[dev-dependencies]
Expand Down
5 changes: 4 additions & 1 deletion payjoin/src/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
Self::BadRequest(e) => e.fmt(f),
Self::Server(e) => write!(f, "Internal Server Error: {}", e),
Self::Server(_) => write!(
f,
r#"{{ "errorCode": "unavailable", "message": "The payjoin endpoint is not available for now." }}"#
),
}
}
}
Expand Down
105 changes: 102 additions & 3 deletions payjoin/src/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct ValidationError {

#[derive(Debug)]
pub(crate) enum InternalValidationError {
Psbt(bitcoin::psbt::PsbtParseError),
Parse,
Io(std::io::Error),
InvalidInputType(InputTypeError),
InvalidProposedInput(crate::psbt::PrevTxOutError),
Expand Down Expand Up @@ -58,7 +58,6 @@ impl fmt::Display for ValidationError {
use InternalValidationError::*;

match &self.internal {
Psbt(e) => write!(f, "couldn't decode PSBT: {}", e),
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),
Expand Down Expand Up @@ -86,6 +85,7 @@ impl fmt::Display for ValidationError {
PayeeTookContributedFee => write!(f, "payee tried to take fee contribution for himself"),
FeeContributionPaysOutputSizeIncrease => write!(f, "fee contribution pays for additional outputs"),
FeeRateBelowMinimum => write!(f, "the fee rate of proposed transaction is below minimum"),
Parse => write!(f, "couldn't decode as PSBT or JSON",),
}
}
}
Expand All @@ -95,7 +95,6 @@ impl std::error::Error for ValidationError {
use InternalValidationError::*;

match &self.internal {
Psbt(error) => Some(error),
Io(error) => Some(error),
InvalidInputType(error) => Some(error),
InvalidProposedInput(error) => Some(error),
Expand Down Expand Up @@ -123,6 +122,7 @@ impl std::error::Error for ValidationError {
PayeeTookContributedFee => None,
FeeContributionPaysOutputSizeIncrease => None,
FeeRateBelowMinimum => None,
Parse => None,
}
}
}
Expand Down Expand Up @@ -210,3 +210,102 @@ impl std::error::Error for CreateRequestError {
impl From<InternalCreateRequestError> for CreateRequestError {
fn from(value: InternalCreateRequestError) -> Self { CreateRequestError(value) }
}

pub enum ResponseError {
// Well known errors with internal message for logs as String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a bit more info here and maybe link to bip78 for reference?
also, should we use /// in the comments so it shows also on docs.rs ?

WellKnown(WellKnownError, String),
// Don't display unknowns to end users, only debug logs
Unrecognized(String, String),

Validation(ValidationError),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing some docs here, I feel that Validation is quite broad

}

impl ResponseError {
pub fn from_json(json: &str) -> Self {
use std::convert::TryInto;

use tinyjson::{JsonParser, JsonValue};

let parsed: JsonValue = json.parse().unwrap();
//.unwrap_or_else( |_| ResponseError::Validation(InternalValidationError::Parse.into()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented code

let maybe_code = parsed["errorCode"].get();
let maybe_message = parsed["message"].get();
if let (Some(error_code), Some(message)) = (maybe_code, maybe_message) {
let well_known_error = WellKnownError::from_str(&error_code);

if let Some(wk_error) = well_known_error {
ResponseError::WellKnown(wk_error, message.to_string())
} else {
ResponseError::Unrecognized(error_code.to_string(), message.to_string())
}
} else {
ResponseError::Validation(InternalValidationError::Parse.into())
}
}
}

impl From<InternalValidationError> 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),
}
}
}

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

#[derive(Debug)]
pub enum WellKnownError {
Unavailable,
NotEnoughMoney,
VersionUnsupported,
OriginalPsbtRejected,
}

impl WellKnownError {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"unavailable" => Some(WellKnownError::Unavailable),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validated the errors and their description against https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#receivers-well-known-errors
looks good!

"not-enough-money" => Some(WellKnownError::NotEnoughMoney),
"version-unsupported" => Some(WellKnownError::VersionUnsupported),
"original-psbt-rejected" => Some(WellKnownError::OriginalPsbtRejected),
_ => None,
}
}
}

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."),
}
}
}
Loading
Loading