Skip to main content

fedimint_mint_client/
error.rs

1//! Error types of the mint client.
2//!
3//! Every failure this module reports to its callers is named here, including
4//! the two types that predate this module and are re-exported from it, so
5//! there is one place to look.
6
7use fedimint_api_client::api::{FederationError, OutputOutcomeError, ServerError};
8use fedimint_client_module::error::{
9    AddStateMachinesError, OperationLookupError, TransactionSubmitError,
10};
11use fedimint_core::config::FederationIdPrefix;
12use fedimint_core::db::DatabaseError;
13use fedimint_core::encoding::DecodeError;
14use fedimint_core::{Amount, PeerId};
15use thiserror::Error;
16
17pub use crate::{InsufficientBalanceError, ReissueExternalNotesError};
18
19/// A failure to pick notes out of the wallet for a spend.
20#[derive(Debug, Error)]
21#[non_exhaustive]
22pub enum SelectNotesError {
23    /// The wallet does not hold enough notes to cover the request.
24    #[error("The wallet does not hold enough notes")]
25    InsufficientBalance(#[from] InsufficientBalanceError),
26
27    /// The requested amount cannot be made exactly from the denominations the
28    /// wallet holds. This does not mean the balance is too low.
29    #[error("The amount {requested} cannot be made exactly; the closest selection is {selected}")]
30    NoExactAmount {
31        /// The amount that was asked for.
32        requested: Amount,
33        /// The total the greedy selection arrived at instead.
34        selected: Amount,
35    },
36
37    /// A note held in the wallet could not be decoded.
38    #[error("A stored note could not be decoded")]
39    Decode(#[from] DecodeError),
40
41    /// A note selector implemented outside this crate failed in a way the
42    /// other variants do not describe.
43    #[error("The note selector failed")]
44    Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
45}
46
47/// A failure to hand e-cash notes out of band.
48#[derive(Debug, Error)]
49#[non_exhaustive]
50pub enum SpendOOBError {
51    /// A spend of zero has nothing to hand out.
52    #[error("Zero-amount out-of-band spends are not supported")]
53    ZeroAmount,
54
55    /// The notes to hand out could not be picked.
56    #[error("The notes to spend could not be selected")]
57    NoteSelection(#[from] SelectNotesError),
58
59    /// The state machines that watch for a refund could not be registered.
60    #[error("Failed to add the spend's state machines")]
61    StateMachines(#[from] AddStateMachinesError),
62
63    /// The spend could not be written to the database.
64    #[error("Database error")]
65    Database(#[from] DatabaseError),
66}
67
68/// A transaction's e-cash outputs did not become spendable.
69#[derive(Debug, Error)]
70#[non_exhaustive]
71pub enum AwaitOutputFinalizedError {
72    /// The federation rejected the transaction, so its outputs never existed.
73    #[error("The transaction was rejected")]
74    TransactionRejected,
75
76    /// The issuance state machine gave up.
77    ///
78    /// `reason` is the text the state machine recorded in the client database
79    /// when it failed; it is read back here, never rewritten.
80    #[error("The notes could not be issued: {reason}")]
81    Failed {
82        /// What the issuance state machine recorded.
83        reason: String,
84    },
85}
86
87/// A failure to hand out e-cash for a requested amount.
88#[derive(Debug, Error)]
89#[non_exhaustive]
90pub enum SendOOBNotesError {
91    /// The federation could not be reached, so the wallet cannot mint itself
92    /// the denominations it is missing.
93    #[error("The federation could not be reached")]
94    Federation(#[source] Box<FederationError>),
95
96    /// The self-reissue that makes the right denominations failed.
97    #[error("The reissue that would make the right denominations failed")]
98    Transaction(#[from] TransactionSubmitError),
99
100    /// The spend could not be written to the database.
101    #[error("Database error")]
102    Database(#[from] DatabaseError),
103}
104
105impl From<FederationError> for SendOOBNotesError {
106    fn from(source: FederationError) -> Self {
107        Self::Federation(Box::new(source))
108    }
109}
110
111/// A failure to follow a reissue operation.
112#[derive(Debug, Error)]
113#[non_exhaustive]
114pub enum SubscribeReissueExternalNotesError {
115    /// No mint operation with this id exists.
116    #[error("The operation could not be looked up")]
117    Operation(#[from] OperationLookupError),
118
119    /// The operation exists, but it is an out-of-band spend.
120    #[error("The operation is an out-of-band spend, not a reissuance")]
121    NotAReissuance,
122
123    /// The operation records no transaction, which a reissuance always has.
124    #[error("The reissue operation records no transaction")]
125    NoTransaction,
126}
127
128/// A failure to follow an out-of-band spend operation.
129#[derive(Debug, Error)]
130#[non_exhaustive]
131pub enum SubscribeSpendNotesError {
132    /// No mint operation with this id exists.
133    #[error("The operation could not be looked up")]
134    Operation(#[from] OperationLookupError),
135
136    /// The operation exists, but it is a reissuance.
137    #[error("The operation is a reissuance, not an out-of-band spend")]
138    NotAnOutOfBandSpend,
139}
140
141/// A note that cannot be spent.
142///
143/// Reported both for notes received out of band and for notes already held in
144/// the wallet, which is why a decoding failure is one of the conditions.
145#[derive(Debug, Error)]
146#[non_exhaustive]
147pub enum ValidateNotesError {
148    /// The notes were issued by a different federation.
149    #[error("The notes were issued by federation {found}, not {expected}")]
150    WrongFederationId {
151        /// The federation this client belongs to.
152        expected: FederationIdPrefix,
153        /// The federation the notes name.
154        found: FederationIdPrefix,
155    },
156
157    /// The note claims a denomination the federation does not issue.
158    #[error("Note {index} claims the amount tier {amount}, which the federation does not issue")]
159    InvalidAmountTier {
160        /// The position of the note in the set that was checked.
161        index: usize,
162        /// The tier the note claims.
163        amount: Amount,
164    },
165
166    /// The note does not carry a valid federation signature.
167    #[error("Note {index} does not carry a valid federation signature")]
168    InvalidSignature {
169        /// The position of the note in the set that was checked.
170        index: usize,
171    },
172
173    /// The note cannot be spent with the key that was supplied with it.
174    #[error("Note {index} cannot be spent with the supplied spend key")]
175    WrongSpendKey {
176        /// The position of the note in the set that was checked.
177        index: usize,
178    },
179
180    /// A note held in the wallet could not be decoded.
181    #[error("A stored note could not be decoded")]
182    Decode(#[from] DecodeError),
183}
184
185/// A string that is not a valid serialization of out-of-band e-cash notes.
186///
187/// Unlike the other errors in this module this one interpolates its cause into
188/// its message: `clap` renders a `FromStr` failure with `Display` alone, and
189/// `OOBNotes`' `Deserialize` impl hands it to `serde::de::Error::custom`, which
190/// keeps only the message. A cause behind `source()` would be dropped by both.
191#[derive(Debug, Error)]
192#[non_exhaustive]
193pub enum OOBNotesParseError {
194    /// The string is neither base32 with the fedimint prefix nor base64.
195    #[error("The e-cash notes are not a well-formed base32 or base64 string")]
196    Encoding,
197
198    /// The decoded bytes are not a valid `OOBNotes` encoding.
199    #[error("The e-cash notes could not be decoded: {0}")]
200    Decode(#[from] DecodeError),
201
202    /// The string decodes, but carries no notes.
203    #[error("The e-cash notes are empty")]
204    Empty,
205}
206
207/// A failure to fetch a slice of the federation's recovery log from one peer.
208#[derive(Debug, Error)]
209#[non_exhaustive]
210pub enum FetchRecoverySliceError {
211    /// The peer did not answer, or answered with an error.
212    #[error("The peer did not serve the recovery slice")]
213    Peer(#[from] ServerError),
214
215    /// The peer's answer is not a decodable recovery slice.
216    #[error("The recovery slice could not be decoded")]
217    Decode(#[from] DecodeError),
218}
219
220/// A failure to assemble the wallet's e-cash backup.
221#[derive(Debug, Error)]
222#[non_exhaustive]
223pub enum PrepareEcashBackupError {
224    /// The federation could not be asked how far consensus has got, which the
225    /// backup records so a restore knows where to resume scanning.
226    #[error("The federation could not be reached")]
227    Federation(#[source] Box<FederationError>),
228
229    /// A note held in the wallet could not be decoded.
230    #[error("A stored note could not be decoded")]
231    Decode(#[from] DecodeError),
232}
233
234impl From<FederationError> for PrepareEcashBackupError {
235    fn from(source: FederationError) -> Self {
236        Self::Federation(Box::new(source))
237    }
238}
239
240/// A failure to repair an inconsistent wallet.
241#[derive(Debug, Error)]
242#[non_exhaustive]
243pub enum RepairWalletError {
244    /// The federation could not be asked whether a note or a nonce was used.
245    #[error("The federation could not be reached")]
246    Federation(#[source] Box<FederationError>),
247
248    /// The repaired wallet could not be written back.
249    #[error("Database error")]
250    Database(#[from] DatabaseError),
251}
252
253impl From<FederationError> for RepairWalletError {
254    fn from(source: FederationError) -> Self {
255        Self::Federation(Box::new(source))
256    }
257}
258
259/// A guardian's blind signature share that cannot be used.
260#[derive(Debug, Error)]
261#[non_exhaustive]
262pub enum VerifyBlindShareError {
263    /// The guardian's answer is not a decodable output outcome.
264    #[error("The output outcome could not be read")]
265    Outcome(#[source] Box<OutputOutcomeError>),
266
267    /// The share came from a peer this client holds no key for.
268    #[error("No public key share is known for peer {peer}")]
269    UnknownPeer {
270        /// The peer that answered.
271        peer: PeerId,
272    },
273
274    /// The federation does not issue notes of this denomination.
275    #[error("The federation issues no notes of the amount tier {amount}")]
276    InvalidAmountTier {
277        /// The tier the outcome claims.
278        amount: Amount,
279    },
280
281    /// The share does not verify against the peer's public key share.
282    #[error("The blind signature share does not verify")]
283    InvalidSignature,
284}
285
286impl From<OutputOutcomeError> for VerifyBlindShareError {
287    fn from(source: OutputOutcomeError) -> Self {
288        Self::Outcome(Box::new(source))
289    }
290}