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

payjoin / rust-payjoin / 12968704432

25 Jan 2025 09:48PM UTC coverage: 78.528% (-0.1%) from 78.641%
12968704432

push

github

web-flow
Produce `receive::JsonError` accurately so that `send` can properly handle it (#506)

- [Isolate receive::JsonError from
fmt::Display](https://github.com/payjoin/rust-payjoin/commit/ffe6281f6)
- [Reject bad v1 requests as
original-psbt-rejected](https://github.com/payjoin/rust-payjoin/commit/9a323ef9e)
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.

22 of 91 new or added lines in 5 files covered. (24.18%)

3 existing lines in 1 file now uncovered.

3650 of 4648 relevant lines covered (78.53%)

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

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 {
3✔
47
        match self {
3✔
48
            Self::Validation(e) => e.to_json(),
1✔
49
            Self::Implementation(_) => serialize_json_error(UNAVAILABLE, "Receiver error"),
2✔
50
        }
51
    }
3✔
52
}
53

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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