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 is reported as
131 /// [`Self::InsufficientFunds`] instead.
132 // The boxed cause narrows to `ClientModuleError` once the module->client
133 // trait boundary is typed (#8821 part E).
134 #[error("The primary module failed")]
135 PrimaryModule(#[source] Box<dyn std::error::Error + Send + Sync>),
136
137 /// Writing the transaction to the database failed.
138 #[error("Database error")]
139 Database(#[from] DatabaseError),
140
141 /// The transaction's state machines could not be registered.
142 #[error("Failed to add the transaction's state machines")]
143 StateMachines(#[from] AddStateMachinesError),
144
145 /// The primary module holds too little balance to fund the transaction.
146 #[error("Insufficient funds")]
147 InsufficientFunds(#[from] InsufficientBalanceError),
148}
149
150impl TransactionSubmitError {
151 /// Whether this failure means the primary module cannot fund the
152 /// transaction, as opposed to a failure of the client, the database or
153 /// the federation.
154 pub fn is_insufficient_funds(&self) -> bool {
155 matches!(self, Self::InsufficientFunds(_))
156 }
157}
158
159/// A failure to find a module able to serve a request.
160#[derive(Debug, Error)]
161#[non_exhaustive]
162pub enum ModuleLookupError {
163 /// The client was not built with a module of this kind, or the federation
164 /// does not offer one.
165 #[error("No module of kind {kind} found")]
166 NoModuleOfKind {
167 /// The kind that was asked for.
168 kind: ModuleKind,
169 },
170
171 /// The client has no module with this instance id.
172 #[error("Unknown module instance {instance_id}")]
173 UnknownInstance {
174 /// The instance id that was asked for.
175 instance_id: ModuleInstanceId,
176 },
177
178 /// The module instance exists, but is not of the requested type.
179 #[error("Module instance {instance_id} is not of type {expected}")]
180 WrongModuleType {
181 /// The instance that was asked for.
182 instance_id: ModuleInstanceId,
183 /// The Rust type the caller asked the instance to be.
184 expected: &'static str,
185 },
186
187 /// No primary module can hold funds of this unit.
188 #[error("No primary module for unit {unit}")]
189 NoPrimaryModule {
190 /// The unit that has no primary module.
191 unit: AmountUnit,
192 },
193}
194
195#[cfg(feature = "uniffi")]
196impl From<ModuleLookupError> for fedimint_core::util::ffi::UniffiError {
197 fn from(e: ModuleLookupError) -> Self {
198 Self::General(e.to_string())
199 }
200}
201
202/// The client and the federation's peers share no core API version.
203///
204/// Module version mismatches are not an error: a module whose versions do not
205/// line up is left out of the negotiated set and stays unusable until one side
206/// is upgraded.
207#[derive(Debug, Error)]
208#[error("Could not find a common core API version")]
209pub struct ApiVersionDiscoveryError;
210
211/// A failure to fetch the federation's meta fields.
212///
213/// The built-in sources produce the specific variants. A [`MetaSource`]
214/// implemented elsewhere reports anything they do not describe through
215/// [`Custom`].
216///
217/// [`MetaSource`]: crate::meta::MetaSource
218/// [`Custom`]: MetaFetchError::Custom
219#[derive(Debug, Error)]
220#[non_exhaustive]
221pub enum MetaFetchError {
222 /// The meta override URL could not be read from the client config.
223 #[error("Failed to read the meta override URL from the client config")]
224 Config(#[from] ModuleConfigError),
225
226 /// The meta override source could not be reached, or its body could not
227 /// be read.
228 #[error("The meta override source could not be fetched")]
229 Http(#[from] reqwest::Error),
230
231 /// The meta override source answered with a non-success status.
232 #[error("The meta override source answered with status {status}")]
233 Status {
234 /// The status the source answered with.
235 status: reqwest::StatusCode,
236 },
237
238 /// The meta override source's body is not the expected JSON.
239 #[error("The meta override source returned invalid JSON")]
240 Json(#[from] serde_json::Error),
241
242 /// The meta override source has no entry for this federation.
243 #[error("The meta override source has no entry for federation {federation_id}")]
244 NoEntry {
245 /// The federation that was looked up.
246 federation_id: FederationId,
247 },
248
249 /// A meta source implemented outside this crate failed in a way the other
250 /// variants do not describe.
251 #[error("The meta source failed")]
252 Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
253}