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

payjoin / rust-payjoin / 13655876030

04 Mar 2025 02:26PM UTC coverage: 79.709% (-0.02%) from 79.725%
13655876030

Pull #560

github

web-flow
Merge 81f4bc826 into 2761db178
Pull Request #560: Instantiate `v2::Reciver` only after data persistence is complete

19 of 24 new or added lines in 3 files covered. (79.17%)

157 existing lines in 6 files now uncovered.

4545 of 5702 relevant lines covered (79.71%)

809.82 hits per line

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

15.13
/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

7
pub type ImplementationError = Box<dyn error::Error + Send + Sync>;
8

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

20
impl From<ReplyableError> for Error {
21
    fn from(e: ReplyableError) -> Self { Error::ReplyToSender(e) }
×
22
}
23

24
impl fmt::Display for Error {
25
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
26
        match self {
1✔
27
            Error::ReplyToSender(e) => write!(f, "replyable error: {}", e),
×
28
            #[cfg(feature = "v2")]
29
            Error::V2(e) => write!(f, "unreplyable error: {}", e),
1✔
30
        }
31
    }
1✔
32
}
33

34
impl error::Error for Error {
35
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
36
        match self {
×
37
            Error::ReplyToSender(e) => e.source(),
×
38
            #[cfg(feature = "v2")]
39
            Error::V2(e) => e.source(),
×
40
        }
41
    }
×
42
}
43

44
#[derive(Debug)]
45
/// Error arising during the persistence of the receiver
46
pub enum PersistanceError {
47
    PersistError(ImplementationError),
48
}
49
impl std::fmt::Display for PersistanceError {
NEW
UNCOV
50
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
NEW
UNCOV
51
        match self {
×
NEW
UNCOV
52
            PersistanceError::PersistError(e) => write!(f, "{}", e),
×
NEW
UNCOV
53
        }
×
NEW
UNCOV
54
    }
×
55
}
56

57
impl std::error::Error for PersistanceError {}
58

59
/// The replyable error type for the payjoin receiver, representing failures need to be
60
/// returned to the sender.
61
///
62
/// The error handling is designed to:
63
/// 1. Provide structured error responses for protocol-level failures
64
/// 2. Hide implementation details of external errors for security
65
/// 3. Support proper error propagation through the receiver stack
66
/// 4. Provide errors according to BIP-78 JSON error specifications for return using [`JsonError::to_json`]
67
#[derive(Debug)]
68
pub enum ReplyableError {
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(crate::receive::v1::RequestError),
74
    /// Error arising due to the specific receiver implementation
75
    ///
76
    /// e.g. database errors, network failures, wallet errors
77
    Implementation(ImplementationError),
78
}
79

80
/// A trait for errors that can be serialized to JSON in a standardized format.
81
///
82
/// The JSON output follows the structure:
83
/// ```json
84
/// {
85
///     "errorCode": "specific-error-code",
86
///     "message": "Human readable error message"
87
/// }
88
/// ```
89
pub trait JsonError {
90
    /// Converts the error into a JSON string representation.
91
    fn to_json(&self) -> String;
92
}
93

94
impl JsonError for ReplyableError {
95
    fn to_json(&self) -> String {
3✔
96
        match self {
3✔
97
            Self::Payload(e) => e.to_json(),
1✔
98
            #[cfg(feature = "v1")]
99
            Self::V1(e) => e.to_json(),
×
100
            Self::Implementation(_) => serialize_json_error(UNAVAILABLE, "Receiver error"),
2✔
101
        }
102
    }
3✔
103
}
104

105
pub(crate) fn serialize_json_error(code: &str, message: impl fmt::Display) -> String {
3✔
106
    format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
3✔
107
}
3✔
108

UNCOV
109
pub(crate) fn serialize_json_plus_fields(
×
110
    code: &str,
×
UNCOV
111
    message: impl fmt::Display,
×
UNCOV
112
    additional_fields: &str,
×
UNCOV
113
) -> String {
×
114
    format!(r#"{{ "errorCode": "{}", "message": "{}", {} }}"#, code, message, additional_fields)
×
115
}
×
116

117
impl fmt::Display for ReplyableError {
118
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
119
        match &self {
×
UNCOV
120
            Self::Payload(e) => e.fmt(f),
×
121
            #[cfg(feature = "v1")]
UNCOV
122
            Self::V1(e) => e.fmt(f),
×
UNCOV
123
            Self::Implementation(e) => write!(f, "Internal Server Error: {}", e),
×
124
        }
UNCOV
125
    }
×
126
}
127

128
impl error::Error for ReplyableError {
UNCOV
129
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
UNCOV
130
        match &self {
×
UNCOV
131
            Self::Payload(e) => e.source(),
×
132
            #[cfg(feature = "v1")]
UNCOV
133
            Self::V1(e) => e.source(),
×
UNCOV
134
            Self::Implementation(e) => Some(e.as_ref()),
×
135
        }
UNCOV
136
    }
×
137
}
138

139
impl From<InternalPayloadError> for ReplyableError {
140
    fn from(e: InternalPayloadError) -> Self { ReplyableError::Payload(e.into()) }
1✔
141
}
142

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

159
impl From<InternalPayloadError> for PayloadError {
160
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
1✔
161
}
162

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

198
impl JsonError for PayloadError {
199
    fn to_json(&self) -> String {
1✔
200
        use InternalPayloadError::*;
201

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

230
impl fmt::Display for PayloadError {
231
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
232
        use InternalPayloadError::*;
233

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

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

279
/// Error that may occur when output substitution fails.
280
///
281
/// This is currently opaque type because we aren't sure which variants will stay.
282
/// You can only display it.
283
#[derive(Debug)]
284
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
285

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

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

310
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
UNCOV
311
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
×
312
}
313

314
impl std::error::Error for OutputSubstitutionError {
UNCOV
315
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
UNCOV
316
        match &self.0 {
×
UNCOV
317
            InternalOutputSubstitutionError::OutputSubstitutionDisabled(_) => None,
×
UNCOV
318
            InternalOutputSubstitutionError::NotEnoughOutputs => None,
×
UNCOV
319
            InternalOutputSubstitutionError::InvalidDrainScript => None,
×
320
        }
UNCOV
321
    }
×
322
}
323

324
/// Error that may occur when coin selection fails.
325
///
326
/// This is currently opaque type because we aren't sure which variants will stay.
327
/// You can only display it.
328
#[derive(Debug)]
329
pub struct SelectionError(InternalSelectionError);
330

331
#[derive(Debug)]
332
pub(crate) enum InternalSelectionError {
333
    /// No candidates available for selection
334
    Empty,
335
    /// Current privacy selection implementation only supports 2-output transactions
336
    UnsupportedOutputLength,
337
    /// No selection candidates improve privacy
338
    NotFound,
339
}
340

341
impl fmt::Display for SelectionError {
UNCOV
342
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
343
        match &self.0 {
×
344
            InternalSelectionError::Empty => write!(f, "No candidates available for selection"),
×
345
            InternalSelectionError::UnsupportedOutputLength => write!(
×
346
                f,
×
347
                "Current privacy selection implementation only supports 2-output transactions"
×
UNCOV
348
            ),
×
349
            InternalSelectionError::NotFound =>
UNCOV
350
                write!(f, "No selection candidates improve privacy"),
×
351
        }
UNCOV
352
    }
×
353
}
354

355
impl error::Error for SelectionError {
UNCOV
356
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
357
        use InternalSelectionError::*;
358

UNCOV
359
        match &self.0 {
×
UNCOV
360
            Empty => None,
×
UNCOV
361
            UnsupportedOutputLength => None,
×
UNCOV
362
            NotFound => None,
×
363
        }
UNCOV
364
    }
×
365
}
366
impl From<InternalSelectionError> for SelectionError {
367
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
2✔
368
}
369

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

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

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

392
impl error::Error for InputContributionError {
UNCOV
393
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
UNCOV
394
        match &self.0 {
×
UNCOV
395
            InternalInputContributionError::ValueTooLow => None,
×
UNCOV
396
        }
×
UNCOV
397
    }
×
398
}
399

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