@@ -30,7 +30,7 @@ impl From<receive::Error> for ReceiverError {
3030 use ReceiverError :: * ;
3131
3232 match value {
33- receive:: Error :: Protocol ( e) => Protocol ( Arc :: new ( ProtocolError ( e ) ) ) ,
33+ receive:: Error :: Protocol ( e) => Protocol ( Arc :: new ( e . into ( ) ) ) ,
3434 receive:: Error :: Implementation ( e) =>
3535 Implementation ( Arc :: new ( ImplementationError :: from ( e) ) ) ,
3636 _ => Unexpected ,
@@ -135,9 +135,165 @@ impl From<payjoin::bitcoin::address::ParseError> for AddressParseError {
135135/// 3. Support proper error propagation through the receiver stack
136136/// 4. Provide errors according to BIP-78 JSON error specifications for return
137137/// after conversion into [`JsonReply`]
138+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , uniffi:: Enum ) ]
139+ pub enum ProtocolErrorKind {
140+ OriginalPayload ,
141+ V1Request ,
142+ V2Session ,
143+ Other ,
144+ }
145+
146+ impl From < receive:: ProtocolErrorKind > for ProtocolErrorKind {
147+ fn from ( value : receive:: ProtocolErrorKind ) -> Self {
148+ match value {
149+ receive:: ProtocolErrorKind :: OriginalPayload => Self :: OriginalPayload ,
150+ receive:: ProtocolErrorKind :: V1Request => Self :: V1Request ,
151+ receive:: ProtocolErrorKind :: V2Session => Self :: V2Session ,
152+ _ => Self :: Other ,
153+ }
154+ }
155+ }
156+
157+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , uniffi:: Enum ) ]
158+ pub enum PayloadErrorKind {
159+ InvalidUtf8 ,
160+ InvalidPsbt ,
161+ UnsupportedVersion ,
162+ InvalidSenderFeeRate ,
163+ InconsistentPsbt ,
164+ PrevTxOut ,
165+ MissingPayment ,
166+ OriginalPsbtNotBroadcastable ,
167+ InputOwned ,
168+ InputSeen ,
169+ PsbtBelowFeeRate ,
170+ FeeTooHigh ,
171+ Other ,
172+ }
173+
174+ impl From < receive:: PayloadErrorKind > for PayloadErrorKind {
175+ fn from ( value : receive:: PayloadErrorKind ) -> Self {
176+ match value {
177+ receive:: PayloadErrorKind :: InvalidUtf8 => Self :: InvalidUtf8 ,
178+ receive:: PayloadErrorKind :: InvalidPsbt => Self :: InvalidPsbt ,
179+ receive:: PayloadErrorKind :: UnsupportedVersion => Self :: UnsupportedVersion ,
180+ receive:: PayloadErrorKind :: InvalidSenderFeeRate => Self :: InvalidSenderFeeRate ,
181+ receive:: PayloadErrorKind :: InconsistentPsbt => Self :: InconsistentPsbt ,
182+ receive:: PayloadErrorKind :: PrevTxOut => Self :: PrevTxOut ,
183+ receive:: PayloadErrorKind :: MissingPayment => Self :: MissingPayment ,
184+ receive:: PayloadErrorKind :: OriginalPsbtNotBroadcastable =>
185+ Self :: OriginalPsbtNotBroadcastable ,
186+ receive:: PayloadErrorKind :: InputOwned => Self :: InputOwned ,
187+ receive:: PayloadErrorKind :: InputSeen => Self :: InputSeen ,
188+ receive:: PayloadErrorKind :: PsbtBelowFeeRate => Self :: PsbtBelowFeeRate ,
189+ receive:: PayloadErrorKind :: FeeTooHigh => Self :: FeeTooHigh ,
190+ _ => Self :: Other ,
191+ }
192+ }
193+ }
194+
195+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , uniffi:: Enum ) ]
196+ pub enum RequestErrorKind {
197+ MissingHeader ,
198+ InvalidContentType ,
199+ InvalidContentLength ,
200+ ContentLengthMismatch ,
201+ Other ,
202+ }
203+
204+ impl From < receive:: v1:: RequestErrorKind > for RequestErrorKind {
205+ fn from ( value : receive:: v1:: RequestErrorKind ) -> Self {
206+ match value {
207+ receive:: v1:: RequestErrorKind :: MissingHeader => Self :: MissingHeader ,
208+ receive:: v1:: RequestErrorKind :: InvalidContentType => Self :: InvalidContentType ,
209+ receive:: v1:: RequestErrorKind :: InvalidContentLength => Self :: InvalidContentLength ,
210+ receive:: v1:: RequestErrorKind :: ContentLengthMismatch => Self :: ContentLengthMismatch ,
211+ _ => Self :: Other ,
212+ }
213+ }
214+ }
215+
216+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , uniffi:: Enum ) ]
217+ pub enum SessionErrorKind {
218+ ParseUrl ,
219+ Expired ,
220+ OhttpEncapsulation ,
221+ Hpke ,
222+ DirectoryResponse ,
223+ Other ,
224+ }
225+
226+ impl From < receive:: v2:: SessionErrorKind > for SessionErrorKind {
227+ fn from ( value : receive:: v2:: SessionErrorKind ) -> Self {
228+ match value {
229+ receive:: v2:: SessionErrorKind :: ParseUrl => Self :: ParseUrl ,
230+ receive:: v2:: SessionErrorKind :: Expired => Self :: Expired ,
231+ receive:: v2:: SessionErrorKind :: OhttpEncapsulation => Self :: OhttpEncapsulation ,
232+ receive:: v2:: SessionErrorKind :: Hpke => Self :: Hpke ,
233+ receive:: v2:: SessionErrorKind :: DirectoryResponse => Self :: DirectoryResponse ,
234+ _ => Self :: Other ,
235+ }
236+ }
237+ }
238+
138239#[ derive( Debug , thiserror:: Error , uniffi:: Object ) ]
139- #[ error( transparent) ]
140- pub struct ProtocolError ( #[ from] receive:: ProtocolError ) ;
240+ #[ error( "{message}" ) ]
241+ pub struct ProtocolError {
242+ kind : ProtocolErrorKind ,
243+ message : String ,
244+ reply : receive:: JsonReply ,
245+ payload_error : Option < Arc < PayloadError > > ,
246+ request_error : Option < Arc < RequestError > > ,
247+ session_error : Option < Arc < SessionError > > ,
248+ }
249+
250+ impl From < receive:: ProtocolError > for ProtocolError {
251+ fn from ( value : receive:: ProtocolError ) -> Self {
252+ let kind = value. kind ( ) . into ( ) ;
253+ let message = value. to_string ( ) ;
254+ let reply = receive:: JsonReply :: from ( & value) ;
255+
256+ match value {
257+ receive:: ProtocolError :: OriginalPayload ( error) => Self {
258+ kind,
259+ message,
260+ reply,
261+ payload_error : Some ( Arc :: new ( error. into ( ) ) ) ,
262+ request_error : None ,
263+ session_error : None ,
264+ } ,
265+ receive:: ProtocolError :: V1 ( error) => Self {
266+ kind,
267+ message,
268+ reply,
269+ payload_error : None ,
270+ request_error : Some ( Arc :: new ( error. into ( ) ) ) ,
271+ session_error : None ,
272+ } ,
273+ receive:: ProtocolError :: V2 ( error) => Self {
274+ kind,
275+ message,
276+ reply,
277+ payload_error : None ,
278+ request_error : None ,
279+ session_error : Some ( Arc :: new ( error. into ( ) ) ) ,
280+ } ,
281+ }
282+ }
283+ }
284+
285+ #[ uniffi:: export]
286+ impl ProtocolError {
287+ pub fn kind ( & self ) -> ProtocolErrorKind { self . kind }
288+
289+ pub fn message ( & self ) -> String { self . message . clone ( ) }
290+
291+ pub fn payload_error ( & self ) -> Option < Arc < PayloadError > > { self . payload_error . clone ( ) }
292+
293+ pub fn request_error ( & self ) -> Option < Arc < RequestError > > { self . request_error . clone ( ) }
294+
295+ pub fn session_error ( & self ) -> Option < Arc < SessionError > > { self . session_error . clone ( ) }
296+ }
141297
142298/// The standard format for errors that can be replied as JSON.
143299///
@@ -160,13 +316,97 @@ impl From<receive::JsonReply> for JsonReply {
160316}
161317
162318impl From < ProtocolError > for JsonReply {
163- fn from ( value : ProtocolError ) -> Self { Self ( ( & value. 0 ) . into ( ) ) }
319+ fn from ( value : ProtocolError ) -> Self { Self ( value. reply ) }
164320}
165321
166322/// Error that may occur during a v2 session typestate change
167323#[ derive( Debug , thiserror:: Error , uniffi:: Object ) ]
168- #[ error( transparent) ]
169- pub struct SessionError ( #[ from] receive:: v2:: SessionError ) ;
324+ #[ error( "{message}" ) ]
325+ pub struct SessionError {
326+ kind : SessionErrorKind ,
327+ message : String ,
328+ }
329+
330+ impl From < receive:: v2:: SessionError > for SessionError {
331+ fn from ( value : receive:: v2:: SessionError ) -> Self {
332+ Self { kind : value. kind ( ) . into ( ) , message : value. to_string ( ) }
333+ }
334+ }
335+
336+ #[ uniffi:: export]
337+ impl SessionError {
338+ pub fn kind ( & self ) -> SessionErrorKind { self . kind }
339+
340+ pub fn message ( & self ) -> String { self . message . clone ( ) }
341+ }
342+
343+ /// Receiver original payload validation error exposed over FFI.
344+ #[ derive( Debug , thiserror:: Error , uniffi:: Object ) ]
345+ #[ error( "{message}" ) ]
346+ pub struct PayloadError {
347+ kind : PayloadErrorKind ,
348+ message : String ,
349+ supported_versions : Option < Vec < u64 > > ,
350+ }
351+
352+ impl From < receive:: PayloadError > for PayloadError {
353+ fn from ( value : receive:: PayloadError ) -> Self {
354+ Self {
355+ kind : value. kind ( ) . into ( ) ,
356+ message : value. to_string ( ) ,
357+ supported_versions : value. supported_versions ( ) ,
358+ }
359+ }
360+ }
361+
362+ #[ uniffi:: export]
363+ impl PayloadError {
364+ pub fn kind ( & self ) -> PayloadErrorKind { self . kind }
365+
366+ pub fn message ( & self ) -> String { self . message . clone ( ) }
367+
368+ pub fn supported_versions ( & self ) -> Option < Vec < u64 > > { self . supported_versions . clone ( ) }
369+ }
370+
371+ /// Receiver v1 request validation error exposed over FFI.
372+ #[ derive( Debug , thiserror:: Error , uniffi:: Object ) ]
373+ #[ error( "{message}" ) ]
374+ pub struct RequestError {
375+ kind : RequestErrorKind ,
376+ message : String ,
377+ header_name : Option < String > ,
378+ invalid_content_type : Option < String > ,
379+ expected_content_length : Option < u64 > ,
380+ actual_content_length : Option < u64 > ,
381+ }
382+
383+ impl From < receive:: v1:: RequestError > for RequestError {
384+ fn from ( value : receive:: v1:: RequestError ) -> Self {
385+ Self {
386+ kind : value. kind ( ) . into ( ) ,
387+ message : value. to_string ( ) ,
388+ header_name : value. header_name ( ) . map ( str:: to_owned) ,
389+ invalid_content_type : value. invalid_content_type ( ) . map ( str:: to_owned) ,
390+ expected_content_length : value. expected_content_length ( ) . map ( |value| value as u64 ) ,
391+ actual_content_length : value. actual_content_length ( ) . map ( |value| value as u64 ) ,
392+ }
393+ }
394+ }
395+
396+ #[ uniffi:: export]
397+ impl RequestError {
398+ pub fn kind ( & self ) -> RequestErrorKind { self . kind }
399+
400+ pub fn message ( & self ) -> String { self . message . clone ( ) }
401+
402+ pub fn header_name ( & self ) -> Option < String > { self . header_name . clone ( ) }
403+
404+ pub fn invalid_content_type ( & self ) -> Option < String > { self . invalid_content_type . clone ( ) }
405+
406+ pub fn expected_content_length ( & self ) -> Option < u64 > { self . expected_content_length }
407+
408+ pub fn actual_content_length ( & self ) -> Option < u64 > { self . actual_content_length }
409+ }
170410
171411/// Protocol error raised during output substitution.
172412#[ derive( Debug , thiserror:: Error , uniffi:: Object ) ]
@@ -237,3 +477,68 @@ impl From<FfiValidationError> for InputPairError {
237477pub struct ReceiverReplayError (
238478 #[ from] payjoin:: error:: ReplayError < receive:: v2:: ReceiveSession , receive:: v2:: SessionEvent > ,
239479) ;
480+
481+ #[ cfg( test) ]
482+ mod tests {
483+ use super :: * ;
484+
485+ struct TestHeaders {
486+ content_type : Option < & ' static str > ,
487+ content_length : Option < String > ,
488+ }
489+
490+ impl receive:: v1:: Headers for TestHeaders {
491+ fn get_header ( & self , key : & str ) -> Option < & str > {
492+ match key {
493+ "content-type" => self . content_type ,
494+ "content-length" => self . content_length . as_deref ( ) ,
495+ _ => None ,
496+ }
497+ }
498+ }
499+
500+ #[ test]
501+ fn test_receiver_error_exposes_payload_kind ( ) {
502+ let body = b"not-a-psbt" ;
503+ let headers = TestHeaders {
504+ content_type : Some ( "text/plain" ) ,
505+ content_length : Some ( body. len ( ) . to_string ( ) ) ,
506+ } ;
507+ let error = receive:: v1:: UncheckedOriginalPayload :: from_request ( body, "" , headers)
508+ . expect_err ( "invalid PSBT should fail" ) ;
509+
510+ let ReceiverError :: Protocol ( protocol) = ReceiverError :: from ( error) else {
511+ panic ! ( "expected protocol error" ) ;
512+ } ;
513+
514+ assert_eq ! ( protocol. kind( ) , ProtocolErrorKind :: OriginalPayload ) ;
515+ assert_eq ! (
516+ protocol. payload_error( ) . expect( "payload error should be present" ) . kind( ) ,
517+ PayloadErrorKind :: InvalidPsbt
518+ ) ;
519+ assert ! ( protocol. request_error( ) . is_none( ) ) ;
520+ assert ! ( protocol. session_error( ) . is_none( ) ) ;
521+ }
522+
523+ #[ test]
524+ fn test_receiver_error_exposes_request_details ( ) {
525+ let body = b"abc" ;
526+ let headers = TestHeaders {
527+ content_type : Some ( "text/plain" ) ,
528+ content_length : Some ( ( body. len ( ) + 1 ) . to_string ( ) ) ,
529+ } ;
530+ let error = receive:: v1:: UncheckedOriginalPayload :: from_request ( body, "" , headers)
531+ . expect_err ( "content length mismatch should fail" ) ;
532+
533+ let ReceiverError :: Protocol ( protocol) = ReceiverError :: from ( error) else {
534+ panic ! ( "expected protocol error" ) ;
535+ } ;
536+
537+ assert_eq ! ( protocol. kind( ) , ProtocolErrorKind :: V1Request ) ;
538+ let request = protocol. request_error ( ) . expect ( "request error should be present" ) ;
539+ assert_eq ! ( request. kind( ) , RequestErrorKind :: ContentLengthMismatch ) ;
540+ assert_eq ! ( request. expected_content_length( ) , Some ( ( body. len( ) + 1 ) as u64 ) ) ;
541+ assert_eq ! ( request. actual_content_length( ) , Some ( body. len( ) as u64 ) ) ;
542+ assert ! ( protocol. payload_error( ) . is_none( ) ) ;
543+ }
544+ }
0 commit comments