• 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

28.3
/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

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

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

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

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

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

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

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

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

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

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

103
impl From<InternalValidationError> for ValidationError {
104
    fn from(value: InternalValidationError) -> Self { ValidationError(value) }
×
105
}
106

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

115
impl fmt::Display for ValidationError {
116
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
117
        use InternalValidationError::*;
118

119
        match &self.0 {
×
120
            Parse => write!(f, "couldn't decode as PSBT or JSON",),
×
NEW
121
            Io(e) => write!(f, "couldn't read PSBT: {e}"),
×
NEW
122
            Proposal(e) => write!(f, "proposal PSBT error: {e}"),
×
123
            #[cfg(feature = "v2")]
NEW
124
            V2Encapsulation(e) => write!(f, "v2 encapsulation error: {e}"),
×
125
        }
126
    }
×
127
}
128

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

133
        match &self.0 {
×
134
            Parse => None,
×
135
            Io(error) => Some(error),
×
136
            Proposal(e) => Some(e),
×
137
            #[cfg(feature = "v2")]
138
            V2Encapsulation(e) => Some(e),
×
139
        }
140
    }
×
141
}
142

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

173
impl From<crate::psbt::AddressTypeError> for InternalProposalError {
174
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
175
        InternalProposalError::InvalidAddressType(value)
×
176
    }
×
177
}
178

179
impl fmt::Display for InternalProposalError {
180
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2✔
181
        use InternalProposalError::*;
182

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

213
impl std::error::Error for InternalProposalError {
214
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
215
        use InternalProposalError::*;
216

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

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

256
    /// Errors caused by malformed responses.
257
    Validation(ValidationError),
258

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

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

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

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

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

308
impl std::error::Error for ResponseError {
309
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
310
        use ResponseError::*;
311

312
        match self {
×
313
            WellKnown(error) => Some(error),
×
314
            Validation(error) => Some(error),
×
315
            Unrecognized { .. } => None,
×
316
        }
317
    }
×
318
}
319

320
impl From<WellKnownError> for ResponseError {
321
    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
2✔
322
}
323

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

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

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

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

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

358
            Self::Unrecognized { error_code, message } => {
×
359
                let json = serde_json::json!({
×
360
                    "errorCode": error_code,
×
361
                    "message": message
×
362
                });
×
NEW
363
                write!(f, "Unrecognized error: {json}")
×
364
            }
365
        }
366
    }
×
367
}
368

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

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

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

389
impl std::error::Error for WellKnownError {}
390

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

408
#[cfg(test)]
409
mod tests {
410
    use 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.code, ErrorCode::VersionUnsupported);
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