gmsol/
error.rs

1use anchor_client::{solana_client::pubsub_client::PubsubClientError, solana_sdk};
2use gmsol_utils::token_config::TokenConfigError;
3use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
4
5pub use gmsol_store::CoreError;
6
7/// Error type for `gmsol`.
8#[derive(Debug, thiserror::Error)]
9pub enum Error {
10    /// Empty deposit.
11    #[error("empty deposit")]
12    EmptyDeposit,
13    /// Anchor Error.
14    #[error("anchor: {0:#?}")]
15    Anchor(AnchorError),
16    /// Client Error.
17    #[error("{0:#?}")]
18    Client(Box<anchor_client::ClientError>),
19    /// Model error.
20    #[error("model: {0}")]
21    Model(#[from] gmsol_model::Error),
22    /// Number out of range.
23    #[error("numer out of range")]
24    NumberOutOfRange,
25    /// Unknown errors.
26    #[error("unknown: {0}")]
27    Unknown(String),
28    /// Eyre errors.
29    #[error("eyre: {0}")]
30    Eyre(#[from] eyre::Error),
31    /// Missing return data.
32    #[error("missing return data")]
33    MissingReturnData,
34    /// Base64 Decode Error.
35    #[error("base64: {0}")]
36    Base64(#[from] base64::DecodeError),
37    /// IO Error.
38    #[error("io: {0}")]
39    Io(#[from] std::io::Error),
40    /// Not found.
41    #[error("not found")]
42    NotFound,
43    /// Bytemuck error.
44    #[error("bytemuck: {0}")]
45    Bytemuck(bytemuck::PodCastError),
46    /// Invalid Arguments.
47    #[error("invalid argument: {0}")]
48    InvalidArgument(String),
49    /// Format error.
50    #[error("fmt: {0}")]
51    Fmt(#[from] std::fmt::Error),
52    /// Reqwest error.
53    #[cfg(feature = "reqwest")]
54    #[error(transparent)]
55    Reqwest(#[from] reqwest::Error),
56    /// Parse url error.
57    #[error("parse url: {0}")]
58    ParseUrl(#[from] url::ParseError),
59    /// SSE error.
60    #[cfg(all(feature = "eventsource-stream", feature = "reqwest"))]
61    #[error("sse: {0}")]
62    Sse(#[from] eventsource_stream::EventStreamError<reqwest::Error>),
63    /// JSON error.
64    #[error("json: {0}")]
65    Json(#[from] serde_json::Error),
66    /// Decode error.
67    #[cfg(feature = "decode")]
68    #[error("decode: {0}")]
69    Decode(#[from] gmsol_decode::DecodeError),
70    /// Lagged.
71    #[error("lagged: {0}")]
72    Lagged(#[from] BroadcastStreamRecvError),
73    /// Pubsub client closed.
74    #[error("pubsub: closed")]
75    PubsubClosed,
76    /// Complie Solana Message Error.
77    #[error("compile message: {0}")]
78    CompileMessage(#[from] solana_sdk::message::CompileError),
79    /// Signer Error.
80    #[error("signer: {0}")]
81    SignerError(#[from] solana_sdk::signer::SignerError),
82    /// Transport Error.
83    #[error("transport: {0}")]
84    Transport(String),
85    /// Switchboard Error.
86    #[error("switchboard: {0}")]
87    Switchboard(String),
88    /// Solana utils error.
89    #[error(transparent)]
90    SolanaUtils(gmsol_solana_utils::Error),
91}
92
93impl Error {
94    /// Create unknown error.
95    pub fn unknown(msg: impl ToString) -> Self {
96        Self::Unknown(msg.to_string())
97    }
98
99    /// Create an "invalid argument" error.
100    pub fn invalid_argument(msg: impl ToString) -> Self {
101        Self::InvalidArgument(msg.to_string())
102    }
103
104    /// Create a transport error.
105    pub fn transport(msg: impl ToString) -> Self {
106        Self::Transport(msg.to_string())
107    }
108
109    /// Create a switchboard error.
110    pub fn switchboard_error(msg: impl ToString) -> Self {
111        Self::Switchboard(msg.to_string())
112    }
113
114    /// Anchor Error Code.
115    pub fn anchor_error_code(&self) -> Option<u32> {
116        let Self::Anchor(error) = self else {
117            return None;
118        };
119        Some(error.error_code_number)
120    }
121}
122
123impl From<anchor_client::ClientError> for Error {
124    fn from(error: anchor_client::ClientError) -> Self {
125        use anchor_client::ClientError;
126
127        match error {
128            ClientError::AccountNotFound => Self::NotFound,
129            ClientError::SolanaClientError(error) => match handle_solana_client_error(&error) {
130                Some(err) => err,
131                None => Self::Client(Box::new(ClientError::SolanaClientError(error))),
132            },
133            ClientError::SolanaClientPubsubError(err) => Self::from(err),
134            err => Self::Client(Box::new(err)),
135        }
136    }
137}
138
139impl From<gmsol_solana_utils::Error> for Error {
140    fn from(value: gmsol_solana_utils::Error) -> Self {
141        match value {
142            gmsol_solana_utils::Error::Client(err) => match handle_solana_client_error(&err) {
143                Some(err) => err,
144                None => Self::SolanaUtils(err.into()),
145            },
146            err => Self::SolanaUtils(err),
147        }
148    }
149}
150
151impl From<anchor_client::anchor_lang::error::Error> for Error {
152    fn from(value: anchor_client::anchor_lang::error::Error) -> Self {
153        Self::Client(Box::new(value.into()))
154    }
155}
156
157impl From<PubsubClientError> for Error {
158    fn from(err: PubsubClientError) -> Self {
159        match err {
160            PubsubClientError::ConnectionClosed(_) => Self::PubsubClosed,
161            err => anchor_client::ClientError::from(err).into(),
162        }
163    }
164}
165
166impl From<TokenConfigError> for Error {
167    fn from(value: TokenConfigError) -> Self {
168        Self::from(anchor_client::anchor_lang::error::Error::from(
169            CoreError::from(value),
170        ))
171    }
172}
173
174/// Anchor Error with owned source.
175#[derive(Debug)]
176pub struct AnchorError {
177    /// Error name.
178    pub error_name: String,
179    /// Error code.
180    pub error_code_number: u32,
181    /// Error message.
182    pub error_msg: String,
183    /// Error origin.
184    pub error_origin: Option<ErrorOrigin>,
185    /// Logs.
186    pub logs: Vec<String>,
187}
188
189/// Error origin with owned source.
190#[derive(Debug)]
191pub enum ErrorOrigin {
192    /// Source.
193    Source(String, u32),
194    /// Account.
195    AccountName(String),
196}
197
198fn handle_solana_client_error(
199    error: &anchor_client::solana_client::client_error::ClientError,
200) -> Option<Error> {
201    use anchor_client::solana_client::{
202        client_error::ClientErrorKind,
203        rpc_request::{RpcError, RpcResponseErrorData},
204    };
205
206    let ClientErrorKind::RpcError(rpc_error) = error.kind() else {
207        return None;
208    };
209
210    let RpcError::RpcResponseError { data, .. } = rpc_error else {
211        return None;
212    };
213
214    let RpcResponseErrorData::SendTransactionPreflightFailure(simulation) = data else {
215        return None;
216    };
217
218    let Some(logs) = &simulation.logs else {
219        return None;
220    };
221
222    for log in logs {
223        if log.starts_with("Program log: AnchorError") {
224            let log = log.trim_start_matches("Program log: AnchorError ");
225            let Some((origin, rest)) = log.split_once("Error Code:") else {
226                break;
227            };
228            let Some((name, rest)) = rest.split_once("Error Number:") else {
229                break;
230            };
231            let Some((number, message)) = rest.split_once("Error Message:") else {
232                break;
233            };
234            let number = number.trim().trim_end_matches('.');
235            let Ok(number) = number.parse() else {
236                break;
237            };
238
239            let origin = origin.trim().trim_end_matches('.');
240
241            let origin = if origin.starts_with("thrown in") {
242                let source = origin.trim_start_matches("thrown in ");
243                if let Some((filename, line)) = source.split_once(':') {
244                    Some(ErrorOrigin::Source(
245                        filename.to_string(),
246                        line.parse().ok().unwrap_or(0),
247                    ))
248                } else {
249                    None
250                }
251            } else if origin.starts_with("caused by account:") {
252                let account = origin.trim_start_matches("caused by account: ");
253                Some(ErrorOrigin::AccountName(account.to_string()))
254            } else {
255                None
256            };
257
258            let error = AnchorError {
259                error_name: name.trim().trim_end_matches('.').to_string(),
260                error_code_number: number,
261                error_msg: message.trim().to_string(),
262                error_origin: origin,
263                logs: logs.clone(),
264            };
265
266            return Some(Error::Anchor(error));
267        }
268    }
269
270    None
271}
272
273impl<T> From<(T, gmsol_solana_utils::Error)> for Error {
274    fn from((_, err): (T, gmsol_solana_utils::Error)) -> Self {
275        Self::SolanaUtils(err)
276    }
277}