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

payjoin / rust-payjoin / 14668081009

25 Apr 2025 03:27PM UTC coverage: 81.936% (+0.1%) from 81.835%
14668081009

push

github

web-flow
Fix uninline format rust 1.88.0 clippy violations (#667)

50 of 145 new or added lines in 30 files covered. (34.48%)

3 existing lines in 2 files now uncovered.

5325 of 6499 relevant lines covered (81.94%)

711.53 hits per line

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

22.86
/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
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✔
NEW
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
52
///    after conversion into [`JsonReply`]
53
#[derive(Debug)]
54
pub enum ReplyableError {
55
    /// Error arising from validation of the original PSBT payload
56
    Payload(PayloadError),
57
    /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks)
58
    #[cfg(feature = "v1")]
59
    V1(crate::receive::v1::RequestError),
60
    /// Error arising due to the specific receiver implementation
61
    ///
62
    /// e.g. database errors, network failures, wallet errors
63
    Implementation(ImplementationError),
64
}
65

66
/// The standard format for errors that can be replied as JSON.
67
///
68
/// The JSON output includes the following fields:
69
/// ```json
70
/// {
71
///     "errorCode": "specific-error-code",
72
///     "message": "Human readable error message"
73
/// }
74
/// ```
75
#[derive(Debug, Clone, PartialEq, Eq)]
76
pub struct JsonReply {
77
    /// The error code
78
    error_code: ErrorCode,
79
    /// The error message to be displayed only in debug logs
80
    message: String,
81
    /// Additional fields to be included in the JSON response
82
    extra: serde_json::Map<String, serde_json::Value>,
83
}
84

85
impl JsonReply {
86
    /// Create a new Reply
87
    pub fn new(error_code: ErrorCode, message: impl fmt::Display) -> Self {
3✔
88
        Self { error_code, message: message.to_string(), extra: serde_json::Map::new() }
3✔
89
    }
3✔
90

91
    /// Add an additional field to the JSON response
92
    pub fn with_extra(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
×
93
        self.extra.insert(key.to_string(), value.into());
×
94
        self
×
95
    }
×
96

97
    /// Serialize the Reply to a JSON string
98
    pub fn to_json(&self) -> serde_json::Value {
3✔
99
        let mut map = serde_json::Map::new();
3✔
100
        map.insert("errorCode".to_string(), self.error_code.to_string().into());
3✔
101
        map.insert("message".to_string(), self.message.clone().into());
3✔
102
        map.extend(self.extra.clone());
3✔
103

3✔
104
        serde_json::Value::Object(map)
3✔
105
    }
3✔
106
}
107

108
impl From<ReplyableError> for JsonReply {
109
    fn from(e: ReplyableError) -> Self {
3✔
110
        use ReplyableError::*;
111
        match e {
3✔
112
            Payload(e) => e.into(),
1✔
113
            #[cfg(feature = "v1")]
114
            V1(e) => e.into(),
×
115
            Implementation(_) => JsonReply::new(Unavailable, "Receiver error"),
2✔
116
        }
117
    }
3✔
118
}
119

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

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

142
impl From<InternalPayloadError> for ReplyableError {
143
    fn from(e: InternalPayloadError) -> Self { ReplyableError::Payload(e.into()) }
2✔
144
}
145

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

162
impl From<InternalPayloadError> for PayloadError {
163
    fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
2✔
164
}
165

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

201
impl From<PayloadError> for JsonReply {
202
    fn from(e: PayloadError) -> Self {
1✔
203
        use InternalPayloadError::*;
204

205
        match &e.0 {
1✔
206
            Utf8(_)
207
            | ParsePsbt(_)
208
            | InconsistentPsbt(_)
209
            | PrevTxOut(_)
210
            | MissingPayment
211
            | OriginalPsbtNotBroadcastable
212
            | InputOwned(_)
213
            | InputWeight(_)
214
            | InputSeen(_)
215
            | PsbtBelowFeeRate(_, _) => JsonReply::new(OriginalPsbtRejected, e),
1✔
216

217
            FeeTooHigh(_, _) => JsonReply::new(NotEnoughMoney, e),
×
218

219
            SenderParams(e) => match e {
×
220
                super::optional_parameters::Error::UnknownVersion { supported_versions } => {
×
221
                    let supported_versions_json =
×
222
                        serde_json::to_string(supported_versions).unwrap_or_default();
×
223
                    JsonReply::new(VersionUnsupported, "This version of payjoin is not supported.")
×
224
                        .with_extra("supported", supported_versions_json)
×
225
                }
226
                super::optional_parameters::Error::FeeRate =>
227
                    JsonReply::new(OriginalPsbtRejected, e),
×
228
            },
229
        }
230
    }
1✔
231
}
232

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

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

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

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

287
#[derive(Debug, PartialEq)]
288
pub(crate) enum InternalOutputSubstitutionError {
289
    /// Output substitution is disabled and output value was decreased
290
    DecreasedValueWhenDisabled,
291
    /// Output substitution is disabled and script pubkey was changed
292
    ScriptPubKeyChangedWhenDisabled,
293
    /// Current output substitution implementation doesn't support reducing the number of outputs
294
    NotEnoughOutputs,
295
    /// The provided drain script could not be identified in the provided replacement outputs
296
    InvalidDrainScript,
297
}
298

299
impl fmt::Display for OutputSubstitutionError {
300
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
301
        match &self.0 {
×
302
            InternalOutputSubstitutionError::DecreasedValueWhenDisabled => write!(f, "Decreasing the receiver output value is not allowed when output substitution is disabled"),
×
303
            InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => write!(f, "Changing the receiver output script pubkey is not allowed when output substitution is disabled"),
×
304
            InternalOutputSubstitutionError::NotEnoughOutputs => write!(
×
305
                f,
×
306
                "Current output substitution implementation doesn't support reducing the number of outputs"
×
307
            ),
×
308
            InternalOutputSubstitutionError::InvalidDrainScript =>
309
                write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
×
310
        }
311
    }
×
312
}
313

314
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
315
    fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
4✔
316
}
317

318
impl std::error::Error for OutputSubstitutionError {
319
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
320
        match &self.0 {
×
321
            InternalOutputSubstitutionError::DecreasedValueWhenDisabled => None,
×
322
            InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => None,
×
323
            InternalOutputSubstitutionError::NotEnoughOutputs => None,
×
324
            InternalOutputSubstitutionError::InvalidDrainScript => None,
×
325
        }
326
    }
×
327
}
328

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

336
#[derive(Debug, PartialEq)]
337
pub(crate) enum InternalSelectionError {
338
    /// No candidates available for selection
339
    Empty,
340
    /// Current privacy selection implementation only supports 2-output transactions
341
    UnsupportedOutputLength,
342
    /// No selection candidates improve privacy
343
    NotFound,
344
}
345

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

360
impl error::Error for SelectionError {
361
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
362
        use InternalSelectionError::*;
363

364
        match &self.0 {
×
365
            Empty => None,
×
366
            UnsupportedOutputLength => None,
×
367
            NotFound => None,
×
368
        }
369
    }
×
370
}
371
impl From<InternalSelectionError> for SelectionError {
372
    fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
7✔
373
}
374

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

382
#[derive(Debug)]
383
pub(crate) enum InternalInputContributionError {
384
    /// Total input value is not enough to cover additional output value
385
    ValueTooLow,
386
}
387

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

397
impl error::Error for InputContributionError {
398
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
399
        match &self.0 {
×
400
            InternalInputContributionError::ValueTooLow => None,
×
401
        }
×
402
    }
×
403
}
404

405
impl From<InternalInputContributionError> for InputContributionError {
406
    fn from(value: InternalInputContributionError) -> Self { InputContributionError(value) }
×
407
}
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