diff --git a/payjoin-ffi/src/receive/error.rs b/payjoin-ffi/src/receive/error.rs index c3fbf8419..5ece66363 100644 --- a/payjoin-ffi/src/receive/error.rs +++ b/payjoin-ffi/src/receive/error.rs @@ -59,9 +59,15 @@ impl ReceiverCreateRequestError { #[derive(Debug, thiserror::Error, uniffi::Error)] #[error(transparent)] pub enum ReceiverPersistedError { - /// rust-payjoin receiver error + /// Transient rust-payjoin receiver error. No state transition was + /// persisted: the session remains in its previous state and the caller + /// may retry the same operation later. #[error(transparent)] - Receiver(ReceiverError), + Transient(ReceiverError), + /// Fatal rust-payjoin receiver error. The session was closed, or + /// transitioned to a terminal error state, and must not be retried. + #[error(transparent)] + Fatal(ReceiverError), /// Storage error that could occur at application storage layer #[error(transparent)] Storage(Arc), @@ -87,12 +93,18 @@ macro_rules! impl_persisted_error_from { if let Some(storage_err) = err.storage_error() { return ReceiverPersistedError::from(ImplementationError::new(storage_err)); } - return ReceiverPersistedError::Receiver(ReceiverError::Unexpected); + return ReceiverPersistedError::Fatal(ReceiverError::Unexpected); } + let transient = err.is_transient(); if let Some(api_err) = err.api_error() { - return ReceiverPersistedError::Receiver($receiver_arm(api_err)); + let receiver_error = $receiver_arm(api_err); + return if transient { + ReceiverPersistedError::Transient(receiver_error) + } else { + ReceiverPersistedError::Fatal(receiver_error) + }; } - ReceiverPersistedError::Receiver(ReceiverError::Unexpected) + ReceiverPersistedError::Fatal(ReceiverError::Unexpected) } } }; diff --git a/payjoin/src/core/persist.rs b/payjoin/src/core/persist.rs index 5961da6cb..26e43b3c7 100644 --- a/payjoin/src/core/persist.rs +++ b/payjoin/src/core/persist.rs @@ -760,6 +760,28 @@ where _ => None, } } + + /// Whether this error was transient. + /// + /// A transient error means no state transition was persisted: the session + /// remains in its previous state and the caller may retry the same + /// operation later. Returns `false` for storage errors; use + /// [`Self::storage_error_ref`] to distinguish those. + pub fn is_transient(&self) -> bool { + matches!(self.0, InternalPersistedError::Api(ApiError::Transient(_))) + } + + /// Whether this error was fatal. + /// + /// A fatal error means the session was closed, or transitioned to a + /// terminal error state, and must not be retried. Returns `false` for + /// storage errors; use [`Self::storage_error_ref`] to distinguish those. + pub fn is_fatal(&self) -> bool { + matches!( + self.0, + InternalPersistedError::Api(ApiError::Fatal(_) | ApiError::FatalWithState(_, _)) + ) + } } impl @@ -1592,6 +1614,8 @@ mod tests { ); assert!(storage_error.storage_error_ref().is_some()); assert!(storage_error.api_error_ref().is_none()); + assert!(!storage_error.is_transient()); + assert!(!storage_error.is_fatal()); // Test Internal API error cases let fatal_error = PersistedError::( @@ -1599,11 +1623,26 @@ mod tests { ); assert!(fatal_error.storage_error_ref().is_none()); assert!(fatal_error.api_error_ref().is_some()); + assert!(!fatal_error.is_transient()); + assert!(fatal_error.is_fatal()); let transient_error = PersistedError::( InternalPersistedError::Api(ApiError::Transient(api_err.clone())), ); assert!(transient_error.storage_error_ref().is_none()); assert!(transient_error.api_error_ref().is_some()); + assert!(transient_error.is_transient()); + assert!(!transient_error.is_fatal()); + + let fatal_with_state_error = PersistedError::( + InternalPersistedError::Api(ApiError::FatalWithState( + api_err.clone(), + "Error state".to_string(), + )), + ); + assert!(fatal_with_state_error.storage_error_ref().is_none()); + assert!(fatal_with_state_error.api_error_ref().is_some()); + assert!(!fatal_with_state_error.is_transient()); + assert!(fatal_with_state_error.is_fatal()); } }