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

payjoin / rust-payjoin / 18168527676

01 Oct 2025 04:16PM UTC coverage: 84.751% (+0.1%) from 84.647%
18168527676

push

github

web-flow
Update sender `process_res` to parse and process error response (#1114)

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

6 existing lines in 4 files now uncovered.

8620 of 10171 relevant lines covered (84.75%)

485.49 hits per line

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

41.0
/payjoin/src/core/send/error.rs
1
use std::fmt;
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
    #[cfg(feature = "v1")]
98
    ContentTooLarge,
99
    Proposal(InternalProposalError),
100
    #[cfg(feature = "v2")]
101
    V2Encapsulation(crate::send::v2::EncapsulationError),
102
}
103

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

274
impl std::error::Error for ResponseError {
275
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
276
        use ResponseError::*;
277

278
        match self {
×
279
            WellKnown(error) => Some(error),
×
280
            Validation(error) => Some(error),
×
281
            Unrecognized { .. } => None,
×
282
        }
283
    }
×
284
}
285

286
impl From<InternalValidationError> for ResponseError {
287
    fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
5✔
288
}
289

290
impl From<InternalProposalError> for ResponseError {
291
    fn from(value: InternalProposalError) -> Self {
×
292
        ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
×
293
    }
×
294
}
295

296
impl fmt::Display for ResponseError {
297
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
298
        match self {
1✔
299
            Self::WellKnown(e) => e.fmt(f),
1✔
300
            Self::Validation(e) => write!(f, "The receiver sent an invalid response: {e}"),
×
301

302
            // Do NOT display unrecognized errors to end users, only debug logs
303
            Self::Unrecognized { .. } => write!(f, "The receiver sent an unrecognized error."),
×
304
        }
305
    }
1✔
306
}
307

308
impl fmt::Debug for ResponseError {
309
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
310
        match self {
×
311
            Self::WellKnown(e) => {
×
312
                let json = serde_json::json!({
×
313
                    "errorCode": e.code.to_string(),
×
314
                    "message": e.message
×
315
                });
316
                write!(f, "Well known error: {json}")
×
317
            }
318
            Self::Validation(e) => write!(f, "Validation({e:?})"),
×
319

320
            Self::Unrecognized { error_code, message } => {
×
321
                let json = serde_json::json!({
×
322
                    "errorCode": error_code,
×
323
                    "message": message
×
324
                });
325
                write!(f, "Unrecognized error: {json}")
×
326
            }
327
        }
328
    }
×
329
}
330

331
impl ResponseError {
332
    pub(crate) fn from_json(json: serde_json::Value) -> Self {
6✔
333
        let message = json
6✔
334
            .as_object()
6✔
335
            .and_then(|v| v.get("message"))
6✔
336
            .and_then(|v| v.as_str())
6✔
337
            .unwrap_or_default()
6✔
338
            .to_string();
6✔
339

340
        let error_code = json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str());
6✔
341

342
        match error_code {
6✔
343
            Some(code) => match ErrorCode::from_str(code) {
4✔
344
                Ok(ErrorCode::VersionUnsupported) => {
345
                    let supported = json
2✔
346
                        .as_object()
2✔
347
                        .and_then(|v| v.get("supported"))
2✔
348
                        .and_then(|v| v.as_array())
2✔
349
                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
2✔
350
                        .unwrap_or_default();
2✔
351
                    WellKnownError::version_unsupported(message, supported).into()
2✔
352
                }
353
                Ok(code) => WellKnownError::new(code, message).into(),
1✔
354
                Err(_) => Self::Unrecognized { error_code: code.to_string(), message },
1✔
355
            },
356
            None => InternalValidationError::Parse.into(),
2✔
357
        }
358
    }
6✔
359
}
360

361
/// A well-known error that can be safely displayed to end users.
362
#[derive(Debug, Clone, PartialEq, Eq)]
363
pub struct WellKnownError {
364
    pub(crate) code: ErrorCode,
365
    pub(crate) message: String,
366
    pub(crate) supported_versions: Option<Vec<u64>>,
367
}
368

369
impl std::error::Error for WellKnownError {}
370

371
impl core::fmt::Display for WellKnownError {
372
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3✔
373
        match self.code {
3✔
374
            ErrorCode::Unavailable => write!(f, "The payjoin endpoint is not available for now."),
×
375
            ErrorCode::NotEnoughMoney => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."),
×
376
            ErrorCode::VersionUnsupported => {
377
                if let Some(supported) = &self.supported_versions {
1✔
378
                    write!(f, "This version of payjoin is not supported. Use version {supported:?}.")
1✔
379
                } else {
380
                    write!(f, "This version of payjoin is not supported.")
×
381
                }
382
            }
383
            ErrorCode::OriginalPsbtRejected => write!(f, "The receiver rejected the original PSBT."),
2✔
384
        }
385
    }
3✔
386
}
387

388
impl From<WellKnownError> for ResponseError {
389
    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
3✔
390
}
391

392
impl WellKnownError {
393
    /// Create a new well-known error with the given code and message.
394
    pub(crate) fn new(code: ErrorCode, message: String) -> Self {
1✔
395
        Self { code, message, supported_versions: None }
1✔
396
    }
1✔
397

398
    /// Create a version unsupported error with the given message and supported versions.
399
    pub(crate) fn version_unsupported(message: String, supported: Vec<u64>) -> Self {
2✔
400
        Self { code: ErrorCode::VersionUnsupported, message, supported_versions: Some(supported) }
2✔
401
    }
2✔
402
}
403

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

408
    use super::*;
409

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