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

payjoin / rust-payjoin / 15889007756

25 Jun 2025 11:05PM UTC coverage: 86.308% (+0.2%) from 86.155%
15889007756

Pull #772

github

web-flow
Merge 454bd170a into ab142b383
Pull Request #772: Add `expected_weight` field to `InputPair`

133 of 137 new or added lines in 4 files covered. (97.08%)

61 existing lines in 4 files now uncovered.

7993 of 9261 relevant lines covered (86.31%)

501.98 hits per line

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

26.43
/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 From<ReplyableError> for Error {
19
    fn from(e: ReplyableError) -> Self { Error::ReplyToSender(e) }
×
20
}
21

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

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

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

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

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

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

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

4✔
102
        serde_json::Value::Object(map)
4✔
103
    }
4✔
104
}
105

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

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

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

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

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

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

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

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

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

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

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

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

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

259
impl std::error::Error for PayloadError {
UNCOV
260
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
261
        use InternalPayloadError::*;
262
        match &self.0 {
×
263
            Utf8(e) => Some(e),
×
264
            ParsePsbt(e) => Some(e),
×
265
            SenderParams(e) => Some(e),
×
266
            InconsistentPsbt(e) => Some(e),
×
267
            PrevTxOut(e) => Some(e),
×
268
            InputWeight(e) => Some(e),
×
269
            PsbtBelowFeeRate(_, _) => None,
×
270
            FeeTooHigh(_, _) => None,
×
271
            MissingPayment => None,
×
272
            OriginalPsbtNotBroadcastable => None,
×
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, PartialEq)]
284
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
285

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

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

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

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

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

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

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

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

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

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

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

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

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

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