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

payjoin / rust-payjoin / 14041215432

24 Mar 2025 04:58PM UTC coverage: 80.865% (+0.5%) from 80.411%
14041215432

push

github

web-flow
Replace OutputSubstitutionDisabled string context with sub variants (#590)

closes #588

82 of 88 new or added lines in 3 files covered. (93.18%)

2 existing lines in 1 file now uncovered.

4936 of 6104 relevant lines covered (80.87%)

757.15 hits per line

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

16.11
/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
/// The replyable error type for the payjoin receiver, representing failures need to be
45
/// returned to the sender.
46
///
47
/// The error handling is designed to:
48
/// 1. Provide structured error responses for protocol-level failures
49
/// 2. Hide implementation details of external errors for security
50
/// 3. Support proper error propagation through the receiver stack
51
/// 4. Provide errors according to BIP-78 JSON error specifications for return using [`JsonError::to_json`]
52
#[derive(Debug)]
53
pub enum ReplyableError {
54
    /// Error arising from validation of the original PSBT payload
55
    Payload(PayloadError),
56
    /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks)
57
    #[cfg(feature = "v1")]
58
    V1(crate::receive::v1::RequestError),
59
    /// Error arising due to the specific receiver implementation
60
    ///
61
    /// e.g. database errors, network failures, wallet errors
62
    Implementation(ImplementationError),
63
}
64

65
/// A trait for errors that can be serialized to JSON in a standardized format.
66
///
67
/// The JSON output follows the structure:
68
/// ```json
69
/// {
70
///     "errorCode": "specific-error-code",
71
///     "message": "Human readable error message"
72
/// }
73
/// ```
74
pub trait JsonError {
75
    /// Converts the error into a JSON string representation.
76
    fn to_json(&self) -> String;
77
}
78

79
impl JsonError for ReplyableError {
80
    fn to_json(&self) -> String {
3✔
81
        match self {
3✔
82
            Self::Payload(e) => e.to_json(),
1✔
83
            #[cfg(feature = "v1")]
84
            Self::V1(e) => e.to_json(),
×
85
            Self::Implementation(_) => serialize_json_error(UNAVAILABLE, "Receiver error"),
2✔
86
        }
87
    }
3✔
88
}
89

90
pub(crate) fn serialize_json_error(code: &str, message: impl fmt::Display) -> String {
3✔
91
    format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message)
3✔
92
}
3✔
93

94
pub(crate) fn serialize_json_plus_fields(
×
95
    code: &str,
×
96
    message: impl fmt::Display,
×
97
    additional_fields: &str,
×
98
) -> String {
×
99
    format!(r#"{{ "errorCode": "{}", "message": "{}", {} }}"#, code, message, additional_fields)
×
100
}
×
101

102
impl fmt::Display for ReplyableError {
103
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
104
        match &self {
×
105
            Self::Payload(e) => e.fmt(f),
×
106
            #[cfg(feature = "v1")]
107
            Self::V1(e) => e.fmt(f),
×
108
            Self::Implementation(e) => write!(f, "Internal Server Error: {}", e),
×
109
        }
110
    }
×
111
}
112

113
impl error::Error for ReplyableError {
114
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
115
        match &self {
×
116
            Self::Payload(e) => e.source(),
×
117
            #[cfg(feature = "v1")]
118
            Self::V1(e) => e.source(),
×
119
            Self::Implementation(e) => Some(e.as_ref()),
×
120
        }
121
    }
×
122
}
123

124
impl From<InternalPayloadError> for ReplyableError {
125
    fn from(e: InternalPayloadError) -> Self { ReplyableError::Payload(e.into()) }
1✔
126
}
127

128
/// An error that occurs during validation of the original PSBT payload sent by the sender.
129
///
130
/// This type provides a public abstraction over internal validation errors while maintaining a stable public API.
131
/// It handles various failure modes like:
132
/// - Invalid UTF-8 encoding
133
/// - PSBT parsing errors
134
/// - BIP-78 specific PSBT validation failures
135
/// - Fee rate validation
136
/// - Input ownership validation
137
/// - Previous transaction output validation
138
///
139
/// The error messages are formatted as JSON strings suitable for HTTP responses according to the BIP-78 spec,
140
/// with appropriate error codes and human-readable messages.
141
#[derive(Debug)]
142
pub struct PayloadError(pub(crate) InternalPayloadError);
143

144
impl From<InternalPayloadError> for PayloadError {
145
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
1✔
146
}
147

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

183
impl JsonError for PayloadError {
184
    fn to_json(&self) -> String {
1✔
185
        use InternalPayloadError::*;
186

187
        match &self.0 {
1✔
188
            Utf8(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
189
            ParsePsbt(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
190
            SenderParams(e) => match e {
×
191
                super::optional_parameters::Error::UnknownVersion { supported_versions } => {
×
192
                    let supported_versions_json =
×
193
                        serde_json::to_string(supported_versions).unwrap_or_default();
×
194
                    serialize_json_plus_fields(
×
195
                        VERSION_UNSUPPORTED,
×
196
                        "This version of payjoin is not supported.",
×
197
                        &format!(r#""supported": {}"#, supported_versions_json),
×
198
                    )
×
199
                }
200
                _ => serialize_json_error("sender-params-error", self),
×
201
            },
202
            InconsistentPsbt(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
203
            PrevTxOut(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
204
            MissingPayment => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
1✔
205
            OriginalPsbtNotBroadcastable => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
206
            InputOwned(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
207
            InputWeight(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
208
            InputSeen(_) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
209
            PsbtBelowFeeRate(_, _) => serialize_json_error(ORIGINAL_PSBT_REJECTED, self),
×
210
            FeeTooHigh(_, _) => serialize_json_error(NOT_ENOUGH_MONEY, self),
×
211
        }
212
    }
1✔
213
}
214

215
impl fmt::Display for PayloadError {
216
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
217
        use InternalPayloadError::*;
218

219
        match &self.0 {
1✔
220
            Utf8(e) => write!(f, "{}", e),
×
221
            ParsePsbt(e) => write!(f, "{}", e),
×
222
            SenderParams(e) => write!(f, "{}", e),
×
223
            InconsistentPsbt(e) => write!(f, "{}", e),
×
224
            PrevTxOut(e) => write!(f, "PrevTxOut Error: {}", e),
×
225
            MissingPayment => write!(f, "Missing payment."),
1✔
226
            OriginalPsbtNotBroadcastable => write!(f, "Can't broadcast. PSBT rejected by mempool."),
×
227
            InputOwned(_) => write!(f, "The receiver rejected the original PSBT."),
×
228
            InputWeight(e) => write!(f, "InputWeight Error: {}", e),
×
229
            InputSeen(_) => write!(f, "The receiver rejected the original PSBT."),
×
230
            PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write!(
×
231
                f,
×
232
                "Original PSBT fee rate too low: {} < {}.",
×
233
                original_psbt_fee_rate, receiver_min_fee_rate
×
234
            ),
×
235
            FeeTooHigh(proposed_fee_rate, max_fee_rate) => write!(
×
236
                f,
×
237
                "Effective receiver feerate exceeds maximum allowed feerate: {} > {}",
×
238
                proposed_fee_rate, max_fee_rate
×
239
            ),
×
240
        }
241
    }
1✔
242
}
243

244
impl std::error::Error for PayloadError {
245
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
246
        use InternalPayloadError::*;
247
        match &self.0 {
×
248
            Utf8(e) => Some(e),
×
249
            ParsePsbt(e) => Some(e),
×
250
            SenderParams(e) => Some(e),
×
251
            InconsistentPsbt(e) => Some(e),
×
252
            PrevTxOut(e) => Some(e),
×
253
            InputWeight(e) => Some(e),
×
254
            PsbtBelowFeeRate(_, _) => None,
×
255
            FeeTooHigh(_, _) => None,
×
256
            MissingPayment => None,
×
257
            OriginalPsbtNotBroadcastable => None,
×
258
            InputOwned(_) => None,
×
259
            InputSeen(_) => None,
×
260
        }
261
    }
×
262
}
263

264
/// Error that may occur when output substitution fails.
265
///
266
/// This is currently opaque type because we aren't sure which variants will stay.
267
/// You can only display it.
268
#[derive(Debug, PartialEq)]
269
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
270

271
#[derive(Debug, PartialEq)]
272
pub(crate) enum InternalOutputSubstitutionError {
273
    /// Output substitution is disabled and output value was decreased
274
    DecreasedValueWhenDisabled,
275
    /// Output substitution is disabled and script pubkey was changed
276
    ScriptPubKeyChangedWhenDisabled,
277
    /// Current output substitution implementation doesn't support reducing the number of outputs
278
    NotEnoughOutputs,
279
    /// The provided drain script could not be identified in the provided replacement outputs
280
    InvalidDrainScript,
281
}
282

283
impl fmt::Display for OutputSubstitutionError {
284
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
285
        match &self.0 {
×
NEW
286
            InternalOutputSubstitutionError::DecreasedValueWhenDisabled => write!(f, "Decreasing the receiver output value is not allowed when output substitution is disabled"),
×
NEW
287
            InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => write!(f, "Changing the receiver output script pubkey is not allowed when output substitution is disabled"),
×
288
            InternalOutputSubstitutionError::NotEnoughOutputs => write!(
×
289
                f,
×
290
                "Current output substitution implementation doesn't support reducing the number of outputs"
×
291
            ),
×
292
            InternalOutputSubstitutionError::InvalidDrainScript =>
293
                write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
×
294
        }
295
    }
×
296
}
297

298
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
299
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
4✔
300
}
301

302
impl std::error::Error for OutputSubstitutionError {
303
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
304
        match &self.0 {
×
NEW
305
            InternalOutputSubstitutionError::DecreasedValueWhenDisabled => None,
×
NEW
306
            InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => None,
×
307
            InternalOutputSubstitutionError::NotEnoughOutputs => None,
×
308
            InternalOutputSubstitutionError::InvalidDrainScript => None,
×
309
        }
310
    }
×
311
}
312

313
/// Error that may occur when coin selection fails.
314
///
315
/// This is currently opaque type because we aren't sure which variants will stay.
316
/// You can only display it.
317
#[derive(Debug)]
318
pub struct SelectionError(InternalSelectionError);
319

320
#[derive(Debug)]
321
pub(crate) enum InternalSelectionError {
322
    /// No candidates available for selection
323
    Empty,
324
    /// Current privacy selection implementation only supports 2-output transactions
325
    UnsupportedOutputLength,
326
    /// No selection candidates improve privacy
327
    NotFound,
328
}
329

330
impl fmt::Display for SelectionError {
331
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
332
        match &self.0 {
×
333
            InternalSelectionError::Empty => write!(f, "No candidates available for selection"),
×
334
            InternalSelectionError::UnsupportedOutputLength => write!(
×
335
                f,
×
336
                "Current privacy selection implementation only supports 2-output transactions"
×
337
            ),
×
338
            InternalSelectionError::NotFound =>
339
                write!(f, "No selection candidates improve privacy"),
×
340
        }
341
    }
×
342
}
343

344
impl error::Error for SelectionError {
345
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
346
        use InternalSelectionError::*;
347

348
        match &self.0 {
×
349
            Empty => None,
×
350
            UnsupportedOutputLength => None,
×
351
            NotFound => None,
×
352
        }
353
    }
×
354
}
355
impl From<InternalSelectionError> for SelectionError {
356
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
2✔
357
}
358

359
/// Error that may occur when input contribution fails.
360
///
361
/// This is currently opaque type because we aren't sure which variants will stay.
362
/// You can only display it.
363
#[derive(Debug)]
364
pub struct InputContributionError(InternalInputContributionError);
365

366
#[derive(Debug)]
367
pub(crate) enum InternalInputContributionError {
368
    /// Total input value is not enough to cover additional output value
369
    ValueTooLow,
370
}
371

372
impl fmt::Display for InputContributionError {
373
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
374
        match &self.0 {
×
375
            InternalInputContributionError::ValueTooLow =>
×
376
                write!(f, "Total input value is not enough to cover additional output value"),
×
377
        }
×
378
    }
×
379
}
380

381
impl error::Error for InputContributionError {
382
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
383
        match &self.0 {
×
384
            InternalInputContributionError::ValueTooLow => None,
×
385
        }
×
386
    }
×
387
}
388

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