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