Skip to main content

fedimint_client_module/
error.rs

1//! Error types shared between the client and its modules.
2//!
3//! The client implements the traits in [`crate::module`] and [`crate`] for its
4//! modules, so the failures those traits report have to be nameable from both
5//! sides; they live here rather than in `fedimint-client`, which the modules do
6//! not depend on.
7
8use fedimint_core::Amount;
9use fedimint_core::config::{FederationId, ModuleConfigError};
10use fedimint_core::core::{ModuleInstanceId, ModuleKind, OperationId};
11use fedimint_core::db::DatabaseError;
12use fedimint_core::module::AmountUnit;
13use thiserror::Error;
14
15/// The primary module cannot fund a transaction: the balance it holds is
16/// below what the transaction needs.
17#[derive(Debug, Clone, Copy, Eq, PartialEq, Error)]
18pub struct InsufficientBalanceError {
19    /// The amount the transaction needed the primary module to fund.
20    pub requested_amount: Amount,
21    /// The total amount the primary module actually holds.
22    pub total_amount: Amount,
23}
24
25impl std::fmt::Display for InsufficientBalanceError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(
28            f,
29            "Insufficient balance: requested {} but only {} available",
30            self.requested_amount, self.total_amount
31        )
32    }
33}
34
35/// A failure to add state machines to the client's executor.
36#[derive(Debug, Error)]
37#[non_exhaustive]
38pub enum AddStateMachinesError {
39    /// One of the states is already in the database.
40    #[error("State already exists in database")]
41    StateAlreadyExists,
42
43    /// A state belongs to a module instance the executor does not know.
44    #[error("Unknown module instance {module_instance_id}")]
45    UnknownModule {
46        /// The instance the state claims to belong to.
47        module_instance_id: ModuleInstanceId,
48    },
49
50    /// A state that can no longer transition was handed to the executor,
51    /// which would never make progress on it.
52    #[error("State is already terminal, adding it to the executor does not make sense")]
53    StateAlreadyTerminal,
54
55    /// The database write failed.
56    #[error("Database error")]
57    Database(#[from] DatabaseError),
58}
59
60/// An operation with the same id already exists in the operation log.
61#[derive(Debug, Error)]
62#[error("An operation with id {} already exists", .operation_id.fmt_short())]
63pub struct OperationAlreadyExistsError {
64    /// The id that is already taken.
65    pub operation_id: OperationId,
66}
67
68/// No operation with the requested id exists in the operation log.
69#[derive(Debug, Error)]
70#[error("No operation with id {}", .operation_id.fmt_short())]
71pub struct OperationNotFoundError {
72    /// The id that was looked up.
73    pub operation_id: OperationId,
74}
75
76/// A failure to look up one of a module's own operations.
77#[derive(Debug, Error)]
78#[non_exhaustive]
79pub enum OperationLookupError {
80    /// The operation log has no entry with this id.
81    #[error("The operation does not exist")]
82    NotFound(#[from] OperationNotFoundError),
83
84    /// The operation exists, but was started by a different module.
85    #[error(
86        "Operation {} was started by module {found}, not {expected}",
87        .operation_id.fmt_short()
88    )]
89    WrongModuleKind {
90        /// The operation that was looked up.
91        operation_id: OperationId,
92        /// The kind of the module doing the lookup.
93        expected: ModuleKind,
94        /// The kind of the module that started the operation.
95        found: String,
96    },
97}
98
99/// A failure to build, submit or complete a client transaction.
100///
101/// Covers the whole path a transaction takes on the client: balancing it with
102/// the primary module, recording its operation, registering its state machines,
103/// and waiting for the primary module's outputs to finalize. Submission to the
104/// federation itself is driven by a state machine and is not reported here.
105#[derive(Debug, Error)]
106#[non_exhaustive]
107pub enum TransactionSubmitError {
108    /// The operation the transaction would be recorded under already exists.
109    #[error("The operation already exists")]
110    OperationAlreadyExists(#[from] OperationAlreadyExistsError),
111
112    /// The finalized transaction is larger than the federation accepts.
113    #[error("The transaction is {size} bytes, over the limit of {max}")]
114    TransactionTooLarge {
115        /// The size of the encoded transaction.
116        size: usize,
117        /// The largest transaction the federation accepts.
118        max: usize,
119    },
120
121    /// No primary module can hold funds of this unit, so the transaction
122    /// cannot be balanced.
123    #[error("No primary module for unit {unit}")]
124    NoPrimaryModule {
125        /// The unit that could not be balanced.
126        unit: AmountUnit,
127    },
128
129    /// The primary module failed to balance the transaction or to complete
130    /// its outputs. An insufficient balance reported while balancing the
131    /// transaction is reported as [`Self::InsufficientFunds`] instead.
132    #[error("The primary module failed")]
133    PrimaryModule(#[source] ClientModuleError),
134
135    /// Writing the transaction to the database failed.
136    #[error("Database error")]
137    Database(#[from] DatabaseError),
138
139    /// The transaction's state machines could not be registered.
140    #[error("Failed to add the transaction's state machines")]
141    StateMachines(#[from] AddStateMachinesError),
142
143    /// The primary module holds too little balance to fund the transaction.
144    #[error("Insufficient funds")]
145    InsufficientFunds(#[from] InsufficientBalanceError),
146
147    /// Calculating the transaction balance or fees overflowed.
148    #[error("Transaction amounts or fees overflowed")]
149    AmountOverflow,
150}
151
152impl TransactionSubmitError {
153    /// Whether this failure means the primary module cannot fund the
154    /// transaction, as opposed to a failure of the client, the database or
155    /// the federation.
156    pub fn is_insufficient_funds(&self) -> bool {
157        matches!(self, Self::InsufficientFunds(_))
158    }
159}
160
161/// A primary module's failure to balance a transaction, as the failure of
162/// that transaction: a balance too low to fund it becomes
163/// [`TransactionSubmitError::InsufficientFunds`], so callers can tell it apart
164/// from any other failure of the module, which becomes
165/// [`TransactionSubmitError::PrimaryModule`].
166impl From<ClientModuleError> for TransactionSubmitError {
167    fn from(error: ClientModuleError) -> Self {
168        match error {
169            ClientModuleError::InsufficientBalance(error) => Self::InsufficientFunds(error),
170            other => Self::PrimaryModule(other),
171        }
172    }
173}
174
175/// A failure a client module reports to the client.
176///
177/// The methods a module implements for the client in [`ClientModule`],
178/// [`ClientModuleInit`] and [`RecoveryFromHistory`] report this type, and so
179/// do the type-erased wrappers the client calls them through. The client wraps
180/// it in the error of the operation that needed the module; the JSON command
181/// handlers `handle_cli_command` and `handle_rpc` hand it to the command-line
182/// or RPC caller as it is.
183///
184/// [`ClientModule`]: crate::module::ClientModule
185/// [`ClientModuleInit`]: crate::module::init::ClientModuleInit
186/// [`RecoveryFromHistory`]: crate::module::init::recovery::RecoveryFromHistory
187#[derive(Debug, Error)]
188#[non_exhaustive]
189pub enum ClientModuleError {
190    /// The primary module's balance cannot fund the transaction it was asked
191    /// to balance.
192    ///
193    /// A primary module reports this from `create_final_inputs_and_outputs`,
194    /// and the client passes it on as
195    /// [`TransactionSubmitError::InsufficientFunds`].
196    #[error("The primary module's balance cannot fund the transaction")]
197    InsufficientBalance(#[from] InsufficientBalanceError),
198
199    /// The module does not implement the operation.
200    ///
201    /// The default bodies of `backup`, `create_final_inputs_and_outputs`,
202    /// `await_primary_module_output`, `leave` and `ClientModuleInit::recover`
203    /// report this. The client only asks a module for an operation the module
204    /// declares support for, through `supports_backup`,
205    /// `supports_being_primary` or `recovery_mode`, so the client sees this
206    /// only from a module that declares support it does not implement.
207    /// Calling the operation on a module directly can see it too. The default
208    /// bodies of `handle_cli_command` and `handle_rpc` report it as well, to a
209    /// caller that sends a command or a request to a module that has none.
210    #[error("Module {kind} does not implement {operation}")]
211    Unsupported {
212        /// The kind of the module.
213        kind: ModuleKind,
214        /// The name of the trait method the module does not implement.
215        operation: &'static str,
216    },
217
218    /// Any other failure, kept whole: the module's own error or the error of a
219    /// library it builds on, such as a federation, database or Bitcoin
220    /// backend failure. Its `Display` and `source()` are those of the error it
221    /// carries.
222    #[error(transparent)]
223    Other(Box<dyn std::error::Error + Send + Sync>),
224}
225
226impl ClientModuleError {
227    /// Wraps a failure the other variants do not describe. Accepts anything
228    /// convertible into a boxed error, which includes an `anyhow::Error` and a
229    /// plain message.
230    ///
231    /// A primary module's shortfall must be [`Self::InsufficientBalance`]
232    /// instead: the client does not look inside the error this carries.
233    pub fn other<E>(error: E) -> Self
234    where
235        E: Into<Box<dyn std::error::Error + Send + Sync>>,
236    {
237        Self::Other(error.into())
238    }
239}
240
241/// A failure to find a module able to serve a request.
242#[derive(Debug, Error)]
243#[non_exhaustive]
244pub enum ModuleLookupError {
245    /// The client was not built with a module of this kind, or the federation
246    /// does not offer one.
247    #[error("No module of kind {kind} found")]
248    NoModuleOfKind {
249        /// The kind that was asked for.
250        kind: ModuleKind,
251    },
252
253    /// The client has no module with this instance id.
254    #[error("Unknown module instance {instance_id}")]
255    UnknownInstance {
256        /// The instance id that was asked for.
257        instance_id: ModuleInstanceId,
258    },
259
260    /// The module instance exists, but is not of the requested type.
261    #[error("Module instance {instance_id} is not of type {expected}")]
262    WrongModuleType {
263        /// The instance that was asked for.
264        instance_id: ModuleInstanceId,
265        /// The Rust type the caller asked the instance to be.
266        expected: &'static str,
267    },
268
269    /// No primary module can hold funds of this unit.
270    #[error("No primary module for unit {unit}")]
271    NoPrimaryModule {
272        /// The unit that has no primary module.
273        unit: AmountUnit,
274    },
275
276    /// None of the primary modules for this unit is of the requested kind.
277    #[error("No primary {kind} module for unit {unit}")]
278    NoPrimaryModuleOfKind {
279        /// The kind of module that was asked for.
280        kind: ModuleKind,
281        /// The unit whose primary modules were searched.
282        unit: AmountUnit,
283    },
284}
285
286/// The client and the federation's peers share no core API version.
287///
288/// Module version mismatches are not an error: a module whose versions do not
289/// line up is left out of the negotiated set and stays unusable until one side
290/// is upgraded.
291#[derive(Debug, Error)]
292#[error("Could not find a common core API version")]
293pub struct ApiVersionDiscoveryError;
294
295/// A failure to fetch the federation's meta fields.
296///
297/// The built-in sources produce the specific variants. A [`MetaSource`]
298/// implemented elsewhere reports anything they do not describe through
299/// [`Custom`].
300///
301/// [`MetaSource`]: crate::meta::MetaSource
302/// [`Custom`]: MetaFetchError::Custom
303#[derive(Debug, Error)]
304#[non_exhaustive]
305pub enum MetaFetchError {
306    /// The meta override URL could not be read from the client config.
307    #[error("Failed to read the meta override URL from the client config")]
308    Config(#[from] ModuleConfigError),
309
310    /// The meta override source could not be reached, or its body could not
311    /// be read.
312    #[error("The meta override source could not be fetched")]
313    Http(#[from] reqwest::Error),
314
315    /// The meta override source answered with a non-success status.
316    #[error("The meta override source answered with status {status}")]
317    Status {
318        /// The status the source answered with.
319        status: reqwest::StatusCode,
320    },
321
322    /// The meta override source's body is not the expected JSON.
323    #[error("The meta override source returned invalid JSON")]
324    Json(#[from] serde_json::Error),
325
326    /// The meta override source has no entry for this federation.
327    #[error("The meta override source has no entry for federation {federation_id}")]
328    NoEntry {
329        /// The federation that was looked up.
330        federation_id: FederationId,
331    },
332
333    /// A meta source implemented outside this crate failed in a way the other
334    /// variants do not describe.
335    #[error("The meta source failed")]
336    Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
337}
338
339#[cfg(test)]
340mod tests;