solana_rpc_client_api/
custom_error.rs

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