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