Skip to main content

solana_rpc_client_api/
custom_error.rs

1//! Implementation defined RPC server errors
2use {
3    crate::response::RpcSimulateTransactionResult,
4    jsonrpc_core::{Error, ErrorCode},
5    serde::{Deserialize, Serialize},
6    solana_clock::Slot,
7    solana_transaction_status_client_types::EncodeError,
8    thiserror::Error,
9};
10
11// Keep in sync with https://github.com/solana-labs/solana-web3.js/blob/master/src/errors.ts
12pub const JSON_RPC_SERVER_ERROR_BLOCK_CLEANED_UP: i64 = -32001;
13pub const JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE: i64 = -32002;
14pub const JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE: i64 = -32003;
15pub const JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE: i64 = -32004;
16pub const JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY: i64 = -32005;
17pub const JSON_RPC_SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE: i64 = -32006;
18pub const JSON_RPC_SERVER_ERROR_SLOT_SKIPPED: i64 = -32007;
19pub const JSON_RPC_SERVER_ERROR_NO_SNAPSHOT: i64 = -32008;
20pub const JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED: i64 = -32009;
21pub const JSON_RPC_SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX: i64 = -32010;
22pub const JSON_RPC_SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE: i64 = -32011;
23pub const JSON_RPC_SCAN_ERROR: i64 = -32012;
24pub const JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH: i64 = -32013;
25pub const JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: i64 = -32014;
26pub const JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION: i64 = -32015;
27pub const JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED: i64 = -32016;
28pub const JSON_RPC_SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE: i64 = -32017;
29pub const JSON_RPC_SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY: i64 = -32018;
30pub const JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE: i64 = -32019;
31pub const JSON_RPC_SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND: i64 = -32020;
32pub const JSON_RPC_SERVER_ERROR_NO_SLOT_HISTORY: i64 = -32021;
33
34#[derive(Error, Debug)]
35#[allow(clippy::large_enum_variant)]
36pub enum RpcCustomError {
37    #[error("BlockCleanedUp")]
38    BlockCleanedUp {
39        slot: Slot,
40        first_available_block: Slot,
41    },
42    #[error("SendTransactionPreflightFailure")]
43    SendTransactionPreflightFailure {
44        message: String,
45        result: RpcSimulateTransactionResult,
46    },
47    #[error("TransactionSignatureVerificationFailure")]
48    TransactionSignatureVerificationFailure,
49    #[error("BlockNotAvailable")]
50    BlockNotAvailable { slot: Slot },
51    #[error("NodeUnhealthy")]
52    NodeUnhealthy { num_slots_behind: Option<Slot> },
53    #[error("TransactionPrecompileVerificationFailure")]
54    TransactionPrecompileVerificationFailure(solana_transaction_error::TransactionError),
55    #[error("SlotSkipped")]
56    SlotSkipped { slot: Slot },
57    #[error("NoSnapshot")]
58    NoSnapshot,
59    #[error("LongTermStorageSlotSkipped")]
60    LongTermStorageSlotSkipped { slot: Slot },
61    #[error("KeyExcludedFromSecondaryIndex")]
62    KeyExcludedFromSecondaryIndex { index_key: String },
63    #[error("TransactionHistoryNotAvailable")]
64    TransactionHistoryNotAvailable,
65    #[error("ScanError")]
66    ScanError { message: String },
67    #[error("TransactionSignatureLenMismatch")]
68    TransactionSignatureLenMismatch,
69    #[error("BlockStatusNotAvailableYet")]
70    BlockStatusNotAvailableYet { slot: Slot },
71    #[error("UnsupportedTransactionVersion")]
72    UnsupportedTransactionVersion(u8),
73    #[error("MinContextSlotNotReached")]
74    MinContextSlotNotReached { context_slot: Slot },
75    #[error("EpochRewardsPeriodActive")]
76    EpochRewardsPeriodActive {
77        slot: Slot,
78        current_block_height: u64,
79        rewards_complete_block_height: u64,
80    },
81    #[error("SlotNotEpochBoundary")]
82    SlotNotEpochBoundary { slot: Slot },
83    #[error("LongTermStorageUnreachable")]
84    LongTermStorageUnreachable,
85    #[error("FilterTransactionNotFound")]
86    FilterTransactionNotFound { signature: String },
87    #[error("NoSlotHistory")]
88    NoSlotHistory,
89}
90
91#[derive(Debug, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct NodeUnhealthyErrorData {
94    pub num_slots_behind: Option<Slot>,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct MinContextSlotNotReachedErrorData {
100    pub context_slot: Slot,
101}
102
103#[cfg_attr(test, derive(PartialEq))]
104#[derive(Debug, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct EpochRewardsPeriodActiveErrorData {
107    pub current_block_height: u64,
108    pub rewards_complete_block_height: u64,
109    pub slot: Option<u64>,
110}
111
112impl From<EncodeError> for RpcCustomError {
113    fn from(err: EncodeError) -> Self {
114        match err {
115            EncodeError::UnsupportedTransactionVersion(version) => {
116                Self::UnsupportedTransactionVersion(version)
117            }
118        }
119    }
120}
121
122impl From<RpcCustomError> for Error {
123    fn from(e: RpcCustomError) -> Self {
124        match e {
125            RpcCustomError::BlockCleanedUp {
126                slot,
127                first_available_block,
128            } => Self {
129                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_BLOCK_CLEANED_UP),
130                message: format!(
131                    "Block {slot} cleaned up, does not exist on node. First available block: \
132                     {first_available_block}",
133                ),
134                data: None,
135            },
136            RpcCustomError::SendTransactionPreflightFailure { message, result } => Self {
137                code: ErrorCode::ServerError(
138                    JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
139                ),
140                message,
141                data: Some(serde_json::json!(result)),
142            },
143            RpcCustomError::TransactionSignatureVerificationFailure => Self {
144                code: ErrorCode::ServerError(
145                    JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE,
146                ),
147                message: "Transaction signature verification failure".to_string(),
148                data: None,
149            },
150            RpcCustomError::BlockNotAvailable { slot } => Self {
151                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE),
152                message: format!("Block not available for slot {slot}"),
153                data: None,
154            },
155            RpcCustomError::NodeUnhealthy { num_slots_behind } => Self {
156                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY),
157                message: if let Some(num_slots_behind) = num_slots_behind {
158                    format!("Node is behind by {num_slots_behind} slots")
159                } else {
160                    "Node is unhealthy".to_string()
161                },
162                data: Some(serde_json::json!(NodeUnhealthyErrorData {
163                    num_slots_behind
164                })),
165            },
166            RpcCustomError::TransactionPrecompileVerificationFailure(e) => Self {
167                code: ErrorCode::ServerError(
168                    JSON_RPC_SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE,
169                ),
170                message: format!("Transaction precompile verification failure {e:?}"),
171                data: None,
172            },
173            RpcCustomError::SlotSkipped { slot } => Self {
174                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_SLOT_SKIPPED),
175                message: format!(
176                    "Slot {slot} was skipped, or missing due to ledger jump to recent snapshot"
177                ),
178                data: None,
179            },
180            RpcCustomError::NoSnapshot => Self {
181                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_NO_SNAPSHOT),
182                message: "No snapshot".to_string(),
183                data: None,
184            },
185            RpcCustomError::LongTermStorageSlotSkipped { slot } => Self {
186                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED),
187                message: format!("Slot {slot} was skipped, or missing in long-term storage"),
188                data: None,
189            },
190            RpcCustomError::KeyExcludedFromSecondaryIndex { index_key } => Self {
191                code: ErrorCode::ServerError(
192                    JSON_RPC_SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX,
193                ),
194                message: format!(
195                    "{index_key} excluded from account secondary indexes; this RPC method \
196                     unavailable for key"
197                ),
198                data: None,
199            },
200            RpcCustomError::TransactionHistoryNotAvailable => Self {
201                code: ErrorCode::ServerError(
202                    JSON_RPC_SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE,
203                ),
204                message: "Transaction history is not available from this node".to_string(),
205                data: None,
206            },
207            RpcCustomError::ScanError { message } => Self {
208                code: ErrorCode::ServerError(JSON_RPC_SCAN_ERROR),
209                message,
210                data: None,
211            },
212            RpcCustomError::TransactionSignatureLenMismatch => Self {
213                code: ErrorCode::ServerError(
214                    JSON_RPC_SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH,
215                ),
216                message: "Transaction signature length mismatch".to_string(),
217                data: None,
218            },
219            RpcCustomError::BlockStatusNotAvailableYet { slot } => Self {
220                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET),
221                message: format!("Block status not yet available for slot {slot}"),
222                data: None,
223            },
224            RpcCustomError::UnsupportedTransactionVersion(version) => Self {
225                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION),
226                message: format!(
227                    "Transaction version ({version}) is not supported by the requesting client. \
228                     Please try the request again with the following configuration parameter: \
229                     \"maxSupportedTransactionVersion\": {version}"
230                ),
231                data: None,
232            },
233            RpcCustomError::MinContextSlotNotReached { context_slot } => Self {
234                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED),
235                message: "Minimum context slot has not been reached".to_string(),
236                data: Some(serde_json::json!(MinContextSlotNotReachedErrorData {
237                    context_slot,
238                })),
239            },
240            RpcCustomError::EpochRewardsPeriodActive {
241                slot,
242                current_block_height,
243                rewards_complete_block_height,
244            } => Self {
245                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE),
246                message: format!("Epoch rewards period still active at slot {slot}"),
247                data: Some(serde_json::json!(EpochRewardsPeriodActiveErrorData {
248                    current_block_height,
249                    rewards_complete_block_height,
250                    slot: Some(slot),
251                })),
252            },
253            RpcCustomError::SlotNotEpochBoundary { slot } => Self {
254                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY),
255                message: format!(
256                    "Rewards cannot be found because slot {slot} is not the epoch boundary. This \
257                     may be due to gap in the queried node's local ledger or long-term storage"
258                ),
259                data: Some(serde_json::json!({
260                    "slot": slot,
261                })),
262            },
263            RpcCustomError::LongTermStorageUnreachable => Self {
264                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE),
265                message: "Failed to query long-term storage; please try again".to_string(),
266                data: None,
267            },
268            RpcCustomError::FilterTransactionNotFound { signature } => Self {
269                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND),
270                message: format!("Transaction {signature} not found"),
271                data: None,
272            },
273            RpcCustomError::NoSlotHistory => Self {
274                code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_NO_SLOT_HISTORY),
275                message: "No slot history".to_string(),
276                data: None,
277            },
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use {
285        crate::custom_error::EpochRewardsPeriodActiveErrorData, serde_json::Value,
286        test_case::test_case,
287    };
288
289    #[test_case(serde_json::json!({
290            "currentBlockHeight": 123,
291            "rewardsCompleteBlockHeight": 456
292        }); "Pre-3.0 schema")]
293    #[test_case(serde_json::json!({
294            "currentBlockHeight": 123,
295            "rewardsCompleteBlockHeight": 456,
296            "slot": 789
297        }); "3.0+ schema")]
298    fn test_deseriailze_epoch_rewards_period_active_error_data(serialized_data: Value) {
299        let expected_current_block_height = serialized_data
300            .get("currentBlockHeight")
301            .map(|v| v.as_u64().unwrap())
302            .unwrap();
303        let expected_rewards_complete_block_height = serialized_data
304            .get("rewardsCompleteBlockHeight")
305            .map(|v| v.as_u64().unwrap())
306            .unwrap();
307        let expected_slot: Option<u64> = serialized_data.get("slot").map(|v| v.as_u64().unwrap());
308        let actual: EpochRewardsPeriodActiveErrorData =
309            serde_json::from_value(serialized_data).expect("Failed to deserialize test fixture");
310        assert_eq!(
311            actual,
312            EpochRewardsPeriodActiveErrorData {
313                current_block_height: expected_current_block_height,
314                rewards_complete_block_height: expected_rewards_complete_block_height,
315                slot: expected_slot,
316            }
317        );
318    }
319}