Skip to main content

ark_client/
error.rs

1use std::error::Error as StdError;
2use std::fmt;
3
4type Source = Box<dyn StdError + Send + Sync + 'static>;
5
6#[derive(Debug)]
7pub struct Error {
8    inner: Box<ErrorImpl>,
9}
10
11#[derive(Debug)]
12struct ErrorImpl {
13    kind: Kind,
14    cause: Option<Error>,
15}
16
17#[derive(Debug)]
18enum Kind {
19    /// Ad-hoc error,
20    AdHoc(AdHocError),
21    /// An error related to interactions with the Ark server.
22    ArkServer(ArkServerError),
23    /// An error from [`ark_core`].
24    Core(CoreError),
25    /// An error related to coin selection of VTXOs and boarding outputs.
26    CoinSelect(CoinSelectError),
27    /// An error related to actions within the wallet.
28    Wallet(WalletError),
29    /// Ark server info changed while processing a request.
30    ServerInfoChanged(ServerInfoChangedError),
31    /// An error thrown by a user of this library
32    Consumer(ConsumerError),
33}
34
35#[derive(Debug)]
36struct AdHocError {
37    source: Source,
38}
39
40#[derive(Debug)]
41struct ArkServerError {
42    source: Source,
43}
44
45#[derive(Debug)]
46struct CoreError {
47    source: ark_core::Error,
48}
49
50#[derive(Debug)]
51struct CoinSelectError {
52    source: Source,
53}
54
55#[derive(Debug)]
56struct WalletError {
57    source: Source,
58}
59
60#[derive(Debug)]
61struct ServerInfoChangedError;
62
63#[derive(Debug)]
64struct ConsumerError {
65    source: Source,
66}
67
68impl Error {
69    fn new(kind: Kind) -> Self {
70        Self {
71            inner: Box::new(ErrorImpl { kind, cause: None }),
72        }
73    }
74
75    pub(crate) fn ad_hoc(source: impl Into<Source>) -> Self {
76        Error::new(Kind::AdHoc(AdHocError {
77            source: source.into(),
78        }))
79    }
80
81    pub(crate) fn ark_server(source: impl Into<Source>) -> Self {
82        Error::new(Kind::ArkServer(ArkServerError {
83            source: source.into(),
84        }))
85    }
86
87    pub(crate) fn coin_select(source: impl Into<Source>) -> Self {
88        Error::new(Kind::CoinSelect(CoinSelectError {
89            source: source.into(),
90        }))
91    }
92
93    pub fn wallet(source: impl Into<Source>) -> Self {
94        Error::new(Kind::Wallet(WalletError {
95            source: source.into(),
96        }))
97    }
98
99    pub fn consumer(source: impl Into<Source>) -> Self {
100        Error::new(Kind::Consumer(ConsumerError {
101            source: source.into(),
102        }))
103    }
104
105    pub(crate) fn server_info_changed(source: impl Into<Error>) -> Self {
106        source
107            .into()
108            .context(Error::new(Kind::ServerInfoChanged(ServerInfoChangedError)))
109    }
110
111    pub fn is_server_info_changed(&self) -> bool {
112        let mut err = self;
113        loop {
114            if matches!(err.inner.kind, Kind::ServerInfoChanged(_)) {
115                return true;
116            }
117            err = match err.inner.cause.as_ref() {
118                Some(err) => err,
119                None => return false,
120            };
121        }
122    }
123
124    /// Returns `true` if this error chain contains an arkd build-version mismatch.
125    pub fn is_version_mismatch(&self) -> bool {
126        let mut err = self;
127        loop {
128            if err.inner.kind.is_version_mismatch() {
129                return true;
130            }
131            err = match err.inner.cause.as_ref() {
132                Some(err) => err,
133                None => return false,
134            };
135        }
136    }
137}
138
139impl Kind {
140    fn is_version_mismatch(&self) -> bool {
141        match self {
142            Kind::ArkServer(err) => {
143                err.source.is::<ark_grpc::Error>()
144                    && err
145                        .source
146                        .downcast_ref::<ark_grpc::Error>()
147                        .is_some_and(ark_grpc::Error::is_version_mismatch)
148            }
149            Kind::AdHoc(_)
150            | Kind::Core(_)
151            | Kind::CoinSelect(_)
152            | Kind::Wallet(_)
153            | Kind::ServerInfoChanged(_)
154            | Kind::Consumer(_) => false,
155        }
156    }
157}
158
159impl fmt::Display for Error {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        let mut err = self;
162        loop {
163            write!(f, "{}", err.inner.kind)?;
164            err = match err.inner.cause.as_ref() {
165                None => break,
166                Some(err) => err,
167            };
168            write!(f, ": ")?;
169        }
170        Ok(())
171    }
172}
173
174impl fmt::Display for Kind {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match *self {
177            Kind::AdHoc(ref err) => err.fmt(f),
178            Kind::ArkServer(ref err) => err.fmt(f),
179            Kind::Core(ref err) => err.fmt(f),
180            Kind::CoinSelect(ref err) => err.fmt(f),
181            Kind::Wallet(ref err) => err.fmt(f),
182            Kind::ServerInfoChanged(ref err) => err.fmt(f),
183            Kind::Consumer(ref err) => err.fmt(f),
184        }
185    }
186}
187
188impl fmt::Display for AdHocError {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        self.source.fmt(f)
191    }
192}
193
194impl fmt::Display for ArkServerError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        self.source.fmt(f)
197    }
198}
199
200impl fmt::Display for CoreError {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        self.source.fmt(f)
203    }
204}
205
206impl fmt::Display for CoinSelectError {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        self.source.fmt(f)
209    }
210}
211
212impl fmt::Display for WalletError {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        self.source.fmt(f)
215    }
216}
217
218impl fmt::Display for ServerInfoChangedError {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        f.write_str("Ark server info changed while processing the request. Server info was refreshed, but the failed operation was not retried. Rebuild the request and retry if safe")
221    }
222}
223
224impl fmt::Display for ConsumerError {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        self.source.fmt(f)
227    }
228}
229
230impl From<ark_core::Error> for Error {
231    fn from(value: ark_core::Error) -> Self {
232        Self::new(Kind::Core(CoreError { source: value }))
233    }
234}
235
236pub trait IntoError {
237    fn into_error(self) -> Error;
238}
239
240impl IntoError for Error {
241    fn into_error(self) -> Error {
242        self
243    }
244}
245
246impl IntoError for &'static str {
247    fn into_error(self) -> Error {
248        Error::ad_hoc(self)
249    }
250}
251
252impl IntoError for String {
253    fn into_error(self) -> Error {
254        Error::ad_hoc(self)
255    }
256}
257
258/// A trait for contextualizing error values.
259///
260/// This makes it easy to contextualize either `Error` or `Result<T, Error>`.
261/// Specifically, in the latter case, it absolves one of the need to call
262/// `map_err` everywhere one wants to add context to an error.
263///
264/// This trick was borrowed from `jiff`, which borrowed it from `anyhow`.
265pub trait ErrorContext {
266    type Output;
267
268    /// Contextualize the given consequent error with this (`self`) error as
269    /// the cause.
270    ///
271    /// This is equivalent to saying that "consequent is caused by self."
272    ///
273    /// Note that if an `Error` is given for `kind`, then this panics if it has
274    /// a cause. (Because the cause would otherwise be dropped. An error causal
275    /// chain is just a linked list, not a tree.)
276    fn context(self, consequent: impl IntoError) -> Self::Output;
277
278    /// Like `context`, but hides error construction within a closure.
279    ///
280    /// This is useful if the creation of the consequent error is not otherwise
281    /// guarded and when error construction is potentially "costly" (i.e., it
282    /// allocates). The closure avoids paying the cost of contextual error
283    /// creation in the happy path.
284    fn with_context<E: IntoError>(self, consequent: impl FnOnce() -> E) -> Self::Output;
285}
286
287impl ErrorContext for Error {
288    type Output = Error;
289
290    fn context(self, consequent: impl IntoError) -> Error {
291        let mut err = consequent.into_error();
292        assert!(
293            err.inner.cause.is_none(),
294            "cause of consequence must be `None`"
295        );
296
297        err.inner.cause = Some(self);
298        err
299    }
300
301    fn with_context<E: IntoError>(self, consequent: impl FnOnce() -> E) -> Error {
302        let mut err = consequent().into_error();
303        assert!(
304            err.inner.cause.is_none(),
305            "cause of consequence must be `None`"
306        );
307
308        err.inner.cause = Some(self);
309        err
310    }
311}
312
313impl<T, E> ErrorContext for Result<T, E>
314where
315    E: StdError + Send + Sync + 'static,
316{
317    type Output = Result<T, Error>;
318
319    fn context(self, consequent: impl IntoError) -> Result<T, Error> {
320        self.map_err(|err| {
321            let err: Box<dyn StdError + Send + Sync + 'static> = Box::new(err);
322            match err.downcast::<Error>() {
323                Ok(err) => (*err).context(consequent),
324                Err(err) => Error::ad_hoc(err).context(consequent),
325            }
326        })
327    }
328
329    fn with_context<C: IntoError>(self, consequent: impl FnOnce() -> C) -> Result<T, Error> {
330        self.map_err(|err| {
331            let err: Box<dyn StdError + Send + Sync + 'static> = Box::new(err);
332            match err.downcast::<Error>() {
333                Ok(err) => (*err).with_context(consequent),
334                Err(err) => Error::ad_hoc(err).with_context(consequent),
335            }
336        })
337    }
338}
339
340impl From<ark_grpc::Error> for Error {
341    fn from(value: ark_grpc::Error) -> Self {
342        if value.is_server_info_changed() {
343            Self::server_info_changed(Self::ark_server(value))
344        } else {
345            Self::ark_server(value)
346        }
347    }
348}
349
350impl StdError for Error {
351    fn source(&self) -> Option<&(dyn StdError + 'static)> {
352        self.inner
353            .cause
354            .as_ref()
355            .map(|e| e as &(dyn StdError + 'static))
356    }
357}