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

payjoin / rust-payjoin / 12933051711

23 Jan 2025 03:57PM UTC coverage: 78.445% (-0.1%) from 78.541%
12933051711

Pull #506

github

web-flow
Merge 1bfe7f379 into e88f97204
Pull Request #506: Produce `receive::JsonError` accurately so that `send` can properly handle it

19 of 84 new or added lines in 4 files covered. (22.62%)

3 existing lines in 1 file now uncovered.

3643 of 4644 relevant lines covered (78.45%)

989.8 hits per line

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

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

3
#[cfg(feature = "v1")]
4
use crate::receive::v1;
5
#[cfg(feature = "v2")]
6
use crate::receive::v2;
7

8
/// The top-level error type for the payjoin receiver, representing all possible failures that can occur
9
/// during the processing of a payjoin request.
10
///
11
/// The error handling is designed to:
12
/// 1. Provide structured error responses for protocol-level failures
13
/// 2. Hide implementation details of external errors for security
14
/// 3. Support proper error propagation through the receiver stack
15
/// 4. Provide errors according to BIP-78 JSON error specifications for return using [`Error::to_json`]
16
#[derive(Debug)]
17
pub enum Error {
18
    /// Error arising from the payjoin state machine
19
    ///
20
    /// e.g. PSBT validation, HTTP request validation, protocol version checks
21
    Validation(ValidationError),
22
    /// Error arising due to the specific receiver implementation
23
    ///
24
    /// e.g. database errors, network failures, wallet errors
25
    Implementation(Box<dyn error::Error + Send + Sync>),
26
}
27

28
/// A trait for errors that can be serialized to JSON in a standardized format.
29
///
30
/// The JSON output follows the structure:
31
/// ```json
32
/// {
33
///     "errorCode": "specific-error-code",
34
///     "message": "Human readable error message"
35
/// }
36
/// ```
37
pub trait JsonError {
38
    /// Converts the error into a JSON string representation.
39
    fn to_json(&self) -> String;
40
}
41

42
impl JsonError for Error {
43
    fn to_json(&self) -> String {
3✔
44
        match self {
3✔
45
            Self::Validation(e) => e.to_json(),
1✔
46
            Self::Implementation(_) => serialize_json_error("unavailable", "Receiver error"),
2✔
47
        }
48
    }
3✔
49
}
50

51
pub(crate) fn serialize_json_error(code: &str, message: impl fmt::Display) -> String {
3✔
52
    format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
3✔
53
}
3✔
54

NEW
55
pub(crate) fn serialize_json_plus_fields(
×
NEW
56
    code: &str,
×
NEW
57
    message: impl fmt::Display,
×
NEW
58
    additional_fields: &str,
×
NEW
59
) -> String {
×
NEW
60
    format!(r#"{{ "errorCode": "{}", "message": "{}", {} }}"#, code, message, additional_fields)
×
NEW
61
}
×
62

63
impl fmt::Display for Error {
64
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
65
        match &self {
1✔
66
            Self::Validation(e) => e.fmt(f),
1✔
67
            Self::Implementation(e) => write!(f, "Internal Server Error: {}", e),
×
68
        }
69
    }
1✔
70
}
71

72
impl error::Error for Error {
73
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
74
        match &self {
×
75
            Self::Validation(e) => e.source(),
×
76
            Self::Implementation(e) => Some(e.as_ref()),
×
77
        }
78
    }
×
79
}
80

81
impl From<InternalPayloadError> for Error {
82
    fn from(e: InternalPayloadError) -> Self {
×
83
        Error::Validation(ValidationError::Payload(e.into()))
×
84
    }
×
85
}
86

87
/// An error that occurs during validation of a payjoin request, encompassing all possible validation
88
/// failures across different protocol versions and stages.
89
///
90
/// This abstraction serves as the primary error type for the validation phase of request processing,
91
/// allowing uniform error handling while maintaining protocol version specifics internally.
92
#[derive(Debug)]
93
pub enum ValidationError {
94
    /// Error arising from validation of the original PSBT payload
95
    Payload(PayloadError),
96
    /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks)
97
    #[cfg(feature = "v1")]
98
    V1(v1::RequestError),
99
    /// Protocol-specific errors for BIP-77 v2 sessions (e.g. session management, OHTTP, HPKE encryption)
100
    #[cfg(feature = "v2")]
101
    V2(v2::SessionError),
102
}
103

104
impl From<InternalPayloadError> for ValidationError {
105
    fn from(e: InternalPayloadError) -> Self { ValidationError::Payload(e.into()) }
1✔
106
}
107

108
#[cfg(feature = "v2")]
109
impl From<v2::InternalSessionError> for ValidationError {
110
    fn from(e: v2::InternalSessionError) -> Self { ValidationError::V2(e.into()) }
1✔
111
}
112

113
impl JsonError for ValidationError {
114
    fn to_json(&self) -> String {
1✔
115
        match self {
1✔
116
            ValidationError::Payload(e) => e.to_json(),
1✔
117
            #[cfg(feature = "v1")]
NEW
118
            ValidationError::V1(e) => e.to_json(),
×
119
            #[cfg(feature = "v2")]
NEW
120
            ValidationError::V2(e) => e.to_json(),
×
121
        }
122
    }
1✔
123
}
124

125
impl fmt::Display for ValidationError {
126
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
127
        match self {
1✔
UNCOV
128
            ValidationError::Payload(e) => write!(f, "{}", e),
×
129
            #[cfg(feature = "v1")]
130
            ValidationError::V1(e) => write!(f, "{}", e),
×
131
            #[cfg(feature = "v2")]
132
            ValidationError::V2(e) => write!(f, "{}", e),
1✔
133
        }
134
    }
1✔
135
}
136

137
impl std::error::Error for ValidationError {
138
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
139
        match self {
×
140
            ValidationError::Payload(e) => Some(e),
×
141
            #[cfg(feature = "v1")]
142
            ValidationError::V1(e) => Some(e),
×
143
            #[cfg(feature = "v2")]
144
            ValidationError::V2(e) => Some(e),
×
145
        }
146
    }
×
147
}
148

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

165
impl From<InternalPayloadError> for PayloadError {
166
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
1✔
167
}
168

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

204
impl JsonError for PayloadError {
205
    fn to_json(&self) -> String {
1✔
206
        use InternalPayloadError::*;
207

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

236
impl fmt::Display for PayloadError {
237
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
238
        use InternalPayloadError::*;
239

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

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

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

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

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

316
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
317
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
×
318
}
319

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

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

337
#[derive(Debug)]
338
pub(crate) enum InternalSelectionError {
339
    /// No candidates available for selection
340
    Empty,
341
    /// Current privacy selection implementation only supports 2-output transactions
342
    TooManyOutputs,
343
    /// No selection candidates improve privacy
344
    NotFound,
345
}
346

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

361
impl From<InternalSelectionError> for SelectionError {
362
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
1✔
363
}
364

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

372
#[derive(Debug)]
373
pub(crate) enum InternalInputContributionError {
374
    /// Total input value is not enough to cover additional output value
375
    ValueTooLow,
376
}
377

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

387
impl From<InternalInputContributionError> for InputContributionError {
388
    fn from(value: InternalInputContributionError) -> Self { InputContributionError(value) }
×
389
}
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