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