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

payjoin / rust-payjoin / 12968704432

25 Jan 2025 09:48PM UTC coverage: 78.528% (-0.1%) from 78.641%
12968704432

push

github

web-flow
Produce `receive::JsonError` accurately so that `send` can properly handle it (#506)

- [Isolate receive::JsonError from
fmt::Display](https://github.com/payjoin/rust-payjoin/commit/ffe6281f6)
- [Reject bad v1 requests as
original-psbt-rejected](https://github.com/payjoin/rust-payjoin/commit/9a323ef9e)
since that's the only way v1 senders can really address their issue.
It's a payload error but that's the same as Original PSBT [payload] in
BIP 78 parlance.

This change also introduces `const` values for well known error
codes for both `send` and `receive` to share to prevent slipped typos
during maintenance.

22 of 91 new or added lines in 5 files covered. (24.18%)

3 existing lines in 1 file now uncovered.

3650 of 4648 relevant lines covered (78.53%)

988.98 hits per line

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

30.59
/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

11
/// Error building a Sender from a SenderBuilder.
12
///
13
/// This error is unrecoverable.
14
#[derive(Debug)]
15
pub struct BuildSenderError(InternalBuildSenderError);
16

17
#[derive(Debug)]
18
pub(crate) enum InternalBuildSenderError {
19
    InvalidOriginalInput(crate::psbt::PsbtInputsError),
20
    InconsistentOriginalPsbt(crate::psbt::InconsistentPsbt),
21
    NoInputs,
22
    PayeeValueNotEqual,
23
    NoOutputs,
24
    MultiplePayeeOutputs,
25
    MissingPayeeOutput,
26
    FeeOutputValueLowerThanFeeContribution,
27
    AmbiguousChangeOutput,
28
    ChangeIndexOutOfBounds,
29
    ChangeIndexPointsAtPayee,
30
    InputWeight(crate::psbt::InputWeightError),
31
    AddressType(crate::psbt::AddressTypeError),
32
}
33

34
impl From<InternalBuildSenderError> for BuildSenderError {
35
    fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
×
36
}
37

38
impl From<crate::psbt::AddressTypeError> for BuildSenderError {
39
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
40
        BuildSenderError(InternalBuildSenderError::AddressType(value))
×
41
    }
×
42
}
43

44
impl fmt::Display for BuildSenderError {
45
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
46
        use InternalBuildSenderError::*;
47

48
        match &self.0 {
×
49
            InvalidOriginalInput(e) => write!(f, "an input in the original transaction is invalid: {:#?}", e),
×
50
            InconsistentOriginalPsbt(e) => write!(f, "the original transaction is inconsistent: {:#?}", e),
×
51
            NoInputs => write!(f, "the original transaction has no inputs"),
×
52
            PayeeValueNotEqual => write!(f, "the value in original transaction doesn't equal value requested in the payment link"),
×
53
            NoOutputs => write!(f, "the original transaction has no outputs"),
×
54
            MultiplePayeeOutputs => write!(f, "the original transaction has more than one output belonging to the payee"),
×
55
            MissingPayeeOutput => write!(f, "the output belonging to payee is missing from the original transaction"),
×
56
            FeeOutputValueLowerThanFeeContribution => write!(f, "the value of fee output is lower than maximum allowed contribution"),
×
57
            AmbiguousChangeOutput => write!(f, "can not determine which output is change because there's more than two outputs"),
×
58
            ChangeIndexOutOfBounds => write!(f, "fee output index is points out of bounds"),
×
59
            ChangeIndexPointsAtPayee => write!(f, "fee output index is points at output belonging to the payee"),
×
60
            AddressType(e) => write!(f, "can not determine input address type: {}", e),
×
61
            InputWeight(e) => write!(f, "can not determine expected input weight: {}", e),
×
62
        }
63
    }
×
64
}
65

66
impl std::error::Error for BuildSenderError {
67
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
68
        use InternalBuildSenderError::*;
69

70
        match &self.0 {
×
71
            InvalidOriginalInput(error) => Some(error),
×
72
            InconsistentOriginalPsbt(error) => Some(error),
×
73
            NoInputs => None,
×
74
            PayeeValueNotEqual => None,
×
75
            NoOutputs => None,
×
76
            MultiplePayeeOutputs => None,
×
77
            MissingPayeeOutput => None,
×
78
            FeeOutputValueLowerThanFeeContribution => None,
×
79
            AmbiguousChangeOutput => None,
×
80
            ChangeIndexOutOfBounds => None,
×
81
            ChangeIndexPointsAtPayee => None,
×
82
            AddressType(error) => Some(error),
×
83
            InputWeight(error) => Some(error),
×
84
        }
85
    }
×
86
}
87

88
/// Error that may occur when the response from receiver is malformed.
89
///
90
/// This is currently opaque type because we aren't sure which variants will stay.
91
/// You can only display it.
92
#[derive(Debug)]
93
pub struct ValidationError(InternalValidationError);
94

95
#[derive(Debug)]
96
pub(crate) enum InternalValidationError {
97
    Parse,
98
    Io(std::io::Error),
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) }
×
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 {
×
118
        use InternalValidationError::*;
119

120
        match &self.0 {
×
121
            Parse => write!(f, "couldn't decode as PSBT or JSON",),
×
122
            Io(e) => write!(f, "couldn't read PSBT: {}", e),
×
123
            Proposal(e) => write!(f, "proposal PSBT error: {}", e),
×
124
            #[cfg(feature = "v2")]
125
            V2Encapsulation(e) => write!(f, "v2 encapsulation error: {}", e),
×
126
        }
127
    }
×
128
}
129

130
impl std::error::Error for ValidationError {
131
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
132
        use InternalValidationError::*;
133

134
        match &self.0 {
×
135
            Parse => None,
×
136
            Io(error) => Some(error),
×
137
            Proposal(e) => Some(e),
×
138
            #[cfg(feature = "v2")]
139
            V2Encapsulation(e) => Some(e),
×
140
        }
141
    }
×
142
}
143

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

174
impl From<crate::psbt::AddressTypeError> for InternalProposalError {
175
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
176
        InternalProposalError::InvalidAddressType(value)
×
177
    }
×
178
}
179

180
impl fmt::Display for InternalProposalError {
181
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
182
        use InternalProposalError::*;
183

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

214
impl std::error::Error for InternalProposalError {
215
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
216
        use InternalProposalError::*;
217

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

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

257
    /// Errors caused by malformed responses.
258
    Validation(ValidationError),
259

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

269
impl ResponseError {
270
    fn from_json(json: serde_json::Value) -> Self {
5✔
271
        // we try to find the errorCode field and
5✔
272
        // if it exists we try to parse it as a well known error
5✔
273
        // if its an unknown error we return the error code and message
5✔
274
        // from original response
5✔
275
        // if errorCode field doesn't exist we return parse error
5✔
276
        let message = json
5✔
277
            .as_object()
5✔
278
            .and_then(|v| v.get("message"))
5✔
279
            .and_then(|v| v.as_str())
5✔
280
            .unwrap_or_default()
5✔
281
            .to_string();
5✔
282
        if let Some(error_code) =
3✔
283
            json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str())
5✔
284
        {
285
            match error_code {
3✔
286
                code if code == VERSION_UNSUPPORTED => {
3✔
287
                    let supported = json
2✔
288
                        .as_object()
2✔
289
                        .and_then(|v| v.get("supported"))
2✔
290
                        .and_then(|v| v.as_array())
2✔
291
                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
2✔
292
                        .unwrap_or_default();
2✔
293
                    WellKnownError::VersionUnsupported { message, supported }.into()
2✔
294
                }
295
                code if code == UNAVAILABLE => WellKnownError::Unavailable(message).into(),
1✔
296
                code if code == NOT_ENOUGH_MONEY => WellKnownError::NotEnoughMoney(message).into(),
1✔
297
                code if code == ORIGINAL_PSBT_REJECTED =>
1✔
NEW
298
                    WellKnownError::OriginalPsbtRejected(message).into(),
×
299
                _ => Self::Unrecognized { error_code: error_code.to_string(), message },
1✔
300
            }
301
        } else {
302
            InternalValidationError::Parse.into()
2✔
303
        }
304
    }
5✔
305

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

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

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

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

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

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

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

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

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

366
#[derive(Debug, Clone, PartialEq, Eq)]
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) -> &str {
1✔
376
        match self {
1✔
NEW
377
            WellKnownError::Unavailable(_) => UNAVAILABLE,
×
NEW
378
            WellKnownError::NotEnoughMoney(_) => NOT_ENOUGH_MONEY,
×
379
            WellKnownError::VersionUnsupported { .. } => VERSION_UNSUPPORTED,
1✔
NEW
380
            WellKnownError::OriginalPsbtRejected(_) => ORIGINAL_PSBT_REJECTED,
×
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(), "version-unsupported");
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