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}