Skip to main content

cdk_bdk/
error.rs

1//! CDK BDK onchain backend errors
2
3use std::path::PathBuf;
4
5use cdk_common::CurrencyUnit;
6use thiserror::Error;
7use uuid::Uuid;
8
9/// CDK BDK onchain backend error
10#[derive(Debug, Error)]
11pub enum Error {
12    /// Fee estimation failed
13    #[error("Fee estimation failed: {0}")]
14    FeeEstimationFailed(String),
15    /// Arithmetic overflow during fee estimation
16    #[error("Fee estimation overflow")]
17    FeeEstimationOverflow,
18    /// Fee estimation unavailable
19    #[error("Fee estimation unavailable")]
20    FeeEstimationUnavailable,
21    /// Wallet has no spendable UTXOs available for an onchain quote
22    #[error("No spendable UTXOs available for onchain payment quote")]
23    NoSpendableUtxos,
24    /// Start called but tasks are already running
25    #[error("Start called but background tasks are already running")]
26    AlreadyStarted,
27
28    /// Invalid backend configuration
29    #[error("Invalid configuration: {0}")]
30    InvalidConfig(String),
31
32    /// Unsupported payment type for onchain backend
33    #[error("Unsupported payment type for onchain backend")]
34    UnsupportedOnchain,
35
36    /// Wallet selected a `fee_index` outside the configured BDK fee options.
37    #[error("unknown fee_index {0}; expected one of the configured BDK fee options")]
38    UnknownFeeIndex(u32),
39
40    /// JSON error
41    #[error("JSON error: {0}")]
42    Json(#[from] serde_json::Error),
43
44    /// Amount conversion error
45    #[error("Amount conversion error: {0}")]
46    AmountConversion(#[from] cdk_common::amount::Error),
47
48    /// A typed amount does not match the payment unit supplied to the backend.
49    #[error("Amount unit {actual} does not match payment unit {expected}")]
50    AmountUnitMismatch {
51        /// Unit supplied to the payment backend.
52        expected: CurrencyUnit,
53        /// Unit carried by the typed amount.
54        actual: CurrencyUnit,
55    },
56
57    /// An onchain payment amount cannot be represented as whole satoshis.
58    #[error("Onchain payment amount {amount_msat} msat is not a whole number of satoshis")]
59    FractionalSatoshiAmount {
60        /// Requested payment amount in millisatoshis.
61        amount_msat: u64,
62    },
63
64    /// Database error
65    #[error("Database error: {0}")]
66    Database(#[from] bdk_wallet::rusqlite::Error),
67
68    /// An existing-mint migration did not find the persisted BDK wallet.
69    #[error("Persisted BDK wallet database is missing at {path}")]
70    ExistingWalletMissing {
71        /// Expected BDK wallet database path.
72        path: PathBuf,
73    },
74
75    /// A BDK SQLite file exists but does not contain an initialized wallet.
76    #[error("Persisted BDK wallet database at {path} does not contain an initialized wallet")]
77    ExistingWalletNotInitialized {
78        /// BDK wallet database path.
79        path: PathBuf,
80    },
81
82    /// Wallet error
83    #[error("Wallet error: {0}")]
84    Wallet(String),
85
86    /// Bitcoin RPC error
87    #[cfg(feature = "bitcoin-rpc")]
88    #[error("Bitcoin RPC error: {0}")]
89    BitcoinRpc(#[from] bdk_bitcoind_rpc::bitcoincore_rpc::Error),
90
91    /// The Bitcoin Core chain tip could not be determined for a fresh wallet.
92    #[cfg(feature = "bitcoin-rpc")]
93    #[error("Failed to determine the Bitcoin Core chain tip for a fresh wallet: {source}")]
94    ChainTipFetchFailed {
95        /// Underlying Bitcoin Core RPC error.
96        #[source]
97        source: bdk_bitcoind_rpc::bitcoincore_rpc::Error,
98    },
99
100    /// The configured fresh-wallet rescan height is above the current chain tip.
101    #[error("Wallet rescan height {requested} is above the current chain tip {tip}")]
102    WalletRescanHeightTooHigh {
103        /// Configured rescan height.
104        requested: u32,
105        /// Current Bitcoin Core chain tip.
106        tip: u32,
107    },
108
109    /// Esplora error
110    #[error("Esplora error: {0}")]
111    Esplora(String),
112
113    /// Electrum error
114    #[cfg(feature = "electrum")]
115    #[error("Electrum error: {0}")]
116    Electrum(String),
117
118    /// Bip32 key derivation error
119    #[error("Bip32 key derivation error: {0}")]
120    Bip32(#[from] bdk_wallet::bitcoin::bip32::Error),
121
122    /// Key derivation error
123    #[error("Key derivation error: {0}")]
124    KeyDerivation(#[from] bdk_wallet::keys::KeyError),
125
126    /// Could not sign transaction
127    #[error("Could not sign transaction")]
128    CouldNotSign,
129
130    /// Path error
131    #[error("Path error")]
132    Path,
133
134    /// IO error
135    #[error("IO error: {0}")]
136    Io(#[from] std::io::Error),
137
138    /// KV Store error
139    #[error("KV Store error: {0}")]
140    KvStore(#[from] cdk_common::database::Error),
141
142    /// Could not find matching output vout in transaction
143    #[error("Could not find matching output vout in transaction")]
144    VoutNotFound,
145
146    /// Send intent not found in storage
147    #[error("Send intent not found: {0}")]
148    SendIntentNotFound(Uuid),
149
150    /// Send batch not found in storage
151    #[error("Send batch not found: {0}")]
152    SendBatchNotFound(Uuid),
153
154    /// Send intent with quote id already exists in storage
155    #[error("Send intent already exists for quote id: {0}")]
156    DuplicateQuoteId(String),
157
158    /// Batch fee exceeds the combined max fee of all included intents
159    #[error("Batch fee {actual_fee} exceeds combined max fee {max_fee}")]
160    BatchFeeTooHigh {
161        /// Actual transaction fee in sats
162        actual_fee: u64,
163        /// Maximum combined fee from included intents
164        max_fee: u64,
165    },
166
167    /// Current fee estimate exceeds the max fee accepted by a melt quote.
168    #[error("Estimated fee {estimated_fee} exceeds max fee {max_fee}")]
169    EstimatedFeeTooHigh {
170        /// Current estimated fee reserve in sats
171        estimated_fee: u64,
172        /// Maximum fee accepted by the quote in sats
173        max_fee: u64,
174    },
175
176    /// No valid fee allocation exists for the batch
177    #[error("No valid fee allocation for batch")]
178    NoValidFeeAllocation,
179
180    /// Requested recipient output is below the dust limit for its script type
181    #[error("Requested output amount {amount} sats is below dust limit {dust_limit} sats")]
182    DustOutput {
183        /// Requested recipient amount in sats
184        amount: u64,
185        /// Minimum non-dust amount for the destination script in sats
186        dust_limit: u64,
187    },
188
189    /// Requested send amount is below the backend's configured minimum.
190    #[error("Requested send amount {amount} sats is below minimum {min} sats")]
191    AmountBelowMinimumSend {
192        /// Requested recipient amount in sats
193        amount: u64,
194        /// Configured minimum send amount in sats
195        min: u64,
196    },
197
198    /// Batch record is missing an output assignment for one of its member intents.
199    ///
200    /// This indicates a persistence invariant violation: every intent ID listed
201    /// in a Signed/Broadcast batch must have a corresponding assignment entry.
202    #[error("Batch {batch_id} is missing an output assignment for intent {intent_id}")]
203    BatchAssignmentMissing {
204        /// Batch that is missing the assignment
205        batch_id: Uuid,
206        /// Intent with no assignment entry
207        intent_id: Uuid,
208    },
209
210    /// Receive intent not found in storage
211    #[error("Receive intent not found: {0}")]
212    ReceiveIntentNotFound(Uuid),
213
214    /// Receive address not found in storage
215    #[error("Receive address not found: {0}")]
216    ReceiveAddressNotFound(String),
217
218    /// No unreserved receive address was found within the retry limit.
219    #[error("Could not reserve a fresh receive address after {attempts} attempts")]
220    ReceiveAddressReservationExhausted {
221        /// Number of addresses derived before giving up.
222        attempts: usize,
223    },
224
225    /// Database
226    #[error("Database error")]
227    BdkPersist,
228}
229
230impl From<Error> for cdk_common::payment::Error {
231    fn from(e: Error) -> Self {
232        Self::Onchain(Box::new(e))
233    }
234}
235
236impl Error {
237    /// Returns `true` when the error is a transient network / upstream
238    /// condition that is expected to resolve on retry.
239    ///
240    /// This is used by the sync supervisor to decide whether to continue
241    /// retrying on the next tick (transient) or to treat the failure as
242    /// part of the backoff/restart policy (non-transient).
243    pub fn is_transient(&self) -> bool {
244        match self {
245            // Chain-source I/O is always transient: network blips, reorg
246            // races, upstream 5xx, DNS/TLS timeouts, etc. The sync loop
247            // retries them on the next tick regardless of the specific
248            // sub-variant, so classifying the whole variant as transient
249            // is accurate for operational purposes.
250            #[cfg(feature = "bitcoin-rpc")]
251            Self::BitcoinRpc(_) => true,
252            #[cfg(feature = "electrum")]
253            Self::Electrum(_) => true,
254            Self::Esplora(_) => true,
255            Self::Io(e) => matches!(
256                e.kind(),
257                std::io::ErrorKind::TimedOut
258                    | std::io::ErrorKind::ConnectionRefused
259                    | std::io::ErrorKind::ConnectionReset
260                    | std::io::ErrorKind::ConnectionAborted
261                    | std::io::ErrorKind::NotConnected
262                    | std::io::ErrorKind::BrokenPipe
263                    | std::io::ErrorKind::Interrupted
264                    | std::io::ErrorKind::UnexpectedEof
265                    | std::io::ErrorKind::WouldBlock
266            ),
267            _ => false,
268        }
269    }
270}