Skip to main content

gmsol_sdk/
error.rs

1pub(crate) use gmsol_programs::anchor_lang::prelude::Error as AnchorLangError;
2
3/// SDK Error.
4#[derive(Debug, thiserror::Error)]
5pub enum Error {
6    /// Error from [`gmsol-solana-utils`].
7    #[error("solana-utils: {0}")]
8    SolanaUtils(gmsol_solana_utils::Error),
9    /// Anchor Lang Error.
10    #[error("anchor-lang: {0}")]
11    AnchorLang(Box<AnchorLangError>),
12    /// Anchor Error.
13    #[error("anchor: {0:#?}")]
14    Anchor(AnchorError),
15    /// Model Error.
16    #[error("model: {0}")]
17    Model(#[from] gmsol_model::Error),
18    /// Base64 decode error.
19    #[error("base64-decode: {0}")]
20    Base64Decode(#[from] base64::DecodeError),
21    /// Bincode error.
22    #[cfg(feature = "bincode")]
23    #[error("bincode: {0}")]
24    Bincode(#[from] bincode::Error),
25    /// Custom error.
26    #[error("custom: {0}")]
27    Custom(String),
28    /// Transport error.
29    #[error("transport: {0}")]
30    Transport(String),
31    /// Market Graph Errors
32    #[cfg(feature = "market-graph")]
33    #[error("market-graph: {0}")]
34    MarketGraph(#[from] crate::market_graph::error::MarketGraphError),
35    /// Parse Pubkey Error.
36    #[error("parse pubkey error: {0}")]
37    ParsePubkey(#[from] solana_sdk::pubkey::ParsePubkeyError),
38    /// Pubsub client closed.
39    #[cfg(feature = "client")]
40    #[error("pubsub: closed")]
41    PubsubClosed,
42    /// Not found error.
43    #[error("not found")]
44    NotFound,
45    /// Decode error.
46    #[cfg(feature = "decode")]
47    #[error("decode: {0}")]
48    Decode(#[from] gmsol_decode::DecodeError),
49    /// Error from [`gmsol_programs`].
50    #[error("programs: {0}")]
51    Programs(#[from] gmsol_programs::Error),
52    /// Reqwest error.
53    #[cfg(feature = "reqwest")]
54    #[error("reqwest: {0}")]
55    Reqwest(#[from] reqwest::Error),
56    /// Json error.
57    #[cfg(feature = "serde_json")]
58    #[error("json: {0}")]
59    Json(#[from] serde_json::Error),
60    /// Market closed error.
61    #[error("market closed: {0}")]
62    MarketClosed(String),
63    /// Convert integer error.
64    #[error(transparent)]
65    ConvertInterger(#[from] std::num::TryFromIntError),
66}
67
68impl Error {
69    /// Create a custom error.
70    pub fn custom(msg: impl ToString) -> Self {
71        Self::Custom(msg.to_string())
72    }
73
74    /// Create a transport error.
75    pub fn transport(msg: impl ToString) -> Self {
76        Self::Transport(msg.to_string())
77    }
78
79    /// Anchor Error Code.
80    pub fn anchor_error_code(&self) -> Option<u32> {
81        let Self::Anchor(error) = self else {
82            return None;
83        };
84        Some(error.error_code_number)
85    }
86
87    /// Create a [`MarketClosed`](Error::MarketClosed) error.
88    pub fn market_closed(msg: impl ToString) -> Self {
89        Self::MarketClosed(msg.to_string())
90    }
91}
92
93impl From<AnchorLangError> for Error {
94    fn from(value: AnchorLangError) -> Self {
95        Self::AnchorLang(Box::new(value))
96    }
97}
98
99#[cfg(feature = "wasm-bindgen")]
100impl From<Error> for wasm_bindgen::JsValue {
101    fn from(value: Error) -> Self {
102        Self::from_str(&value.to_string())
103    }
104}
105
106/// Anchor Error with owned source.
107#[derive(Debug, Clone, Default)]
108pub struct AnchorError {
109    /// Error name.
110    pub error_name: String,
111    /// Error code.
112    pub error_code_number: u32,
113    /// Error message.
114    pub error_msg: String,
115    /// Error origin.
116    pub error_origin: Option<ErrorOrigin>,
117    /// Logs.
118    pub logs: Vec<String>,
119}
120
121/// Error origin with owned source.
122#[derive(Debug, Clone)]
123pub enum ErrorOrigin {
124    /// Source.
125    Source(String, u32),
126    /// Account.
127    AccountName(String),
128}
129
130#[cfg(feature = "solana-client")]
131fn handle_solana_client_error(error: &solana_client::client_error::ClientError) -> Option<Error> {
132    use solana_client::{
133        client_error::ClientErrorKind,
134        rpc_request::{RpcError, RpcResponseErrorData},
135    };
136
137    let ClientErrorKind::RpcError(rpc_error) = error.kind() else {
138        return None;
139    };
140
141    let RpcError::RpcResponseError { data, .. } = rpc_error else {
142        return None;
143    };
144
145    let RpcResponseErrorData::SendTransactionPreflightFailure(simulation) = data else {
146        return None;
147    };
148
149    let Some(logs) = &simulation.logs else {
150        return None;
151    };
152
153    for log in logs {
154        if log.starts_with("Program log: AnchorError") {
155            let log = log.trim_start_matches("Program log: AnchorError ");
156            let Some((origin, rest)) = log.split_once("Error Code:") else {
157                break;
158            };
159            let Some((name, rest)) = rest.split_once("Error Number:") else {
160                break;
161            };
162            let Some((number, message)) = rest.split_once("Error Message:") else {
163                break;
164            };
165            let number = number.trim().trim_end_matches('.');
166            let Ok(number) = number.parse() else {
167                break;
168            };
169
170            let origin = origin.trim().trim_end_matches('.');
171
172            let origin = if origin.starts_with("thrown in") {
173                let source = origin.trim_start_matches("thrown in ");
174                if let Some((filename, line)) = source.split_once(':') {
175                    Some(ErrorOrigin::Source(
176                        filename.to_string(),
177                        line.parse().ok().unwrap_or(0),
178                    ))
179                } else {
180                    None
181                }
182            } else if origin.starts_with("caused by account:") {
183                let account = origin.trim_start_matches("caused by account: ");
184                Some(ErrorOrigin::AccountName(account.to_string()))
185            } else {
186                None
187            };
188
189            let error = AnchorError {
190                error_name: name.trim().trim_end_matches('.').to_string(),
191                error_code_number: number,
192                error_msg: message.trim().to_string(),
193                error_origin: origin,
194                logs: logs.clone(),
195            };
196
197            return Some(Error::Anchor(error));
198        }
199    }
200
201    None
202}
203
204impl From<gmsol_solana_utils::Error> for Error {
205    fn from(value: gmsol_solana_utils::Error) -> Self {
206        match value {
207            #[cfg(feature = "solana-client")]
208            gmsol_solana_utils::Error::Client(err) => match handle_solana_client_error(&err) {
209                Some(err) => err,
210                None => Self::SolanaUtils(err.into()),
211            },
212            err => Self::SolanaUtils(err),
213        }
214    }
215}
216
217impl<T> From<(T, gmsol_solana_utils::Error)> for Error {
218    fn from((_, err): (T, gmsol_solana_utils::Error)) -> Self {
219        Self::from(err)
220    }
221}