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