Skip to main content

cdk_bdk/
error.rs

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