Skip to content

Commit 27f7813

Browse files
authored
Produce receive::JsonError accurately so that send can properly handle it (payjoin#506)
- [Isolate receive::JsonError from fmt::Display](payjoin@ffe6281) - [Reject bad v1 requests as original-psbt-rejected](payjoin@9a323ef) since that's the only way v1 senders can really address their issue. It's a payload error but that's the same as Original PSBT [payload] in BIP 78 parlance. This change also introduces `const` values for well known error codes for both `send` and `receive` to share to prevent slipped typos during maintenance.
2 parents 9176ab6 + a158cf2 commit 27f7813

File tree

8 files changed

+162
-88
lines changed

8 files changed

+162
-88
lines changed

payjoin/src/error_codes.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! Well-known error codes as defined in BIP-78
2+
//! See: <https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#receivers-well-known-errors>
3+
4+
/// The payjoin endpoint is not available for now.
5+
pub const UNAVAILABLE: &str = "unavailable";
6+
7+
/// The receiver added some inputs but could not bump the fee of the payjoin proposal.
8+
pub const NOT_ENOUGH_MONEY: &str = "not-enough-money";
9+
10+
/// This version of payjoin is not supported.
11+
pub const VERSION_UNSUPPORTED: &str = "version-unsupported";
12+
13+
/// The receiver rejected the original PSBT.
14+
pub const ORIGINAL_PSBT_REJECTED: &str = "original-psbt-rejected";

payjoin/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,6 @@ pub use bitcoin::base64;
5555
pub use uri::{PjParseError, PjUri, Uri, UriExt};
5656
#[cfg(feature = "_core")]
5757
pub use url::{ParseError, Url};
58+
59+
#[cfg(feature = "_core")]
60+
pub(crate) mod error_codes;

payjoin/src/receive/error.rs

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use std::{error, fmt};
22

3+
use crate::error_codes::{
4+
NOT_ENOUGH_MONEY, ORIGINAL_PSBT_REJECTED, UNAVAILABLE, VERSION_UNSUPPORTED,
5+
};
36
#[cfg(feature = "v1")]
47
use crate::receive::v1;
58
#[cfg(feature = "v2")]
@@ -25,16 +28,41 @@ pub enum Error {
2528
Implementation(Box<dyn error::Error + Send + Sync>),
2629
}
2730

28-
impl Error {
29-
pub fn to_json(&self) -> String {
31+
/// A trait for errors that can be serialized to JSON in a standardized format.
32+
///
33+
/// The JSON output follows the structure:
34+
/// ```json
35+
/// {
36+
/// "errorCode": "specific-error-code",
37+
/// "message": "Human readable error message"
38+
/// }
39+
/// ```
40+
pub trait JsonError {
41+
/// Converts the error into a JSON string representation.
42+
fn to_json(&self) -> String;
43+
}
44+
45+
impl JsonError for Error {
46+
fn to_json(&self) -> String {
3047
match self {
31-
Self::Validation(e) => e.to_string(),
32-
Self::Implementation(_) =>
33-
"{{ \"errorCode\": \"unavailable\", \"message\": \"Receiver error\" }}".to_string(),
48+
Self::Validation(e) => e.to_json(),
49+
Self::Implementation(_) => serialize_json_error(UNAVAILABLE, "Receiver error"),
3450
}
3551
}
3652
}
3753

54+
pub(crate) fn serialize_json_error(code: &str, message: impl fmt::Display) -> String {
55+
format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
56+
}
57+
58+
pub(crate) fn serialize_json_plus_fields(
59+
code: &str,
60+
message: impl fmt::Display,
61+
additional_fields: &str,
62+
) -> String {
63+
format!(r#"{{ "errorCode": "{}", "message": "{}", {} }}"#, code, message, additional_fields)
64+
}
65+
3866
impl fmt::Display for Error {
3967
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4068
match &self {
@@ -85,6 +113,18 @@ impl From<v2::InternalSessionError> for ValidationError {
85113
fn from(e: v2::InternalSessionError) -> Self { ValidationError::V2(e.into()) }
86114
}
87115

116+
impl JsonError for ValidationError {
117+
fn to_json(&self) -> String {
118+
match self {
119+
ValidationError::Payload(e) => e.to_json(),
120+
#[cfg(feature = "v1")]
121+
ValidationError::V1(e) => e.to_json(),
122+
#[cfg(feature = "v2")]
123+
ValidationError::V2(e) => e.to_json(),
124+
}
125+
}
126+
}
127+
88128
impl fmt::Display for ValidationError {
89129
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
90130
match self {
@@ -164,65 +204,62 @@ pub(crate) enum InternalPayloadError {
164204
FeeTooHigh(bitcoin::FeeRate, bitcoin::FeeRate),
165205
}
166206

167-
impl fmt::Display for PayloadError {
168-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207+
impl JsonError for PayloadError {
208+
fn to_json(&self) -> String {
169209
use InternalPayloadError::*;
170210

171-
fn write_error(
172-
f: &mut fmt::Formatter,
173-
code: &str,
174-
message: impl fmt::Display,
175-
) -> fmt::Result {
176-
write!(f, r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
177-
}
178-
179211
match &self.0 {
180-
Utf8(e) => write_error(f, "original-psbt-rejected", e),
181-
ParsePsbt(e) => write_error(f, "original-psbt-rejected", e),
212+
Utf8(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
213+
ParsePsbt(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
182214
SenderParams(e) => match e {
183215
super::optional_parameters::Error::UnknownVersion { supported_versions } => {
184-
write!(
185-
f,
186-
r#"{{
187-
"errorCode": "version-unsupported",
188-
"supported": "{}",
189-
"message": "This version of payjoin is not supported."
190-
}}"#,
191-
serde_json::to_string(supported_versions).map_err(|_| fmt::Error)?
216+
let supported_versions_json =
217+
serde_json::to_string(supported_versions).unwrap_or_default();
218+
serialize_json_plus_fields(
219+
VERSION_UNSUPPORTED,
220+
"This version of payjoin is not supported.",
221+
&format!(r#""supported": {}"#, supported_versions_json),
192222
)
193223
}
194-
_ => write_error(f, "sender-params-error", e),
224+
_ => serialize_json_error("sender-params-error", self),
195225
},
196-
InconsistentPsbt(e) => write_error(f, "original-psbt-rejected", e),
197-
PrevTxOut(e) =>
198-
write_error(f, "original-psbt-rejected", format!("PrevTxOut Error: {}", e)),
199-
MissingPayment => write_error(f, "original-psbt-rejected", "Missing payment."),
200-
OriginalPsbtNotBroadcastable => write_error(
201-
f,
202-
"original-psbt-rejected",
203-
"Can't broadcast. PSBT rejected by mempool.",
204-
),
205-
InputOwned(_) =>
206-
write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."),
207-
InputWeight(e) =>
208-
write_error(f, "original-psbt-rejected", format!("InputWeight Error: {}", e)),
209-
InputSeen(_) =>
210-
write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."),
211-
PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write_error(
226+
InconsistentPsbt(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
227+
PrevTxOut(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
228+
MissingPayment => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
229+
OriginalPsbtNotBroadcastable => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
230+
InputOwned(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
231+
InputWeight(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
232+
InputSeen(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
233+
PsbtBelowFeeRate(_, _) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
234+
FeeTooHigh(_, _) => serialize_json_error(NOT_ENOUGH_MONEY, self),
235+
}
236+
}
237+
}
238+
239+
impl fmt::Display for PayloadError {
240+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
241+
use InternalPayloadError::*;
242+
243+
match &self.0 {
244+
Utf8(e) => write!(f, "{}", e),
245+
ParsePsbt(e) => write!(f, "{}", e),
246+
SenderParams(e) => write!(f, "{}", e),
247+
InconsistentPsbt(e) => write!(f, "{}", e),
248+
PrevTxOut(e) => write!(f, "PrevTxOut Error: {}", e),
249+
MissingPayment => write!(f, "Missing payment."),
250+
OriginalPsbtNotBroadcastable => write!(f, "Can't broadcast. PSBT rejected by mempool."),
251+
InputOwned(_) => write!(f, "The receiver rejected the original PSBT."),
252+
InputWeight(e) => write!(f, "InputWeight Error: {}", e),
253+
InputSeen(_) => write!(f, "The receiver rejected the original PSBT."),
254+
PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write!(
212255
f,
213-
"original-psbt-rejected",
214-
format!(
215-
"Original PSBT fee rate too low: {} < {}.",
216-
original_psbt_fee_rate, receiver_min_fee_rate
217-
),
256+
"Original PSBT fee rate too low: {} < {}.",
257+
original_psbt_fee_rate, receiver_min_fee_rate
218258
),
219-
FeeTooHigh(proposed_fee_rate, max_fee_rate) => write_error(
259+
FeeTooHigh(proposed_fee_rate, max_fee_rate) => write!(
220260
f,
221-
"original-psbt-rejected",
222-
format!(
223-
"Effective receiver feerate exceeds maximum allowed feerate: {} > {}",
224-
proposed_fee_rate, max_fee_rate
225-
),
261+
"Effective receiver feerate exceeds maximum allowed feerate: {} > {}",
262+
proposed_fee_rate, max_fee_rate
226263
),
227264
}
228265
}

payjoin/src/receive/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use bitcoin::{psbt, AddressType, TxIn, TxOut};
22
pub(crate) use error::InternalPayloadError;
3-
pub use error::{Error, OutputSubstitutionError, PayloadError, SelectionError};
3+
pub use error::{Error, JsonError, OutputSubstitutionError, PayloadError, SelectionError};
44

55
pub use crate::psbt::PsbtInputError;
66
use crate::psbt::{InternalInputPair, InternalPsbtInputError};

payjoin/src/receive/v1/exclusive/error.rs

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use core::fmt;
22
use std::error;
33

4-
use crate::receive::error::ValidationError;
4+
use crate::receive::error::{JsonError, ValidationError};
55

66
/// Error that occurs during validation of an incoming v1 payjoin request.
77
///
@@ -42,32 +42,31 @@ impl From<InternalRequestError> for ValidationError {
4242
fn from(e: InternalRequestError) -> Self { ValidationError::V1(e.into()) }
4343
}
4444

45-
impl fmt::Display for RequestError {
46-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47-
fn write_error(
48-
f: &mut fmt::Formatter,
49-
code: &str,
50-
message: impl fmt::Display,
51-
) -> fmt::Result {
52-
write!(f, r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
45+
impl JsonError for RequestError {
46+
fn to_json(&self) -> String {
47+
use InternalRequestError::*;
48+
49+
use crate::receive::error::serialize_json_error;
50+
match &self.0 {
51+
Io(_) => serialize_json_error("original-psbt-rejected", self),
52+
MissingHeader(_) => serialize_json_error("original-psbt-rejected", self),
53+
InvalidContentType(_) => serialize_json_error("original-psbt-rejected", self),
54+
InvalidContentLength(_) => serialize_json_error("original-psbt-rejected", self),
55+
ContentLengthTooLarge(_) => serialize_json_error("original-psbt-rejected", self),
5356
}
57+
}
58+
}
5459

60+
impl fmt::Display for RequestError {
61+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5562
match &self.0 {
56-
InternalRequestError::Io(e) => write_error(f, "io-error", e),
57-
InternalRequestError::MissingHeader(header) =>
58-
write_error(f, "missing-header", format!("Missing header: {}", header)),
59-
InternalRequestError::InvalidContentType(content_type) => write_error(
60-
f,
61-
"invalid-content-type",
62-
format!("Invalid content type: {}", content_type),
63-
),
64-
InternalRequestError::InvalidContentLength(e) =>
65-
write_error(f, "invalid-content-length", e),
66-
InternalRequestError::ContentLengthTooLarge(length) => write_error(
67-
f,
68-
"content-length-too-large",
69-
format!("Content length too large: {}.", length),
70-
),
63+
InternalRequestError::Io(e) => write!(f, "{}", e),
64+
InternalRequestError::MissingHeader(header) => write!(f, "Missing header: {}", header),
65+
InternalRequestError::InvalidContentType(content_type) =>
66+
write!(f, "Invalid content type: {}", content_type),
67+
InternalRequestError::InvalidContentLength(e) => write!(f, "{}", e),
68+
InternalRequestError::ContentLengthTooLarge(length) =>
69+
write!(f, "Content length too large: {}.", length),
7170
}
7271
}
7372
}

payjoin/src/receive/v2/error.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::error;
44
use super::Error;
55
use crate::hpke::HpkeError;
66
use crate::ohttp::OhttpEncapsulationError;
7+
use crate::receive::JsonError;
78

89
/// Error that may occur during a v2 session typestate change
910
///
@@ -48,6 +49,21 @@ impl From<HpkeError> for Error {
4849
fn from(e: HpkeError) -> Self { InternalSessionError::Hpke(e).into() }
4950
}
5051

52+
impl JsonError for SessionError {
53+
fn to_json(&self) -> String {
54+
use InternalSessionError::*;
55+
56+
use crate::receive::error::serialize_json_error;
57+
match &self.0 {
58+
Expired(_) => serialize_json_error("session-expired", self),
59+
OhttpEncapsulation(_) => serialize_json_error("ohttp-encapsulation-error", self),
60+
Hpke(_) => serialize_json_error("hpke-error", self),
61+
UnexpectedResponseSize(_) => serialize_json_error("unexpected-response-size", self),
62+
UnexpectedStatusCode(_) => serialize_json_error("unexpected-status-code", self),
63+
}
64+
}
65+
}
66+
5167
impl fmt::Display for SessionError {
5268
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5369
match &self.0 {

payjoin/src/receive/v2/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
1111
use url::Url;
1212

1313
use super::error::InputContributionError;
14-
use super::{v1, Error, InternalPayloadError, OutputSubstitutionError, SelectionError};
14+
use super::{v1, Error, InternalPayloadError, JsonError, OutputSubstitutionError, SelectionError};
1515
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
1616
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate, OhttpEncapsulationError, OhttpKeys};
1717
use crate::psbt::PsbtExt;
@@ -615,7 +615,7 @@ mod test {
615615
.unwrap();
616616
assert_eq!(
617617
server_error.to_json(),
618-
"{{ \"errorCode\": \"unavailable\", \"message\": \"Receiver error\" }}"
618+
r#"{ "errorCode": "unavailable", "message": "Receiver error" }"#
619619
);
620620
let (_req, _ctx) = proposal.clone().extract_err_req(&server_error, &EXAMPLE_OHTTP_RELAY)?;
621621

payjoin/src/send/error.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ use bitcoin::locktime::absolute::LockTime;
44
use bitcoin::transaction::Version;
55
use bitcoin::Sequence;
66

7+
use crate::error_codes::{
8+
NOT_ENOUGH_MONEY, ORIGINAL_PSBT_REJECTED, UNAVAILABLE, VERSION_UNSUPPORTED,
9+
};
10+
711
/// Error building a Sender from a SenderBuilder.
812
///
913
/// This error is unrecoverable.
@@ -279,7 +283,7 @@ impl ResponseError {
279283
json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str())
280284
{
281285
match error_code {
282-
"version-unsupported" => {
286+
code if code == VERSION_UNSUPPORTED => {
283287
let supported = json
284288
.as_object()
285289
.and_then(|v| v.get("supported"))
@@ -288,9 +292,10 @@ impl ResponseError {
288292
.unwrap_or_default();
289293
WellKnownError::VersionUnsupported { message, supported }.into()
290294
}
291-
"unavailable" => WellKnownError::Unavailable(message).into(),
292-
"not-enough-money" => WellKnownError::NotEnoughMoney(message).into(),
293-
"original-psbt-rejected" => WellKnownError::OriginalPsbtRejected(message).into(),
295+
code if code == UNAVAILABLE => WellKnownError::Unavailable(message).into(),
296+
code if code == NOT_ENOUGH_MONEY => WellKnownError::NotEnoughMoney(message).into(),
297+
code if code == ORIGINAL_PSBT_REJECTED =>
298+
WellKnownError::OriginalPsbtRejected(message).into(),
294299
_ => Self::Unrecognized { error_code: error_code.to_string(), message },
295300
}
296301
} else {
@@ -369,10 +374,10 @@ pub enum WellKnownError {
369374
impl WellKnownError {
370375
pub fn error_code(&self) -> &str {
371376
match self {
372-
WellKnownError::Unavailable(_) => "unavailable",
373-
WellKnownError::NotEnoughMoney(_) => "not-enough-money",
374-
WellKnownError::VersionUnsupported { .. } => "version-unsupported",
375-
WellKnownError::OriginalPsbtRejected(_) => "original-psbt-rejected",
377+
WellKnownError::Unavailable(_) => UNAVAILABLE,
378+
WellKnownError::NotEnoughMoney(_) => NOT_ENOUGH_MONEY,
379+
WellKnownError::VersionUnsupported { .. } => VERSION_UNSUPPORTED,
380+
WellKnownError::OriginalPsbtRejected(_) => ORIGINAL_PSBT_REJECTED,
376381
}
377382
}
378383
pub fn message(&self) -> &str {

0 commit comments

Comments
 (0)