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