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

payjoin / rust-payjoin / 14117620655

27 Mar 2025 10:08PM UTC coverage: 81.699% (+0.2%) from 81.45%
14117620655

Pull #606

github

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

74 of 111 new or added lines in 7 files covered. (66.67%)

5 existing lines in 3 files now uncovered.

5107 of 6251 relevant lines covered (81.7%)

739.49 hits per line

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

27.23
/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, WellKnownError};
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
    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
                }
NEW
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

310
impl From<WellKnownError> for ResponseError {
311
    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
2✔
312
}
313

314
impl From<InternalValidationError> for ResponseError {
315
    fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
2✔
316
}
317

318
impl From<InternalProposalError> for ResponseError {
319
    fn from(value: InternalProposalError) -> Self {
×
320
        ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
×
321
    }
×
322
}
323

324
impl Display for ResponseError {
325
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
326
        match self {
×
327
            Self::WellKnown(e) => e.fmt(f),
×
328
            Self::Validation(e) => write!(f, "The receiver sent an invalid response: {}", e),
×
329

330
            // Do NOT display unrecognized errors to end users, only debug logs
331
            Self::Unrecognized { .. } => write!(f, "The receiver sent an unrecognized error."),
×
332
        }
333
    }
×
334
}
335

336
impl fmt::Debug for ResponseError {
337
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
338
        match self {
×
NEW
339
            Self::WellKnown(e) => {
×
NEW
340
                let json = serde_json::json!({
×
NEW
341
                    "errorCode": e.code.to_string(),
×
NEW
342
                    "message": e.message
×
NEW
343
                });
×
NEW
344
                write!(f, "Well known error: {}", json)
×
345
            }
UNCOV
346
            Self::Validation(e) => write!(f, "Validation({:?})", e),
×
347

NEW
348
            Self::Unrecognized { error_code, message } => {
×
NEW
349
                let json = serde_json::json!({
×
NEW
350
                    "errorCode": error_code,
×
NEW
351
                    "message": message
×
NEW
352
                });
×
NEW
353
                write!(f, "Unrecognized error: {}", json)
×
354
            }
355
        }
UNCOV
356
    }
×
357
}
358

359
#[cfg(test)]
360
mod tests {
361
    use serde_json::json;
362

363
    use super::*;
364

365
    #[test]
366
    fn test_parse_json() {
1✔
367
        let known_str_error = r#"{"errorCode":"version-unsupported", "message":"custom message here", "supported": [1, 2]}"#;
1✔
368
        match ResponseError::parse(known_str_error) {
1✔
369
            ResponseError::WellKnown(e) => {
1✔
370
                assert_eq!(e.code, ErrorCode::VersionUnsupported);
1✔
371
                assert_eq!(e.message, "custom message here");
1✔
372
                assert_eq!(
1✔
373
                    e.to_string(),
1✔
374
                    "This version of payjoin is not supported. Use version [1, 2]."
1✔
375
                );
1✔
376
            }
377
            _ => panic!("Expected WellKnown error"),
×
378
        };
379
        let unrecognized_error = r#"{"errorCode":"random", "message":"random"}"#;
1✔
380
        assert!(matches!(
1✔
381
            ResponseError::parse(unrecognized_error),
1✔
382
            ResponseError::Unrecognized { .. }
383
        ));
384
        let invalid_json_error = json!({
1✔
385
            "err": "random",
1✔
386
            "message": "This version of payjoin is not supported."
1✔
387
        });
1✔
388
        assert!(matches!(
1✔
389
            ResponseError::from_json(invalid_json_error),
1✔
390
            ResponseError::Validation(_)
391
        ));
392
    }
1✔
393
}
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