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

payjoin / rust-payjoin / 12918406633

22 Jan 2025 10:39PM UTC coverage: 65.801% (-6.3%) from 72.12%
12918406633

Pull #502

github

web-flow
Merge 2152c4b39 into 7d8116e03
Pull Request #502: Introduce `directory` feature module

69 of 129 new or added lines in 11 files covered. (53.49%)

270 existing lines in 14 files now uncovered.

3219 of 4892 relevant lines covered (65.8%)

933.94 hits per line

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

15.83
/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
impl Error {
29
    pub fn to_json(&self) -> String {
3✔
30
        match self {
3✔
31
            Self::Validation(e) => e.to_string(),
1✔
32
            Self::Implementation(_) =>
33
                "{{ \"errorCode\": \"unavailable\", \"message\": \"Receiver error\" }}".to_string(),
2✔
34
        }
35
    }
3✔
36
}
37

38
impl fmt::Display for Error {
UNCOV
39
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
40
        match &self {
×
UNCOV
41
            Self::Validation(e) => e.fmt(f),
×
42
            Self::Implementation(e) => write!(f, "Internal Server Error: {}", e),
×
43
        }
UNCOV
44
    }
×
45
}
46

47
impl error::Error for Error {
48
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
49
        match &self {
×
50
            Self::Validation(e) => e.source(),
×
51
            Self::Implementation(e) => Some(e.as_ref()),
×
52
        }
53
    }
×
54
}
55

56
impl From<InternalPayloadError> for Error {
57
    fn from(e: InternalPayloadError) -> Self {
×
58
        Error::Validation(ValidationError::Payload(e.into()))
×
59
    }
×
60
}
61

62
/// An error that occurs during validation of a payjoin request, encompassing all possible validation
63
/// failures across different protocol versions and stages.
64
///
65
/// This abstraction serves as the primary error type for the validation phase of request processing,
66
/// allowing uniform error handling while maintaining protocol version specifics internally.
67
#[derive(Debug)]
68
pub enum ValidationError {
69
    /// Error arising from validation of the original PSBT payload
70
    Payload(PayloadError),
71
    /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks)
72
    #[cfg(feature = "v1")]
73
    V1(v1::RequestError),
74
    /// Protocol-specific errors for BIP-77 v2 sessions (e.g. session management, OHTTP, HPKE encryption)
75
    #[cfg(feature = "v2")]
76
    V2(v2::SessionError),
77
}
78

79
impl From<InternalPayloadError> for ValidationError {
80
    fn from(e: InternalPayloadError) -> Self { ValidationError::Payload(e.into()) }
1✔
81
}
82

83
#[cfg(feature = "v2")]
84
impl From<v2::InternalSessionError> for ValidationError {
UNCOV
85
    fn from(e: v2::InternalSessionError) -> Self { ValidationError::V2(e.into()) }
×
86
}
87

88
impl fmt::Display for ValidationError {
89
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
90
        match self {
1✔
91
            ValidationError::Payload(e) => write!(f, "{}", e),
1✔
92
            #[cfg(feature = "v1")]
UNCOV
93
            ValidationError::V1(e) => write!(f, "{}", e),
×
94
            #[cfg(feature = "v2")]
UNCOV
95
            ValidationError::V2(e) => write!(f, "{}", e),
×
96
        }
97
    }
1✔
98
}
99

100
impl std::error::Error for ValidationError {
101
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
102
        match self {
×
103
            ValidationError::Payload(e) => Some(e),
×
104
            #[cfg(feature = "v1")]
UNCOV
105
            ValidationError::V1(e) => Some(e),
×
106
            #[cfg(feature = "v2")]
107
            ValidationError::V2(e) => Some(e),
×
108
        }
109
    }
×
110
}
111

112
/// An error that occurs during validation of the original PSBT payload sent by the sender.
113
///
114
/// This type provides a public abstraction over internal validation errors while maintaining a stable public API.
115
/// It handles various failure modes like:
116
/// - Invalid UTF-8 encoding
117
/// - PSBT parsing errors
118
/// - BIP-78 specific PSBT validation failures
119
/// - Fee rate validation
120
/// - Input ownership validation
121
/// - Previous transaction output validation
122
///
123
/// The error messages are formatted as JSON strings suitable for HTTP responses according to the BIP-78 spec,
124
/// with appropriate error codes and human-readable messages.
125
#[derive(Debug)]
126
pub struct PayloadError(pub(crate) InternalPayloadError);
127

128
impl From<InternalPayloadError> for PayloadError {
129
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
1✔
130
}
131

132
#[derive(Debug)]
133
pub(crate) enum InternalPayloadError {
134
    /// The payload is not valid utf-8
135
    Utf8(std::string::FromUtf8Error),
136
    /// The payload is not a valid PSBT
137
    ParsePsbt(bitcoin::psbt::PsbtParseError),
138
    /// Invalid sender parameters
139
    SenderParams(super::optional_parameters::Error),
140
    /// The raw PSBT fails bip78-specific validation.
141
    InconsistentPsbt(crate::psbt::InconsistentPsbt),
142
    /// The prevtxout is missing
143
    PrevTxOut(crate::psbt::PrevTxOutError),
144
    /// The Original PSBT has no output for the receiver.
145
    MissingPayment,
146
    /// The original PSBT transaction fails the broadcast check
147
    OriginalPsbtNotBroadcastable,
148
    #[allow(dead_code)]
149
    /// The sender is trying to spend the receiver input
150
    InputOwned(bitcoin::ScriptBuf),
151
    /// The expected input weight cannot be determined
152
    InputWeight(crate::psbt::InputWeightError),
153
    #[allow(dead_code)]
154
    /// Original PSBT input has been seen before. Only automatic receivers, aka "interactive" in the spec
155
    /// look out for these to prevent probing attacks.
156
    InputSeen(bitcoin::OutPoint),
157
    /// Original PSBT fee rate is below minimum fee rate set by the receiver.
158
    ///
159
    /// First argument is the calculated fee rate of the original PSBT.
160
    ///
161
    /// Second argument is the minimum fee rate optionaly set by the receiver.
162
    PsbtBelowFeeRate(bitcoin::FeeRate, bitcoin::FeeRate),
163
    /// Effective receiver feerate exceeds maximum allowed feerate
164
    FeeTooHigh(bitcoin::FeeRate, bitcoin::FeeRate),
165
}
166

167
impl fmt::Display for PayloadError {
168
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
169
        use InternalPayloadError::*;
170

171
        fn write_error(
1✔
172
            f: &mut fmt::Formatter,
1✔
173
            code: &str,
1✔
174
            message: impl fmt::Display,
1✔
175
        ) -> fmt::Result {
1✔
176
            write!(f, r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
1✔
177
        }
1✔
178

179
        match &self.0 {
1✔
180
            Utf8(e) => write_error(f, "original-psbt-rejected", e),
×
181
            ParsePsbt(e) => write_error(f, "original-psbt-rejected", e),
×
182
            SenderParams(e) => match e {
×
183
                super::optional_parameters::Error::UnknownVersion { supported_versions } => {
×
184
                    write!(
×
185
                        f,
×
186
                        r#"{{
×
187
                            "errorCode": "version-unsupported",
×
188
                            "supported": "{}",
×
189
                            "message": "This version of payjoin is not supported."
×
190
                        }}"#,
×
191
                        serde_json::to_string(supported_versions).map_err(|_| fmt::Error)?
×
192
                    )
193
                }
194
                _ => write_error(f, "sender-params-error", e),
×
195
            },
196
            InconsistentPsbt(e) => write_error(f, "original-psbt-rejected", e),
×
197
            PrevTxOut(e) =>
×
198
                write_error(f, "original-psbt-rejected", format!("PrevTxOut Error: {}", e)),
×
199
            MissingPayment => write_error(f, "original-psbt-rejected", "Missing payment."),
1✔
200
            OriginalPsbtNotBroadcastable => write_error(
×
201
                f,
×
202
                "original-psbt-rejected",
×
203
                "Can't broadcast. PSBT rejected by mempool.",
×
204
            ),
×
205
            InputOwned(_) =>
206
                write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."),
×
207
            InputWeight(e) =>
×
208
                write_error(f, "original-psbt-rejected", format!("InputWeight Error: {}", e)),
×
209
            InputSeen(_) =>
210
                write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."),
×
211
            PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write_error(
×
212
                f,
×
213
                "original-psbt-rejected",
×
214
                format!(
×
215
                    "Original PSBT fee rate too low: {} < {}.",
×
216
                    original_psbt_fee_rate, receiver_min_fee_rate
×
217
                ),
×
218
            ),
×
219
            FeeTooHigh(proposed_feerate, max_feerate) => write_error(
×
220
                f,
×
221
                "original-psbt-rejected",
×
222
                format!(
×
223
                    "Effective receiver feerate exceeds maximum allowed feerate: {} > {}",
×
224
                    proposed_feerate, max_feerate
×
225
                ),
×
226
            ),
×
227
        }
228
    }
1✔
229
}
230

231
impl std::error::Error for PayloadError {
232
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
233
        use InternalPayloadError::*;
234
        match &self.0 {
×
235
            Utf8(e) => Some(e),
×
236
            ParsePsbt(e) => Some(e),
×
237
            SenderParams(e) => Some(e),
×
238
            InconsistentPsbt(e) => Some(e),
×
239
            PrevTxOut(e) => Some(e),
×
240
            InputWeight(e) => Some(e),
×
241
            PsbtBelowFeeRate(_, _) => None,
×
242
            FeeTooHigh(_, _) => None,
×
243
            MissingPayment => None,
×
244
            OriginalPsbtNotBroadcastable => None,
×
245
            InputOwned(_) => None,
×
246
            InputSeen(_) => None,
×
247
        }
248
    }
×
249
}
250

251
/// Error that may occur when output substitution fails.
252
///
253
/// This is currently opaque type because we aren't sure which variants will stay.
254
/// You can only display it.
255
#[derive(Debug)]
256
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
257

258
#[derive(Debug)]
259
pub(crate) enum InternalOutputSubstitutionError {
260
    /// Output substitution is disabled
261
    OutputSubstitutionDisabled(&'static str),
262
    /// Current output substitution implementation doesn't support reducing the number of outputs
263
    NotEnoughOutputs,
264
    /// The provided drain script could not be identified in the provided replacement outputs
265
    InvalidDrainScript,
266
}
267

268
impl fmt::Display for OutputSubstitutionError {
269
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
270
        match &self.0 {
×
271
            InternalOutputSubstitutionError::OutputSubstitutionDisabled(reason) => write!(f, "{}", &format!("Output substitution is disabled: {}", reason)),
×
272
            InternalOutputSubstitutionError::NotEnoughOutputs => write!(
×
273
                f,
×
274
                "Current output substitution implementation doesn't support reducing the number of outputs"
×
275
            ),
×
276
            InternalOutputSubstitutionError::InvalidDrainScript =>
277
                write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
×
278
        }
279
    }
×
280
}
281

282
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
283
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
×
284
}
285

286
impl std::error::Error for OutputSubstitutionError {
287
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
288
        match &self.0 {
×
289
            InternalOutputSubstitutionError::OutputSubstitutionDisabled(_) => None,
×
290
            InternalOutputSubstitutionError::NotEnoughOutputs => None,
×
291
            InternalOutputSubstitutionError::InvalidDrainScript => None,
×
292
        }
293
    }
×
294
}
295

296
/// Error that may occur when coin selection fails.
297
///
298
/// This is currently opaque type because we aren't sure which variants will stay.
299
/// You can only display it.
300
#[derive(Debug)]
301
pub struct SelectionError(InternalSelectionError);
302

303
#[derive(Debug)]
304
pub(crate) enum InternalSelectionError {
305
    /// No candidates available for selection
306
    Empty,
307
    /// Current privacy selection implementation only supports 2-output transactions
308
    TooManyOutputs,
309
    /// No selection candidates improve privacy
310
    NotFound,
311
}
312

313
impl fmt::Display for SelectionError {
314
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
315
        match &self.0 {
×
316
            InternalSelectionError::Empty => write!(f, "No candidates available for selection"),
×
317
            InternalSelectionError::TooManyOutputs => write!(
×
318
                f,
×
319
                "Current privacy selection implementation only supports 2-output transactions"
×
320
            ),
×
321
            InternalSelectionError::NotFound =>
322
                write!(f, "No selection candidates improve privacy"),
×
323
        }
324
    }
×
325
}
326

327
impl From<InternalSelectionError> for SelectionError {
UNCOV
328
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
×
329
}
330

331
/// Error that may occur when input contribution 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 InputContributionError(InternalInputContributionError);
337

338
#[derive(Debug)]
339
pub(crate) enum InternalInputContributionError {
340
    /// The address type could not be determined
341
    AddressType(crate::psbt::AddressTypeError),
342
    /// The original PSBT has no inputs
343
    NoSenderInputs,
344
    /// The proposed receiver inputs would introduce mixed input script types
345
    MixedInputScripts(bitcoin::AddressType, bitcoin::AddressType),
346
    /// Total input value is not enough to cover additional output value
347
    ValueTooLow,
348
}
349

350
impl fmt::Display for InputContributionError {
351
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
352
        match &self.0 {
×
353
            InternalInputContributionError::AddressType(e) =>
×
354
                write!(f, "The address type could not be determined: {}", e),
×
355
            InternalInputContributionError::NoSenderInputs =>
356
                write!(f, "The original PSBT has no inputs"),
×
357
            InternalInputContributionError::MixedInputScripts(type_a, type_b) => write!(
×
358
                f,
×
359
                "The proposed receiver inputs would introduce mixed input script types: {}; {}.",
×
360
                type_a, type_b
×
361
            ),
×
362
            InternalInputContributionError::ValueTooLow =>
363
                write!(f, "Total input value is not enough to cover additional output value"),
×
364
        }
365
    }
×
366
}
367

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