Skip to main content

kaccy_bitcoin/
error.rs

1//! Bitcoin error types
2
3use bitcoin::Network;
4use thiserror::Error;
5
6/// Top-level error type for all kaccy-bitcoin operations.
7#[derive(Error, Debug)]
8pub enum BitcoinError {
9    /// Bitcoin Core RPC communication error
10    #[error("RPC error: {0}")]
11    Rpc(#[from] bitcoincore_rpc::Error),
12
13    /// Address generation failed
14    #[error("Address generation error: {0}")]
15    AddressGeneration(String),
16
17    /// Transaction not found in the blockchain
18    #[error("Transaction not found: {0}")]
19    TransactionNotFound(String),
20
21    /// Transaction does not have enough confirmations
22    #[error("Insufficient confirmations: {current}/{required}")]
23    InsufficientConfirmations {
24        /// Number of confirmations the transaction currently has.
25        current: u32,
26        /// Minimum number of confirmations required.
27        required: u32,
28    },
29
30    /// Invalid Bitcoin address format
31    #[error("Invalid address: {0}")]
32    InvalidAddress(String),
33
34    /// Network mismatch between address and configured network
35    #[error(
36        "Network mismatch: address is for {address_network:?} but client is configured for {configured_network:?}"
37    )]
38    NetworkMismatch {
39        /// The network encoded in the address (e.g. Testnet).
40        address_network: Network,
41        /// The network the client is configured for (e.g. Mainnet).
42        configured_network: Network,
43    },
44
45    /// Connection to Bitcoin Core failed
46    #[error("Connection failed: {0}")]
47    ConnectionFailed(String),
48
49    /// Connection timeout
50    #[error("Connection timeout after {timeout_secs} seconds")]
51    ConnectionTimeout {
52        /// Number of seconds elapsed before the timeout was triggered.
53        timeout_secs: u64,
54    },
55
56    /// Connection pool exhausted
57    #[error("Connection pool exhausted: all connections are in use")]
58    ConnectionPoolExhausted,
59
60    /// Wallet operation failed
61    #[error("Wallet error: {0}")]
62    Wallet(String),
63
64    /// Payment amount mismatch
65    #[error("Payment amount mismatch: expected {expected} sats, received {received} sats")]
66    PaymentMismatch {
67        /// Expected payment amount in satoshis.
68        expected: u64,
69        /// Actual received amount in satoshis.
70        received: u64,
71    },
72
73    /// Underpayment detected
74    #[error(
75        "Underpayment: expected {expected} sats, received {received} sats (short by {shortfall} sats)"
76    )]
77    Underpayment {
78        /// Expected payment amount in satoshis.
79        expected: u64,
80        /// Actual received amount in satoshis.
81        received: u64,
82        /// Shortfall amount in satoshis (`expected - received`).
83        shortfall: u64,
84    },
85
86    /// Overpayment detected
87    #[error(
88        "Overpayment: expected {expected} sats, received {received} sats (excess {excess} sats)"
89    )]
90    Overpayment {
91        /// Expected payment amount in satoshis.
92        expected: u64,
93        /// Actual received amount in satoshis.
94        received: u64,
95        /// Excess amount in satoshis (`received - expected`).
96        excess: u64,
97    },
98
99    /// Transaction was replaced (RBF)
100    #[error("Transaction replaced: original {original_txid}, replacement {replacement_txid}")]
101    TransactionReplaced {
102        /// Txid of the original transaction that was replaced.
103        original_txid: String,
104        /// Txid of the RBF replacement transaction.
105        replacement_txid: String,
106    },
107
108    /// Fee estimation failed
109    #[error("Fee estimation failed for target {target_blocks} blocks: {reason}")]
110    FeeEstimationFailed {
111        /// Confirmation target (number of blocks) used for the fee estimate.
112        target_blocks: u32,
113        /// Human-readable description of why the estimate failed.
114        reason: String,
115    },
116
117    /// UTXO not found
118    #[error("UTXO not found: {txid}:{vout}")]
119    UtxoNotFound {
120        /// Transaction ID of the missing UTXO.
121        txid: String,
122        /// Output index within the transaction.
123        vout: u32,
124    },
125
126    /// Transaction broadcast failed
127    #[error("Transaction broadcast failed: {0}")]
128    BroadcastFailed(String),
129
130    /// Block reorganization detected
131    #[error(
132        "Block reorganization detected at height {height}: expected {expected_hash}, got {actual_hash}"
133    )]
134    Reorganization {
135        /// Block height at which the reorganization was detected.
136        height: u64,
137        /// Block hash that was expected (previously known tip or ancestor).
138        expected_hash: String,
139        /// Block hash that was actually observed on the new chain.
140        actual_hash: String,
141    },
142
143    /// Order not found for payment
144    #[error("No order found for payment address: {address}")]
145    OrderNotFound {
146        /// Bitcoin address for which no matching order was found.
147        address: String,
148    },
149
150    /// Payment expired
151    #[error(
152        "Payment expired: order was created at {created_at} and expired after {expiry_hours} hours"
153    )]
154    PaymentExpired {
155        /// ISO-8601 timestamp when the order was created.
156        created_at: String,
157        /// Configured expiry window in hours.
158        expiry_hours: u32,
159    },
160
161    /// Invalid transaction format
162    #[error("Invalid transaction: {0}")]
163    InvalidTransaction(String),
164
165    /// Mempool rejection
166    #[error("Transaction rejected by mempool: {reason}")]
167    MempoolRejection {
168        /// Rejection reason returned by the node (e.g. "mempool min fee not met").
169        reason: String,
170    },
171
172    /// Invalid extended public key (xpub/zpub)
173    #[error("Invalid xpub: {0}")]
174    InvalidXpub(String),
175
176    /// HD wallet derivation failed
177    #[error("Derivation failed: {0}")]
178    DerivationFailed(String),
179
180    /// Address not found in wallet
181    #[error("Address not found in wallet: {0}")]
182    AddressNotInWallet(String),
183
184    /// Transaction limit exceeded
185    #[error("Transaction limit exceeded: {0}")]
186    LimitExceeded(String),
187
188    /// Insufficient funds
189    #[error("Insufficient funds: {0}")]
190    InsufficientFunds(String),
191
192    /// Generic RPC error (for string-based errors)
193    #[error("RPC error: {0}")]
194    RpcError(String),
195
196    /// Validation error
197    #[error("Validation error: {0}")]
198    Validation(String),
199
200    /// PSBT error
201    #[error("PSBT error: {0}")]
202    Psbt(String),
203
204    /// Resource not found
205    #[error("Not found: {0}")]
206    NotFound(String),
207
208    /// Invalid input
209    #[error("Invalid input: {0}")]
210    InvalidInput(String),
211}
212
213impl From<bitcoin::psbt::Error> for BitcoinError {
214    fn from(err: bitcoin::psbt::Error) -> Self {
215        BitcoinError::Psbt(err.to_string())
216    }
217}
218
219impl From<bitcoin::address::ParseError> for BitcoinError {
220    fn from(err: bitcoin::address::ParseError) -> Self {
221        BitcoinError::InvalidAddress(err.to_string())
222    }
223}
224
225impl From<bitcoin::consensus::encode::Error> for BitcoinError {
226    fn from(err: bitcoin::consensus::encode::Error) -> Self {
227        BitcoinError::InvalidTransaction(err.to_string())
228    }
229}
230
231impl BitcoinError {
232    /// Check if this error is recoverable (can be retried)
233    pub fn is_recoverable(&self) -> bool {
234        matches!(
235            self,
236            BitcoinError::ConnectionFailed(_)
237                | BitcoinError::ConnectionTimeout { .. }
238                | BitcoinError::InsufficientConfirmations { .. }
239        )
240    }
241
242    /// Check if this error indicates a payment issue
243    pub fn is_payment_error(&self) -> bool {
244        matches!(
245            self,
246            BitcoinError::PaymentMismatch { .. }
247                | BitcoinError::Underpayment { .. }
248                | BitcoinError::Overpayment { .. }
249                | BitcoinError::PaymentExpired { .. }
250        )
251    }
252
253    /// Create an underpayment error
254    pub fn underpayment(expected: u64, received: u64) -> Self {
255        BitcoinError::Underpayment {
256            expected,
257            received,
258            shortfall: expected.saturating_sub(received),
259        }
260    }
261
262    /// Create an overpayment error
263    pub fn overpayment(expected: u64, received: u64) -> Self {
264        BitcoinError::Overpayment {
265            expected,
266            received,
267            excess: received.saturating_sub(expected),
268        }
269    }
270}
271
272/// Convenience `Result` alias that uses [`BitcoinError`] as the error type.
273pub type Result<T> = std::result::Result<T, BitcoinError>;