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

payjoin / rust-payjoin / 15169166399

21 May 2025 05:54PM UTC coverage: 83.525% (+0.09%) from 83.435%
15169166399

Pull #586

github

web-flow
Merge 29ffade80 into c4bc5ce4c
Pull Request #586: Limit response sizes for v1

41 of 48 new or added lines in 2 files covered. (85.42%)

2 existing lines in 1 file now uncovered.

5947 of 7120 relevant lines covered (83.53%)

650.34 hits per line

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

39.72
/payjoin/src/send/error.rs
1
use std::fmt::{self, Display};
2
use std::str::FromStr;
3

4
use bitcoin::locktime::absolute::LockTime;
5
use bitcoin::transaction::Version;
6
use bitcoin::Sequence;
7

8
use crate::error_codes::ErrorCode;
9
use crate::MAX_CONTENT_LENGTH;
10

11
/// Error building a Sender from a SenderBuilder.
12
///
13
/// This error is unrecoverable.
14
#[derive(Debug)]
15
pub struct BuildSenderError(InternalBuildSenderError);
16

17
#[derive(Debug, PartialEq)]
18
pub(crate) enum InternalBuildSenderError {
19
    InvalidOriginalInput(crate::psbt::PsbtInputsError),
20
    InconsistentOriginalPsbt(crate::psbt::InconsistentPsbt),
21
    NoInputs,
22
    PayeeValueNotEqual,
23
    NoOutputs,
24
    MultiplePayeeOutputs,
25
    MissingPayeeOutput,
26
    FeeOutputValueLowerThanFeeContribution,
27
    AmbiguousChangeOutput,
28
    ChangeIndexOutOfBounds,
29
    ChangeIndexPointsAtPayee,
30
    InputWeight(crate::psbt::InputWeightError),
31
    AddressType(crate::psbt::AddressTypeError),
32
}
33

34
impl From<InternalBuildSenderError> for BuildSenderError {
35
    fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
×
36
}
37

38
impl From<crate::psbt::AddressTypeError> for BuildSenderError {
39
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
40
        BuildSenderError(InternalBuildSenderError::AddressType(value))
×
41
    }
×
42
}
43

44
impl fmt::Display for BuildSenderError {
45
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
46
        use InternalBuildSenderError::*;
47

48
        match &self.0 {
×
49
            InvalidOriginalInput(e) => write!(f, "an input in the original transaction is invalid: {e:#?}"),
×
50
            InconsistentOriginalPsbt(e) => write!(f, "the original transaction is inconsistent: {e:#?}"),
×
51
            NoInputs => write!(f, "the original transaction has no inputs"),
×
52
            PayeeValueNotEqual => write!(f, "the value in original transaction doesn't equal value requested in the payment link"),
×
53
            NoOutputs => write!(f, "the original transaction has no outputs"),
×
54
            MultiplePayeeOutputs => write!(f, "the original transaction has more than one output belonging to the payee"),
×
55
            MissingPayeeOutput => write!(f, "the output belonging to payee is missing from the original transaction"),
×
56
            FeeOutputValueLowerThanFeeContribution => write!(f, "the value of fee output is lower than maximum allowed contribution"),
×
57
            AmbiguousChangeOutput => write!(f, "can not determine which output is change because there's more than two outputs"),
×
58
            ChangeIndexOutOfBounds => write!(f, "fee output index is points out of bounds"),
×
59
            ChangeIndexPointsAtPayee => write!(f, "fee output index is points at output belonging to the payee"),
×
60
            AddressType(e) => write!(f, "can not determine input address type: {e}"),
×
61
            InputWeight(e) => write!(f, "can not determine expected input weight: {e}"),
×
62
        }
63
    }
×
64
}
65

66
impl std::error::Error for BuildSenderError {
67
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
68
        use InternalBuildSenderError::*;
69

70
        match &self.0 {
×
71
            InvalidOriginalInput(error) => Some(error),
×
72
            InconsistentOriginalPsbt(error) => Some(error),
×
73
            NoInputs => None,
×
74
            PayeeValueNotEqual => None,
×
75
            NoOutputs => None,
×
76
            MultiplePayeeOutputs => None,
×
77
            MissingPayeeOutput => None,
×
78
            FeeOutputValueLowerThanFeeContribution => None,
×
79
            AmbiguousChangeOutput => None,
×
80
            ChangeIndexOutOfBounds => None,
×
81
            ChangeIndexPointsAtPayee => None,
×
82
            AddressType(error) => Some(error),
×
83
            InputWeight(error) => Some(error),
×
84
        }
85
    }
×
86
}
87

88
/// Error that may occur when the response from receiver is malformed.
89
///
90
/// This is currently opaque type because we aren't sure which variants will stay.
91
/// You can only display it.
92
#[derive(Debug)]
93
pub struct ValidationError(InternalValidationError);
94

95
#[derive(Debug)]
96
pub(crate) enum InternalValidationError {
97
    Parse,
98
    Io(std::io::Error),
99
    ContentTooLarge,
100
    Proposal(InternalProposalError),
101
    #[cfg(feature = "v2")]
102
    V2Encapsulation(crate::send::v2::EncapsulationError),
103
}
104

105
impl From<InternalValidationError> for ValidationError {
106
    fn from(value: InternalValidationError) -> Self { ValidationError(value) }
2✔
107
}
108

109
impl From<crate::psbt::AddressTypeError> for ValidationError {
110
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
111
        ValidationError(InternalValidationError::Proposal(
×
112
            InternalProposalError::InvalidAddressType(value),
×
113
        ))
×
114
    }
×
115
}
116

117
impl fmt::Display for ValidationError {
118
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4✔
119
        use InternalValidationError::*;
120

121
        match &self.0 {
4✔
122
            Parse => write!(f, "couldn't decode as PSBT or JSON",),
4✔
123
            Io(e) => write!(f, "couldn't read PSBT: {e}"),
×
NEW
124
            ContentTooLarge => write!(f, "content is larger than {MAX_CONTENT_LENGTH} bytes"),
×
UNCOV
125
            Proposal(e) => write!(f, "proposal PSBT error: {e}"),
×
126
            #[cfg(feature = "v2")]
127
            V2Encapsulation(e) => write!(f, "v2 encapsulation error: {e}"),
×
128
        }
129
    }
4✔
130
}
131

132
impl std::error::Error for ValidationError {
133
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
134
        use InternalValidationError::*;
135

136
        match &self.0 {
×
137
            Parse => None,
×
138
            Io(error) => Some(error),
×
NEW
139
            ContentTooLarge => None,
×
UNCOV
140
            Proposal(e) => Some(e),
×
141
            #[cfg(feature = "v2")]
142
            V2Encapsulation(e) => Some(e),
×
143
        }
144
    }
×
145
}
146

147
/// Error that may occur when the proposal PSBT from receiver is malformed.
148
#[derive(Debug)]
149
pub(crate) enum InternalProposalError {
150
    InvalidAddressType(crate::psbt::AddressTypeError),
151
    NoInputs,
152
    PrevTxOut(crate::psbt::PrevTxOutError),
153
    InputWeight(crate::psbt::InputWeightError),
154
    VersionsDontMatch { proposed: Version, original: Version },
155
    LockTimesDontMatch { proposed: LockTime, original: LockTime },
156
    SenderTxinSequenceChanged { proposed: Sequence, original: Sequence },
157
    SenderTxinContainsFinalScriptSig,
158
    SenderTxinContainsFinalScriptWitness,
159
    TxInContainsKeyPaths,
160
    ContainsPartialSigs,
161
    ReceiverTxinNotFinalized,
162
    ReceiverTxinMissingUtxoInfo,
163
    MixedSequence,
164
    MissingOrShuffledInputs,
165
    TxOutContainsKeyPaths,
166
    FeeContributionExceedsMaximum,
167
    DisallowedOutputSubstitution,
168
    OutputValueDecreased,
169
    MissingOrShuffledOutputs,
170
    AbsoluteFeeDecreased,
171
    PayeeTookContributedFee,
172
    FeeContributionPaysOutputSizeIncrease,
173
    FeeRateBelowMinimum,
174
    Psbt(bitcoin::psbt::Error),
175
}
176

177
impl From<crate::psbt::AddressTypeError> for InternalProposalError {
178
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
179
        InternalProposalError::InvalidAddressType(value)
×
180
    }
×
181
}
182

183
impl fmt::Display for InternalProposalError {
184
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40✔
185
        use InternalProposalError::*;
186

187
        match &self {
40✔
188
            InvalidAddressType(e) => write!(f, "invalid input address type: {e}"),
×
189
            NoInputs => write!(f, "PSBT doesn't have any inputs"),
×
190
            PrevTxOut(e) => write!(f, "missing previous txout information: {e}"),
×
191
            InputWeight(e) => write!(f, "can not determine expected input weight: {e}"),
×
192
            VersionsDontMatch { proposed, original, } => write!(f, "proposed transaction version {proposed} doesn't match the original {original}"),
2✔
193
            LockTimesDontMatch { proposed, original, } => write!(f, "proposed transaction lock time {proposed} doesn't match the original {original}"),
2✔
194
            SenderTxinSequenceChanged { proposed, original, } => write!(f, "proposed transaction sequence number {proposed} doesn't match the original {original}"),
2✔
195
            SenderTxinContainsFinalScriptSig => write!(f, "an input in proposed transaction belonging to the sender contains finalized non-witness signature"),
2✔
196
            SenderTxinContainsFinalScriptWitness => write!(f, "an input in proposed transaction belonging to the sender contains finalized witness signature"),
2✔
197
            TxInContainsKeyPaths => write!(f, "proposed transaction inputs contain key paths"),
2✔
198
            ContainsPartialSigs => write!(f, "an input in proposed transaction belonging to the sender contains partial signatures"),
2✔
199
            ReceiverTxinNotFinalized => write!(f, "an input in proposed transaction belonging to the receiver is not finalized"),
2✔
200
            ReceiverTxinMissingUtxoInfo => write!(f, "an input in proposed transaction belonging to the receiver is missing UTXO information"),
2✔
201
            MixedSequence => write!(f, "inputs of proposed transaction contain mixed sequence numbers"),
2✔
202
            MissingOrShuffledInputs => write!(f, "proposed transaction is missing inputs of the sender or they are shuffled"),
2✔
203
            TxOutContainsKeyPaths => write!(f, "proposed transaction outputs contain key paths"),
2✔
204
            FeeContributionExceedsMaximum => write!(f, "fee contribution exceeds allowed maximum"),
2✔
205
            DisallowedOutputSubstitution => write!(f, "the receiver change output despite it being disallowed"),
2✔
206
            OutputValueDecreased => write!(f, "the amount in our non-fee output was decreased"),
2✔
207
            MissingOrShuffledOutputs => write!(f, "proposed transaction is missing outputs of the sender or they are shuffled"),
2✔
208
            AbsoluteFeeDecreased => write!(f, "abslute fee of proposed transaction is lower than original"),
2✔
209
            PayeeTookContributedFee => write!(f, "payee tried to take fee contribution for himself"),
2✔
210
            FeeContributionPaysOutputSizeIncrease => write!(f, "fee contribution pays for additional outputs"),
2✔
211
            FeeRateBelowMinimum =>  write!(f, "the fee rate of proposed transaction is below minimum"),
2✔
212
            Psbt(e) => write!(f, "psbt error: {e}"),
×
213
        }
214
    }
40✔
215
}
216

217
impl std::error::Error for InternalProposalError {
218
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
219
        use InternalProposalError::*;
220

221
        match self {
×
222
            InvalidAddressType(error) => Some(error),
×
223
            NoInputs => None,
×
224
            PrevTxOut(error) => Some(error),
×
225
            InputWeight(error) => Some(error),
×
226
            VersionsDontMatch { proposed: _, original: _ } => None,
×
227
            LockTimesDontMatch { proposed: _, original: _ } => None,
×
228
            SenderTxinSequenceChanged { proposed: _, original: _ } => None,
×
229
            SenderTxinContainsFinalScriptSig => None,
×
230
            SenderTxinContainsFinalScriptWitness => None,
×
231
            TxInContainsKeyPaths => None,
×
232
            ContainsPartialSigs => None,
×
233
            ReceiverTxinNotFinalized => None,
×
234
            ReceiverTxinMissingUtxoInfo => None,
×
235
            MixedSequence => None,
×
236
            MissingOrShuffledInputs => None,
×
237
            TxOutContainsKeyPaths => None,
×
238
            FeeContributionExceedsMaximum => None,
×
239
            DisallowedOutputSubstitution => None,
×
240
            OutputValueDecreased => None,
×
241
            MissingOrShuffledOutputs => None,
×
242
            AbsoluteFeeDecreased => None,
×
243
            PayeeTookContributedFee => None,
×
244
            FeeContributionPaysOutputSizeIncrease => None,
×
245
            FeeRateBelowMinimum => None,
×
246
            Psbt(error) => Some(error),
×
247
        }
248
    }
×
249
}
250

251
/// Represent an error returned by Payjoin receiver.
252
pub enum ResponseError {
253
    /// `WellKnown` Errors are defined in the [`BIP78::ReceiverWellKnownError`] spec.
254
    ///
255
    /// It is safe to display `WellKnown` errors to end users.
256
    ///
257
    /// [`BIP78::ReceiverWellKnownError`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors
258
    WellKnown(WellKnownError),
259

260
    /// Errors caused by malformed responses.
261
    Validation(ValidationError),
262

263
    /// `Unrecognized` Errors are NOT defined in the [`BIP78::ReceiverWellKnownError`] spec.
264
    ///
265
    /// It is NOT safe to display `Unrecognized` errors to end users as they could be used
266
    /// maliciously to phish a non technical user. Only display them in debug logs.
267
    ///
268
    /// [`BIP78::ReceiverWellKnownError`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors
269
    Unrecognized { error_code: String, message: String },
270
}
271

272
impl ResponseError {
273
    pub(crate) fn from_json(json: serde_json::Value) -> Self {
5✔
274
        let message = json
5✔
275
            .as_object()
5✔
276
            .and_then(|v| v.get("message"))
5✔
277
            .and_then(|v| v.as_str())
5✔
278
            .unwrap_or_default()
5✔
279
            .to_string();
5✔
280

5✔
281
        let error_code = json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str());
5✔
282

5✔
283
        match error_code {
5✔
284
            Some(code) => match ErrorCode::from_str(code) {
3✔
285
                Ok(ErrorCode::VersionUnsupported) => {
286
                    let supported = json
2✔
287
                        .as_object()
2✔
288
                        .and_then(|v| v.get("supported"))
2✔
289
                        .and_then(|v| v.as_array())
2✔
290
                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
2✔
291
                        .unwrap_or_default();
2✔
292
                    WellKnownError::version_unsupported(message, supported).into()
2✔
293
                }
294
                Ok(code) => WellKnownError::new(code, message).into(),
×
295
                Err(_) => Self::Unrecognized { error_code: code.to_string(), message },
1✔
296
            },
297
            None => InternalValidationError::Parse.into(),
2✔
298
        }
299
    }
5✔
300

301
    /// Parse a response from the receiver.
302
    ///
303
    /// response must be valid JSON string.
304
    pub(crate) fn parse(response: &str) -> Self {
5✔
305
        match serde_json::from_str(response) {
5✔
306
            Ok(json) => Self::from_json(json),
4✔
307
            Err(_) => InternalValidationError::Parse.into(),
1✔
308
        }
309
    }
5✔
310
}
311

312
impl std::error::Error for ResponseError {
313
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
314
        use ResponseError::*;
315

316
        match self {
×
317
            WellKnown(error) => Some(error),
×
318
            Validation(error) => Some(error),
×
319
            Unrecognized { .. } => None,
×
320
        }
321
    }
×
322
}
323

324
impl From<WellKnownError> for ResponseError {
325
    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
2✔
326
}
327

328
impl From<InternalValidationError> for ResponseError {
329
    fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
4✔
330
}
331

332
impl From<InternalProposalError> for ResponseError {
333
    fn from(value: InternalProposalError) -> Self {
×
334
        ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
×
335
    }
×
336
}
337

338
impl Display for ResponseError {
339
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
340
        match self {
×
341
            Self::WellKnown(e) => e.fmt(f),
×
342
            Self::Validation(e) => write!(f, "The receiver sent an invalid response: {e}"),
×
343

344
            // Do NOT display unrecognized errors to end users, only debug logs
345
            Self::Unrecognized { .. } => write!(f, "The receiver sent an unrecognized error."),
×
346
        }
347
    }
×
348
}
349

350
impl fmt::Debug for ResponseError {
351
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
352
        match self {
×
353
            Self::WellKnown(e) => {
×
354
                let json = serde_json::json!({
×
355
                    "errorCode": e.code.to_string(),
×
356
                    "message": e.message
×
357
                });
×
358
                write!(f, "Well known error: {json}")
×
359
            }
360
            Self::Validation(e) => write!(f, "Validation({e:?})"),
×
361

362
            Self::Unrecognized { error_code, message } => {
×
363
                let json = serde_json::json!({
×
364
                    "errorCode": error_code,
×
365
                    "message": message
×
366
                });
×
367
                write!(f, "Unrecognized error: {json}")
×
368
            }
369
        }
370
    }
×
371
}
372

373
/// A well-known error that can be safely displayed to end users.
374
#[derive(Debug, Clone, PartialEq, Eq)]
375
pub struct WellKnownError {
376
    pub(crate) code: ErrorCode,
377
    pub(crate) message: String,
378
    pub(crate) supported_versions: Option<Vec<u64>>,
379
}
380

381
impl WellKnownError {
382
    /// Create a new well-known error with the given code and message.
383
    pub(crate) fn new(code: ErrorCode, message: String) -> Self {
×
384
        Self { code, message, supported_versions: None }
×
385
    }
×
386

387
    /// Create a version unsupported error with the given message and supported versions.
388
    pub(crate) fn version_unsupported(message: String, supported: Vec<u64>) -> Self {
2✔
389
        Self { code: ErrorCode::VersionUnsupported, message, supported_versions: Some(supported) }
2✔
390
    }
2✔
391
}
392

393
impl std::error::Error for WellKnownError {}
394

395
impl core::fmt::Display for WellKnownError {
396
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1✔
397
        match self.code {
1✔
398
            ErrorCode::Unavailable => write!(f, "The payjoin endpoint is not available for now."),
×
399
            ErrorCode::NotEnoughMoney => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."),
×
400
            ErrorCode::VersionUnsupported => {
401
                if let Some(supported) = &self.supported_versions {
1✔
402
                    write!(f, "This version of payjoin is not supported. Use version {supported:?}.")
1✔
403
                } else {
404
                    write!(f, "This version of payjoin is not supported.")
×
405
                }
406
            }
407
            ErrorCode::OriginalPsbtRejected => write!(f, "The receiver rejected the original PSBT."),
×
408
        }
409
    }
1✔
410
}
411

412
#[cfg(test)]
413
mod tests {
414
    use serde_json::json;
415

416
    use super::*;
417

418
    #[test]
419
    fn test_parse_json() {
1✔
420
        let known_str_error = r#"{"errorCode":"version-unsupported", "message":"custom message here", "supported": [1, 2]}"#;
1✔
421
        match ResponseError::parse(known_str_error) {
1✔
422
            ResponseError::WellKnown(e) => {
1✔
423
                assert_eq!(e.code, ErrorCode::VersionUnsupported);
1✔
424
                assert_eq!(e.message, "custom message here");
1✔
425
                assert_eq!(
1✔
426
                    e.to_string(),
1✔
427
                    "This version of payjoin is not supported. Use version [1, 2]."
1✔
428
                );
1✔
429
            }
430
            _ => panic!("Expected WellKnown error"),
×
431
        };
432
        let unrecognized_error = r#"{"errorCode":"random", "message":"random"}"#;
1✔
433
        assert!(matches!(
1✔
434
            ResponseError::parse(unrecognized_error),
1✔
435
            ResponseError::Unrecognized { .. }
436
        ));
437
        let invalid_json_error = json!({
1✔
438
            "err": "random",
1✔
439
            "message": "This version of payjoin is not supported."
1✔
440
        });
1✔
441
        assert!(matches!(
1✔
442
            ResponseError::from_json(invalid_json_error),
1✔
443
            ResponseError::Validation(_)
444
        ));
445
    }
1✔
446
}
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