• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 13143813652

04 Feb 2025 08:01PM UTC coverage: 78.544% (-0.03%) from 78.576%
13143813652

Pull #526

github

web-flow
Merge 553a64477 into 20620b236
Pull Request #526: Only derive JsonError for errors that can return Json

121 of 162 new or added lines in 8 files covered. (74.69%)

22 existing lines in 3 files now uncovered.

3668 of 4670 relevant lines covered (78.54%)

984.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.18
/payjoin/src/receive/error.rs
1
use std::{error, fmt};
2

3
use crate::error_codes::{
4
    NOT_ENOUGH_MONEY, ORIGINAL_PSBT_REJECTED, UNAVAILABLE, VERSION_UNSUPPORTED,
5
};
6
#[cfg(feature = "v1")]
7
use crate::receive::v1;
8
#[cfg(feature = "v2")]
9
use crate::receive::v2;
10

11
/// The top-level error type for the payjoin receiver
12
#[derive(Debug)]
13
pub enum Error {
14
    /// Errors that can be replied to the sender
15
    Replyable(ReplyableError),
16
    /// Errors that prevent forming a response
17
    Infrastructure(InfrastructureError),
18
}
19

20
impl From<ReplyableError> for Error {
21
    fn from(e: ReplyableError) -> Self { Error::Replyable(e) }
1✔
22
}
23

24
impl fmt::Display for Error {
25
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
26
        match self {
1✔
NEW
27
            Error::Replyable(e) => write!(f, "{}", e),
×
28
            Error::Infrastructure(e) => write!(f, "{}", e),
1✔
29
        }
30
    }
1✔
31
}
32

33
impl error::Error for Error {
NEW
34
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
NEW
35
        match self {
×
NEW
36
            Error::Replyable(e) => e.source(),
×
NEW
37
            Error::Infrastructure(e) => e.source(),
×
38
        }
NEW
39
    }
×
40
}
41

42
/// The replyable error type for the payjoin receiver, representing failures need to be
43
/// returned to the sender.
44
///
45
/// The error handling is designed to:
46
/// 1. Provide structured error responses for protocol-level failures
47
/// 2. Hide implementation details of external errors for security
48
/// 3. Support proper error propagation through the receiver stack
49
/// 4. Provide errors according to BIP-78 JSON error specifications for return using [`Error::to_json`]
50
#[derive(Debug)]
51
pub enum ReplyableError {
52
    /// Error arising from validation of the original PSBT payload
53
    Payload(PayloadError),
54
    /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks)
55
    #[cfg(feature = "v1")]
56
    V1(v1::RequestError),
57
    /// Error arising due to the specific receiver implementation
58
    ///
59
    /// e.g. database errors, network failures, wallet errors
60
    Implementation(Box<dyn error::Error + Send + Sync>),
61
}
62

63
/// A trait for errors that can be serialized to JSON in a standardized format.
64
///
65
/// The JSON output follows the structure:
66
/// ```json
67
/// {
68
///     "errorCode": "specific-error-code",
69
///     "message": "Human readable error message"
70
/// }
71
/// ```
72
pub trait JsonError {
73
    /// Converts the error into a JSON string representation.
74
    fn to_json(&self) -> String;
75
}
76

77
impl JsonError for ReplyableError {
78
    fn to_json(&self) -> String {
3✔
79
        match self {
3✔
80
            Self::Payload(e) => e.to_json(),
1✔
81
            #[cfg(feature = "v1")]
NEW
82
            Self::V1(e) => e.to_json(),
×
83
            Self::Implementation(_) => serialize_json_error(UNAVAILABLE, "Receiver error"),
2✔
84
        }
85
    }
3✔
86
}
87

88
pub(crate) fn serialize_json_error(code: &str, message: impl fmt::Display) -> String {
3✔
89
    format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
3✔
90
}
3✔
91

92
pub(crate) fn serialize_json_plus_fields(
×
93
    code: &str,
×
94
    message: impl fmt::Display,
×
95
    additional_fields: &str,
×
96
) -> String {
×
97
    format!(r#"{{ "errorCode": "{}", "message": "{}", {} }}"#, code, message, additional_fields)
×
98
}
×
99

100
impl fmt::Display for ReplyableError {
UNCOV
101
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
102
        match &self {
×
NEW
103
            Self::Payload(e) => e.fmt(f),
×
104
            #[cfg(feature = "v1")]
NEW
105
            Self::V1(e) => e.fmt(f),
×
UNCOV
106
            Self::Implementation(e) => write!(f, "Internal Server Error: {}", e),
×
107
        }
UNCOV
108
    }
×
109
}
110

111
impl error::Error for ReplyableError {
112
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
113
        match &self {
×
NEW
114
            Self::Payload(e) => e.source(),
×
115
            #[cfg(feature = "v1")]
NEW
116
            Self::V1(e) => e.source(),
×
UNCOV
117
            Self::Implementation(e) => Some(e.as_ref()),
×
118
        }
119
    }
×
120
}
121

122
impl From<InternalPayloadError> for ReplyableError {
123
    fn from(e: InternalPayloadError) -> Self { ReplyableError::Payload(e.into()) }
1✔
124
}
125

126
/// Errors that prevent a response from being formed
127
#[derive(Debug)]
128
pub enum InfrastructureError {
129
    /// V2-specific session/encryption errors
130
    #[cfg(feature = "v2")]
131
    Session(v2::SessionError),
132
    /// I/O or network errors
133
    Transport(std::io::Error),
134
}
135

136
impl fmt::Display for InfrastructureError {
137
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
138
        match self {
1✔
139
            InfrastructureError::Session(e) => write!(f, "Session error: {}", e),
1✔
NEW
140
            InfrastructureError::Transport(e) => write!(f, "Transport error: {}", e),
×
141
        }
142
    }
1✔
143
}
144

145
impl error::Error for InfrastructureError {
NEW
146
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
147
        match self {
×
NEW
148
            InfrastructureError::Session(e) => Some(e),
×
NEW
149
            InfrastructureError::Transport(e) => Some(e),
×
150
        }
151
    }
×
152
}
153

154
/// An error that occurs during validation of the original PSBT payload sent by the sender.
155
///
156
/// This type provides a public abstraction over internal validation errors while maintaining a stable public API.
157
/// It handles various failure modes like:
158
/// - Invalid UTF-8 encoding
159
/// - PSBT parsing errors
160
/// - BIP-78 specific PSBT validation failures
161
/// - Fee rate validation
162
/// - Input ownership validation
163
/// - Previous transaction output validation
164
///
165
/// The error messages are formatted as JSON strings suitable for HTTP responses according to the BIP-78 spec,
166
/// with appropriate error codes and human-readable messages.
167
#[derive(Debug)]
168
pub struct PayloadError(pub(crate) InternalPayloadError);
169

170
impl From<InternalPayloadError> for PayloadError {
171
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
1✔
172
}
173

174
#[derive(Debug)]
175
pub(crate) enum InternalPayloadError {
176
    /// The payload is not valid utf-8
177
    Utf8(std::string::FromUtf8Error),
178
    /// The payload is not a valid PSBT
179
    ParsePsbt(bitcoin::psbt::PsbtParseError),
180
    /// Invalid sender parameters
181
    SenderParams(super::optional_parameters::Error),
182
    /// The raw PSBT fails bip78-specific validation.
183
    InconsistentPsbt(crate::psbt::InconsistentPsbt),
184
    /// The prevtxout is missing
185
    PrevTxOut(crate::psbt::PrevTxOutError),
186
    /// The Original PSBT has no output for the receiver.
187
    MissingPayment,
188
    /// The original PSBT transaction fails the broadcast check
189
    OriginalPsbtNotBroadcastable,
190
    #[allow(dead_code)]
191
    /// The sender is trying to spend the receiver input
192
    InputOwned(bitcoin::ScriptBuf),
193
    /// The expected input weight cannot be determined
194
    InputWeight(crate::psbt::InputWeightError),
195
    #[allow(dead_code)]
196
    /// Original PSBT input has been seen before. Only automatic receivers, aka "interactive" in the spec
197
    /// look out for these to prevent probing attacks.
198
    InputSeen(bitcoin::OutPoint),
199
    /// Original PSBT fee rate is below minimum fee rate set by the receiver.
200
    ///
201
    /// First argument is the calculated fee rate of the original PSBT.
202
    ///
203
    /// Second argument is the minimum fee rate optionaly set by the receiver.
204
    PsbtBelowFeeRate(bitcoin::FeeRate, bitcoin::FeeRate),
205
    /// Effective receiver feerate exceeds maximum allowed feerate
206
    FeeTooHigh(bitcoin::FeeRate, bitcoin::FeeRate),
207
}
208

209
impl JsonError for PayloadError {
210
    fn to_json(&self) -> String {
1✔
211
        use InternalPayloadError::*;
212

213
        match &self.0 {
1✔
214
            Utf8(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
215
            ParsePsbt(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
216
            SenderParams(e) => match e {
×
217
                super::optional_parameters::Error::UnknownVersion { supported_versions } => {
×
218
                    let supported_versions_json =
×
219
                        serde_json::to_string(supported_versions).unwrap_or_default();
×
220
                    serialize_json_plus_fields(
×
221
                        VERSION_UNSUPPORTED,
×
222
                        "This version of payjoin is not supported.",
×
223
                        &format!(r#""supported": {}"#, supported_versions_json),
×
224
                    )
×
225
                }
226
                _ => serialize_json_error("sender-params-error", self),
×
227
            },
228
            InconsistentPsbt(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
229
            PrevTxOut(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
230
            MissingPayment => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
1✔
231
            OriginalPsbtNotBroadcastable => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
232
            InputOwned(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
233
            InputWeight(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
234
            InputSeen(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
235
            PsbtBelowFeeRate(_, _) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
236
            FeeTooHigh(_, _) => serialize_json_error(NOT_ENOUGH_MONEY, self),
×
237
        }
238
    }
1✔
239
}
240

241
impl fmt::Display for PayloadError {
242
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
243
        use InternalPayloadError::*;
244

245
        match &self.0 {
1✔
246
            Utf8(e) => write!(f, "{}", e),
×
247
            ParsePsbt(e) => write!(f, "{}", e),
×
248
            SenderParams(e) => write!(f, "{}", e),
×
249
            InconsistentPsbt(e) => write!(f, "{}", e),
×
250
            PrevTxOut(e) => write!(f, "PrevTxOut Error: {}", e),
×
251
            MissingPayment => write!(f, "Missing payment."),
1✔
252
            OriginalPsbtNotBroadcastable => write!(f, "Can't broadcast. PSBT rejected by mempool."),
×
253
            InputOwned(_) => write!(f, "The receiver rejected the original PSBT."),
×
254
            InputWeight(e) => write!(f, "InputWeight Error: {}", e),
×
255
            InputSeen(_) => write!(f, "The receiver rejected the original PSBT."),
×
256
            PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write!(
×
257
                f,
×
258
                "Original PSBT fee rate too low: {} < {}.",
×
259
                original_psbt_fee_rate, receiver_min_fee_rate
×
260
            ),
×
261
            FeeTooHigh(proposed_fee_rate, max_fee_rate) => write!(
×
262
                f,
×
263
                "Effective receiver feerate exceeds maximum allowed feerate: {} > {}",
×
264
                proposed_fee_rate, max_fee_rate
×
265
            ),
×
266
        }
267
    }
1✔
268
}
269

270
impl std::error::Error for PayloadError {
271
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
272
        use InternalPayloadError::*;
273
        match &self.0 {
×
274
            Utf8(e) => Some(e),
×
275
            ParsePsbt(e) => Some(e),
×
276
            SenderParams(e) => Some(e),
×
277
            InconsistentPsbt(e) => Some(e),
×
278
            PrevTxOut(e) => Some(e),
×
279
            InputWeight(e) => Some(e),
×
280
            PsbtBelowFeeRate(_, _) => None,
×
281
            FeeTooHigh(_, _) => None,
×
282
            MissingPayment => None,
×
283
            OriginalPsbtNotBroadcastable => None,
×
284
            InputOwned(_) => None,
×
285
            InputSeen(_) => None,
×
286
        }
287
    }
×
288
}
289

290
/// Error that may occur when output substitution fails.
291
///
292
/// This is currently opaque type because we aren't sure which variants will stay.
293
/// You can only display it.
294
#[derive(Debug)]
295
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
296

297
#[derive(Debug)]
298
pub(crate) enum InternalOutputSubstitutionError {
299
    /// Output substitution is disabled
300
    OutputSubstitutionDisabled(&'static str),
301
    /// Current output substitution implementation doesn't support reducing the number of outputs
302
    NotEnoughOutputs,
303
    /// The provided drain script could not be identified in the provided replacement outputs
304
    InvalidDrainScript,
305
}
306

307
impl fmt::Display for OutputSubstitutionError {
308
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
309
        match &self.0 {
×
310
            InternalOutputSubstitutionError::OutputSubstitutionDisabled(reason) => write!(f, "{}", &format!("Output substitution is disabled: {}", reason)),
×
311
            InternalOutputSubstitutionError::NotEnoughOutputs => write!(
×
312
                f,
×
313
                "Current output substitution implementation doesn't support reducing the number of outputs"
×
314
            ),
×
315
            InternalOutputSubstitutionError::InvalidDrainScript =>
316
                write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
×
317
        }
318
    }
×
319
}
320

321
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
322
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
×
323
}
324

325
impl std::error::Error for OutputSubstitutionError {
326
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
327
        match &self.0 {
×
328
            InternalOutputSubstitutionError::OutputSubstitutionDisabled(_) => None,
×
329
            InternalOutputSubstitutionError::NotEnoughOutputs => None,
×
330
            InternalOutputSubstitutionError::InvalidDrainScript => None,
×
331
        }
332
    }
×
333
}
334

335
/// Error that may occur when coin selection fails.
336
///
337
/// This is currently opaque type because we aren't sure which variants will stay.
338
/// You can only display it.
339
#[derive(Debug)]
340
pub struct SelectionError(InternalSelectionError);
341

342
#[derive(Debug)]
343
pub(crate) enum InternalSelectionError {
344
    /// No candidates available for selection
345
    Empty,
346
    /// Current privacy selection implementation only supports 2-output transactions
347
    TooManyOutputs,
348
    /// No selection candidates improve privacy
349
    NotFound,
350
}
351

352
impl fmt::Display for SelectionError {
353
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
354
        match &self.0 {
×
355
            InternalSelectionError::Empty => write!(f, "No candidates available for selection"),
×
356
            InternalSelectionError::TooManyOutputs => write!(
×
357
                f,
×
358
                "Current privacy selection implementation only supports 2-output transactions"
×
359
            ),
×
360
            InternalSelectionError::NotFound =>
361
                write!(f, "No selection candidates improve privacy"),
×
362
        }
363
    }
×
364
}
365

366
impl From<InternalSelectionError> for SelectionError {
367
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
1✔
368
}
369

370
/// Error that may occur when input contribution fails.
371
///
372
/// This is currently opaque type because we aren't sure which variants will stay.
373
/// You can only display it.
374
#[derive(Debug)]
375
pub struct InputContributionError(InternalInputContributionError);
376

377
#[derive(Debug)]
378
pub(crate) enum InternalInputContributionError {
379
    /// Total input value is not enough to cover additional output value
380
    ValueTooLow,
381
}
382

383
impl fmt::Display for InputContributionError {
384
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
385
        match &self.0 {
×
386
            InternalInputContributionError::ValueTooLow =>
×
387
                write!(f, "Total input value is not enough to cover additional output value"),
×
388
        }
×
389
    }
×
390
}
391

392
impl From<InternalInputContributionError> for InputContributionError {
393
    fn from(value: InternalInputContributionError) -> Self { InputContributionError(value) }
×
394
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc