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

payjoin / rust-payjoin / 15655272815

14 Jun 2025 07:13PM UTC coverage: 84.695% (-1.4%) from 86.047%
15655272815

Pull #768

github

web-flow
Merge 6ba5c92f0 into 910f32e7e
Pull Request #768: Add PartialEq/Eq to errors for easier comparison

22 of 158 new or added lines in 13 files covered. (13.92%)

304 existing lines in 9 files now uncovered.

7144 of 8435 relevant lines covered (84.69%)

550.72 hits per line

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

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

3
use crate::error_codes::ErrorCode::{
4
    self, NotEnoughMoney, OriginalPsbtRejected, Unavailable, VersionUnsupported,
5
};
6

7
/// The top-level error type for the payjoin receiver
8
#[derive(Debug)]
9
#[non_exhaustive]
10
pub enum Error {
11
    /// Errors that can be replied to the sender
12
    ReplyToSender(ReplyableError),
13
    #[cfg(feature = "v2")]
14
    /// V2-specific errors that are infeasable to reply to the sender
15
    V2(crate::receive::v2::SessionError),
16
}
17

18
impl PartialEq for Error {
NEW
19
    fn eq(&self, other: &Self) -> bool {
×
NEW
UNCOV
20
        match (self, other) {
×
NEW
UNCOV
21
            (Error::ReplyToSender(_), Error::ReplyToSender(_)) => true,
×
22
            #[cfg(feature = "v2")]
NEW
UNCOV
23
            (Error::V2(e1), Error::V2(e2)) => e1 == e2,
×
NEW
UNCOV
24
            _ => false,
×
25
        }
NEW
UNCOV
26
    }
×
27
}
28

29
impl Eq for Error {}
30

31
impl From<ReplyableError> for Error {
UNCOV
32
    fn from(e: ReplyableError) -> Self { Error::ReplyToSender(e) }
×
33
}
34

35
impl fmt::Display for Error {
36
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
37
        match self {
1✔
UNCOV
38
            Error::ReplyToSender(e) => write!(f, "replyable error: {e}"),
×
39
            #[cfg(feature = "v2")]
40
            Error::V2(e) => write!(f, "unreplyable error: {e}"),
1✔
41
        }
42
    }
1✔
43
}
44

45
impl error::Error for Error {
UNCOV
46
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
UNCOV
47
        match self {
×
UNCOV
48
            Error::ReplyToSender(e) => e.source(),
×
49
            #[cfg(feature = "v2")]
UNCOV
50
            Error::V2(e) => e.source(),
×
51
        }
UNCOV
52
    }
×
53
}
54

55
/// The replyable error type for the payjoin receiver, representing failures need to be
56
/// returned to the sender.
57
///
58
/// The error handling is designed to:
59
/// 1. Provide structured error responses for protocol-level failures
60
/// 2. Hide implementation details of external errors for security
61
/// 3. Support proper error propagation through the receiver stack
62
/// 4. Provide errors according to BIP-78 JSON error specifications for return
63
///    after conversion into [`JsonReply`]
64
#[derive(Debug, PartialEq, Eq)]
65
pub enum ReplyableError {
66
    /// Error arising from validation of the original PSBT payload
67
    Payload(PayloadError),
68
    /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks)
69
    #[cfg(feature = "v1")]
70
    V1(crate::receive::v1::RequestError),
71
    /// Error arising due to the specific receiver implementation
72
    ///
73
    /// e.g. database errors, network failures, wallet errors
74
    Implementation(crate::ImplementationError),
75
}
76

77
impl ReplyableError {
NEW
UNCOV
78
    pub fn implementation(e: impl error::Error + Send + Sync + 'static) -> Self {
×
NEW
UNCOV
79
        ReplyableError::Implementation(crate::ImplementationError::new(e))
×
NEW
UNCOV
80
    }
×
81
}
82

83
/// The standard format for errors that can be replied as JSON.
84
///
85
/// The JSON output includes the following fields:
86
/// ```json
87
/// {
88
///     "errorCode": "specific-error-code",
89
///     "message": "Human readable error message"
90
/// }
91
/// ```
92
#[derive(Debug, Clone, PartialEq, Eq)]
93
pub struct JsonReply {
94
    /// The error code
95
    error_code: ErrorCode,
96
    /// The error message to be displayed only in debug logs
97
    message: String,
98
    /// Additional fields to be included in the JSON response
99
    extra: serde_json::Map<String, serde_json::Value>,
100
}
101

102
impl JsonReply {
103
    /// Create a new Reply
104
    pub fn new(error_code: ErrorCode, message: impl fmt::Display) -> Self {
4✔
105
        Self { error_code, message: message.to_string(), extra: serde_json::Map::new() }
4✔
106
    }
4✔
107

108
    /// Add an additional field to the JSON response
UNCOV
109
    pub fn with_extra(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
×
UNCOV
110
        self.extra.insert(key.to_string(), value.into());
×
UNCOV
111
        self
×
112
    }
×
113

114
    /// Serialize the Reply to a JSON string
115
    pub fn to_json(&self) -> serde_json::Value {
4✔
116
        let mut map = serde_json::Map::new();
4✔
117
        map.insert("errorCode".to_string(), self.error_code.to_string().into());
4✔
118
        map.insert("message".to_string(), self.message.clone().into());
4✔
119
        map.extend(self.extra.clone());
4✔
120

4✔
121
        serde_json::Value::Object(map)
4✔
122
    }
4✔
123
}
124

125
impl From<ReplyableError> for JsonReply {
126
    fn from(e: ReplyableError) -> Self {
4✔
127
        use ReplyableError::*;
128
        match e {
4✔
129
            Payload(e) => e.into(),
1✔
130
            #[cfg(feature = "v1")]
131
            V1(e) => e.into(),
×
132
            Implementation(_) => JsonReply::new(Unavailable, "Receiver error"),
3✔
133
        }
134
    }
4✔
135
}
136

137
impl fmt::Display for ReplyableError {
UNCOV
138
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
139
        match &self {
×
UNCOV
140
            Self::Payload(e) => e.fmt(f),
×
141
            #[cfg(feature = "v1")]
UNCOV
142
            Self::V1(e) => e.fmt(f),
×
UNCOV
143
            Self::Implementation(e) => write!(f, "Internal Server Error: {e}"),
×
144
        }
UNCOV
145
    }
×
146
}
147

148
impl error::Error for ReplyableError {
UNCOV
149
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
UNCOV
150
        match &self {
×
UNCOV
151
            Self::Payload(e) => e.source(),
×
152
            #[cfg(feature = "v1")]
UNCOV
153
            Self::V1(e) => e.source(),
×
NEW
UNCOV
154
            Self::Implementation(e) => e.source(),
×
155
        }
UNCOV
156
    }
×
157
}
158

159
impl From<InternalPayloadError> for ReplyableError {
160
    fn from(e: InternalPayloadError) -> Self { ReplyableError::Payload(e.into()) }
2✔
161
}
162

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

179
impl From<InternalPayloadError> for PayloadError {
180
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
2✔
181
}
182

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

218
impl PartialEq for InternalPayloadError {
NEW
219
    fn eq(&self, other: &Self) -> bool {
×
NEW
220
        match (self, other) {
×
NEW
221
            (InternalPayloadError::Utf8(u1), InternalPayloadError::Utf8(u2)) => u1 == u2,
×
NEW
222
            (InternalPayloadError::ParsePsbt(_), InternalPayloadError::ParsePsbt(_)) => true,
×
223
            (
NEW
UNCOV
224
                InternalPayloadError::InconsistentPsbt(i1),
×
NEW
225
                InternalPayloadError::InconsistentPsbt(i2),
×
NEW
UNCOV
226
            ) => i1 == i2,
×
NEW
UNCOV
227
            (InternalPayloadError::PrevTxOut(p1), InternalPayloadError::PrevTxOut(p2)) => p1 == p2,
×
NEW
UNCOV
228
            (InternalPayloadError::MissingPayment, InternalPayloadError::MissingPayment) => true,
×
229
            (
230
                InternalPayloadError::OriginalPsbtNotBroadcastable,
231
                InternalPayloadError::OriginalPsbtNotBroadcastable,
NEW
UNCOV
232
            ) => true,
×
NEW
UNCOV
233
            (InternalPayloadError::InputOwned(i1), InternalPayloadError::InputOwned(i2)) =>
×
NEW
UNCOV
234
                i1 == i2,
×
NEW
UNCOV
235
            (InternalPayloadError::InputWeight(i1), InternalPayloadError::InputWeight(i2)) =>
×
NEW
236
                i1 == i2,
×
NEW
237
            (InternalPayloadError::InputSeen(i1), InternalPayloadError::InputSeen(i2)) => i1 == i2,
×
238
            (
NEW
239
                InternalPayloadError::PsbtBelowFeeRate(f1, f2),
×
NEW
240
                InternalPayloadError::PsbtBelowFeeRate(f3, f4),
×
NEW
UNCOV
241
            ) => f1 == f2 && f3 == f4,
×
242
            (
NEW
243
                InternalPayloadError::FeeTooHigh(f1, f2),
×
NEW
244
                InternalPayloadError::FeeTooHigh(f3, f4),
×
NEW
245
            ) => f1 == f2 && f3 == f4,
×
NEW
246
            _ => false,
×
247
        }
NEW
248
    }
×
249
}
250

251
impl Eq for InternalPayloadError {}
252

253
impl From<PayloadError> for JsonReply {
254
    fn from(e: PayloadError) -> Self {
1✔
255
        use InternalPayloadError::*;
256

257
        match &e.0 {
1✔
258
            Utf8(_)
259
            | ParsePsbt(_)
260
            | InconsistentPsbt(_)
261
            | PrevTxOut(_)
262
            | MissingPayment
263
            | OriginalPsbtNotBroadcastable
264
            | InputOwned(_)
265
            | InputWeight(_)
266
            | InputSeen(_)
267
            | PsbtBelowFeeRate(_, _) => JsonReply::new(OriginalPsbtRejected, e),
1✔
268

269
            FeeTooHigh(_, _) => JsonReply::new(NotEnoughMoney, e),
×
270

271
            SenderParams(e) => match e {
×
272
                super::optional_parameters::Error::UnknownVersion { supported_versions } => {
×
273
                    let supported_versions_json =
×
UNCOV
274
                        serde_json::to_string(supported_versions).unwrap_or_default();
×
275
                    JsonReply::new(VersionUnsupported, "This version of payjoin is not supported.")
×
UNCOV
276
                        .with_extra("supported", supported_versions_json)
×
277
                }
278
                super::optional_parameters::Error::FeeRate =>
UNCOV
279
                    JsonReply::new(OriginalPsbtRejected, e),
×
280
            },
281
        }
282
    }
1✔
283
}
284

285
impl fmt::Display for PayloadError {
286
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
287
        use InternalPayloadError::*;
288

289
        match &self.0 {
1✔
UNCOV
290
            Utf8(e) => write!(f, "{e}"),
×
UNCOV
291
            ParsePsbt(e) => write!(f, "{e}"),
×
UNCOV
292
            SenderParams(e) => write!(f, "{e}"),
×
UNCOV
293
            InconsistentPsbt(e) => write!(f, "{e}"),
×
UNCOV
294
            PrevTxOut(e) => write!(f, "PrevTxOut Error: {e}"),
×
295
            MissingPayment => write!(f, "Missing payment."),
1✔
UNCOV
296
            OriginalPsbtNotBroadcastable => write!(f, "Can't broadcast. PSBT rejected by mempool."),
×
UNCOV
297
            InputOwned(_) => write!(f, "The receiver rejected the original PSBT."),
×
298
            InputWeight(e) => write!(f, "InputWeight Error: {e}"),
×
299
            InputSeen(_) => write!(f, "The receiver rejected the original PSBT."),
×
300
            PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write!(
×
301
                f,
×
302
                "Original PSBT fee rate too low: {original_psbt_fee_rate} < {receiver_min_fee_rate}."
×
303
            ),
×
304
            FeeTooHigh(proposed_fee_rate, max_fee_rate) => write!(
×
305
                f,
×
UNCOV
306
                "Effective receiver feerate exceeds maximum allowed feerate: {proposed_fee_rate} > {max_fee_rate}"
×
307
            ),
×
308
        }
309
    }
1✔
310
}
311

312
impl std::error::Error for PayloadError {
UNCOV
313
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
314
        use InternalPayloadError::*;
UNCOV
315
        match &self.0 {
×
UNCOV
316
            Utf8(e) => Some(e),
×
317
            ParsePsbt(e) => Some(e),
×
318
            SenderParams(e) => Some(e),
×
319
            InconsistentPsbt(e) => Some(e),
×
320
            PrevTxOut(e) => Some(e),
×
321
            InputWeight(e) => Some(e),
×
322
            PsbtBelowFeeRate(_, _) => None,
×
UNCOV
323
            FeeTooHigh(_, _) => None,
×
324
            MissingPayment => None,
×
UNCOV
325
            OriginalPsbtNotBroadcastable => None,
×
UNCOV
326
            InputOwned(_) => None,
×
UNCOV
327
            InputSeen(_) => None,
×
328
        }
UNCOV
329
    }
×
330
}
331

332
/// Error that may occur when output substitution fails.
333
///
334
/// This is currently opaque type because we aren't sure which variants will stay.
335
/// You can only display it.
336
#[derive(Debug, PartialEq)]
337
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
338

339
#[derive(Debug, PartialEq, Eq)]
340
pub(crate) enum InternalOutputSubstitutionError {
341
    /// Output substitution is disabled and output value was decreased
342
    DecreasedValueWhenDisabled,
343
    /// Output substitution is disabled and script pubkey was changed
344
    ScriptPubKeyChangedWhenDisabled,
345
    /// Current output substitution implementation doesn't support reducing the number of outputs
346
    NotEnoughOutputs,
347
    /// The provided drain script could not be identified in the provided replacement outputs
348
    InvalidDrainScript,
349
}
350

351
impl fmt::Display for OutputSubstitutionError {
UNCOV
352
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
353
        match &self.0 {
×
UNCOV
354
            InternalOutputSubstitutionError::DecreasedValueWhenDisabled => write!(f, "Decreasing the receiver output value is not allowed when output substitution is disabled"),
×
355
            InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => write!(f, "Changing the receiver output script pubkey is not allowed when output substitution is disabled"),
×
UNCOV
356
            InternalOutputSubstitutionError::NotEnoughOutputs => write!(
×
UNCOV
357
                f,
×
UNCOV
358
                "Current output substitution implementation doesn't support reducing the number of outputs"
×
359
            ),
×
360
            InternalOutputSubstitutionError::InvalidDrainScript =>
UNCOV
361
                write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
×
362
        }
363
    }
×
364
}
365

366
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
367
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
4✔
368
}
369

370
impl std::error::Error for OutputSubstitutionError {
UNCOV
371
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
UNCOV
372
        match &self.0 {
×
UNCOV
373
            InternalOutputSubstitutionError::DecreasedValueWhenDisabled => None,
×
UNCOV
374
            InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => None,
×
UNCOV
375
            InternalOutputSubstitutionError::NotEnoughOutputs => None,
×
UNCOV
376
            InternalOutputSubstitutionError::InvalidDrainScript => None,
×
377
        }
UNCOV
378
    }
×
379
}
380

381
/// Error that may occur when coin selection fails.
382
///
383
/// This is currently opaque type because we aren't sure which variants will stay.
384
/// You can only display it.
385
#[derive(Debug, PartialEq, Eq)]
386
pub struct SelectionError(InternalSelectionError);
387

388
#[derive(Debug, PartialEq, Eq)]
389
pub(crate) enum InternalSelectionError {
390
    /// No candidates available for selection
391
    Empty,
392
    /// Current privacy selection implementation only supports 2-output transactions
393
    UnsupportedOutputLength,
394
    /// No selection candidates improve privacy
395
    NotFound,
396
}
397

398
impl fmt::Display for SelectionError {
399
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
400
        match &self.0 {
×
UNCOV
401
            InternalSelectionError::Empty => write!(f, "No candidates available for selection"),
×
UNCOV
402
            InternalSelectionError::UnsupportedOutputLength => write!(
×
UNCOV
403
                f,
×
404
                "Current privacy selection implementation only supports 2-output transactions"
×
UNCOV
405
            ),
×
406
            InternalSelectionError::NotFound =>
UNCOV
407
                write!(f, "No selection candidates improve privacy"),
×
408
        }
UNCOV
409
    }
×
410
}
411

412
impl error::Error for SelectionError {
UNCOV
413
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
414
        use InternalSelectionError::*;
415

UNCOV
416
        match &self.0 {
×
UNCOV
417
            Empty => None,
×
UNCOV
418
            UnsupportedOutputLength => None,
×
UNCOV
419
            NotFound => None,
×
420
        }
UNCOV
421
    }
×
422
}
423
impl From<InternalSelectionError> for SelectionError {
424
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
7✔
425
}
426

427
/// Error that may occur when input contribution fails.
428
///
429
/// This is currently opaque type because we aren't sure which variants will stay.
430
/// You can only display it.
431
#[derive(Debug, PartialEq, Eq)]
432
pub struct InputContributionError(InternalInputContributionError);
433

434
#[derive(Debug, PartialEq, Eq)]
435
pub(crate) enum InternalInputContributionError {
436
    /// Total input value is not enough to cover additional output value
437
    ValueTooLow,
438
}
439

440
impl fmt::Display for InputContributionError {
UNCOV
441
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
442
        match &self.0 {
×
UNCOV
443
            InternalInputContributionError::ValueTooLow =>
×
UNCOV
444
                write!(f, "Total input value is not enough to cover additional output value"),
×
UNCOV
445
        }
×
UNCOV
446
    }
×
447
}
448

449
impl error::Error for InputContributionError {
UNCOV
450
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
UNCOV
451
        match &self.0 {
×
UNCOV
452
            InternalInputContributionError::ValueTooLow => None,
×
UNCOV
453
        }
×
UNCOV
454
    }
×
455
}
456

457
impl From<InternalInputContributionError> for InputContributionError {
UNCOV
458
    fn from(value: InternalInputContributionError) -> Self { InputContributionError(value) }
×
459
}
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