Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions payjoin-ffi/src/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,75 @@ impl From<FfiValidationError> for OutputSubstitutionError {
#[error(transparent)]
pub struct CoinSelectionError(#[from] receive::CoinSelectionError);

/// The category of a [`CoinSelectionError`].
///
/// Mirrors [`payjoin::receive::CoinSelectionErrorKind`], with an `Other`
/// catch-all so categories added upstream do not break bindings. Unrecognized
/// categories should be handled conservatively.
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum CoinSelectionErrorKind {
/// No candidates were available for selection.
Empty,
/// The transaction shape is not supported by the current selection
/// implementation. Retrying with different candidates will not help.
UnsupportedOutputLength,
/// No candidate improved privacy. A different candidate set may succeed.
NotFound,
/// A category this version of the bindings does not know about.
Other,
}

#[uniffi::export]
impl CoinSelectionError {
/// Returns the category of this error.
pub fn kind(&self) -> CoinSelectionErrorKind {
match self.0.kind() {
receive::CoinSelectionErrorKind::Empty => CoinSelectionErrorKind::Empty,
receive::CoinSelectionErrorKind::UnsupportedOutputLength =>
CoinSelectionErrorKind::UnsupportedOutputLength,
receive::CoinSelectionErrorKind::NotFound => CoinSelectionErrorKind::NotFound,
_ => CoinSelectionErrorKind::Other,
}
}
}

/// Error that may occur when input contribution fails.
#[derive(Debug, thiserror::Error, uniffi::Object)]
#[uniffi::export(Debug, Display)]
#[error(transparent)]
pub struct InputContributionError(#[from] receive::InputContributionError);

/// The category of an [`InputContributionError`].
///
/// Mirrors [`payjoin::receive::InputContributionErrorKind`], with an `Other`
/// catch-all so categories added upstream do not break bindings. Unrecognized
/// categories should be handled conservatively.
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum InputContributionErrorKind {
/// Total input value does not cover the additional output value. Removing
/// candidates cannot help; only higher-value candidates can.
ValueTooLow,
/// The selected input's outpoint is already present in the transaction.
/// Contribution may succeed with a different candidate.
DuplicateInput,
/// A category this version of the bindings does not know about.
Other,
}

#[uniffi::export]
impl InputContributionError {
/// Returns the category of this error.
pub fn kind(&self) -> InputContributionErrorKind {
match self.0.kind() {
receive::InputContributionErrorKind::ValueTooLow =>
InputContributionErrorKind::ValueTooLow,
receive::InputContributionErrorKind::DuplicateInput =>
InputContributionErrorKind::DuplicateInput,
_ => InputContributionErrorKind::Other,
}
}
}

/// Error validating a PSBT Input
#[derive(Debug, thiserror::Error, uniffi::Object)]
#[uniffi::export(Debug, Display)]
Expand Down
96 changes: 94 additions & 2 deletions payjoin/src/core/receive/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,42 @@ impl std::error::Error for OutputSubstitutionError {
/// Error that may occur when coin selection fails.
///
/// This is currently opaque type because we aren't sure which variants will stay.
/// You can only display it.
/// You can display it, or classify it with [`CoinSelectionError::kind`].
#[derive(Debug, PartialEq, Eq)]
pub struct CoinSelectionError(InternalCoinSelectionError);

/// The category of a [`CoinSelectionError`].
///
/// Unlike the error itself, the kind is a stable, matchable classification
/// intended to let callers decide how to react — in particular whether trying
/// again with a different candidate set could succeed. The enum is
/// non-exhaustive so categories can be added without breaking callers;
/// unrecognized categories should be handled conservatively.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoinSelectionErrorKind {
/// No candidates were available for selection. Selection can only succeed
/// once candidates exist.
Empty,
/// The transaction shape is not supported by the current selection
/// implementation. Retrying with different candidates will not help.
UnsupportedOutputLength,
/// No candidate improved privacy. A different candidate set may succeed.
NotFound,
}

impl CoinSelectionError {
/// Returns the category of this error.
pub fn kind(&self) -> CoinSelectionErrorKind {
match &self.0 {
InternalCoinSelectionError::Empty => CoinSelectionErrorKind::Empty,
InternalCoinSelectionError::UnsupportedOutputLength =>
CoinSelectionErrorKind::UnsupportedOutputLength,
InternalCoinSelectionError::NotFound => CoinSelectionErrorKind::NotFound,
}
}
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InternalCoinSelectionError {
/// No candidates available for selection
Expand Down Expand Up @@ -398,10 +430,39 @@ impl From<InternalCoinSelectionError> for CoinSelectionError {
/// Error that may occur when input contribution fails.
///
/// This is currently opaque type because we aren't sure which variants will stay.
/// You can only display it.
/// You can display it, or classify it with [`InputContributionError::kind`].
#[derive(Debug, PartialEq, Eq)]
pub struct InputContributionError(InternalInputContributionError);

/// The category of an [`InputContributionError`].
///
/// Unlike the error itself, the kind is a stable, matchable classification
/// intended to let callers decide how to react — in particular whether trying
/// again with a different candidate set could succeed. The enum is
/// non-exhaustive so categories can be added without breaking callers;
/// unrecognized categories should be handled conservatively.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InputContributionErrorKind {
/// Total input value does not cover the additional output value. Removing
/// candidates cannot help; only higher-value candidates can.
ValueTooLow,
/// The selected input's outpoint is already present in the transaction.
/// Contribution may succeed with a different candidate.
DuplicateInput,
}

impl InputContributionError {
/// Returns the category of this error.
pub fn kind(&self) -> InputContributionErrorKind {
match &self.0 {
InternalInputContributionError::ValueTooLow => InputContributionErrorKind::ValueTooLow,
InternalInputContributionError::DuplicateInput(_) =>
InputContributionErrorKind::DuplicateInput,
}
}
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InternalInputContributionError {
/// Total input value is not enough to cover additional output value
Expand Down Expand Up @@ -439,6 +500,37 @@ mod tests {
use super::*;
use crate::ImplementationError;

#[test]
fn test_coin_selection_error_kind_classifies_every_variant() {
assert_eq!(
CoinSelectionError::from(InternalCoinSelectionError::Empty).kind(),
CoinSelectionErrorKind::Empty
);
assert_eq!(
CoinSelectionError::from(InternalCoinSelectionError::UnsupportedOutputLength).kind(),
CoinSelectionErrorKind::UnsupportedOutputLength
);
assert_eq!(
CoinSelectionError::from(InternalCoinSelectionError::NotFound).kind(),
CoinSelectionErrorKind::NotFound
);
}

#[test]
fn test_input_contribution_error_kind_classifies_every_variant() {
assert_eq!(
InputContributionError::from(InternalInputContributionError::ValueTooLow).kind(),
InputContributionErrorKind::ValueTooLow
);
assert_eq!(
InputContributionError::from(InternalInputContributionError::DuplicateInput(
bitcoin::OutPoint::null()
))
.kind(),
InputContributionErrorKind::DuplicateInput
);
}

#[test]
fn test_json_reply_from_implementation_error() {
struct AlwaysPanics;
Expand Down
4 changes: 2 additions & 2 deletions payjoin/src/core/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use bitcoin::{
};
pub(crate) use error::InternalPayloadError;
pub use error::{
CoinSelectionError, Error, InputContributionError, JsonReply, OutputSubstitutionError,
PayloadError, ProtocolError,
CoinSelectionError, CoinSelectionErrorKind, Error, InputContributionError,
InputContributionErrorKind, JsonReply, OutputSubstitutionError, PayloadError, ProtocolError,
};
use optional_parameters::Params;
use serde::{Deserialize, Serialize};
Expand Down
Loading