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#[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#[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#[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
248pub enum ResponseError {
250 WellKnown(WellKnownError),
256
257 Validation(ValidationError),
259
260 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 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 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#[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 pub(crate) fn new(code: ErrorCode, message: String) -> Self {
381 Self { code, message, supported_versions: None }
382 }
383
384 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}