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}