payjoin 0.25.0

Payjoin Library implementing BIP 78 and BIP 77 batching protocols.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
use std::fmt;
use std::str::FromStr;

use bitcoin::locktime::absolute::LockTime;
use bitcoin::transaction::Version;
use bitcoin::Sequence;

use crate::error_codes::ErrorCode;

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

#[derive(Debug, PartialEq)]
pub(crate) enum InternalBuildSenderError {
    InvalidOriginalInput(crate::psbt::PsbtInputsError),
    InconsistentOriginalPsbt(crate::psbt::InconsistentPsbt),
    NoInputs,
    PayeeValueNotEqual,
    NoOutputs,
    MultiplePayeeOutputs,
    MissingPayeeOutput,
    FeeOutputValueLowerThanFeeContribution,
    AmbiguousChangeOutput,
    ChangeIndexOutOfBounds,
    ChangeIndexPointsAtPayee,
    InputWeight(crate::psbt::InputWeightError),
    AddressType(crate::psbt::AddressTypeError),
}

impl From<InternalBuildSenderError> for BuildSenderError {
    fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
}

impl From<crate::psbt::AddressTypeError> for BuildSenderError {
    fn from(value: crate::psbt::AddressTypeError) -> Self {
        BuildSenderError(InternalBuildSenderError::AddressType(value))
    }
}

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

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

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

        match &self.0 {
            InvalidOriginalInput(error) => Some(error),
            InconsistentOriginalPsbt(error) => Some(error),
            NoInputs => None,
            PayeeValueNotEqual => None,
            NoOutputs => None,
            MultiplePayeeOutputs => None,
            MissingPayeeOutput => None,
            FeeOutputValueLowerThanFeeContribution => None,
            AmbiguousChangeOutput => None,
            ChangeIndexOutOfBounds => None,
            ChangeIndexPointsAtPayee => None,
            AddressType(error) => Some(error),
            InputWeight(error) => Some(error),
        }
    }
}

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

#[derive(Debug)]
pub(crate) enum InternalValidationError {
    Parse,
    #[cfg(feature = "v1")]
    ContentTooLarge,
    Proposal(InternalProposalError),
    #[cfg(feature = "v2")]
    V2Encapsulation(crate::send::v2::EncapsulationError),
}

impl From<InternalValidationError> for ValidationError {
    fn from(value: InternalValidationError) -> Self { ValidationError(value) }
}

impl From<crate::psbt::AddressTypeError> for ValidationError {
    fn from(value: crate::psbt::AddressTypeError) -> Self {
        ValidationError(InternalValidationError::Proposal(
            InternalProposalError::InvalidAddressType(value),
        ))
    }
}

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

        match &self.0 {
            Parse => write!(f, "couldn't decode as PSBT or JSON",),
            #[cfg(feature = "v1")]
            ContentTooLarge => {
                use crate::MAX_CONTENT_LENGTH;
                write!(f, "content is larger than {MAX_CONTENT_LENGTH} bytes")
            }
            Proposal(e) => write!(f, "proposal PSBT error: {e}"),
            #[cfg(feature = "v2")]
            V2Encapsulation(e) => write!(f, "v2 encapsulation error: {e}"),
        }
    }
}

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

        match &self.0 {
            Parse => None,
            #[cfg(feature = "v1")]
            ContentTooLarge => None,
            Proposal(e) => Some(e),
            #[cfg(feature = "v2")]
            V2Encapsulation(e) => Some(e),
        }
    }
}

/// Error that may occur when the proposal PSBT from receiver is malformed.
#[derive(Debug)]
pub(crate) enum InternalProposalError {
    InvalidAddressType(crate::psbt::AddressTypeError),
    NoInputs,
    PrevTxOut(crate::psbt::PrevTxOutError),
    InputWeight(crate::psbt::InputWeightError),
    VersionsDontMatch { proposed: Version, original: Version },
    LockTimesDontMatch { proposed: LockTime, original: LockTime },
    SenderTxinSequenceChanged { proposed: Sequence, original: Sequence },
    SenderTxinContainsFinalScriptSig,
    SenderTxinContainsFinalScriptWitness,
    TxInContainsKeyPaths,
    ContainsPartialSigs,
    ReceiverTxinNotFinalized,
    ReceiverTxinMissingUtxoInfo,
    MixedSequence,
    MissingOrShuffledInputs,
    TxOutContainsKeyPaths,
    FeeContributionExceedsMaximum,
    DisallowedOutputSubstitution,
    OutputValueDecreased,
    MissingOrShuffledOutputs,
    AbsoluteFeeDecreased,
    PayeeTookContributedFee,
    FeeContributionPaysOutputSizeIncrease,
    FeeRateBelowMinimum,
    Psbt(bitcoin::psbt::Error),
}

impl From<crate::psbt::AddressTypeError> for InternalProposalError {
    fn from(value: crate::psbt::AddressTypeError) -> Self {
        InternalProposalError::InvalidAddressType(value)
    }
}

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

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

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

        match self {
            InvalidAddressType(error) => Some(error),
            NoInputs => None,
            PrevTxOut(error) => Some(error),
            InputWeight(error) => Some(error),
            VersionsDontMatch { proposed: _, original: _ } => None,
            LockTimesDontMatch { proposed: _, original: _ } => None,
            SenderTxinSequenceChanged { proposed: _, original: _ } => None,
            SenderTxinContainsFinalScriptSig => None,
            SenderTxinContainsFinalScriptWitness => None,
            TxInContainsKeyPaths => None,
            ContainsPartialSigs => None,
            ReceiverTxinNotFinalized => None,
            ReceiverTxinMissingUtxoInfo => None,
            MixedSequence => None,
            MissingOrShuffledInputs => None,
            TxOutContainsKeyPaths => None,
            FeeContributionExceedsMaximum => None,
            DisallowedOutputSubstitution => None,
            OutputValueDecreased => None,
            MissingOrShuffledOutputs => None,
            AbsoluteFeeDecreased => None,
            PayeeTookContributedFee => None,
            FeeContributionPaysOutputSizeIncrease => None,
            FeeRateBelowMinimum => None,
            Psbt(error) => Some(error),
        }
    }
}

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

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

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

impl std::error::Error for ResponseError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        use ResponseError::*;

        match self {
            WellKnown(error) => Some(error),
            Validation(error) => Some(error),
            Unrecognized { .. } => None,
        }
    }
}

impl From<InternalValidationError> for ResponseError {
    fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
}

impl From<InternalProposalError> for ResponseError {
    fn from(value: InternalProposalError) -> Self {
        ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
    }
}

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

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

impl fmt::Debug for ResponseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::WellKnown(e) => {
                let json = serde_json::json!({
                    "errorCode": e.code.to_string(),
                    "message": e.message
                });
                write!(f, "Well known error: {json}")
            }
            Self::Validation(e) => write!(f, "Validation({e:?})"),

            Self::Unrecognized { error_code, message } => {
                let json = serde_json::json!({
                    "errorCode": error_code,
                    "message": message
                });
                write!(f, "Unrecognized error: {json}")
            }
        }
    }
}

impl ResponseError {
    pub(crate) fn from_json(json: serde_json::Value) -> Self {
        let message = json
            .as_object()
            .and_then(|v| v.get("message"))
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string();

        let error_code = json.as_object().and_then(|v| v.get("errorCode")).and_then(|v| v.as_str());

        match error_code {
            Some(code) => match ErrorCode::from_str(code) {
                Ok(ErrorCode::VersionUnsupported) => {
                    let supported = json
                        .as_object()
                        .and_then(|v| v.get("supported"))
                        .and_then(|v| v.as_array())
                        .map(|array| array.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>())
                        .unwrap_or_default();
                    WellKnownError::version_unsupported(message, supported).into()
                }
                Ok(code) => WellKnownError::new(code, message).into(),
                Err(_) => Self::Unrecognized { error_code: code.to_string(), message },
            },
            None => InternalValidationError::Parse.into(),
        }
    }
}

/// A well-known error that can be safely displayed to end users.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WellKnownError {
    pub(crate) code: ErrorCode,
    pub(crate) message: String,
    pub(crate) supported_versions: Option<Vec<u64>>,
}

impl std::error::Error for WellKnownError {}

impl core::fmt::Display for WellKnownError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self.code {
            ErrorCode::Unavailable => write!(f, "The payjoin endpoint is not available for now."),
            ErrorCode::NotEnoughMoney => write!(f, "The receiver added some inputs but could not bump the fee of the payjoin proposal."),
            ErrorCode::VersionUnsupported => {
                if let Some(supported) = &self.supported_versions {
                    write!(f, "This version of payjoin is not supported. Use version {supported:?}.")
                } else {
                    write!(f, "This version of payjoin is not supported.")
                }
            }
            ErrorCode::OriginalPsbtRejected => write!(f, "The receiver rejected the original PSBT."),
        }
    }
}

impl From<WellKnownError> for ResponseError {
    fn from(value: WellKnownError) -> Self { Self::WellKnown(value) }
}

impl WellKnownError {
    /// Create a new well-known error with the given code and message.
    pub(crate) fn new(code: ErrorCode, message: String) -> Self {
        Self { code, message, supported_versions: None }
    }

    /// Create a version unsupported error with the given message and supported versions.
    pub(crate) fn version_unsupported(message: String, supported: Vec<u64>) -> Self {
        Self { code: ErrorCode::VersionUnsupported, message, supported_versions: Some(supported) }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn test_parse_json() {
        let known_str_error = r#"{"errorCode":"version-unsupported", "message":"custom message here", "supported": [1, 2]}"#;
        match ResponseError::parse(known_str_error) {
            ResponseError::WellKnown(e) => {
                assert_eq!(e.code, ErrorCode::VersionUnsupported);
                assert_eq!(e.message, "custom message here");
                assert_eq!(
                    e.to_string(),
                    "This version of payjoin is not supported. Use version [1, 2]."
                );
            }
            _ => panic!("Expected WellKnown error"),
        };
        let unrecognized_error = r#"{"errorCode":"random", "message":"random"}"#;
        assert!(matches!(
            ResponseError::parse(unrecognized_error),
            ResponseError::Unrecognized { .. }
        ));
        let invalid_json_error = json!({
            "err": "random",
            "message": "This version of payjoin is not supported."
        });
        assert!(matches!(
            ResponseError::from_json(invalid_json_error),
            ResponseError::Validation(_)
        ));
    }
}