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

payjoin / rust-payjoin / 12954852147

24 Jan 2025 05:51PM UTC coverage: 78.428% (-0.1%) from 78.541%
12954852147

Pull #506

github

web-flow
Merge ef5a21743 into 3511be3c2
Pull Request #506: Produce `receive::JsonError` accurately so that `send` can properly handle it

24 of 93 new or added lines in 5 files covered. (25.81%)

3 existing lines in 1 file now uncovered.

3643 of 4645 relevant lines covered (78.43%)

989.58 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
use crate::error_codes::{NOT_ENOUGH_MONEY, ORIGINAL_PSBT_REJECTED, UNAVAILABLE, VERSION_UNSUPPORTED};
4
#[cfg(feature = "v1")]
5
use crate::receive::v1;
6
#[cfg(feature = "v2")]
7
use crate::receive::v2;
8

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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