clementine_core/
errors.rs1use crate::{
64 actor::VerificationError,
65 builder::transaction::input::SpendableTxInError,
66 extended_bitcoin_rpc::BitcoinRPCError,
67 header_chain_prover::HeaderChainProverError,
68 rpc::{aggregator::AggregatorError, ParserError},
69};
70#[cfg(feature = "automation")]
71use crate::{states::StateMachineError, tx_sender::SendTxError};
72use bitcoin::{secp256k1::PublicKey, OutPoint, Txid, XOnlyPublicKey};
73use clap::builder::StyledStr;
74use core::fmt::Debug;
75use hex::FromHexError;
76use http::StatusCode;
77use thiserror::Error;
78use tonic::Status;
79
80pub use crate::builder::transaction::TxError;
81
82#[derive(Debug, Error)]
84#[non_exhaustive]
85pub enum BridgeError {
86 #[error("Header chain prover returned an error: {0}")]
87 Prover(#[from] HeaderChainProverError),
88 #[error("Failed to build transactions: {0}")]
89 Transaction(#[from] TxError),
90 #[cfg(feature = "automation")]
91 #[error("Failed to send transactions: {0}")]
92 SendTx(#[from] SendTxError),
93 #[error("Aggregator error: {0}")]
94 Aggregator(#[from] AggregatorError),
95 #[error("Failed to parse request: {0}")]
96 Parser(#[from] ParserError),
97 #[error("SpendableTxIn error: {0}")]
98 SpendableTxIn(#[from] SpendableTxInError),
99 #[error("Bitcoin RPC error: {0}")]
100 BitcoinRPC(#[from] BitcoinRPCError),
101 #[cfg(feature = "automation")]
102 #[error("State machine error: {0}")]
103 StateMachine(#[from] StateMachineError),
104 #[error("RPC authentication error: {0}")]
105 RPCAuthError(#[from] VerificationError),
106
107 #[error("Unsupported network")]
109 UnsupportedNetwork,
110 #[error("Invalid configuration: {0}")]
111 ConfigError(String),
112 #[error("Missing environment variable {1}: {0}")]
113 EnvVarNotSet(std::env::VarError, &'static str),
114 #[error("Environment variable {0} is malformed: {1}")]
115 EnvVarMalformed(&'static str, String),
116
117 #[error("Failed to convert between integer types")]
118 IntConversionError,
119 #[error("Failed to encode/decode data using borsh")]
120 BorshError,
121 #[error("Operator x-only public key {0} was not found in the DB")]
122 OperatorNotFound(XOnlyPublicKey),
123 #[error("Verifier with public key {0} was not found among the verifier clients")]
124 VerifierNotFound(PublicKey),
125 #[error("Deposit not found in DB: {0:?}")]
126 DepositNotFound(OutPoint),
127 #[error("Deposit is invalid due to {0}")]
128 InvalidDeposit(String),
129 #[error("Operator data mismatch. Data already stored in DB and received by set_operator doesn't match for xonly_pk: {0}")]
130 OperatorDataMismatch(XOnlyPublicKey),
131 #[error("Deposit data mismatch. Data already stored in DB doesn't match the new data for deposit {0:?}")]
132 DepositDataMismatch(OutPoint),
133 #[error("Operator winternitz public keys mismatch. Data already stored in DB doesn't match the new data for operator {0}")]
134 OperatorWinternitzPublicKeysMismatch(XOnlyPublicKey),
135 #[error("BitVM setup data mismatch. Data already stored in DB doesn't match the new data for operator {0} and deposit {1:?}")]
136 BitvmSetupDataMismatch(XOnlyPublicKey, OutPoint),
137 #[error("BitVM replacement data will exhaust memory. The maximum number of operations is {0}")]
138 BitvmReplacementResourceExhaustion(usize),
139 #[error("Operator challenge ack hashes mismatch. Data already stored in DB doesn't match the new data for operator {0} and deposit {1:?}")]
140 OperatorChallengeAckHashesMismatch(XOnlyPublicKey, OutPoint),
141 #[error("Invalid BitVM public keys")]
142 InvalidBitVMPublicKeys,
143 #[error("Invalid challenge ack hashes")]
144 InvalidChallengeAckHashes,
145 #[error("Invalid operator index")]
146 InvalidOperatorIndex,
147 #[error("Invalid protocol paramset")]
148 InvalidProtocolParamset,
149 #[error("Deposit already signed and move txid {0} is in chain")]
150 DepositAlreadySigned(Txid),
151 #[error("Invalid withdrawal ECDSA verification signature")]
152 InvalidECDSAVerificationSignature,
153 #[error("Withdrawal ECDSA verification signature missing")]
154 ECDSAVerificationSignatureMissing,
155 #[error("Clementine versions or configs are not compatible: {0}")]
156 ClementineNotCompatible(String),
157
158 #[error("Failed to call database: {0}")]
160 DatabaseError(#[from] sqlx::Error),
161 #[error("Failed to convert hex string: {0}")]
162 FromHexError(#[from] FromHexError),
163 #[error("Failed to convert to hash from slice: {0}")]
164 FromSliceError(#[from] bitcoin::hashes::FromSliceError),
165 #[error("Error while calling EVM contract: {0}")]
166 AlloyContract(#[from] alloy::contract::Error),
167 #[error("Error while calling EVM RPC function: {0}")]
168 AlloyRpc(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
169 #[error("Error while encoding/decoding EVM type: {0}")]
170 AlloySolTypes(#[from] alloy::sol_types::Error),
171 #[error("{0}")]
172 CLIDisplayAndExit(StyledStr),
173 #[error(transparent)]
174 RPCStatus(#[from] Box<Status>),
175
176 #[error("Arithmetic overflow occurred: {0}")]
177 ArithmeticOverflow(&'static str),
178 #[error("Insufficient funds: {0}")]
179 InsufficientFunds(&'static str),
180
181 #[error(transparent)]
183 Eyre(#[from] eyre::Report),
184}
185
186#[derive(Debug, Error)]
187pub(crate) enum FeeErr {
188 #[error("request timed out")]
189 Timeout,
190 #[error("transport/decode error: {0}")]
191 Transport(#[from] reqwest::Error),
192 #[error("http status {0}")]
193 Status(StatusCode),
194 #[error("json decode error: {0}")]
195 JsonDecode(reqwest::Error),
196 #[error("'fastestFee' field not found or invalid in API response")]
197 MissingField,
198}
199
200pub trait ErrorExt: Sized {
203 fn into_eyre(self) -> eyre::Report;
207 fn into_status(self) -> tonic::Status;
211}
212
213pub trait ResultExt: Sized {
216 type Output;
217
218 fn map_to_eyre(self) -> Result<Self::Output, eyre::Report>;
219 #[allow(clippy::result_large_err)]
220 fn map_to_status(self) -> Result<Self::Output, tonic::Status>;
221}
222
223impl<T: Into<BridgeError>> ErrorExt for T {
224 fn into_eyre(self) -> eyre::Report {
225 match self.into() {
226 BridgeError::Eyre(report) => report,
227 other => eyre::eyre!(other),
228 }
229 }
230 fn into_status(self) -> tonic::Status {
231 self.into().into()
232 }
233}
234
235impl<U: Sized, T: Into<BridgeError>> ResultExt for Result<U, T> {
236 type Output = U;
237
238 fn map_to_eyre(self) -> Result<Self::Output, eyre::Report> {
239 self.map_err(ErrorExt::into_eyre)
240 }
241
242 fn map_to_status(self) -> Result<Self::Output, tonic::Status> {
243 self.map_err(ErrorExt::into_status)
244 }
245}
246
247impl From<Status> for BridgeError {
248 fn from(status: Status) -> Self {
249 BridgeError::RPCStatus(Box::new(status))
250 }
251}
252
253impl From<BridgeError> for tonic::Status {
254 fn from(val: BridgeError) -> Self {
255 let err = format!("{val:#}");
256 let flattened = err
258 .replace("\\n", " ") .replace("\n", " ") .replace("\"", "") .replace("\\", ""); let whitespace_removed = flattened.split_whitespace().collect::<Vec<_>>().join(" ");
263 tonic::Status::internal(whitespace_removed)
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use eyre::Context;
270
271 use super::*;
272 #[test]
273 fn test_downcast() {
274 assert_eq!(
275 BridgeError::IntConversionError
276 .into_eyre()
277 .wrap_err("Some other error")
278 .into_eyre()
279 .wrap_err("some other")
280 .downcast_ref::<BridgeError>()
281 .unwrap()
282 .to_string(),
283 BridgeError::IntConversionError.to_string()
284 );
285 }
286
287 #[test]
288 fn test_status_shows_all_errors_in_chain() {
289 let err: BridgeError = Err::<(), BridgeError>(BridgeError::BitcoinRPC(
290 BitcoinRPCError::TransactionNotConfirmed,
291 ))
292 .wrap_err(tonic::Status::deadline_exceeded("Error A"))
293 .wrap_err("Error B")
294 .unwrap_err()
295 .into();
296
297 let status: Status = err.into();
298 assert!(status.message().contains("Error A"));
299 assert!(status.message().contains("Error B"));
300 assert!(status.message().contains("Bitcoin"));
301 }
302}