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

payjoin / rust-payjoin / 14092300312

26 Mar 2025 07:56PM UTC coverage: 81.419% (-0.03%) from 81.45%
14092300312

Pull #606

github

web-flow
Merge 7de044fd6 into ded33f19f
Pull Request #606: Reply json

36 of 76 new or added lines in 5 files covered. (47.37%)

1 existing line in 1 file now uncovered.

5105 of 6270 relevant lines covered (81.42%)

737.25 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 {
×
347
            Self::WellKnown(e) => write!(
×
348
                f,
×
349
                r#"Well known error: {{ "errorCode": "{}",
×
350
                "message": "{}" }}"#,
×
351
                e.error_code(),
×
352
                e.message()
×
353
            ),
×
354
            Self::Validation(e) => write!(f, "Validation({:?})", e),
×
355

356
            Self::Unrecognized { error_code, message } => write!(
×
357
                f,
×
358
                r#"Unrecognized error: {{ "errorCode": "{}", "message": "{}" }}"#,
×
359
                error_code, message
×
360
            ),
×
361
        }
362
    }
×
363
}
364

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

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

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

404
#[cfg(test)]
405
mod tests {
406
    use bitcoind::bitcoincore_rpc::jsonrpc::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.error_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]."
1✔
420
                );
1✔
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
        });
1✔
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