kaccy_bitcoin/
error.rs

1//! Bitcoin error types
2
3use bitcoin::Network;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum BitcoinError {
8    /// Bitcoin Core RPC communication error
9    #[error("RPC error: {0}")]
10    Rpc(#[from] bitcoincore_rpc::Error),
11
12    /// Address generation failed
13    #[error("Address generation error: {0}")]
14    AddressGeneration(String),
15
16    /// Transaction not found in the blockchain
17    #[error("Transaction not found: {0}")]
18    TransactionNotFound(String),
19
20    /// Transaction does not have enough confirmations
21    #[error("Insufficient confirmations: {current}/{required}")]
22    InsufficientConfirmations { current: u32, required: u32 },
23
24    /// Invalid Bitcoin address format
25    #[error("Invalid address: {0}")]
26    InvalidAddress(String),
27
28    /// Network mismatch between address and configured network
29    #[error(
30        "Network mismatch: address is for {address_network:?} but client is configured for {configured_network:?}"
31    )]
32    NetworkMismatch {
33        address_network: Network,
34        configured_network: Network,
35    },
36
37    /// Connection to Bitcoin Core failed
38    #[error("Connection failed: {0}")]
39    ConnectionFailed(String),
40
41    /// Connection timeout
42    #[error("Connection timeout after {timeout_secs} seconds")]
43    ConnectionTimeout { timeout_secs: u64 },
44
45    /// Connection pool exhausted
46    #[error("Connection pool exhausted: all connections are in use")]
47    ConnectionPoolExhausted,
48
49    /// Wallet operation failed
50    #[error("Wallet error: {0}")]
51    Wallet(String),
52
53    /// Payment amount mismatch
54    #[error("Payment amount mismatch: expected {expected} sats, received {received} sats")]
55    PaymentMismatch { expected: u64, received: u64 },
56
57    /// Underpayment detected
58    #[error(
59        "Underpayment: expected {expected} sats, received {received} sats (short by {shortfall} sats)"
60    )]
61    Underpayment {
62        expected: u64,
63        received: u64,
64        shortfall: u64,
65    },
66
67    /// Overpayment detected
68    #[error(
69        "Overpayment: expected {expected} sats, received {received} sats (excess {excess} sats)"
70    )]
71    Overpayment {
72        expected: u64,
73        received: u64,
74        excess: u64,
75    },
76
77    /// Transaction was replaced (RBF)
78    #[error("Transaction replaced: original {original_txid}, replacement {replacement_txid}")]
79    TransactionReplaced {
80        original_txid: String,
81        replacement_txid: String,
82    },
83
84    /// Fee estimation failed
85    #[error("Fee estimation failed for target {target_blocks} blocks: {reason}")]
86    FeeEstimationFailed { target_blocks: u32, reason: String },
87
88    /// UTXO not found
89    #[error("UTXO not found: {txid}:{vout}")]
90    UtxoNotFound { txid: String, vout: u32 },
91
92    /// Transaction broadcast failed
93    #[error("Transaction broadcast failed: {0}")]
94    BroadcastFailed(String),
95
96    /// Block reorganization detected
97    #[error(
98        "Block reorganization detected at height {height}: expected {expected_hash}, got {actual_hash}"
99    )]
100    Reorganization {
101        height: u64,
102        expected_hash: String,
103        actual_hash: String,
104    },
105
106    /// Order not found for payment
107    #[error("No order found for payment address: {address}")]
108    OrderNotFound { address: String },
109
110    /// Payment expired
111    #[error(
112        "Payment expired: order was created at {created_at} and expired after {expiry_hours} hours"
113    )]
114    PaymentExpired {
115        created_at: String,
116        expiry_hours: u32,
117    },
118
119    /// Invalid transaction format
120    #[error("Invalid transaction: {0}")]
121    InvalidTransaction(String),
122
123    /// Mempool rejection
124    #[error("Transaction rejected by mempool: {reason}")]
125    MempoolRejection { reason: String },
126
127    /// Invalid extended public key (xpub/zpub)
128    #[error("Invalid xpub: {0}")]
129    InvalidXpub(String),
130
131    /// HD wallet derivation failed
132    #[error("Derivation failed: {0}")]
133    DerivationFailed(String),
134
135    /// Address not found in wallet
136    #[error("Address not found in wallet: {0}")]
137    AddressNotInWallet(String),
138
139    /// Transaction limit exceeded
140    #[error("Transaction limit exceeded: {0}")]
141    LimitExceeded(String),
142
143    /// Insufficient funds
144    #[error("Insufficient funds: {0}")]
145    InsufficientFunds(String),
146
147    /// Generic RPC error (for string-based errors)
148    #[error("RPC error: {0}")]
149    RpcError(String),
150
151    /// Validation error
152    #[error("Validation error: {0}")]
153    Validation(String),
154
155    /// PSBT error
156    #[error("PSBT error: {0}")]
157    Psbt(String),
158
159    /// Resource not found
160    #[error("Not found: {0}")]
161    NotFound(String),
162
163    /// Invalid input
164    #[error("Invalid input: {0}")]
165    InvalidInput(String),
166}
167
168impl From<bitcoin::psbt::Error> for BitcoinError {
169    fn from(err: bitcoin::psbt::Error) -> Self {
170        BitcoinError::Psbt(err.to_string())
171    }
172}
173
174impl From<bitcoin::address::ParseError> for BitcoinError {
175    fn from(err: bitcoin::address::ParseError) -> Self {
176        BitcoinError::InvalidAddress(err.to_string())
177    }
178}
179
180impl From<bitcoin::consensus::encode::Error> for BitcoinError {
181    fn from(err: bitcoin::consensus::encode::Error) -> Self {
182        BitcoinError::InvalidTransaction(err.to_string())
183    }
184}
185
186impl BitcoinError {
187    /// Check if this error is recoverable (can be retried)
188    pub fn is_recoverable(&self) -> bool {
189        matches!(
190            self,
191            BitcoinError::ConnectionFailed(_)
192                | BitcoinError::ConnectionTimeout { .. }
193                | BitcoinError::InsufficientConfirmations { .. }
194        )
195    }
196
197    /// Check if this error indicates a payment issue
198    pub fn is_payment_error(&self) -> bool {
199        matches!(
200            self,
201            BitcoinError::PaymentMismatch { .. }
202                | BitcoinError::Underpayment { .. }
203                | BitcoinError::Overpayment { .. }
204                | BitcoinError::PaymentExpired { .. }
205        )
206    }
207
208    /// Create an underpayment error
209    pub fn underpayment(expected: u64, received: u64) -> Self {
210        BitcoinError::Underpayment {
211            expected,
212            received,
213            shortfall: expected.saturating_sub(received),
214        }
215    }
216
217    /// Create an overpayment error
218    pub fn overpayment(expected: u64, received: u64) -> Self {
219        BitcoinError::Overpayment {
220            expected,
221            received,
222            excess: received.saturating_sub(expected),
223        }
224    }
225}
226
227pub type Result<T> = std::result::Result<T, BitcoinError>;