1use {
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
11pub 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}