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