use std::{error, fmt};
use crate::error_codes::ErrorCode::{
self, NotEnoughMoney, OriginalPsbtRejected, Unavailable, VersionUnsupported,
};
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Protocol(ProtocolError),
Implementation(crate::ImplementationError),
}
impl From<&Error> for JsonReply {
fn from(e: &Error) -> Self {
match e {
Error::Protocol(e) => e.into(),
Error::Implementation(_) => JsonReply::new(Unavailable, "Receiver error"),
}
}
}
impl From<ProtocolError> for Error {
fn from(e: ProtocolError) -> Self { Error::Protocol(e) }
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Protocol(e) => write!(f, "Protocol error: {e}"),
Error::Implementation(e) => write!(f, "Implementation error: {e}"),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::Protocol(e) => e.source(),
Error::Implementation(e) => e.source(),
}
}
}
#[derive(Debug)]
pub enum ProtocolError {
OriginalPayload(PayloadError),
#[cfg(feature = "v1")]
V1(crate::receive::v1::RequestError),
#[cfg(feature = "v2")]
V2(crate::receive::v2::SessionError),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct JsonReply {
error_code: ErrorCode,
message: String,
extra: serde_json::Map<String, serde_json::Value>,
}
impl JsonReply {
pub(crate) fn new(error_code: ErrorCode, message: impl fmt::Display) -> Self {
Self { error_code, message: message.to_string(), extra: serde_json::Map::new() }
}
pub fn with_extra(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
self.extra.insert(key.to_string(), value.into());
self
}
pub fn to_json(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert("errorCode".to_string(), self.error_code.to_string().into());
map.insert("message".to_string(), self.message.clone().into());
map.extend(self.extra.clone());
serde_json::Value::Object(map)
}
pub fn status_code(&self) -> u16 {
match self.error_code {
ErrorCode::Unavailable => http::StatusCode::INTERNAL_SERVER_ERROR,
ErrorCode::NotEnoughMoney
| ErrorCode::VersionUnsupported
| ErrorCode::OriginalPsbtRejected => http::StatusCode::BAD_REQUEST,
}
.as_u16()
}
}
impl From<&ProtocolError> for JsonReply {
fn from(e: &ProtocolError) -> Self {
use ProtocolError::*;
match e {
OriginalPayload(e) => e.into(),
#[cfg(feature = "v1")]
V1(e) => JsonReply::new(OriginalPsbtRejected, e),
#[cfg(feature = "v2")]
V2(_) => JsonReply::new(Unavailable, "Receiver error"),
}
}
}
impl fmt::Display for ProtocolError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
Self::OriginalPayload(e) => e.fmt(f),
#[cfg(feature = "v1")]
Self::V1(e) => e.fmt(f),
#[cfg(feature = "v2")]
Self::V2(e) => e.fmt(f),
}
}
}
impl error::Error for ProtocolError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self {
Self::OriginalPayload(e) => e.source(),
#[cfg(feature = "v1")]
Self::V1(e) => e.source(),
#[cfg(feature = "v2")]
Self::V2(e) => e.source(),
}
}
}
impl From<InternalPayloadError> for Error {
fn from(e: InternalPayloadError) -> Self {
Error::Protocol(ProtocolError::OriginalPayload(e.into()))
}
}
#[derive(Debug)]
pub struct PayloadError(pub(crate) InternalPayloadError);
impl From<InternalPayloadError> for PayloadError {
fn from(value: InternalPayloadError) -> Self { PayloadError(value) }
}
#[derive(Debug)]
pub(crate) enum InternalPayloadError {
Utf8(std::str::Utf8Error),
ParsePsbt(bitcoin::psbt::PsbtParseError),
SenderParams(super::optional_parameters::Error),
InconsistentPsbt(crate::psbt::InconsistentPsbt),
PrevTxOut(crate::psbt::PrevTxOutError),
MissingPayment,
OriginalPsbtNotBroadcastable,
#[allow(dead_code)]
InputOwned(bitcoin::ScriptBuf),
#[allow(dead_code)]
InputSeen(bitcoin::OutPoint),
PsbtBelowFeeRate(bitcoin::FeeRate, bitcoin::FeeRate),
FeeTooHigh(bitcoin::FeeRate, bitcoin::FeeRate),
}
impl From<&PayloadError> for JsonReply {
fn from(e: &PayloadError) -> Self {
use InternalPayloadError::*;
match &e.0 {
Utf8(_)
| ParsePsbt(_)
| InconsistentPsbt(_)
| PrevTxOut(_)
| MissingPayment
| OriginalPsbtNotBroadcastable
| InputOwned(_)
| InputSeen(_)
| PsbtBelowFeeRate(_, _) => JsonReply::new(OriginalPsbtRejected, e),
FeeTooHigh(_, _) => JsonReply::new(NotEnoughMoney, e),
SenderParams(e) => match e {
super::optional_parameters::Error::UnknownVersion { supported_versions } => {
let supported_versions_json =
serde_json::to_string(supported_versions).unwrap_or_default();
JsonReply::new(VersionUnsupported, "This version of payjoin is not supported.")
.with_extra("supported", supported_versions_json)
}
super::optional_parameters::Error::FeeRate =>
JsonReply::new(OriginalPsbtRejected, e),
},
}
}
}
impl fmt::Display for PayloadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) }
}
impl fmt::Display for InternalPayloadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use InternalPayloadError::*;
match &self {
Utf8(e) => write!(f, "{e}"),
ParsePsbt(e) => write!(f, "{e}"),
SenderParams(e) => write!(f, "{e}"),
InconsistentPsbt(e) => write!(f, "{e}"),
PrevTxOut(e) => write!(f, "PrevTxOut Error: {e}"),
MissingPayment => write!(f, "Missing payment."),
OriginalPsbtNotBroadcastable => write!(f, "Can't broadcast. PSBT rejected by mempool."),
InputOwned(_) => write!(f, "The receiver rejected the original PSBT."),
InputSeen(_) => write!(f, "The receiver rejected the original PSBT."),
PsbtBelowFeeRate(original_psbt_fee_rate, receiver_min_fee_rate) => write!(
f,
"Original PSBT fee rate too low: {original_psbt_fee_rate} < {receiver_min_fee_rate}."
),
FeeTooHigh(proposed_fee_rate, max_fee_rate) => write!(
f,
"Effective receiver feerate exceeds maximum allowed feerate: {proposed_fee_rate} > {max_fee_rate}"
),
}
}
}
impl std::error::Error for PayloadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use InternalPayloadError::*;
match &self.0 {
Utf8(e) => Some(e),
ParsePsbt(e) => Some(e),
SenderParams(e) => Some(e),
InconsistentPsbt(e) => Some(e),
PrevTxOut(e) => Some(e),
PsbtBelowFeeRate(_, _) => None,
FeeTooHigh(_, _) => None,
MissingPayment => None,
OriginalPsbtNotBroadcastable => None,
InputOwned(_) => None,
InputSeen(_) => None,
}
}
}
#[derive(Debug, PartialEq)]
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InternalOutputSubstitutionError {
DecreasedValueWhenDisabled,
ScriptPubKeyChangedWhenDisabled,
NotEnoughOutputs,
InvalidDrainScript,
}
impl fmt::Display for OutputSubstitutionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
InternalOutputSubstitutionError::DecreasedValueWhenDisabled => write!(f, "Decreasing the receiver output value is not allowed when output substitution is disabled"),
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => write!(f, "Changing the receiver output script pubkey is not allowed when output substitution is disabled"),
InternalOutputSubstitutionError::NotEnoughOutputs => write!(
f,
"Current output substitution implementation doesn't support reducing the number of outputs"
),
InternalOutputSubstitutionError::InvalidDrainScript =>
write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
}
}
}
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
}
impl std::error::Error for OutputSubstitutionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.0 {
InternalOutputSubstitutionError::DecreasedValueWhenDisabled => None,
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => None,
InternalOutputSubstitutionError::NotEnoughOutputs => None,
InternalOutputSubstitutionError::InvalidDrainScript => None,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct SelectionError(InternalSelectionError);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InternalSelectionError {
Empty,
UnsupportedOutputLength,
NotFound,
}
impl fmt::Display for SelectionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
InternalSelectionError::Empty => write!(f, "No candidates available for selection"),
InternalSelectionError::UnsupportedOutputLength => write!(
f,
"Current privacy selection implementation only supports 2-output transactions"
),
InternalSelectionError::NotFound =>
write!(f, "No selection candidates improve privacy"),
}
}
}
impl error::Error for SelectionError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use InternalSelectionError::*;
match &self.0 {
Empty => None,
UnsupportedOutputLength => None,
NotFound => None,
}
}
}
impl From<InternalSelectionError> for SelectionError {
fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
}
#[derive(Debug, PartialEq, Eq)]
pub struct InputContributionError(InternalInputContributionError);
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InternalInputContributionError {
ValueTooLow,
DuplicateInput(bitcoin::OutPoint),
}
impl fmt::Display for InputContributionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
InternalInputContributionError::ValueTooLow =>
write!(f, "Total input value is not enough to cover additional output value"),
InternalInputContributionError::DuplicateInput(outpoint) =>
write!(f, "Duplicate input detected: {outpoint}"),
}
}
}
impl error::Error for InputContributionError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.0 {
InternalInputContributionError::ValueTooLow => None,
InternalInputContributionError::DuplicateInput(_) => None,
}
}
}
impl From<InternalInputContributionError> for InputContributionError {
fn from(value: InternalInputContributionError) -> Self { InputContributionError(value) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ImplementationError;
#[test]
fn test_json_reply_from_implementation_error() {
struct AlwaysPanics;
impl fmt::Display for AlwaysPanics {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
panic!("internal error should never display when converting to JsonReply");
}
}
impl fmt::Debug for AlwaysPanics {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
panic!("internal error should never debug when converting to JsonReply");
}
}
impl error::Error for AlwaysPanics {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
panic!("internal error should never be examined when converting to JsonReply");
}
}
let internal = AlwaysPanics;
let error = Error::Implementation(ImplementationError::new(internal));
let reply = JsonReply::from(&error);
let expected = JsonReply {
error_code: ErrorCode::Unavailable,
message: "Receiver error".to_string(),
extra: serde_json::Map::new(),
};
assert_eq!(reply, expected);
let json = reply.to_json();
assert_eq!(
json,
serde_json::json!({
"errorCode": ErrorCode::Unavailable.to_string(),
"message": "Receiver error",
})
);
}
#[test]
fn test_json_reply_with_500_status_code() {
let error = Error::Implementation(ImplementationError::from("test error"));
let reply = JsonReply::from(&error);
assert_eq!(reply.status_code(), http::StatusCode::INTERNAL_SERVER_ERROR.as_u16());
let json = reply.to_json();
assert_eq!(json["errorCode"], "unavailable");
assert_eq!(json["message"], "Receiver error");
}
#[test]
fn test_json_reply_with_400_status_code() {
let payload_error = PayloadError(InternalPayloadError::MissingPayment);
let error = Error::Protocol(ProtocolError::OriginalPayload(payload_error));
let reply = JsonReply::from(&error);
assert_eq!(reply.status_code(), http::StatusCode::BAD_REQUEST.as_u16());
let json = reply.to_json();
assert_eq!(json["errorCode"], "original-psbt-rejected");
assert_eq!(json["message"], "Missing payment.");
}
}