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;
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}