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

payjoin / rust-payjoin / 12932809402

23 Jan 2025 03:44PM UTC coverage: 78.587% (+0.05%) from 78.535%
12932809402

Pull #505

github

web-flow
Merge 8f627c693 into dad5c1f82
Pull Request #505: Accomodate Updated BIP 78 Spec

3648 of 4642 relevant lines covered (78.59%)

990.78 hits per line

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

30.45
/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::{AddressType, Sequence};
6

7
/// Error building a Sender from a SenderBuilder.
8
///
9
/// This error is unrecoverable.
10
#[derive(Debug)]
11
pub struct BuildSenderError(InternalBuildSenderError);
12

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

30
impl From<InternalBuildSenderError> for BuildSenderError {
31
    fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
×
32
}
33

34
impl From<crate::psbt::AddressTypeError> for BuildSenderError {
35
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
36
        BuildSenderError(InternalBuildSenderError::AddressType(value))
×
37
    }
×
38
}
39

40
impl fmt::Display for BuildSenderError {
41
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
42
        use InternalBuildSenderError::*;
43

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

62
impl std::error::Error for BuildSenderError {
63
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
64
        use InternalBuildSenderError::*;
65

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

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

91
#[derive(Debug)]
92
pub(crate) enum InternalValidationError {
93
    Parse,
94
    Io(std::io::Error),
95
    Proposal(InternalProposalError),
96
    #[cfg(feature = "v2")]
97
    V2Encapsulation(crate::send::v2::EncapsulationError),
98
}
99

100
impl From<InternalValidationError> for ValidationError {
101
    fn from(value: InternalValidationError) -> Self { ValidationError(value) }
×
102
}
103

104
impl From<crate::psbt::AddressTypeError> for ValidationError {
105
    fn from(value: crate::psbt::AddressTypeError) -> Self {
×
106
        ValidationError(InternalValidationError::Proposal(
×
107
            InternalProposalError::InvalidAddressType(value),
×
108
        ))
×
109
    }
×
110
}
111

112
impl fmt::Display for ValidationError {
113
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
114
        use InternalValidationError::*;
115

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

126
impl std::error::Error for ValidationError {
127
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
128
        use InternalValidationError::*;
129

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

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

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

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

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

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

216
        match self {
×
217
            InvalidAddressType(error) => Some(error),
×
218
            NoInputs => None,
×
219
            PrevTxOut(error) => Some(error),
×
220
            InputWeight(error) => Some(error),
×
221
            VersionsDontMatch { proposed: _, original: _ } => None,
×
222
            LockTimesDontMatch { proposed: _, original: _ } => None,
×
223
            SenderTxinSequenceChanged { proposed: _, original: _ } => None,
×
224
            SenderTxinContainsFinalScriptSig => None,
×
225
            SenderTxinContainsFinalScriptWitness => None,
×
226
            TxInContainsKeyPaths => None,
×
227
            ContainsPartialSigs => None,
×
228
            ReceiverTxinNotFinalized => None,
×
229
            ReceiverTxinMissingUtxoInfo => None,
×
230
            MixedSequence => None,
×
231
            MixedInputTypes { .. } => 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 error_code {
3✔
285
                "version-unsupported" => {
3✔
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
                }
294
                "unavailable" => WellKnownError::Unavailable(message).into(),
1✔
295
                "not-enough-money" => WellKnownError::NotEnoughMoney(message).into(),
1✔
296
                "original-psbt-rejected" => WellKnownError::OriginalPsbtRejected(message).into(),
1✔
297
                _ => Self::Unrecognized { error_code: error_code.to_string(), message },
1✔
298
            }
299
        } else {
300
            InternalValidationError::Parse.into()
2✔
301
        }
302
    }
5✔
303

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

315
impl std::error::Error for ResponseError {}
316

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

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

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

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

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

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

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

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

372
impl WellKnownError {
373
    pub fn error_code(&self) -> &str {
1✔
374
        match self {
1✔
375
            WellKnownError::Unavailable(_) => "unavailable",
×
376
            WellKnownError::NotEnoughMoney(_) => "not-enough-money",
×
377
            WellKnownError::VersionUnsupported { .. } => "version-unsupported",
1✔
378
            WellKnownError::OriginalPsbtRejected(_) => "original-psbt-rejected",
×
379
        }
380
    }
1✔
381
    pub fn message(&self) -> &str {
1✔
382
        match self {
1✔
383
            WellKnownError::Unavailable(m) => m,
×
384
            WellKnownError::NotEnoughMoney(m) => m,
×
385
            WellKnownError::VersionUnsupported { message: m, .. } => m,
1✔
386
            WellKnownError::OriginalPsbtRejected(m) => m,
×
387
        }
388
    }
1✔
389
}
390

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

402
#[cfg(test)]
403
mod tests {
404
    use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::json;
405

406
    use super::*;
407

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