1use bitcoin::Network;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum BitcoinError {
9 #[error("RPC error: {0}")]
11 Rpc(#[from] bitcoincore_rpc::Error),
12
13 #[error("Address generation error: {0}")]
15 AddressGeneration(String),
16
17 #[error("Transaction not found: {0}")]
19 TransactionNotFound(String),
20
21 #[error("Insufficient confirmations: {current}/{required}")]
23 InsufficientConfirmations {
24 current: u32,
26 required: u32,
28 },
29
30 #[error("Invalid address: {0}")]
32 InvalidAddress(String),
33
34 #[error(
36 "Network mismatch: address is for {address_network:?} but client is configured for {configured_network:?}"
37 )]
38 NetworkMismatch {
39 address_network: Network,
41 configured_network: Network,
43 },
44
45 #[error("Connection failed: {0}")]
47 ConnectionFailed(String),
48
49 #[error("Connection timeout after {timeout_secs} seconds")]
51 ConnectionTimeout {
52 timeout_secs: u64,
54 },
55
56 #[error("Connection pool exhausted: all connections are in use")]
58 ConnectionPoolExhausted,
59
60 #[error("Wallet error: {0}")]
62 Wallet(String),
63
64 #[error("Payment amount mismatch: expected {expected} sats, received {received} sats")]
66 PaymentMismatch {
67 expected: u64,
69 received: u64,
71 },
72
73 #[error(
75 "Underpayment: expected {expected} sats, received {received} sats (short by {shortfall} sats)"
76 )]
77 Underpayment {
78 expected: u64,
80 received: u64,
82 shortfall: u64,
84 },
85
86 #[error(
88 "Overpayment: expected {expected} sats, received {received} sats (excess {excess} sats)"
89 )]
90 Overpayment {
91 expected: u64,
93 received: u64,
95 excess: u64,
97 },
98
99 #[error("Transaction replaced: original {original_txid}, replacement {replacement_txid}")]
101 TransactionReplaced {
102 original_txid: String,
104 replacement_txid: String,
106 },
107
108 #[error("Fee estimation failed for target {target_blocks} blocks: {reason}")]
110 FeeEstimationFailed {
111 target_blocks: u32,
113 reason: String,
115 },
116
117 #[error("UTXO not found: {txid}:{vout}")]
119 UtxoNotFound {
120 txid: String,
122 vout: u32,
124 },
125
126 #[error("Transaction broadcast failed: {0}")]
128 BroadcastFailed(String),
129
130 #[error(
132 "Block reorganization detected at height {height}: expected {expected_hash}, got {actual_hash}"
133 )]
134 Reorganization {
135 height: u64,
137 expected_hash: String,
139 actual_hash: String,
141 },
142
143 #[error("No order found for payment address: {address}")]
145 OrderNotFound {
146 address: String,
148 },
149
150 #[error(
152 "Payment expired: order was created at {created_at} and expired after {expiry_hours} hours"
153 )]
154 PaymentExpired {
155 created_at: String,
157 expiry_hours: u32,
159 },
160
161 #[error("Invalid transaction: {0}")]
163 InvalidTransaction(String),
164
165 #[error("Transaction rejected by mempool: {reason}")]
167 MempoolRejection {
168 reason: String,
170 },
171
172 #[error("Invalid xpub: {0}")]
174 InvalidXpub(String),
175
176 #[error("Derivation failed: {0}")]
178 DerivationFailed(String),
179
180 #[error("Address not found in wallet: {0}")]
182 AddressNotInWallet(String),
183
184 #[error("Transaction limit exceeded: {0}")]
186 LimitExceeded(String),
187
188 #[error("Insufficient funds: {0}")]
190 InsufficientFunds(String),
191
192 #[error("RPC error: {0}")]
194 RpcError(String),
195
196 #[error("Validation error: {0}")]
198 Validation(String),
199
200 #[error("PSBT error: {0}")]
202 Psbt(String),
203
204 #[error("Not found: {0}")]
206 NotFound(String),
207
208 #[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 pub fn is_recoverable(&self) -> bool {
234 matches!(
235 self,
236 BitcoinError::ConnectionFailed(_)
237 | BitcoinError::ConnectionTimeout { .. }
238 | BitcoinError::InsufficientConfirmations { .. }
239 )
240 }
241
242 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 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 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
272pub type Result<T> = std::result::Result<T, BitcoinError>;