Skip to main content

cdk_bdk/
error.rs

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