Skip to main content

worgl_client/
error.rs

1//! What can go wrong talking to a mint or keeping a purse.
2
3use std::fmt;
4use std::io;
5use std::time::Duration;
6
7use serde::Deserialize;
8use worgl::NoteError;
9use worgl_sdk::{CheckError, LoadError, NoLayers, PayError, RequestError};
10
11/// An error from a [`Client`](crate::Client) or a [`Purse`](crate::Purse).
12#[derive(Debug, thiserror::Error)]
13pub enum Error {
14    /// The request did not complete: no connection, a timeout, or an answer
15    /// that was not what the route returns. A transfer may still have been
16    /// applied; resending it is safe.
17    #[error("talking to the mint failed: {0}")]
18    Http(#[from] reqwest::Error),
19    /// The mint answered with an error.
20    #[error("the mint refused: {0}")]
21    Refused(Refusal),
22    /// The server URL is not an `http` or `https` URL.
23    #[error("{0} is not an http or https URL")]
24    Url(String),
25    /// The keys a mint served belong to another issuer.
26    #[error("the mint served the signing keys of another issuer")]
27    Keys,
28    /// A note handed over does not carry the mint's signature.
29    #[error("a note is not signed by its mint: {0}")]
30    Note(#[from] NoteError),
31    /// The mint's info lists no layers.
32    #[error(transparent)]
33    NoLayers(#[from] NoLayers),
34    /// A payment request could not be made.
35    #[error(transparent)]
36    Request(#[from] RequestError),
37    /// A payment could not be made.
38    #[error(transparent)]
39    Pay(#[from] PayError),
40    /// A playground faucet hands out notes of an amount its mint cannot
41    /// make as one note.
42    #[error("the faucet hands out notes of {0}, which its mint cannot make as one note")]
43    FaucetAmount(u64),
44    /// A payment does not pay what was asked.
45    #[error(transparent)]
46    Check(#[from] CheckError),
47    /// The store could not be read.
48    #[error("the saved store cannot be read: {0}")]
49    Load(#[from] LoadError),
50    /// The store could not be read or written. Nothing was handed out that
51    /// depends on the change that failed to save.
52    #[error("the store could not be saved or read: {0}")]
53    Store(#[from] io::Error),
54}
55
56impl Error {
57    /// The mint's error code, when the mint answered with one; see
58    /// [`Refusal::code`].
59    pub fn code(&self) -> Option<&str> {
60        match self {
61            Self::Refused(refusal) => Some(&refusal.code),
62            _ => None,
63        }
64    }
65
66    /// Whether trying again later may succeed: the request did not complete,
67    /// or the mint was paused, busy or failed to store the change. Transfers
68    /// and burns are safe to resend.
69    ///
70    /// `layer_full` does not count, though the mint answers it with 503: a
71    /// transfer refused so is refused every time, since its notes would land
72    /// in the same full generation after every rotation until the note it
73    /// spends expires. A mint request refused so may pass after the next
74    /// rotation, which brings a newer generation to the mint layer.
75    pub fn is_transient(&self) -> bool {
76        match self {
77            Self::Http(error) => !error.is_decode() && !error.is_builder(),
78            Self::Refused(refusal) => match refusal.code.as_str() {
79                "paused" | "busy" | "storage" | "internal" => true,
80                "layer_full" => false,
81                _ => refusal.status >= 500,
82            },
83            _ => false,
84        }
85    }
86
87    /// How long the mint asked to wait before trying again, when it
88    /// answered with a `Retry-After`, as it does with `busy`; see
89    /// [`Refusal::retry_after`].
90    pub fn retry_after(&self) -> Option<Duration> {
91        match self {
92            Self::Refused(refusal) => refusal.retry_after,
93            _ => None,
94        }
95    }
96}
97
98/// An error answer from the mint.
99#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
100pub struct Refusal {
101    /// The HTTP status.
102    #[serde(skip)]
103    pub status: u16,
104    /// A stable code: `spent`, `expired`, `frozen`, `paused`, `policy`,
105    /// `unknown_mint` and so on, as the server documents them. `http` when
106    /// the answer carried none, such as from a proxy.
107    #[serde(rename = "error")]
108    pub code: String,
109    /// What went wrong, for people.
110    pub message: String,
111    /// How long to wait before sending the request again, from the
112    /// answer's `Retry-After` in seconds. A `busy` mint sends one; it took
113    /// nothing, and has room again after that.
114    #[serde(skip)]
115    pub retry_after: Option<Duration>,
116}
117
118impl fmt::Display for Refusal {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "{} ({}, HTTP {})", self.message, self.code, self.status)
121    }
122}