Skip to main content

fedimint_wallet_client/
error.rs

1//! Error types of the wallet client.
2//!
3//! Every failure this module reports to its callers is named here, so there is
4//! one place for an integrator to look.
5
6use bitcoin::Network;
7use fedimint_api_client::api::FederationError;
8use fedimint_bitcoind::BitcoinRpcError;
9use fedimint_client_module::error::{
10    OperationAlreadyExistsError, OperationLookupError, TransactionSubmitError,
11};
12use fedimint_core::core::OperationId;
13use fedimint_core::db::{AutocommitError, DatabaseError};
14#[cfg(feature = "uniffi")]
15use fedimint_core::util::FmtCompact as _;
16use thiserror::Error;
17
18use crate::client_db::TweakIdx;
19
20/// A failure to look up or drive one of this client's deposit addresses.
21///
22/// The peg-in side of the wallet is address-oriented: a caller names a deposit
23/// by its address, by the operation that allocated it, or by the tweak index
24/// behind both, and then waits for the federation to claim what was sent
25/// there. Every way that can go wrong is named here.
26#[derive(Debug, Error)]
27#[non_exhaustive]
28pub enum PegInError {
29    /// The address is not one this client derived.
30    #[error("The address is not one of this client's deposit addresses")]
31    AddressNotDerived,
32
33    /// No deposit address was allocated under this operation.
34    #[error("No deposit address belongs to operation {}", .operation_id.fmt_short())]
35    NoAddressForOperation {
36        /// The operation that was looked up.
37        operation_id: OperationId,
38    },
39
40    /// The client has no record of this deposit address index.
41    #[error("No deposit address is recorded for {tweak_idx}")]
42    TweakIdxNotFound {
43        /// The index that was looked up.
44        tweak_idx: TweakIdx,
45    },
46
47    /// A database operation failed.
48    #[error("Database error")]
49    Database(#[from] DatabaseError),
50
51    /// The federation rejected the transaction that would have claimed the
52    /// deposit.
53    ///
54    /// The payload is the message the submission recorded rather than an error
55    /// value, so it is part of this error's own message.
56    #[error("The transaction claiming the deposit was rejected: {reason}")]
57    TransactionRejected {
58        /// What the submission reported.
59        reason: String,
60    },
61
62    /// The peg-in monitor stopped, so no further deposit will ever be claimed.
63    #[error("The peg-in monitor is no longer running")]
64    MonitorStopped,
65}
66
67impl From<AutocommitError<PegInError>> for PegInError {
68    fn from(e: AutocommitError<Self>) -> Self {
69        match e {
70            AutocommitError::ClosureError { error, .. } => error,
71            AutocommitError::CommitFailed { last_error, .. } => Self::Database(last_error),
72        }
73    }
74}
75
76/// A failure to hand out a deposit address.
77///
78/// Covers both the plain allocation and the pooled one, which can also lose a
79/// race against a deposit landing on the address it was about to reuse.
80#[derive(Debug, Error)]
81#[non_exhaustive]
82pub enum DepositAddressError {
83    /// The client has never been online to confirm that the federation's
84    /// wallet module handles every deposit safely.
85    #[error("The federation was not verified to support safe deposits")]
86    SafeDepositUnverified,
87
88    /// An operation for this deposit address already exists.
89    #[error("The deposit address's operation already exists")]
90    OperationAlreadyExists(#[from] OperationAlreadyExistsError),
91
92    /// The bitcoin backend would not start watching the address, so a deposit
93    /// to it would never be noticed.
94    #[error("The bitcoin backend could not watch the deposit address")]
95    BitcoinRpc(#[from] BitcoinRpcError),
96
97    /// The deposit address could not be written to the database.
98    #[error("Database error")]
99    Database(#[from] DatabaseError),
100
101    /// A pooled address vanished from the database between being offered for
102    /// reuse and being reused.
103    #[error("The pooled deposit address {tweak_idx} disappeared while it was being reused")]
104    PooledAddressDisappeared {
105        /// The address index that was being reused.
106        tweak_idx: TweakIdx,
107    },
108
109    /// A deposit landed on a pooled address between it being offered for reuse
110    /// and being reused, so it is no longer free.
111    #[error("The pooled deposit address {tweak_idx} was used while it was being reused")]
112    PooledAddressUsed {
113        /// The address index that was being reused.
114        tweak_idx: TweakIdx,
115    },
116}
117
118impl From<AutocommitError<DepositAddressError>> for DepositAddressError {
119    fn from(e: AutocommitError<Self>) -> Self {
120        match e {
121            AutocommitError::ClosureError { error, .. } => error,
122            AutocommitError::CommitFailed { last_error, .. } => Self::Database(last_error),
123        }
124    }
125}
126
127#[cfg(feature = "uniffi")]
128impl From<DepositAddressError> for fedimint_core::util::ffi::UniffiError {
129    fn from(e: DepositAddressError) -> Self {
130        Self::General(e.fmt_compact().to_string())
131    }
132}
133
134/// A failure to follow a deposit operation.
135#[derive(Debug, Error)]
136#[non_exhaustive]
137pub enum SubscribeDepositError {
138    /// The operation could not be looked up, or belongs to another module.
139    #[error("The deposit operation could not be looked up")]
140    Operation(#[from] OperationLookupError),
141
142    /// The operation exists and belongs to the wallet, but it is a withdrawal
143    /// (or an RBF bump of one) rather than a deposit.
144    #[error("The operation is not a deposit")]
145    NotADeposit,
146
147    /// The deposit address recorded with the operation is not valid on the
148    /// network this client is configured for.
149    #[error("The deposit address is not valid on {expected}")]
150    WrongNetwork {
151        /// The network this client expects.
152        expected: Network,
153    },
154
155    /// The deposit predates the 0.4 release, is still pending, and has no
156    /// state machine left to report progress from.
157    #[error("An old pending deposit cannot be subscribed to")]
158    OldPendingDeposit,
159
160    /// The deposit predates the 0.4 release and the outcome recorded for it is
161    /// not one of the final ones.
162    #[error("The recorded outcome of an old deposit is not final")]
163    NonFinalOutcome,
164}
165
166#[cfg(feature = "uniffi")]
167impl From<SubscribeDepositError> for fedimint_core::util::ffi::UniffiError {
168    fn from(e: SubscribeDepositError) -> Self {
169        Self::General(e.fmt_compact().to_string())
170    }
171}
172
173/// A failure to quote the on-chain fees of a peg-out.
174#[derive(Debug, Error)]
175#[non_exhaustive]
176pub enum WithdrawFeesError {
177    /// The federation could not be asked, or its guardians disagreed.
178    #[error("The peg-out fees could not be fetched")]
179    Federation(#[source] Box<FederationError>),
180
181    /// The federation was reached and agreed, but it has no quote to give for
182    /// this amount.
183    #[error("The federation did not quote peg-out fees")]
184    NoQuote,
185}
186
187impl From<FederationError> for WithdrawFeesError {
188    fn from(source: FederationError) -> Self {
189        Self::Federation(Box::new(source))
190    }
191}
192
193/// A failure to work out the largest amount a "withdraw everything" sweep can
194/// send.
195#[derive(Debug, Error)]
196#[non_exhaustive]
197pub enum MaxWithdrawableAmountError {
198    /// The on-chain fees the answer is computed against could not be quoted.
199    #[error("The peg-out fees could not be quoted")]
200    Fees(#[from] WithdrawFeesError),
201
202    /// The balance cannot cover the destination's dust limit plus the fees, so
203    /// there is no amount to withdraw.
204    #[error("The balance {balance} is too low to cover the dust limit {dust_limit} and fees")]
205    BalanceTooLow {
206        /// The balance the sweep was computed against.
207        balance: fedimint_core::Amount,
208        /// The destination's dust limit, the smallest output it can receive.
209        dust_limit: bitcoin::Amount,
210    },
211
212    /// The fee probe failed for a reason unrelated to the balance.
213    #[error("The fee quote for the withdrawal failed")]
214    Quote(#[source] TransactionSubmitError),
215}
216
217/// A failure to start an on-chain withdrawal from a peg-out request.
218#[derive(Debug, Error)]
219#[non_exhaustive]
220pub enum PegOutError {
221    /// The destination address is not valid on the network this client is
222    /// configured for.
223    #[error("The destination address is not valid on {expected}")]
224    WrongNetwork {
225        /// The network this client expects.
226        expected: Network,
227    },
228
229    /// The on-chain fees of the withdrawal could not be quoted.
230    #[error("The peg-out fees could not be quoted")]
231    Fees(#[from] WithdrawFeesError),
232
233    /// The withdrawal transaction could not be built or submitted.
234    #[error("The withdrawal transaction could not be submitted")]
235    Transaction(#[from] TransactionSubmitError),
236}
237
238#[cfg(feature = "uniffi")]
239impl From<PegOutError> for fedimint_core::util::ffi::UniffiError {
240    fn from(e: PegOutError) -> Self {
241        Self::General(e.fmt_compact().to_string())
242    }
243}
244
245/// A failure to follow a withdrawal operation.
246#[derive(Debug, Error)]
247#[non_exhaustive]
248pub enum SubscribeWithdrawError {
249    /// The operation could not be looked up, or belongs to another module.
250    #[error("The withdrawal operation could not be looked up")]
251    Operation(#[from] OperationLookupError),
252
253    /// The operation exists and belongs to the wallet, but it is a deposit
254    /// rather than a withdrawal.
255    #[error("The operation is not a withdrawal")]
256    NotAWithdrawal,
257}
258
259#[cfg(feature = "uniffi")]
260impl From<SubscribeWithdrawError> for fedimint_core::util::ffi::UniffiError {
261    fn from(e: SubscribeWithdrawError) -> Self {
262        Self::General(e.fmt_compact().to_string())
263    }
264}
265
266/// A failure to vote for activating the next wallet module consensus version.
267#[derive(Debug, Error)]
268#[non_exhaustive]
269pub enum ConsensusVersionVotingError {
270    /// Voting is a guardian action and this client holds no admin
271    /// credentials.
272    #[error("Admin auth is not set")]
273    AdminAuthMissing,
274
275    /// The vote could not be submitted to the federation.
276    #[error("The vote could not be submitted to the federation")]
277    Federation(#[source] Box<FederationError>),
278}
279
280impl From<FederationError> for ConsensusVersionVotingError {
281    fn from(source: FederationError) -> Self {
282        Self::Federation(Box::new(source))
283    }
284}