Skip to content

Commit 2de6e18

Browse files
committed
Replace JsonError trait with JsonReply struct
This concrete simple struct can more easily cross the foreign language boundaries for bindings than the errors with complex internal variants that implement the JsonError trait.
1 parent 33a58f7 commit 2de6e18

File tree

4 files changed

+73
-57
lines changed

4 files changed

+73
-57
lines changed

payjoin/src/receive/error.rs

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ impl error::Error for Error {
4848
/// 1. Provide structured error responses for protocol-level failures
4949
/// 2. Hide implementation details of external errors for security
5050
/// 3. Support proper error propagation through the receiver stack
51-
/// 4. Provide errors according to BIP-78 JSON error specifications for return using [`JsonError::to_json`]
51+
/// 4. Provide errors according to BIP-78 JSON error specifications for return
52+
/// after conversion into [`JsonReply`]
5253
#[derive(Debug)]
5354
pub enum ReplyableError {
5455
/// Error arising from validation of the original PSBT payload
@@ -62,41 +63,54 @@ pub enum ReplyableError {
6263
Implementation(ImplementationError),
6364
}
6465

65-
/// A trait for errors that can be serialized to JSON in a standardized format.
66+
/// The standard format for errors that can be replied as JSON.
6667
///
67-
/// The JSON output follows the structure:
68+
/// The JSON output includes the following fields:
6869
/// ```json
6970
/// {
7071
/// "errorCode": "specific-error-code",
7172
/// "message": "Human readable error message"
7273
/// }
7374
/// ```
74-
pub trait JsonError {
75-
/// Converts the error into a JSON string representation.
76-
fn to_json(&self) -> String;
75+
pub struct JsonReply {
76+
/// The error code
77+
error_code: ErrorCode,
78+
/// The error message to be displayed only in debug logs
79+
message: String,
80+
/// Additional fields to be added to the JSON
81+
additional_fields: Vec<String>,
7782
}
7883

79-
impl JsonError for ReplyableError {
80-
fn to_json(&self) -> String {
81-
match self {
82-
Self::Payload(e) => e.to_json(),
83-
#[cfg(feature = "v1")]
84-
Self::V1(e) => e.to_json(),
85-
Self::Implementation(_) => serialize_json_error(Unavailable, "Receiver error"),
86-
}
84+
impl JsonReply {
85+
/// Create a new Reply
86+
pub fn new(error_code: ErrorCode, message: impl fmt::Display) -> Self {
87+
Self { error_code, message: message.to_string(), additional_fields: vec![] }
8788
}
88-
}
8989

90-
pub(crate) fn serialize_json_error(code: ErrorCode, message: impl fmt::Display) -> String {
91-
format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
90+
/// Serialize the Reply to a JSON string
91+
pub fn to_json(&self) -> String {
92+
if self.additional_fields.is_empty() {
93+
format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, self.error_code, self.message)
94+
} else {
95+
format!(
96+
r#"{{ "errorCode": "{}", "message": "{}", {} }}"#,
97+
self.error_code,
98+
self.message,
99+
self.additional_fields.join(", ")
100+
)
101+
}
102+
}
92103
}
93104

94-
pub(crate) fn serialize_json_plus_fields(
95-
code: ErrorCode,
96-
message: impl fmt::Display,
97-
additional_fields: &str,
98-
) -> String {
99-
format!(r#"{{ "errorCode": "{}", "message": "{}", {} }}"#, code, message, additional_fields)
105+
impl From<&ReplyableError> for JsonReply {
106+
fn from(e: &ReplyableError) -> Self {
107+
match e {
108+
ReplyableError::Payload(e) => e.into(),
109+
#[cfg(feature = "v1")]
110+
ReplyableError::V1(e) => e.into(),
111+
ReplyableError::Implementation(_) => JsonReply::new(Unavailable, "Receiver error"),
112+
}
113+
}
100114
}
101115

102116
impl fmt::Display for ReplyableError {
@@ -180,34 +194,37 @@ pub(crate) enum InternalPayloadError {
180194
FeeTooHigh(bitcoin::FeeRate, bitcoin::FeeRate),
181195
}
182196

183-
impl JsonError for PayloadError {
184-
fn to_json(&self) -> String {
197+
impl From<&PayloadError> for JsonReply {
198+
fn from(e: &PayloadError) -> Self {
185199
use InternalPayloadError::*;
186200

187-
match &self.0 {
188-
Utf8(_) => serialize_json_error(OriginalPsbtRejected, self),
189-
ParsePsbt(_) => serialize_json_error(OriginalPsbtRejected, self),
201+
match &e.0 {
202+
Utf8(_) => JsonReply::new(OriginalPsbtRejected, e),
203+
ParsePsbt(_) => JsonReply::new(OriginalPsbtRejected, e),
190204
SenderParams(e) => match e {
191205
super::optional_parameters::Error::UnknownVersion { supported_versions } => {
192206
let supported_versions_json =
193207
serde_json::to_string(supported_versions).unwrap_or_default();
194-
serialize_json_plus_fields(
195-
VersionUnsupported,
196-
"This version of payjoin is not supported.",
197-
&format!(r#""supported": {}"#, supported_versions_json),
198-
)
208+
JsonReply {
209+
error_code: VersionUnsupported,
210+
message: "This version of payjoin is not supported.".to_string(),
211+
additional_fields: vec![format!(
212+
r#""supported": {}"#,
213+
supported_versions_json
214+
)],
215+
}
199216
}
200-
_ => serialize_json_error(OriginalPsbtRejected, self),
217+
_ => JsonReply::new(OriginalPsbtRejected, e),
201218
},
202-
InconsistentPsbt(_) => serialize_json_error(OriginalPsbtRejected, self),
203-
PrevTxOut(_) => serialize_json_error(OriginalPsbtRejected, self),
204-
MissingPayment => serialize_json_error(OriginalPsbtRejected, self),
205-
OriginalPsbtNotBroadcastable => serialize_json_error(OriginalPsbtRejected, self),
206-
InputOwned(_) => serialize_json_error(OriginalPsbtRejected, self),
207-
InputWeight(_) => serialize_json_error(OriginalPsbtRejected, self),
208-
InputSeen(_) => serialize_json_error(OriginalPsbtRejected, self),
209-
PsbtBelowFeeRate(_, _) => serialize_json_error(OriginalPsbtRejected, self),
210-
FeeTooHigh(_, _) => serialize_json_error(NotEnoughMoney, self),
219+
InconsistentPsbt(_) => JsonReply::new(OriginalPsbtRejected, e),
220+
PrevTxOut(_) => JsonReply::new(OriginalPsbtRejected, e),
221+
MissingPayment => JsonReply::new(OriginalPsbtRejected, e),
222+
OriginalPsbtNotBroadcastable => JsonReply::new(OriginalPsbtRejected, e),
223+
InputOwned(_) => JsonReply::new(OriginalPsbtRejected, e),
224+
InputWeight(_) => JsonReply::new(OriginalPsbtRejected, e),
225+
InputSeen(_) => JsonReply::new(OriginalPsbtRejected, e),
226+
PsbtBelowFeeRate(_, _) => JsonReply::new(OriginalPsbtRejected, e),
227+
FeeTooHigh(_, _) => JsonReply::new(NotEnoughMoney, e),
211228
}
212229
}
213230
}

payjoin/src/receive/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::str::FromStr;
1414
use bitcoin::{psbt, AddressType, Psbt, TxIn, TxOut};
1515
pub(crate) use error::InternalPayloadError;
1616
pub use error::{
17-
Error, ImplementationError, InputContributionError, JsonError, OutputSubstitutionError,
17+
Error, ImplementationError, InputContributionError, JsonReply, OutputSubstitutionError,
1818
PayloadError, ReplyableError, SelectionError,
1919
};
2020
use optional_parameters::Params;

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

Lines changed: 9 additions & 10 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::JsonError;
4+
use crate::receive::JsonReply;
55

66
/// Error that occurs during validation of an incoming v1 payjoin request.
77
///
@@ -38,18 +38,17 @@ impl From<InternalRequestError> for super::ReplyableError {
3838
fn from(e: InternalRequestError) -> Self { super::ReplyableError::V1(e.into()) }
3939
}
4040

41-
impl JsonError for RequestError {
42-
fn to_json(&self) -> String {
41+
impl From<&RequestError> for JsonReply {
42+
fn from(e: &RequestError) -> Self {
4343
use InternalRequestError::*;
4444

4545
use crate::error_codes::ErrorCode::OriginalPsbtRejected;
46-
use crate::receive::error::serialize_json_error;
47-
match &self.0 {
48-
Io(_) => serialize_json_error(OriginalPsbtRejected, self),
49-
MissingHeader(_) => serialize_json_error(OriginalPsbtRejected, self),
50-
InvalidContentType(_) => serialize_json_error(OriginalPsbtRejected, self),
51-
InvalidContentLength(_) => serialize_json_error(OriginalPsbtRejected, self),
52-
ContentLengthTooLarge(_) => serialize_json_error(OriginalPsbtRejected, self),
46+
match &e.0 {
47+
Io(_) => JsonReply::new(OriginalPsbtRejected, e),
48+
MissingHeader(_) => JsonReply::new(OriginalPsbtRejected, e),
49+
InvalidContentType(_) => JsonReply::new(OriginalPsbtRejected, e),
50+
InvalidContentLength(_) => JsonReply::new(OriginalPsbtRejected, e),
51+
ContentLengthTooLarge(_) => JsonReply::new(OriginalPsbtRejected, e),
5352
}
5453
}
5554
}

payjoin/src/receive/v2/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use url::Url;
1313

1414
use super::error::{Error, InputContributionError};
1515
use super::{
16-
v1, ImplementationError, InternalPayloadError, JsonError, OutputSubstitutionError,
16+
v1, ImplementationError, InternalPayloadError, JsonReply, OutputSubstitutionError,
1717
ReplyableError, SelectionError,
1818
};
1919
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
@@ -286,7 +286,7 @@ impl UncheckedProposal {
286286
&mut self.context.ohttp_keys,
287287
"POST",
288288
subdir.as_str(),
289-
Some(err.to_json().as_bytes()),
289+
Some(JsonReply::from(err).to_json().as_bytes()),
290290
)
291291
.map_err(InternalSessionError::OhttpEncapsulation)?;
292292
let req = Request::new_v2(&self.context.full_relay_url(ohttp_relay)?, &body);
@@ -626,7 +626,7 @@ mod test {
626626
.err()
627627
.ok_or("expected error but got success")?;
628628
assert_eq!(
629-
server_error.to_json(),
629+
JsonReply::from(&server_error).to_json(),
630630
r#"{ "errorCode": "unavailable", "message": "Receiver error" }"#
631631
);
632632
let (_req, _ctx) = proposal.clone().extract_err_req(&server_error, &*EXAMPLE_URL)?;

0 commit comments

Comments
 (0)