1use bitcoin::Network;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum BitcoinError {
8 #[error("RPC error: {0}")]
10 Rpc(#[from] bitcoincore_rpc::Error),
11
12 #[error("Address generation error: {0}")]
14 AddressGeneration(String),
15
16 #[error("Transaction not found: {0}")]
18 TransactionNotFound(String),
19
20 #[error("Insufficient confirmations: {current}/{required}")]
22 InsufficientConfirmations { current: u32, required: u32 },
23
24 #[error("Invalid address: {0}")]
26 InvalidAddress(String),
27
28 #[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 #[error("Connection failed: {0}")]
39 ConnectionFailed(String),
40
41 #[error("Connection timeout after {timeout_secs} seconds")]
43 ConnectionTimeout { timeout_secs: u64 },
44
45 #[error("Connection pool exhausted: all connections are in use")]
47 ConnectionPoolExhausted,
48
49 #[error("Wallet error: {0}")]
51 Wallet(String),
52
53 #[error("Payment amount mismatch: expected {expected} sats, received {received} sats")]
55 PaymentMismatch { expected: u64, received: u64 },
56
57 #[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 #[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 #[error("Transaction replaced: original {original_txid}, replacement {replacement_txid}")]
79 TransactionReplaced {
80 original_txid: String,
81 replacement_txid: String,
82 },
83
84 #[error("Fee estimation failed for target {target_blocks} blocks: {reason}")]
86 FeeEstimationFailed { target_blocks: u32, reason: String },
87
88 #[error("UTXO not found: {txid}:{vout}")]
90 UtxoNotFound { txid: String, vout: u32 },
91
92 #[error("Transaction broadcast failed: {0}")]
94 BroadcastFailed(String),
95
96 #[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 #[error("No order found for payment address: {address}")]
108 OrderNotFound { address: String },
109
110 #[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 #[error("Invalid transaction: {0}")]
121 InvalidTransaction(String),
122
123 #[error("Transaction rejected by mempool: {reason}")]
125 MempoolRejection { reason: String },
126
127 #[error("Invalid xpub: {0}")]
129 InvalidXpub(String),
130
131 #[error("Derivation failed: {0}")]
133 DerivationFailed(String),
134
135 #[error("Address not found in wallet: {0}")]
137 AddressNotInWallet(String),
138
139 #[error("Transaction limit exceeded: {0}")]
141 LimitExceeded(String),
142
143 #[error("Insufficient funds: {0}")]
145 InsufficientFunds(String),
146
147 #[error("RPC error: {0}")]
149 RpcError(String),
150
151 #[error("Validation error: {0}")]
153 Validation(String),
154
155 #[error("PSBT error: {0}")]
157 Psbt(String),
158
159 #[error("Not found: {0}")]
161 NotFound(String),
162
163 #[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 pub fn is_recoverable(&self) -> bool {
189 matches!(
190 self,
191 BitcoinError::ConnectionFailed(_)
192 | BitcoinError::ConnectionTimeout { .. }
193 | BitcoinError::InsufficientConfirmations { .. }
194 )
195 }
196
197 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 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 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>;