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

payjoin / rust-payjoin / 14088935960

26 Mar 2025 04:50PM UTC coverage: 81.159% (-0.3%) from 81.45%
14088935960

Pull #552

github

web-flow
Merge ac39945cc into ded33f19f
Pull Request #552: Persistable payjoin types

122 of 164 new or added lines in 8 files covered. (74.39%)

4 existing lines in 1 file now uncovered.

5182 of 6385 relevant lines covered (81.16%)

724.18 hits per line

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

31.14
/payjoin/src/send/error.rs
1
use std::fmt::{self, Display};
2

3
use bitcoin::locktime::absolute::LockTime;
4
use bitcoin::transaction::Version;
5
use bitcoin::Sequence;
6

7
use crate::error_codes::{
8
    NOT_ENOUGH_MONEY, ORIGINAL_PSBT_REJECTED, UNAVAILABLE, VERSION_UNSUPPORTED,
9
};
10
#[cfg(feature = "v2")]
11
#[derive(Debug)]
12
pub struct ErrorBox(Box<dyn std::error::Error + Send + Sync>);
13

14
#[cfg(feature = "v2")]
15
impl ErrorBox {
NEW
16
    pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
×
NEW
17
        Self(Box::new(error))
×
NEW
18
    }
×
19
}
20

21
#[cfg(feature = "v2")]
22
impl PartialEq for ErrorBox {
NEW
23
    fn eq(&self, other: &Self) -> bool { self.0.to_string() == other.0.to_string() }
×
24
}
25

26
#[cfg(feature = "v2")]
27
impl fmt::Display for ErrorBox {
NEW
28
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) }
×
29
}
30

31
#[cfg(feature = "v2")]
32
impl std::error::Error for ErrorBox {}
33

34
/// Error building a Sender from a SenderBuilder.
35
///
36
/// This error is unrecoverable.
37
#[derive(Debug)]
38
pub struct BuildSenderError(InternalBuildSenderError);
39

40
#[derive(Debug, PartialEq)]
41
pub(crate) enum InternalBuildSenderError {
42
    InvalidOriginalInput(crate::psbt::PsbtInputsError),
43
    InconsistentOriginalPsbt(crate::psbt::InconsistentPsbt),
44
    NoInputs,
45
    PayeeValueNotEqual,
46
    NoOutputs,
47
    MultiplePayeeOutputs,
48
    MissingPayeeOutput,
49
    FeeOutputValueLowerThanFeeContribution,
50
    AmbiguousChangeOutput,
51
    ChangeIndexOutOfBounds,
52
    ChangeIndexPointsAtPayee,
53
    InputWeight(crate::psbt::InputWeightError),
54
    AddressType(crate::psbt::AddressTypeError),
55
    #[cfg(feature = "v2")]
56
    FailedToPersistSender(ErrorBox),
57
    #[cfg(feature = "v2")]
58
    FailedToLoadSender(ErrorBox),
59
}
60

61
impl From<InternalBuildSenderError> for BuildSenderError {
62
    fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
×
63
}
64

65
impl From<crate::psbt::AddressTypeError> for BuildSenderError {
66
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
67
        BuildSenderError(InternalBuildSenderError::AddressType(value))
×
68
    }
×
69
}
70

71
impl fmt::Display for BuildSenderError {
72
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
73
        use InternalBuildSenderError::*;
74

75
        match &self.0 {
×
76
            InvalidOriginalInput(e) => write!(f, "an input in the original transaction is invalid: {:#?}", e),
×
77
            InconsistentOriginalPsbt(e) => write!(f, "the original transaction is inconsistent: {:#?}", e),
×
78
            NoInputs => write!(f, "the original transaction has no inputs"),
×
79
            PayeeValueNotEqual => write!(f, "the value in original transaction doesn't equal value requested in the payment link"),
×
80
            NoOutputs => write!(f, "the original transaction has no outputs"),
×
81
            MultiplePayeeOutputs => write!(f, "the original transaction has more than one output belonging to the payee"),
×
82
            MissingPayeeOutput => write!(f, "the output belonging to payee is missing from the original transaction"),
×
83
            FeeOutputValueLowerThanFeeContribution => write!(f, "the value of fee output is lower than maximum allowed contribution"),
×
84
            AmbiguousChangeOutput => write!(f, "can not determine which output is change because there's more than two outputs"),
×
85
            ChangeIndexOutOfBounds => write!(f, "fee output index is points out of bounds"),
×
86
            ChangeIndexPointsAtPayee => write!(f, "fee output index is points at output belonging to the payee"),
×
87
            AddressType(e) => write!(f, "can not determine input address type: {}", e),
×
88
            InputWeight(e) => write!(f, "can not determine expected input weight: {}", e),
×
89
            #[cfg(feature = "v2")]
NEW
90
            FailedToPersistSender(e) => write!(f, "failed to persist sender: {:#?}", e),
×
91
            #[cfg(feature = "v2")]
NEW
92
            FailedToLoadSender(e) => write!(f, "failed to load sender: {:#?}", e),
×
93
        }
94
    }
×
95
}
96

97
impl std::error::Error for BuildSenderError {
98
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
99
        use InternalBuildSenderError::*;
100

101
        match &self.0 {
×
102
            InvalidOriginalInput(error) => Some(error),
×
103
            InconsistentOriginalPsbt(error) => Some(error),
×
104
            NoInputs => None,
×
105
            PayeeValueNotEqual => None,
×
106
            NoOutputs => None,
×
107
            MultiplePayeeOutputs => None,
×
108
            MissingPayeeOutput => None,
×
109
            FeeOutputValueLowerThanFeeContribution => None,
×
110
            AmbiguousChangeOutput => None,
×
111
            ChangeIndexOutOfBounds => None,
×
112
            ChangeIndexPointsAtPayee => None,
×
113
            AddressType(error) => Some(error),
×
114
            InputWeight(error) => Some(error),
×
115
            #[cfg(feature = "v2")]
NEW
116
            FailedToPersistSender(e) => e.0.source(),
×
117
            #[cfg(feature = "v2")]
NEW
118
            FailedToLoadSender(e) => e.0.source(),
×
119
        }
120
    }
×
121
}
122

123
/// Error that may occur when the response from receiver is malformed.
124
///
125
/// This is currently opaque type because we aren't sure which variants will stay.
126
/// You can only display it.
127
#[derive(Debug)]
128
pub struct ValidationError(InternalValidationError);
129

130
#[derive(Debug)]
131
pub(crate) enum InternalValidationError {
132
    Parse,
133
    Io(std::io::Error),
134
    Proposal(InternalProposalError),
135
    #[cfg(feature = "v2")]
136
    V2Encapsulation(crate::send::v2::EncapsulationError),
137
}
138

139
impl From<InternalValidationError> for ValidationError {
140
    fn from(value: InternalValidationError) -> Self { ValidationError(value) }
×
141
}
142

143
impl From<crate::psbt::AddressTypeError> for ValidationError {
144
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
145
        ValidationError(InternalValidationError::Proposal(
×
146
            InternalProposalError::InvalidAddressType(value),
×
147
        ))
×
148
    }
×
149
}
150

151
impl fmt::Display for ValidationError {
152
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
153
        use InternalValidationError::*;
154

155
        match &self.0 {
×
156
            Parse => write!(f, "couldn't decode as PSBT or JSON",),
×
157
            Io(e) => write!(f, "couldn't read PSBT: {}", e),
×
158
            Proposal(e) => write!(f, "proposal PSBT error: {}", e),
×
159
            #[cfg(feature = "v2")]
160
            V2Encapsulation(e) => write!(f, "v2 encapsulation error: {}", e),
×
161
        }
162
    }
×
163
}
164

165
impl std::error::Error for ValidationError {
166
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
167
        use InternalValidationError::*;
168

169
        match &self.0 {
×
170
            Parse => None,
×
171
            Io(error) => Some(error),
×
172
            Proposal(e) => Some(e),
×
173
            #[cfg(feature = "v2")]
174
            V2Encapsulation(e) => Some(e),
×
175
        }
176
    }
×
177
}
178

179
/// Error that may occur when the proposal PSBT from receiver is malformed.
180
#[derive(Debug)]
181
pub(crate) enum InternalProposalError {
182
    InvalidAddressType(crate::psbt::AddressTypeError),
183
    NoInputs,
184
    PrevTxOut(crate::psbt::PrevTxOutError),
185
    InputWeight(crate::psbt::InputWeightError),
186
    VersionsDontMatch { proposed: Version, original: Version },
187
    LockTimesDontMatch { proposed: LockTime, original: LockTime },
188
    SenderTxinSequenceChanged { proposed: Sequence, original: Sequence },
189
    SenderTxinContainsFinalScriptSig,
190
    SenderTxinContainsFinalScriptWitness,
191
    TxInContainsKeyPaths,
192
    ContainsPartialSigs,
193
    ReceiverTxinNotFinalized,
194
    ReceiverTxinMissingUtxoInfo,
195
    MixedSequence,
196
    MissingOrShuffledInputs,
197
    TxOutContainsKeyPaths,
198
    FeeContributionExceedsMaximum,
199
    DisallowedOutputSubstitution,
200
    OutputValueDecreased,
201
    MissingOrShuffledOutputs,
202
    AbsoluteFeeDecreased,
203
    PayeeTookContributedFee,
204
    FeeContributionPaysOutputSizeIncrease,
205
    FeeRateBelowMinimum,
206
    Psbt(bitcoin::psbt::Error),
207
}
208

209
impl From<crate::psbt::AddressTypeError> for InternalProposalError {
210
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
211
        InternalProposalError::InvalidAddressType(value)
×
212
    }
×
213
}
214

215
impl fmt::Display for InternalProposalError {
216
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2✔
217
        use InternalProposalError::*;
218

219
        match &self {
2✔
220
            InvalidAddressType(e) => write!(f, "invalid input address type: {}", e),
×
221
            NoInputs => write!(f, "PSBT doesn't have any inputs"),
×
222
            PrevTxOut(e) => write!(f, "missing previous txout information: {}", e),
×
223
            InputWeight(e) => write!(f, "can not determine expected input weight: {}", e),
×
224
            VersionsDontMatch { proposed, original, } => write!(f, "proposed transaction version {} doesn't match the original {}", proposed, original),
×
225
            LockTimesDontMatch { proposed, original, } => write!(f, "proposed transaction lock time {} doesn't match the original {}", proposed, original),
×
226
            SenderTxinSequenceChanged { proposed, original, } => write!(f, "proposed transaction sequence number {} doesn't match the original {}", proposed, original),
×
227
            SenderTxinContainsFinalScriptSig => write!(f, "an input in proposed transaction belonging to the sender contains finalized non-witness signature"),
×
228
            SenderTxinContainsFinalScriptWitness => write!(f, "an input in proposed transaction belonging to the sender contains finalized witness signature"),
×
229
            TxInContainsKeyPaths => write!(f, "proposed transaction inputs contain key paths"),
×
230
            ContainsPartialSigs => write!(f, "an input in proposed transaction belonging to the sender contains partial signatures"),
×
231
            ReceiverTxinNotFinalized => write!(f, "an input in proposed transaction belonging to the receiver is not finalized"),
×
232
            ReceiverTxinMissingUtxoInfo => write!(f, "an input in proposed transaction belonging to the receiver is missing UTXO information"),
×
233
            MixedSequence => write!(f, "inputs of proposed transaction contain mixed sequence numbers"),
×
234
            MissingOrShuffledInputs => write!(f, "proposed transaction is missing inputs of the sender or they are shuffled"),
×
235
            TxOutContainsKeyPaths => write!(f, "proposed transaction outputs contain key paths"),
×
236
            FeeContributionExceedsMaximum => write!(f, "fee contribution exceeds allowed maximum"),
2✔
237
            DisallowedOutputSubstitution => write!(f, "the receiver change output despite it being disallowed"),
×
238
            OutputValueDecreased => write!(f, "the amount in our non-fee output was decreased"),
×
239
            MissingOrShuffledOutputs => write!(f, "proposed transaction is missing outputs of the sender or they are shuffled"),
×
240
            AbsoluteFeeDecreased => write!(f, "abslute fee of proposed transaction is lower than original"),
×
241
            PayeeTookContributedFee => write!(f, "payee tried to take fee contribution for himself"),
×
242
            FeeContributionPaysOutputSizeIncrease => write!(f, "fee contribution pays for additional outputs"),
×
243
            FeeRateBelowMinimum =>  write!(f, "the fee rate of proposed transaction is below minimum"),
×
244
            Psbt(e) => write!(f, "psbt error: {}", e),
×
245
        }
246
    }
2✔
247
}
248

249
impl std::error::Error for InternalProposalError {
250
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
251
        use InternalProposalError::*;
252

253
        match self {
×
254
            InvalidAddressType(error) => Some(error),
×
255
            NoInputs => None,
×
256
            PrevTxOut(error) => Some(error),
×
257
            InputWeight(error) => Some(error),
×
258
            VersionsDontMatch { proposed: _, original: _ } => None,
×
259
            LockTimesDontMatch { proposed: _, original: _ } => None,
×
260
            SenderTxinSequenceChanged { proposed: _, original: _ } => None,
×
261
            SenderTxinContainsFinalScriptSig => None,
×
262
            SenderTxinContainsFinalScriptWitness => None,
×
263
            TxInContainsKeyPaths => None,
×
264
            ContainsPartialSigs => None,
×
265
            ReceiverTxinNotFinalized => None,
×
266
            ReceiverTxinMissingUtxoInfo => None,
×
267
            MixedSequence => None,
×
268
            MissingOrShuffledInputs => None,
×
269
            TxOutContainsKeyPaths => None,
×
270
            FeeContributionExceedsMaximum => None,
×
271
            DisallowedOutputSubstitution => None,
×
272
            OutputValueDecreased => None,
×
273
            MissingOrShuffledOutputs => None,
×
274
            AbsoluteFeeDecreased => None,
×
275
            PayeeTookContributedFee => None,
×
276
            FeeContributionPaysOutputSizeIncrease => None,
×
277
            FeeRateBelowMinimum => None,
×
278
            Psbt(error) => Some(error),
×
279
        }
280
    }
×
281
}
282

283
/// Represent an error returned by Payjoin receiver.
284
pub enum ResponseError {
285
    /// `WellKnown` Errors are defined in the [`BIP78::ReceiverWellKnownError`] spec.
286
    ///
287
    /// It is safe to display `WellKnown` errors to end users.
288
    ///
289
    /// [`BIP78::ReceiverWellKnownError`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors
290
    WellKnown(WellKnownError),
291

292
    /// Errors caused by malformed responses.
293
    Validation(ValidationError),
294

295
    /// `Unrecognized` Errors are NOT defined in the [`BIP78::ReceiverWellKnownError`] spec.
296
    ///
297
    /// It is NOT safe to display `Unrecognized` errors to end users as they could be used
298
    /// maliciously to phish a non technical user. Only display them in debug logs.
299
    ///
300
    /// [`BIP78::ReceiverWellKnownError`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors
301
    Unrecognized { error_code: String, message: String },
302
}
303

304
impl ResponseError {
305
    fn from_json(json: serde_json::Value) -> Self {
5✔
306
        // we try to find the errorCode field and
5✔
307
        // if it exists we try to parse it as a well known error
5✔
308
        // if its an unknown error we return the error code and message
5✔
309
        // from original response
5✔
310
        // if errorCode field doesn't exist we return parse error
5✔
311
        let message = json
5✔
312
            .as_object()
5✔
313
            .and_then(|v| v.get("message"))
5✔
314
            .and_then(|v| v.as_str())
5✔
315
            .unwrap_or_default()
5✔
316
            .to_string();
5✔
317
        if let Some(error_code) =
3✔
318
            json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str())
5✔
319
        {
320
            match error_code {
3✔
321
                code if code == VERSION_UNSUPPORTED => {
3✔
322
                    let supported = json
2✔
323
                        .as_object()
2✔
324
                        .and_then(|v| v.get("supported"))
2✔
325
                        .and_then(|v| v.as_array())
2✔
326
                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
2✔
327
                        .unwrap_or_default();
2✔
328
                    WellKnownError::VersionUnsupported { message, supported }.into()
2✔
329
                }
330
                code if code == UNAVAILABLE => WellKnownError::Unavailable(message).into(),
1✔
331
                code if code == NOT_ENOUGH_MONEY => WellKnownError::NotEnoughMoney(message).into(),
1✔
332
                code if code == ORIGINAL_PSBT_REJECTED =>
1✔
333
                    WellKnownError::OriginalPsbtRejected(message).into(),
×
334
                _ => Self::Unrecognized { error_code: error_code.to_string(), message },
1✔
335
            }
336
        } else {
337
            InternalValidationError::Parse.into()
2✔
338
        }
339
    }
5✔
340

341
    /// Parse a response from the receiver.
342
    ///
343
    /// response must be valid JSON string.
344
    pub fn parse(response: &str) -> Self {
4✔
345
        match serde_json::from_str(response) {
4✔
346
            Ok(json) => Self::from_json(json),
4✔
347
            Err(_) => InternalValidationError::Parse.into(),
×
348
        }
349
    }
4✔
350
}
351

352
impl std::error::Error for ResponseError {}
353

354
impl From<WellKnownError> for ResponseError {
355
    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
2✔
356
}
357

358
impl From<InternalValidationError> for ResponseError {
359
    fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
2✔
360
}
361

362
impl From<InternalProposalError> for ResponseError {
363
    fn from(value: InternalProposalError) -> Self {
×
364
        ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
×
365
    }
×
366
}
367

368
impl Display for ResponseError {
369
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
370
        match self {
×
371
            Self::WellKnown(e) => e.fmt(f),
×
372
            Self::Validation(e) => write!(f, "The receiver sent an invalid response: {}", e),
×
373

374
            // Do NOT display unrecognized errors to end users, only debug logs
375
            Self::Unrecognized { .. } => write!(f, "The receiver sent an unrecognized error."),
×
376
        }
377
    }
×
378
}
379

380
impl fmt::Debug for ResponseError {
381
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
382
        match self {
×
383
            Self::WellKnown(e) => write!(
×
384
                f,
×
385
                r#"Well known error: {{ "errorCode": "{}",
×
386
                "message": "{}" }}"#,
×
387
                e.error_code(),
×
388
                e.message()
×
389
            ),
×
390
            Self::Validation(e) => write!(f, "Validation({:?})", e),
×
391

392
            Self::Unrecognized { error_code, message } => write!(
×
393
                f,
×
394
                r#"Unrecognized error: {{ "errorCode": "{}", "message": "{}" }}"#,
×
395
                error_code, message
×
396
            ),
×
397
        }
398
    }
×
399
}
400

401
#[derive(Debug, Clone, PartialEq, Eq)]
402
#[non_exhaustive]
403
pub enum WellKnownError {
404
    Unavailable(String),
405
    NotEnoughMoney(String),
406
    VersionUnsupported { message: String, supported: Vec<u64> },
407
    OriginalPsbtRejected(String),
408
}
409

410
impl WellKnownError {
411
    pub fn error_code(&self) -> &str {
1✔
412
        match self {
1✔
413
            WellKnownError::Unavailable(_) => UNAVAILABLE,
×
414
            WellKnownError::NotEnoughMoney(_) => NOT_ENOUGH_MONEY,
×
415
            WellKnownError::VersionUnsupported { .. } => VERSION_UNSUPPORTED,
1✔
416
            WellKnownError::OriginalPsbtRejected(_) => ORIGINAL_PSBT_REJECTED,
×
417
        }
418
    }
1✔
419
    pub fn message(&self) -> &str {
1✔
420
        match self {
1✔
421
            WellKnownError::Unavailable(m) => m,
×
422
            WellKnownError::NotEnoughMoney(m) => m,
×
423
            WellKnownError::VersionUnsupported { message: m, .. } => m,
1✔
424
            WellKnownError::OriginalPsbtRejected(m) => m,
×
425
        }
426
    }
1✔
427
}
428

429
impl Display for WellKnownError {
430
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1✔
431
        match self {
1✔
432
            Self::Unavailable(_) => write!(f, "The payjoin endpoint is not available for now."),
×
433
            Self::NotEnoughMoney(_) => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."),
×
434
            Self::VersionUnsupported { supported: v, .. }=> write!(f, "This version of payjoin is not supported. Use version {:?}.", v),
1✔
435
            Self::OriginalPsbtRejected(_) => write!(f, "The receiver rejected the original PSBT."),
×
436
        }
437
    }
1✔
438
}
439

440
#[cfg(test)]
441
mod tests {
442
    use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::json;
443

444
    use super::*;
445

446
    #[test]
447
    fn test_parse_json() {
1✔
448
        let known_str_error = r#"{"errorCode":"version-unsupported", "message":"custom message here", "supported": [1, 2]}"#;
1✔
449
        match ResponseError::parse(known_str_error) {
1✔
450
            ResponseError::WellKnown(e) => {
1✔
451
                assert_eq!(e.error_code(), "version-unsupported");
1✔
452
                assert_eq!(e.message(), "custom message here");
1✔
453
                assert_eq!(
1✔
454
                    e.to_string(),
1✔
455
                    "This version of payjoin is not supported. Use version [1, 2]."
1✔
456
                );
1✔
457
            }
458
            _ => panic!("Expected WellKnown error"),
×
459
        };
460
        let unrecognized_error = r#"{"errorCode":"random", "message":"random"}"#;
1✔
461
        assert!(matches!(
1✔
462
            ResponseError::parse(unrecognized_error),
1✔
463
            ResponseError::Unrecognized { .. }
464
        ));
465
        let invalid_json_error = json!({
1✔
466
            "err": "random",
1✔
467
            "message": "This version of payjoin is not supported."
1✔
468
        });
1✔
469
        assert!(matches!(
1✔
470
            ResponseError::from_json(invalid_json_error),
1✔
471
            ResponseError::Validation(_)
472
        ));
473
    }
1✔
474
}
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