Skip to content

Commit 387094f

Browse files
committed
Preserve Persisted Error Classification
Expose persisted and replay error classification in core so the FFI can preserve storage, transient, fatal, and fatal-with-state semantics. This keeps sender and receiver bindings from losing recovery guidance at the persistence boundary, and it snapshots replay failures with stable kind and detail accessors. The receiver save helpers now retain replyable error state instead of flattening it into implementation errors, which lets bindings continue the protocol after fatal-with-state failures.
1 parent 332755b commit 387094f

7 files changed

Lines changed: 764 additions & 72 deletions

File tree

payjoin-ffi/src/error.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ impl From<ImplementationError> for payjoin::ImplementationError {
1717
fn from(value: ImplementationError) -> Self { value.0 }
1818
}
1919

20+
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
21+
pub enum ReplayErrorKind {
22+
NoEvents,
23+
InvalidEvent,
24+
Expired,
25+
PersistenceFailure,
26+
}
27+
28+
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
29+
pub enum ReplayInvalidEventKind {
30+
InitialEvent,
31+
SessionTransition,
32+
}
33+
2034
#[derive(Debug, thiserror::Error, uniffi::Object)]
2135
#[error("Error de/serializing JSON object: {0}")]
2236
pub struct SerdeJsonError(#[from] serde_json::Error);

payjoin-ffi/src/receive/error.rs

Lines changed: 307 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
use std::sync::Arc;
22

3+
use payjoin::error::ReplayErrorVariant as CoreReplayErrorVariant;
4+
use payjoin::persist::PersistedErrorVariant;
35
use payjoin::receive;
46

5-
use crate::error::{FfiValidationError, ImplementationError};
7+
use crate::error::{
8+
FfiValidationError, ImplementationError, ReplayErrorKind as FfiReplayErrorKind,
9+
ReplayInvalidEventKind as FfiReplayInvalidEventKind,
10+
};
11+
use crate::receive::HasReplyableError;
612
use crate::uri::error::IntoUrlError;
713

814
/// The top-level error type for the payjoin receiver
@@ -40,20 +46,36 @@ impl From<receive::Error> for ReceiverError {
4046

4147
/// Error that may occur during state machine transitions
4248
#[derive(Debug, thiserror::Error, uniffi::Error)]
43-
#[error(transparent)]
4449
pub enum ReceiverPersistedError {
45-
/// rust-payjoin receiver error
46-
#[error(transparent)]
47-
Receiver(ReceiverError),
48-
/// Storage error that could occur at application storage layer
50+
/// Storage failure while persisting session state.
4951
#[error(transparent)]
5052
Storage(Arc<ImplementationError>),
53+
/// Retry the same receiver transition from the current session state.
54+
#[error("Transient receiver error: {error}")]
55+
Transient { error: ReceiverError },
56+
/// The receiver session terminated and should not be resumed.
57+
#[error("Fatal receiver error: {error}")]
58+
Fatal { error: ReceiverError },
59+
/// The receiver transitioned into a replyable error state.
60+
///
61+
/// Continue the protocol with the returned `state` to reply to the sender.
62+
#[error("Fatal receiver error with state: {error}")]
63+
FatalWithState { error: ReceiverError, state: Arc<HasReplyableError> },
64+
/// Unexpected error shape that should not occur for receiver transitions.
65+
#[error("An unexpected error occurred")]
66+
Unexpected,
5167
}
5268

5369
impl From<ImplementationError> for ReceiverPersistedError {
5470
fn from(value: ImplementationError) -> Self { ReceiverPersistedError::Storage(Arc::new(value)) }
5571
}
5672

73+
impl From<crate::error::ForeignError> for ReceiverPersistedError {
74+
fn from(value: crate::error::ForeignError) -> Self {
75+
ReceiverPersistedError::from(ImplementationError::new(value))
76+
}
77+
}
78+
5779
macro_rules! impl_persisted_error_from {
5880
(
5981
$api_error_ty:ty,
@@ -64,16 +86,16 @@ macro_rules! impl_persisted_error_from {
6486
S: std::error::Error + Send + Sync + 'static,
6587
{
6688
fn from(err: payjoin::persist::PersistedError<$api_error_ty, S>) -> Self {
67-
if err.storage_error_ref().is_some() {
68-
if let Some(storage_err) = err.storage_error() {
69-
return ReceiverPersistedError::from(ImplementationError::new(storage_err));
70-
}
71-
return ReceiverPersistedError::Receiver(ReceiverError::Unexpected);
72-
}
73-
if let Some(api_err) = err.api_error() {
74-
return ReceiverPersistedError::Receiver($receiver_arm(api_err));
89+
match err.into_variant() {
90+
PersistedErrorVariant::Storage(storage_err) =>
91+
ReceiverPersistedError::from(ImplementationError::new(storage_err)),
92+
PersistedErrorVariant::Transient(api_err) =>
93+
ReceiverPersistedError::Transient { error: $receiver_arm(api_err) },
94+
PersistedErrorVariant::Fatal(api_err) =>
95+
ReceiverPersistedError::Fatal { error: $receiver_arm(api_err) },
96+
PersistedErrorVariant::FatalWithState(_, _) =>
97+
ReceiverPersistedError::Unexpected,
7598
}
76-
ReceiverPersistedError::Receiver(ReceiverError::Unexpected)
7799
}
78100
}
79101
};
@@ -89,6 +111,47 @@ impl_persisted_error_from!(payjoin::IntoUrlError, |api_err: payjoin::IntoUrlErro
89111
ReceiverError::IntoUrl(Arc::new(api_err.into()))
90112
});
91113

114+
impl_persisted_error_from!(
115+
payjoin::ImplementationError,
116+
|api_err: payjoin::ImplementationError| {
117+
ReceiverError::Implementation(Arc::new(api_err.into()))
118+
}
119+
);
120+
121+
impl<S>
122+
From<
123+
payjoin::persist::PersistedError<
124+
receive::Error,
125+
S,
126+
payjoin::receive::v2::Receiver<payjoin::receive::v2::HasReplyableError>,
127+
>,
128+
> for ReceiverPersistedError
129+
where
130+
S: std::error::Error + Send + Sync + 'static,
131+
{
132+
fn from(
133+
err: payjoin::persist::PersistedError<
134+
receive::Error,
135+
S,
136+
payjoin::receive::v2::Receiver<payjoin::receive::v2::HasReplyableError>,
137+
>,
138+
) -> Self {
139+
match err.into_variant() {
140+
PersistedErrorVariant::Storage(storage_err) =>
141+
ReceiverPersistedError::from(ImplementationError::new(storage_err)),
142+
PersistedErrorVariant::Transient(api_err) =>
143+
ReceiverPersistedError::Transient { error: api_err.into() },
144+
PersistedErrorVariant::Fatal(api_err) =>
145+
ReceiverPersistedError::Fatal { error: api_err.into() },
146+
PersistedErrorVariant::FatalWithState(api_err, state) =>
147+
ReceiverPersistedError::FatalWithState {
148+
error: api_err.into(),
149+
state: Arc::new(state.into()),
150+
},
151+
}
152+
}
153+
}
154+
92155
/// Error that may occur when building a receiver session.
93156
#[derive(Debug, thiserror::Error, uniffi::Error)]
94157
#[non_exhaustive]
@@ -231,9 +294,233 @@ impl From<FfiValidationError> for InputPairError {
231294
fn from(value: FfiValidationError) -> Self { InputPairError::FfiValidation(value) }
232295
}
233296

234-
/// Error that may occur when a receiver event log is replayed
297+
/// Error that may occur when a receiver event log is replayed.
235298
#[derive(Debug, thiserror::Error, uniffi::Object)]
236-
#[error(transparent)]
237-
pub struct ReceiverReplayError(
238-
#[from] payjoin::error::ReplayError<receive::v2::ReceiveSession, receive::v2::SessionEvent>,
239-
);
299+
#[error("{message}")]
300+
pub struct ReceiverReplayError {
301+
kind: FfiReplayErrorKind,
302+
message: String,
303+
invalid_event_kind: Option<FfiReplayInvalidEventKind>,
304+
expired_at_unix_seconds: Option<u32>,
305+
persistence_failure: Option<Arc<ImplementationError>>,
306+
}
307+
308+
impl From<payjoin::error::ReplayError<receive::v2::ReceiveSession, receive::v2::SessionEvent>>
309+
for ReceiverReplayError
310+
{
311+
fn from(
312+
value: payjoin::error::ReplayError<receive::v2::ReceiveSession, receive::v2::SessionEvent>,
313+
) -> Self {
314+
let message = value.to_string();
315+
match value.into_variant() {
316+
CoreReplayErrorVariant::NoEvents => Self {
317+
kind: FfiReplayErrorKind::NoEvents,
318+
message,
319+
invalid_event_kind: None,
320+
expired_at_unix_seconds: None,
321+
persistence_failure: None,
322+
},
323+
CoreReplayErrorVariant::InvalidFirstEvent => Self {
324+
kind: FfiReplayErrorKind::InvalidEvent,
325+
message,
326+
invalid_event_kind: Some(FfiReplayInvalidEventKind::InitialEvent),
327+
expired_at_unix_seconds: None,
328+
persistence_failure: None,
329+
},
330+
CoreReplayErrorVariant::InvalidEventForState => Self {
331+
kind: FfiReplayErrorKind::InvalidEvent,
332+
message,
333+
invalid_event_kind: Some(FfiReplayInvalidEventKind::SessionTransition),
334+
expired_at_unix_seconds: None,
335+
persistence_failure: None,
336+
},
337+
CoreReplayErrorVariant::Expired { expired_at_unix_seconds } => Self {
338+
kind: FfiReplayErrorKind::Expired,
339+
message,
340+
invalid_event_kind: None,
341+
expired_at_unix_seconds: Some(expired_at_unix_seconds),
342+
persistence_failure: None,
343+
},
344+
CoreReplayErrorVariant::PersistenceFailure(error) => Self {
345+
kind: FfiReplayErrorKind::PersistenceFailure,
346+
message,
347+
invalid_event_kind: None,
348+
expired_at_unix_seconds: None,
349+
persistence_failure: Some(Arc::new(error.into())),
350+
},
351+
}
352+
}
353+
}
354+
355+
#[uniffi::export]
356+
impl ReceiverReplayError {
357+
pub fn kind(&self) -> FfiReplayErrorKind { self.kind }
358+
359+
pub fn message(&self) -> String { self.message.clone() }
360+
361+
pub fn invalid_event_kind(&self) -> Option<FfiReplayInvalidEventKind> {
362+
self.invalid_event_kind
363+
}
364+
365+
pub fn expired_at_unix_seconds(&self) -> Option<u32> { self.expired_at_unix_seconds }
366+
367+
pub fn persistence_failure(&self) -> Option<Arc<ImplementationError>> {
368+
self.persistence_failure.clone()
369+
}
370+
}
371+
372+
#[cfg(all(test, feature = "_test-utils"))]
373+
mod tests {
374+
use std::sync::{Arc, Mutex};
375+
use std::time::Duration;
376+
377+
use payjoin_test_utils::TestServices;
378+
use tokio::time::sleep;
379+
380+
use super::ReceiverPersistedError;
381+
use crate::error::{ForeignError, ReplayErrorKind};
382+
use crate::receive::{
383+
InitializedTransitionOutcome, JsonReceiverSessionPersister, ReceiverBuilder,
384+
};
385+
use crate::send::{JsonSenderSessionPersister, SenderBuilder};
386+
387+
#[derive(Default)]
388+
struct InMemoryReceiverPersister {
389+
events: Mutex<Vec<String>>,
390+
}
391+
392+
impl JsonReceiverSessionPersister for InMemoryReceiverPersister {
393+
fn save(&self, event: String) -> Result<(), ForeignError> {
394+
self.events.lock().expect("lock").push(event);
395+
Ok(())
396+
}
397+
398+
fn load(&self) -> Result<Vec<String>, ForeignError> {
399+
Ok(self.events.lock().expect("lock").clone())
400+
}
401+
402+
fn close(&self) -> Result<(), ForeignError> { Ok(()) }
403+
}
404+
405+
#[derive(Default)]
406+
struct InMemorySenderPersister {
407+
events: Mutex<Vec<String>>,
408+
}
409+
410+
impl JsonSenderSessionPersister for InMemorySenderPersister {
411+
fn save(&self, event: String) -> Result<(), ForeignError> {
412+
self.events.lock().expect("lock").push(event);
413+
Ok(())
414+
}
415+
416+
fn load(&self) -> Result<Vec<String>, ForeignError> {
417+
Ok(self.events.lock().expect("lock").clone())
418+
}
419+
420+
fn close(&self) -> Result<(), ForeignError> { Ok(()) }
421+
}
422+
423+
#[derive(Default)]
424+
struct EmptyReceiverPersister;
425+
426+
impl JsonReceiverSessionPersister for EmptyReceiverPersister {
427+
fn save(&self, _: String) -> Result<(), ForeignError> { Ok(()) }
428+
429+
fn load(&self) -> Result<Vec<String>, ForeignError> { Ok(Vec::new()) }
430+
431+
fn close(&self) -> Result<(), ForeignError> { Ok(()) }
432+
}
433+
434+
struct RejectBroadcast;
435+
436+
impl crate::receive::CanBroadcast for RejectBroadcast {
437+
fn callback(&self, _: Vec<u8>) -> Result<bool, ForeignError> { Ok(false) }
438+
}
439+
440+
async fn post_request(services: &TestServices, request: crate::Request) -> Vec<u8> {
441+
let response = services
442+
.http_agent()
443+
.post(request.url)
444+
.header("Content-Type", request.content_type)
445+
.body(request.body)
446+
.send()
447+
.await
448+
.expect("request should succeed");
449+
response.bytes().await.expect("response bytes").to_vec()
450+
}
451+
452+
#[tokio::test]
453+
async fn test_receiver_persisted_error_preserves_fatal_with_state() {
454+
let services = TestServices::initialize().await.expect("services initialize");
455+
services.wait_for_services_ready().await.expect("services ready");
456+
457+
let receiver_persister = Arc::new(InMemoryReceiverPersister::default());
458+
let initialized = ReceiverBuilder::new(
459+
"2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7".to_string(),
460+
services.directory_url(),
461+
Arc::new(services.fetch_ohttp_keys().await.expect("fetch ohttp keys").into()),
462+
)
463+
.expect("receiver builder")
464+
.build()
465+
.save(receiver_persister.clone())
466+
.expect("save initialized receiver");
467+
468+
let sender_persister = Arc::new(InMemorySenderPersister::default());
469+
let with_reply_key =
470+
SenderBuilder::new(crate::test_utils::original_psbt(), Arc::new(initialized.pj_uri()))
471+
.expect("sender builder")
472+
.build_recommended(1000)
473+
.expect("build sender")
474+
.save(sender_persister)
475+
.expect("save sender");
476+
477+
let sender_request =
478+
with_reply_key.create_v2_post_request(services.ohttp_relay_url()).unwrap();
479+
let _ = post_request(&services, sender_request.request).await;
480+
481+
let mut initialized = initialized;
482+
let unchecked = loop {
483+
let poll = initialized.create_poll_request(services.ohttp_relay_url()).unwrap();
484+
let response = post_request(&services, poll.request).await;
485+
let outcome = initialized
486+
.process_response(&response, poll.client_response.as_ref())
487+
.save(receiver_persister.clone())
488+
.expect("persist initialized transition");
489+
match outcome {
490+
InitializedTransitionOutcome::Progress { inner } => break inner,
491+
InitializedTransitionOutcome::Stasis { inner } => {
492+
initialized = Arc::unwrap_or_clone(inner);
493+
sleep(Duration::from_millis(20)).await;
494+
}
495+
}
496+
};
497+
498+
let error = unchecked
499+
.check_broadcast_suitability(None, Arc::new(RejectBroadcast))
500+
.expect("validation inputs")
501+
.save(receiver_persister);
502+
let error = match error {
503+
Ok(_) => panic!("non-broadcastable original should produce replyable error state"),
504+
Err(error) => error,
505+
};
506+
507+
match error {
508+
ReceiverPersistedError::FatalWithState { state, .. } => {
509+
state.create_error_request(services.ohttp_relay_url()).expect("state preserved");
510+
}
511+
other => panic!("unexpected receiver persisted error: {other:?}"),
512+
}
513+
}
514+
515+
#[test]
516+
fn test_receiver_replay_error_exposes_no_events_kind() {
517+
let error =
518+
match crate::receive::replay_receiver_event_log(Arc::new(EmptyReceiverPersister)) {
519+
Ok(_) => panic!("empty event log should fail"),
520+
Err(error) => error,
521+
};
522+
assert_eq!(error.kind(), ReplayErrorKind::NoEvents);
523+
assert!(error.persistence_failure().is_none());
524+
assert!(error.expired_at_unix_seconds().is_none());
525+
}
526+
}

0 commit comments

Comments
 (0)