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

payjoin / rust-payjoin / 14111798823

27 Mar 2025 04:29PM UTC coverage: 81.667% (+0.2%) from 81.45%
14111798823

Pull #606

github

web-flow
Merge 28f41b350 into 752fceebf
Pull Request #606: Reply json

57 of 91 new or added lines in 6 files covered. (62.64%)

4 existing lines in 2 files now uncovered.

5114 of 6262 relevant lines covered (81.67%)

738.19 hits per line

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

30.88
/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 {
×
48
            InvalidOriginalInput(e) => write!(f, "an input in the original transaction is invalid: {:#?}", e),
×
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"),
×
59
            AddressType(e) => write!(f, "can not determine input address type: {}", e),
×
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",),
×
121
            Io(e) => write!(f, "couldn't read PSBT: {}", e),
×
122
            Proposal(e) => write!(f, "proposal PSBT error: {}", e),
×
123
            #[cfg(feature = "v2")]
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✔
184
            InvalidAddressType(e) => write!(f, "invalid input address type: {}", e),
×
185
            NoInputs => write!(f, "PSBT doesn't have any inputs"),
×
186
            PrevTxOut(e) => write!(f, "missing previous txout information: {}", e),
×
187
            InputWeight(e) => write!(f, "can not determine expected input weight: {}", e),
×
188
            VersionsDontMatch { proposed, original, } => write!(f, "proposed transaction version {} doesn't match the original {}", proposed, original),
×
189
            LockTimesDontMatch { proposed, original, } => write!(f, "proposed transaction lock time {} doesn't match the original {}", proposed, original),
×
190
            SenderTxinSequenceChanged { proposed, original, } => write!(f, "proposed transaction sequence number {} doesn't match the original {}", proposed, 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"),
×
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
    fn from_json(json: serde_json::Value) -> Self {
5✔
270
        // we try to find the errorCode field and
5✔
271
        // if it exists we try to parse it as a well known error
5✔
272
        // if its an unknown error we return the error code and message
5✔
273
        // from original response
5✔
274
        // if errorCode field doesn't exist we return parse error
5✔
275
        let message = json
5✔
276
            .as_object()
5✔
277
            .and_then(|v| v.get("message"))
5✔
278
            .and_then(|v| v.as_str())
5✔
279
            .unwrap_or_default()
5✔
280
            .to_string();
5✔
281
        if let Some(error_code) =
3✔
282
            json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str())
5✔
283
        {
284
            match ErrorCode::from_str(error_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::VersionUnsupported { message, supported }.into()
2✔
293
                }
NEW
294
                Ok(ErrorCode::Unavailable) => WellKnownError::Unavailable(message).into(),
×
NEW
295
                Ok(ErrorCode::NotEnoughMoney) => WellKnownError::NotEnoughMoney(message).into(),
×
296
                Ok(ErrorCode::OriginalPsbtRejected) =>
UNCOV
297
                    WellKnownError::OriginalPsbtRejected(message).into(),
×
298
                _ => Self::Unrecognized { error_code: error_code.to_string(), message },
1✔
299
            }
300
        } else {
301
            InternalValidationError::Parse.into()
2✔
302
        }
303
    }
5✔
304

305
    /// Parse a response from the receiver.
306
    ///
307
    /// response must be valid JSON string.
308
    pub fn parse(response: &str) -> Self {
4✔
309
        match serde_json::from_str(response) {
4✔
310
            Ok(json) => Self::from_json(json),
4✔
311
            Err(_) => InternalValidationError::Parse.into(),
×
312
        }
313
    }
4✔
314
}
315

316
impl std::error::Error for ResponseError {}
317

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

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

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

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

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

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

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

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

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

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

406
#[cfg(test)]
407
mod tests {
408
    use serde_json::json;
409

410
    use super::*;
411

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