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

payjoin / rust-payjoin / 13961157578

20 Mar 2025 03:10AM UTC coverage: 80.606% (+0.2%) from 80.378%
13961157578

Pull #586

github

web-flow
Merge f4ad7f9bf into 6e5c7c83a
Pull Request #586: Limit response sizes for v1

46 of 54 new or added lines in 3 files covered. (85.19%)

2 existing lines in 1 file now uncovered.

4896 of 6074 relevant lines covered (80.61%)

760.9 hits per line

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

36.2
/payjoin/src/send/error.rs
1
use std::fmt::{self, Display};
2

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

7
use crate::error_codes::{
8
    NOT_ENOUGH_MONEY, ORIGINAL_PSBT_REJECTED, UNAVAILABLE, VERSION_UNSUPPORTED,
9
};
10
use crate::MAX_CONTENT_LENGTH;
11

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

273
impl ResponseError {
274
    fn from_json(json: serde_json::Value) -> Self {
5✔
275
        // we try to find the errorCode field and
5✔
276
        // if it exists we try to parse it as a well known error
5✔
277
        // if its an unknown error we return the error code and message
5✔
278
        // from original response
5✔
279
        // if errorCode field doesn't exist we return parse error
5✔
280
        let message = json
5✔
281
            .as_object()
5✔
282
            .and_then(|v| v.get("message"))
5✔
283
            .and_then(|v| v.as_str())
5✔
284
            .unwrap_or_default()
5✔
285
            .to_string();
5✔
286
        if let Some(error_code) =
3✔
287
            json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str())
5✔
288
        {
289
            match error_code {
3✔
290
                code if code == VERSION_UNSUPPORTED => {
3✔
291
                    let supported = json
2✔
292
                        .as_object()
2✔
293
                        .and_then(|v| v.get("supported"))
2✔
294
                        .and_then(|v| v.as_array())
2✔
295
                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
2✔
296
                        .unwrap_or_default();
2✔
297
                    WellKnownError::VersionUnsupported { message, supported }.into()
2✔
298
                }
299
                code if code == UNAVAILABLE => WellKnownError::Unavailable(message).into(),
1✔
300
                code if code == NOT_ENOUGH_MONEY => WellKnownError::NotEnoughMoney(message).into(),
1✔
301
                code if code == ORIGINAL_PSBT_REJECTED =>
1✔
302
                    WellKnownError::OriginalPsbtRejected(message).into(),
×
303
                _ => Self::Unrecognized { error_code: error_code.to_string(), message },
1✔
304
            }
305
        } else {
306
            InternalValidationError::Parse.into()
2✔
307
        }
308
    }
5✔
309

310
    /// Parse a response from the receiver.
311
    ///
312
    /// response must be valid JSON string.
313
    pub fn parse(response: &str) -> Self {
5✔
314
        match serde_json::from_str(response) {
5✔
315
            Ok(json) => Self::from_json(json),
4✔
316
            Err(_) => InternalValidationError::Parse.into(),
1✔
317
        }
318
    }
5✔
319
}
320

321
impl std::error::Error for ResponseError {}
322

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

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

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

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

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

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

361
            Self::Unrecognized { error_code, message } => write!(
×
362
                f,
×
363
                r#"Unrecognized error: {{ "errorCode": "{}", "message": "{}" }}"#,
×
364
                error_code, message
×
365
            ),
×
366
        }
367
    }
×
368
}
369

370
#[derive(Debug, Clone, PartialEq, Eq)]
371
pub enum WellKnownError {
372
    Unavailable(String),
373
    NotEnoughMoney(String),
374
    VersionUnsupported { message: String, supported: Vec<u64> },
375
    OriginalPsbtRejected(String),
376
}
377

378
impl WellKnownError {
379
    pub fn error_code(&self) -> &str {
1✔
380
        match self {
1✔
381
            WellKnownError::Unavailable(_) => UNAVAILABLE,
×
382
            WellKnownError::NotEnoughMoney(_) => NOT_ENOUGH_MONEY,
×
383
            WellKnownError::VersionUnsupported { .. } => VERSION_UNSUPPORTED,
1✔
384
            WellKnownError::OriginalPsbtRejected(_) => ORIGINAL_PSBT_REJECTED,
×
385
        }
386
    }
1✔
387
    pub fn message(&self) -> &str {
1✔
388
        match self {
1✔
389
            WellKnownError::Unavailable(m) => m,
×
390
            WellKnownError::NotEnoughMoney(m) => m,
×
391
            WellKnownError::VersionUnsupported { message: m, .. } => m,
1✔
392
            WellKnownError::OriginalPsbtRejected(m) => m,
×
393
        }
394
    }
1✔
395
}
396

397
impl Display for WellKnownError {
398
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
399
        match self {
1✔
400
            Self::Unavailable(_) => write!(f, "The payjoin endpoint is not available for now."),
×
401
            Self::NotEnoughMoney(_) => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."),
×
402
            Self::VersionUnsupported { supported: v, .. }=> write!(f, "This version of payjoin is not supported. Use version {:?}.", v),
1✔
403
            Self::OriginalPsbtRejected(_) => write!(f, "The receiver rejected the original PSBT."),
×
404
        }
405
    }
1✔
406
}
407

408
#[cfg(test)]
409
mod tests {
410
    use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::json;
411

412
    use super::*;
413

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