Skip to main content

surfpool_core/
error.rs

1use std::{fmt::Display, future::Future, pin::Pin};
2
3use crossbeam_channel::TrySendError;
4use jsonrpc_core::{Error, Result};
5use litesvm::error::LiteSVMError;
6use serde::Serialize;
7use serde_json::json;
8use solana_client::{client_error::ClientError, rpc_request::TokenAccountsFilter};
9use solana_clock::Slot;
10use solana_pubkey::Pubkey;
11use solana_transaction::TransactionError;
12use solana_transaction_status::EncodeError;
13
14use crate::storage::StorageError;
15
16pub type SurfpoolResult<T> = std::result::Result<T, SurfpoolError>;
17
18#[derive(Debug, Clone)]
19pub struct SurfpoolError(Error);
20
21impl From<SurfpoolError> for String {
22    fn from(e: SurfpoolError) -> Self {
23        e.0.to_string()
24    }
25}
26
27impl From<SurfpoolError> for Error {
28    fn from(e: SurfpoolError) -> Self {
29        e.0
30    }
31}
32
33impl From<EncodeError> for SurfpoolError {
34    fn from(e: EncodeError) -> Self {
35        let mut error = Error::internal_error();
36        error.data = Some(json!(format!(
37            "Transaction encoding error: {}",
38            e.to_string()
39        )));
40        Self(error)
41    }
42}
43
44impl std::error::Error for SurfpoolError {}
45
46impl std::fmt::Display for SurfpoolError {
47    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
48        let Error {
49            code,
50            message,
51            data,
52        } = &self.0;
53
54        let core = if code.description().eq(message) {
55            code.description()
56        } else {
57            format!("{}: {}", code.description(), message)
58        };
59
60        if let Some(data_value) = data {
61            write!(f, "{}: {}", core, data_value.to_string().as_str())
62        } else {
63            write!(f, "{}", core)
64        }
65    }
66}
67
68impl<T> From<SurfpoolError> for Pin<Box<dyn Future<Output = Result<T>> + Send>> {
69    fn from(e: SurfpoolError) -> Self {
70        Box::pin(async move { Err(e.into()) })
71    }
72}
73
74impl<T> From<TrySendError<T>> for SurfpoolError {
75    fn from(val: TrySendError<T>) -> Self {
76        SurfpoolError::from_try_send_error(val)
77    }
78}
79
80impl From<solana_client::client_error::ClientError> for SurfpoolError {
81    fn from(e: solana_client::client_error::ClientError) -> Self {
82        SurfpoolError::client_error(e)
83    }
84}
85
86impl SurfpoolError {
87    pub fn from_try_send_error<T>(e: TrySendError<T>) -> Self {
88        let mut error = Error::internal_error();
89        error.data = Some(json!(format!(
90            "Failed to send command on channel: {}",
91            e.to_string()
92        )));
93        Self(error)
94    }
95
96    pub fn client_error(e: solana_client::client_error::ClientError) -> Self {
97        let mut error = Error::internal_error();
98        error.data = Some(json!(format!("Solana RPC client error: {}", e.to_string())));
99        Self(error)
100    }
101
102    pub fn missing_context() -> Self {
103        let mut error = Error::internal_error();
104        error.data = Some(json!("Failed to access internal Surfnet context"));
105        Self(error)
106    }
107
108    pub fn disable_cheatcode(e: String) -> Self {
109        let mut error = Error::invalid_request();
110        error.data = Some(json!(format!("Unable to disable cheatcode: {}", e)));
111        Self(error)
112    }
113
114    pub fn enable_cheatcode(e: String) -> Self {
115        let mut error = Error::invalid_request();
116        error.data = Some(json!(format!("Unable to enable cheatcode: {}", e)));
117        Self(error)
118    }
119
120    pub fn set_account<T>(pubkey: Pubkey, e: T) -> Self
121    where
122        T: ToString,
123    {
124        let mut error = Error::internal_error();
125        error.data = Some(json!(format!(
126            "Failed to set account {}: {}",
127            pubkey,
128            e.to_string()
129        )));
130        Self(error)
131    }
132    pub fn get_account<T>(pubkey: Pubkey, e: T) -> Self
133    where
134        T: ToString,
135    {
136        let mut error = Error::internal_error();
137        error.data = Some(json!(format!(
138            "Failed to fetch account {} from remote: {}",
139            pubkey,
140            e.to_string()
141        )));
142        Self(error)
143    }
144
145    pub fn get_token_accounts<T>(owner: Pubkey, filter: &TokenAccountsFilter, e: T) -> Self
146    where
147        T: ToString,
148    {
149        let mut error = Error::internal_error();
150        error.data = Some(json!(format!(
151            "Failed to get token accounts by owner {owner} for {}: {}",
152            match filter {
153                TokenAccountsFilter::ProgramId(token_program) => format!("program {token_program}"),
154                TokenAccountsFilter::Mint(mint) => format!("mint {mint}"),
155            },
156            e.to_string()
157        )));
158        Self(error)
159    }
160
161    pub fn get_token_accounts_by_delegate_error<T>(
162        delegate: Pubkey,
163        filter: &TokenAccountsFilter,
164        e: T,
165    ) -> Self
166    where
167        T: ToString,
168    {
169        let mut error = Error::internal_error();
170
171        let filter_description = match filter {
172            TokenAccountsFilter::ProgramId(program_id) => {
173                let program_name = if *program_id == spl_token_interface::ID {
174                    "SPL Token program"
175                } else if *program_id == spl_token_2022_interface::ID {
176                    "Token 2022 program"
177                } else {
178                    "custom token program"
179                };
180                format!("{} ({})", program_id, program_name)
181            }
182            TokenAccountsFilter::Mint(mint) => format!("mint {}", mint),
183        };
184
185        error.data = Some(json!(format!(
186            "Failed to get token accounts by delegate {} for {}: {}",
187            delegate,
188            filter_description,
189            e.to_string()
190        )));
191
192        Self(error)
193    }
194
195    pub fn unsupported_token_program(program_id: Pubkey) -> Self {
196        let mut error = Error::internal_error();
197        error.data = Some(json!(format!(
198            "Unsupported token program: {}. Only SPL Token ({}) and Token 2022 ({}) are currently supported.",
199            program_id,
200            spl_token_interface::ID,
201            spl_token_2022_interface::ID
202        )));
203        Self(error)
204    }
205
206    pub fn get_program_accounts<T>(program_id: Pubkey, e: T) -> Self
207    where
208        T: ToString,
209    {
210        let mut error = Error::internal_error();
211        error.data = Some(json!(format!(
212            "Failed to fetch program accounts for {program_id}: {}",
213            e.to_string()
214        )));
215        Self(error)
216    }
217
218    pub fn get_token_largest_accounts<T>(mint: Pubkey, e: T) -> Self
219    where
220        T: ToString,
221    {
222        let mut error = Error::internal_error();
223        error.data = Some(json!(format!(
224            "Failed to get largest token accounts for mint {mint}: {}",
225            e.to_string()
226        )));
227        Self(error)
228    }
229
230    pub fn get_multiple_accounts<T>(e: T) -> Self
231    where
232        T: ToString,
233    {
234        let mut error = Error::internal_error();
235        error.data = Some(json!(format!(
236            "Failed to fetch accounts from remote: {}",
237            e.to_string()
238        )));
239        Self(error)
240    }
241    pub fn get_largest_accounts<T>(e: T) -> Self
242    where
243        T: ToString,
244    {
245        let mut error = Error::internal_error();
246        error.data = Some(json!(format!(
247            "Failed to fetch largest accounts from remote: {}",
248            e.to_string()
249        )));
250        Self(error)
251    }
252
253    pub fn get_signatures_for_address<T>(e: T) -> Self
254    where
255        T: ToString,
256    {
257        let mut error = Error::internal_error();
258        error.data = Some(json!(format!(
259            "Failed to fetch signatures for address from remote: {}",
260            e.to_string()
261        )));
262        Self(error)
263    }
264
265    pub fn invalid_pubkey<D>(pubkey: &str, data: D) -> Self
266    where
267        D: Serialize,
268    {
269        let mut error = Error::invalid_params(format!("Invalid pubkey '{pubkey}'"));
270        error.data = Some(json!(data));
271        Self(error)
272    }
273
274    pub fn invalid_pubkey_at_index<D>(pubkey: &str, index: usize, data: D) -> Self
275    where
276        D: Serialize,
277    {
278        let mut error =
279            Error::invalid_params(format!("Invalid pubkey '{pubkey}' at index {index}"));
280        error.data = Some(json!(data));
281        Self(error)
282    }
283
284    pub fn invalid_signature<D>(signature: &str, data: D) -> Self
285    where
286        D: Serialize,
287    {
288        let mut error = Error::invalid_params(format!("Invalid signature {signature}"));
289        error.data = Some(json!(data));
290        Self(error)
291    }
292
293    pub fn invalid_program_account<P, D>(program_id: P, data: D) -> Self
294    where
295        P: Display,
296        D: Serialize,
297    {
298        let mut error = Error::invalid_params(format!("Invalid program account {program_id}"));
299        error.data = Some(json!(data));
300        Self(error)
301    }
302
303    pub fn invalid_program_data_account<P, D>(program_data_id: P, data: D) -> Self
304    where
305        P: Display,
306        D: Serialize,
307    {
308        let mut error =
309            Error::invalid_params(format!("Invalid program data account {program_data_id}"));
310        error.data = Some(json!(data));
311        Self(error)
312    }
313
314    pub fn expected_program_account<P>(program_id: P) -> Self
315    where
316        P: Display,
317    {
318        let error = Error::invalid_params(format!("Account {program_id} is not a program account"));
319        Self(error)
320    }
321
322    pub fn account_not_found<P>(pubkey: P) -> Self
323    where
324        P: Display,
325    {
326        let error = Error::invalid_params(format!("Account {pubkey} not found"));
327        Self(error)
328    }
329
330    pub fn transaction_not_found<S>(signature: S) -> Self
331    where
332        S: Display,
333    {
334        let error = Error::invalid_params(format!("Transaction {signature} not found"));
335        Self(error)
336    }
337
338    pub fn invalid_account_data<P, D, M>(pubkey: P, data: D, message: Option<M>) -> Self
339    where
340        P: Display,
341        D: Serialize,
342        M: Display,
343    {
344        let base_msg = format!("invalid account data {pubkey}");
345        let full_msg = if let Some(msg) = message {
346            format!("{base_msg}: {msg}")
347        } else {
348            base_msg
349        };
350        let mut error = Error::invalid_params(full_msg);
351        error.data = Some(json!(data));
352        Self(error)
353    }
354
355    pub fn invalid_account_owner<P, M>(pubkey: P, message: Option<M>) -> Self
356    where
357        P: Display,
358        M: Display,
359    {
360        let base_msg = format!("invalid account owner {pubkey}");
361        let full_msg = if let Some(msg) = message {
362            format!("{base_msg}: {msg}")
363        } else {
364            base_msg
365        };
366        let error = Error::invalid_params(full_msg);
367        Self(error)
368    }
369    pub fn invalid_lookup_index<P>(pubkey: P) -> Self
370    where
371        P: Display,
372    {
373        let error =
374            Error::invalid_params(format!("Address lookup {pubkey} contains an invalid index"));
375        Self(error)
376    }
377
378    pub fn invalid_base64_data<D>(typing: &str, data: D) -> Self
379    where
380        D: Display,
381    {
382        let mut error = Error::invalid_params(format!("Invalid base64 {typing}"));
383        error.data = Some(json!(data.to_string()));
384        Self(error)
385    }
386
387    pub fn deserialize_error<D>(typing: &str, data: D) -> Self
388    where
389        D: Display,
390    {
391        let mut error = Error::invalid_params(format!("Failed to deserialize {typing}"));
392        error.data = Some(json!(data.to_string()));
393        Self(error)
394    }
395
396    pub fn rpc_method_not_supported() -> Self {
397        let mut error = Error::internal_error();
398        error.message = "RPC method not supported".to_string();
399        Self(error)
400    }
401
402    pub fn internal<D>(data: D) -> Self
403    where
404        D: Serialize,
405    {
406        let mut error = Error::internal_error();
407        error.data = Some(json!(data));
408        Self(error)
409    }
410
411    pub fn sig_verify_replace_recent_blockhash_collision() -> Self {
412        Self(Error::invalid_params(
413            "sigVerify may not be used with replaceRecentBlockhash",
414        ))
415    }
416
417    pub fn slot_too_old(slot: Slot) -> Self {
418        Self(Error::invalid_params(format!(
419            "Requested {slot} is before the first local slot, and no remote RPC was provided."
420        )))
421    }
422
423    pub fn get_block(e: ClientError, block: Slot) -> Self {
424        let mut error = Error::internal_error();
425        error.data = Some(json!(format!(
426            "Failed to get block {block} from remote: {e}"
427        )));
428        Self(error)
429    }
430
431    pub fn token_mint_not_found(mint: Pubkey) -> Self {
432        let mut error = Error::internal_error();
433        error.message = format!("Token mint {mint} not found");
434        Self(error)
435    }
436
437    pub fn unpack_token_account() -> Self {
438        let mut error = Error::parse_error();
439        error.message = "Failed to unpack token account".to_string();
440        Self(error)
441    }
442
443    pub fn unpack_mint_account() -> Self {
444        let mut error = Error::parse_error();
445        error.message = "Failed to unpack mint account".to_string();
446        Self(error)
447    }
448
449    pub fn invalid_token_account_state(state: &str) -> Self {
450        let error = Error::invalid_params(format!("Invalid token account state {state}"));
451        Self(error)
452    }
453
454    pub fn tag_not_found(tag: &str) -> Self {
455        let mut error = Error::internal_error();
456        error.message = format!("Profile result associated with tag '{tag}' not found in the SVM");
457        Self(error)
458    }
459
460    pub(crate) fn expected_profile_not_found(key: &surfpool_types::UuidOrSignature) -> Self {
461        let mut error = Error::internal_error();
462        error.message = format!("Expected profile not found for key {key}");
463        Self(error)
464    }
465}
466
467impl From<StorageError> for SurfpoolError {
468    fn from(e: StorageError) -> Self {
469        let mut error = Error::internal_error();
470        error.data = Some(json!(format!("Storage error: {}", e.to_string())));
471        SurfpoolError(error)
472    }
473}
474
475impl From<LiteSVMError> for SurfpoolError {
476    fn from(e: LiteSVMError) -> Self {
477        let mut error = Error::internal_error();
478        error.data = Some(json!(format!("LiteSVM error: {}", e.to_string())));
479        SurfpoolError(error)
480    }
481}
482
483impl From<TransactionError> for SurfpoolError {
484    fn from(e: TransactionError) -> Self {
485        let mut error = Error::internal_error();
486        error.data = Some(json!(format!("Transaction error: {}", e.to_string())));
487        SurfpoolError(error)
488    }
489}
490
491/// Error returned by [`crate::surfnet::svm::SurfnetSvm::airdrop`] when the
492/// requested airdrop is rejected up front, before any synthetic transaction
493/// is constructed or any account state is touched.
494///
495/// `ZeroAmount` and `BelowRentExemption` are the two pre-validation rejects.
496/// Anything else (storage errors, account fetch failures, etc.) is bubbled
497/// through `Other` so it stays representable as a `SurfpoolError`.
498#[derive(Debug, Clone)]
499pub enum AirdropError {
500    ZeroAmount,
501    BelowRentExemption { lamports: u64, min_rent: u64 },
502    Other(SurfpoolError),
503}
504
505impl From<SurfpoolError> for AirdropError {
506    fn from(e: SurfpoolError) -> Self {
507        AirdropError::Other(e)
508    }
509}
510
511impl From<StorageError> for AirdropError {
512    fn from(e: StorageError) -> Self {
513        AirdropError::Other(SurfpoolError::from(e))
514    }
515}
516
517impl From<AirdropError> for SurfpoolError {
518    fn from(e: AirdropError) -> Self {
519        match e {
520            AirdropError::ZeroAmount => SurfpoolError(Error::invalid_params(
521                "Airdrop amount must be greater than zero",
522            )),
523            // Real Solana surfaces sub-rent airdrops as
524            // RpcCustomError::SendTransactionPreflightFailure (-32002) carrying
525            // a real TransactionError::InsufficientFundsForRent from the
526            // faucet's transfer transaction. Surfpool rejects up front instead
527            // of routing through the tx pipeline, so we deliberately do not
528            // claim that wire shape — the message names the minimum.
529            AirdropError::BelowRentExemption { lamports, min_rent } => {
530                SurfpoolError(Error::invalid_params(format!(
531                    "Airdrop amount {lamports} is below the rent-exempt minimum of {min_rent} lamports"
532                )))
533            }
534            AirdropError::Other(e) => e,
535        }
536    }
537}
538
539impl From<AirdropError> for Error {
540    fn from(e: AirdropError) -> Self {
541        SurfpoolError::from(e).into()
542    }
543}
544
545impl Display for AirdropError {
546    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547        match self {
548            AirdropError::ZeroAmount => write!(f, "airdrop amount must be greater than zero"),
549            AirdropError::BelowRentExemption { lamports, min_rent } => write!(
550                f,
551                "airdrop amount {lamports} is below the rent-exempt minimum of {min_rent} lamports"
552            ),
553            AirdropError::Other(e) => Display::fmt(e, f),
554        }
555    }
556}
557
558impl std::error::Error for AirdropError {}