payjoin/core/send/
error.rs

1use std::fmt::{self, Display};
2use std::str::FromStr;
3
4use bitcoin::locktime::absolute::LockTime;
5use bitcoin::transaction::Version;
6use bitcoin::Sequence;
7
8use crate::error_codes::ErrorCode;
9use crate::MAX_CONTENT_LENGTH;
10
11/// Error building a Sender from a SenderBuilder.
12///
13/// This error is unrecoverable.
14#[derive(Debug)]
15pub struct BuildSenderError(InternalBuildSenderError);
16
17#[derive(Debug, PartialEq)]
18pub(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
34impl From<InternalBuildSenderError> for BuildSenderError {
35    fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
36}
37
38impl From<crate::psbt::AddressTypeError> for BuildSenderError {
39    fn from(value: crate::psbt::AddressTypeError) -> Self {
40        BuildSenderError(InternalBuildSenderError::AddressType(value))
41    }
42}
43
44impl 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
66impl 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)]
93pub struct ValidationError(InternalValidationError);
94
95#[derive(Debug)]
96pub(crate) enum InternalValidationError {
97    Parse,
98    ContentTooLarge,
99    Proposal(InternalProposalError),
100    #[cfg(feature = "v2")]
101    V2Encapsulation(crate::send::v2::EncapsulationError),
102}
103
104impl From<InternalValidationError> for ValidationError {
105    fn from(value: InternalValidationError) -> Self { ValidationError(value) }
106}
107
108impl 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
116impl 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            ContentTooLarge => write!(f, "content is larger than {MAX_CONTENT_LENGTH} bytes"),
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
130impl 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            ContentTooLarge => None,
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)]
146pub(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
174impl From<crate::psbt::AddressTypeError> for InternalProposalError {
175    fn from(value: crate::psbt::AddressTypeError) -> Self {
176        InternalProposalError::InvalidAddressType(value)
177    }
178}
179
180impl 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 {proposed} doesn't match the original {original}"),
190            LockTimesDontMatch { proposed, original, } => write!(f, "proposed transaction lock time {proposed} doesn't match the original {original}"),
191            SenderTxinSequenceChanged { proposed, original, } => write!(f, "proposed transaction sequence number {proposed} doesn't match the original {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
214impl 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.
249pub 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
269impl ResponseError {
270    pub(crate) fn from_json(json: serde_json::Value) -> Self {
271        let message = json
272            .as_object()
273            .and_then(|v| v.get("message"))
274            .and_then(|v| v.as_str())
275            .unwrap_or_default()
276            .to_string();
277
278        let error_code = json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str());
279
280        match error_code {
281            Some(code) => match ErrorCode::from_str(code) {
282                Ok(ErrorCode::VersionUnsupported) => {
283                    let supported = json
284                        .as_object()
285                        .and_then(|v| v.get("supported"))
286                        .and_then(|v| v.as_array())
287                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
288                        .unwrap_or_default();
289                    WellKnownError::version_unsupported(message, supported).into()
290                }
291                Ok(code) => WellKnownError::new(code, message).into(),
292                Err(_) => Self::Unrecognized { error_code: code.to_string(), message },
293            },
294            None => InternalValidationError::Parse.into(),
295        }
296    }
297
298    /// Parse a response from the receiver.
299    ///
300    /// response must be valid JSON string.
301    pub(crate) fn parse(response: &str) -> Self {
302        match serde_json::from_str(response) {
303            Ok(json) => Self::from_json(json),
304            Err(_) => InternalValidationError::Parse.into(),
305        }
306    }
307}
308
309impl std::error::Error for ResponseError {
310    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
311        use ResponseError::*;
312
313        match self {
314            WellKnown(error) => Some(error),
315            Validation(error) => Some(error),
316            Unrecognized { .. } => None,
317        }
318    }
319}
320
321impl From<WellKnownError> for ResponseError {
322    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
323}
324
325impl From<InternalValidationError> for ResponseError {
326    fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
327}
328
329impl From<InternalProposalError> for ResponseError {
330    fn from(value: InternalProposalError) -> Self {
331        ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
332    }
333}
334
335impl Display for ResponseError {
336    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
337        match self {
338            Self::WellKnown(e) => e.fmt(f),
339            Self::Validation(e) => write!(f, "The receiver sent an invalid response: {e}"),
340
341            // Do NOT display unrecognized errors to end users, only debug logs
342            Self::Unrecognized { .. } => write!(f, "The receiver sent an unrecognized error."),
343        }
344    }
345}
346
347impl fmt::Debug for ResponseError {
348    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
349        match self {
350            Self::WellKnown(e) => {
351                let json = serde_json::json!({
352                    "errorCode": e.code.to_string(),
353                    "message": e.message
354                });
355                write!(f, "Well known error: {json}")
356            }
357            Self::Validation(e) => write!(f, "Validation({e:?})"),
358
359            Self::Unrecognized { error_code, message } => {
360                let json = serde_json::json!({
361                    "errorCode": error_code,
362                    "message": message
363                });
364                write!(f, "Unrecognized error: {json}")
365            }
366        }
367    }
368}
369
370/// A well-known error that can be safely displayed to end users.
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct WellKnownError {
373    pub(crate) code: ErrorCode,
374    pub(crate) message: String,
375    pub(crate) supported_versions: Option<Vec<u64>>,
376}
377
378impl WellKnownError {
379    /// Create a new well-known error with the given code and message.
380    pub(crate) fn new(code: ErrorCode, message: String) -> Self {
381        Self { code, message, supported_versions: None }
382    }
383
384    /// Create a version unsupported error with the given message and supported versions.
385    pub(crate) fn version_unsupported(message: String, supported: Vec<u64>) -> Self {
386        Self { code: ErrorCode::VersionUnsupported, message, supported_versions: Some(supported) }
387    }
388}
389
390impl std::error::Error for WellKnownError {}
391
392impl core::fmt::Display for WellKnownError {
393    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
394        match self.code {
395            ErrorCode::Unavailable => write!(f, "The payjoin endpoint is not available for now."),
396            ErrorCode::NotEnoughMoney => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."),
397            ErrorCode::VersionUnsupported => {
398                if let Some(supported) = &self.supported_versions {
399                    write!(f, "This version of payjoin is not supported. Use version {supported:?}.")
400                } else {
401                    write!(f, "This version of payjoin is not supported.")
402                }
403            }
404            ErrorCode::OriginalPsbtRejected => write!(f, "The receiver rejected the original PSBT."),
405        }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use serde_json::json;
412
413    use super::*;
414
415    #[test]
416    fn test_parse_json() {
417        let known_str_error = r#"{"errorCode":"version-unsupported", "message":"custom message here", "supported": [1, 2]}"#;
418        match ResponseError::parse(known_str_error) {
419            ResponseError::WellKnown(e) => {
420                assert_eq!(e.code, ErrorCode::VersionUnsupported);
421                assert_eq!(e.message, "custom message here");
422                assert_eq!(
423                    e.to_string(),
424                    "This version of payjoin is not supported. Use version [1, 2]."
425                );
426            }
427            _ => panic!("Expected WellKnown error"),
428        };
429        let unrecognized_error = r#"{"errorCode":"random", "message":"random"}"#;
430        assert!(matches!(
431            ResponseError::parse(unrecognized_error),
432            ResponseError::Unrecognized { .. }
433        ));
434        let invalid_json_error = json!({
435            "err": "random",
436            "message": "This version of payjoin is not supported."
437        });
438        assert!(matches!(
439            ResponseError::from_json(invalid_json_error),
440            ResponseError::Validation(_)
441        ));
442    }
443}