Skip to main content

paykit_lib/
error.rs

1use thiserror::Error;
2
3/// Domain-specific error type.
4///
5/// Variants that wrap an underlying cause carry a `source` field backed by
6/// [`anyhow::Error`] so that the standard [`std::error::Error::source`] chain
7/// is preserved while keeping the public API decoupled from upstream error
8/// types. Callers can downcast the source via [`anyhow::Error::downcast_ref`]
9/// when they need the original typed error.
10#[derive(Debug, Error)]
11pub enum PaykitError {
12    /// Wrapper for transport layer failures.
13    ///
14    /// Most user-facing failures bubble up through this variant, encapsulating
15    /// lower-level SDK/network errors (timeouts, connection refused, permission
16    /// denied, etc.).
17    #[error("transport error: {context}")]
18    Transport {
19        /// Human-readable description of what went wrong.
20        context: String,
21        /// The underlying error that caused this failure.
22        #[source]
23        source: anyhow::Error,
24    },
25
26    /// The requested resource does not exist.
27    ///
28    /// Returned when a payment endpoint or other resource is not found (404/GONE).
29    #[error("not found: {0}")]
30    NotFound(String),
31
32    /// Retrieved data is corrupt or structurally invalid.
33    ///
34    /// Returned when a resource was successfully fetched from the network but its
35    /// content cannot be interpreted — for example invalid UTF-8 bytes or an
36    /// unparseable resource path. This is distinct from
37    /// [`PaykitError::Transport`] (the network call itself failed).
38    #[error("invalid data: {context}")]
39    InvalidData {
40        /// Human-readable description of the data problem.
41        context: String,
42        /// The underlying error, when available.
43        #[source]
44        source: Option<anyhow::Error>,
45    },
46
47    /// Input failed validation.
48    ///
49    /// Returned when a caller-supplied value (such as a [`crate::PaymentEndpointIdentifier`]) violates
50    /// structural invariants — for example containing path-traversal sequences,
51    /// null bytes, or characters outside the allowed set.
52    #[error("validation error: {0}")]
53    Validation(String),
54}
55
56#[derive(Debug, Error)]
57#[error("pubky-noise send_message failed with non-retryable error: {0:?}")]
58pub(crate) struct NonRetryablePrivateSendError(pub(crate) pubky_noise::PubkyNoiseError);
59
60#[derive(Debug, Error)]
61#[error("pubky-noise receive_message failed with non-retryable error: {0:?}")]
62pub(crate) struct NonRetryablePrivateReceiveError(pub(crate) pubky_noise::PubkyNoiseError);
63
64impl PaykitError {
65    /// Returns true when this error came from a deterministic Encrypted Link
66    /// private-message send failure that should trigger link recovery instead
67    /// of ordinary transport retry.
68    pub fn is_non_retryable_private_send_error(&self) -> bool {
69        matches!(
70            self,
71            Self::Transport { source, .. }
72                if source
73                    .downcast_ref::<NonRetryablePrivateSendError>()
74                    .is_some()
75        )
76    }
77
78    /// Returns true when this error came from a deterministic Encrypted Link
79    /// private-message receive failure that should trigger link recovery
80    /// instead of ordinary transport retry.
81    pub fn is_non_retryable_private_receive_error(&self) -> bool {
82        matches!(
83            self,
84            Self::Transport { source, .. }
85                if source
86                    .downcast_ref::<NonRetryablePrivateReceiveError>()
87                    .is_some()
88        )
89    }
90}
91
92pub(crate) fn map_error(label: &'static str, err: PaykitError) -> PaykitError {
93    match err {
94        PaykitError::Transport { context, source } => PaykitError::Transport {
95            context: format!("{label}: {context}"),
96            source,
97        },
98        PaykitError::NotFound(msg) => PaykitError::NotFound(format!("{label}: {msg}")),
99        PaykitError::InvalidData { context, source } => PaykitError::InvalidData {
100            context: format!("{label}: {context}"),
101            source,
102        },
103        PaykitError::Validation(msg) => PaykitError::Validation(format!("{label}: {msg}")),
104    }
105}