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

payjoin / rust-payjoin / 14091976193

26 Mar 2025 07:35PM UTC coverage: 81.388% (-0.004%) from 81.392%
14091976193

Pull #604

github

web-flow
Merge 37457a23d into ded33f19f
Pull Request #604: Make feature = "v2" ⇒ `pub send::V1Context`

36 of 76 new or added lines in 5 files covered. (47.37%)

17 existing lines in 2 files now uncovered.

5103 of 6270 relevant lines covered (81.39%)

737.24 hits per line

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

18.06
/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✔
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
pub struct JsonReply {
76
    /// The error code
77
    error_code: ErrorCode,
78
    /// The error message to be displayed only in debug logs
79
    message: String,
80
    /// Additional fields to be added to the JSON
81
    additional_fields: Vec<String>,
82
}
83

84
impl JsonReply {
85
    /// Create a new Reply
86
    pub fn new(error_code: ErrorCode, message: impl fmt::Display) -> Self {
3✔
87
        Self { error_code, message: message.to_string(), additional_fields: vec![] }
3✔
88
    }
3✔
89

90
    /// Serialize the Reply to a JSON string
91
    pub fn to_json(&self) -> String {
3✔
92
        if self.additional_fields.is_empty() {
3✔
93
            format!(r#"{{ "errorCode": "{}", "message": "{}" }}"#, self.error_code, self.message)
3✔
94
        } else {
NEW
95
            format!(
×
NEW
96
                r#"{{ "errorCode": "{}", "message": "{}", {} }}"#,
×
NEW
97
                self.error_code,
×
NEW
98
                self.message,
×
NEW
99
                self.additional_fields.join(", ")
×
NEW
100
            )
×
101
        }
102
    }
3✔
103
}
104

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

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

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

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

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

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

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

197
impl From<&PayloadError> for JsonReply {
198
    fn from(e: &PayloadError) -> Self {
1✔
199
        use InternalPayloadError::*;
200

201
        match &e.0 {
1✔
NEW
202
            Utf8(_) => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
203
            ParsePsbt(_) => JsonReply::new(OriginalPsbtRejected, e),
×
204
            SenderParams(e) => match e {
×
205
                super::optional_parameters::Error::UnknownVersion { supported_versions } => {
×
206
                    let supported_versions_json =
×
207
                        serde_json::to_string(supported_versions).unwrap_or_default();
×
NEW
208
                    JsonReply {
×
NEW
209
                        error_code: VersionUnsupported,
×
NEW
210
                        message: "This version of payjoin is not supported.".to_string(),
×
NEW
211
                        additional_fields: vec![format!(
×
NEW
212
                            r#""supported": {}"#,
×
NEW
213
                            supported_versions_json
×
NEW
214
                        )],
×
NEW
215
                    }
×
216
                }
NEW
217
                _ => JsonReply::new(OriginalPsbtRejected, e),
×
218
            },
NEW
219
            InconsistentPsbt(_) => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
220
            PrevTxOut(_) => JsonReply::new(OriginalPsbtRejected, e),
×
221
            MissingPayment => JsonReply::new(OriginalPsbtRejected, e),
1✔
NEW
222
            OriginalPsbtNotBroadcastable => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
223
            InputOwned(_) => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
224
            InputWeight(_) => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
225
            InputSeen(_) => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
226
            PsbtBelowFeeRate(_, _) => JsonReply::new(OriginalPsbtRejected, e),
×
NEW
227
            FeeTooHigh(_, _) => JsonReply::new(NotEnoughMoney, e),
×
228
        }
229
    }
1✔
230
}
231

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

236
        match &self.0 {
1✔
237
            Utf8(e) => write!(f, "{}", e),
×
238
            ParsePsbt(e) => write!(f, "{}", e),
×
239
            SenderParams(e) => write!(f, "{}", e),
×
240
            InconsistentPsbt(e) => write!(f, "{}", e),
×
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."),
×
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: {} < {}.",
×
250
                original_psbt_fee_rate, receiver_min_fee_rate
×
251
            ),
×
252
            FeeTooHigh(proposed_fee_rate, max_fee_rate) => write!(
×
253
                f,
×
254
                "Effective receiver feerate exceeds maximum allowed feerate: {} > {}",
×
255
                proposed_fee_rate, max_fee_rate
×
256
            ),
×
257
        }
258
    }
1✔
259
}
260

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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