1use serde_json::json;
2use starknet_core::error::{ContractExecutionError, TransactionValidationError};
3use starknet_rs_core::types::Felt;
4use starknet_types;
5use starknet_types::felt::Nonce;
6use starknet_types::starknet_api::core::ContractAddress;
7use thiserror::Error;
8use tracing::error;
9
10use crate::api::models::{JsonRpcResponse, WILDCARD_RPC_ERROR_CODE};
11use crate::rpc_core::error::RpcError;
12
13#[allow(unused)]
14#[derive(Error, Debug)]
15pub enum ApiError {
16 #[error(transparent)]
17 StarknetDevnetError(#[from] starknet_core::error::Error),
18 #[error("Types error")]
19 TypesError(#[from] starknet_types::error::Error),
20 #[error("Rpc error {0:?}")]
21 RpcError(RpcError),
22 #[error("Block not found")]
23 BlockNotFound,
24 #[error("Contract not found")]
25 ContractNotFound,
26 #[error("Transaction hash not found")]
27 TransactionNotFound,
28 #[error("Invalid transaction index in a block")]
29 InvalidTransactionIndexInBlock,
30 #[error("Class hash not found")]
31 ClassHashNotFound,
32 #[error("Contract error")]
33 ContractError(ContractExecutionError),
34 #[error("Transaction execution error")]
35 TransactionExecutionError { failure_index: usize, execution_error: ContractExecutionError },
36 #[error("There are no blocks")]
37 NoBlocks,
38 #[error("Requested page size is too big")]
39 RequestPageSizeTooBig,
40 #[error("The supplied continuation token is invalid or unknown")]
41 InvalidContinuationToken,
42 #[error("Too many keys provided in a filter")]
43 TooManyKeysInFilter,
44 #[error("Class already declared")]
45 ClassAlreadyDeclared,
46 #[error("Invalid contract class")]
47 InvalidContractClass,
48 #[error("{msg}")]
49 UnsupportedAction { msg: String },
50 #[error("Invalid transaction nonce")]
51 InvalidTransactionNonce {
52 address: ContractAddress,
53 account_nonce: Nonce,
54 incoming_tx_nonce: Nonce,
55 },
56 #[error("The transaction's resources don't cover validation or the minimal transaction fee")]
57 InsufficientResourcesForValidate,
58 #[error(
59 "Account balance is smaller than the transaction's maximal fee (calculated as the sum of \
60 each resource's limit x max price)"
61 )]
62 InsufficientAccountBalance,
63 #[error("Account validation failed")]
64 ValidationFailure { reason: String },
65 #[error("No trace available for transaction")]
66 NoTraceAvailable,
67 #[error("{msg}")]
68 NoStateAtBlock { msg: String },
69 #[error("the compiled class hash did not match the one supplied in the transaction")]
70 CompiledClassHashMismatch,
71 #[error("Requested entrypoint does not exist in the contract")]
72 EntrypointNotFound,
73 #[error("Cannot go back more than 1024 blocks")]
74 TooManyBlocksBack,
75 #[error("Invalid subscription id")]
76 InvalidSubscriptionId,
77 #[error("Devnet doesn't support storage proofs")] StorageProofNotSupported,
79 #[error("Contract class size is too large")]
80 ContractClassSizeIsTooLarge,
81 #[error("Minting reverted")]
82 MintingReverted { tx_hash: Felt, revert_reason: Option<String> },
83 #[error("The dump operation failed: {msg}")]
84 DumpError { msg: String },
85 #[error("Messaging error: {msg}")]
86 MessagingError { msg: String },
87 #[error("Invalid address: {msg}")]
88 InvalidAddress { msg: String },
89}
90
91impl ApiError {
92 pub fn api_error_to_rpc_error(self) -> RpcError {
93 let error_message = self.to_string();
94 match self {
95 ApiError::RpcError(rpc_error) => rpc_error,
96 ApiError::BlockNotFound => RpcError {
97 code: crate::rpc_core::error::ErrorCode::ServerError(24),
98 message: error_message.into(),
99 data: None,
100 },
101 ApiError::ContractNotFound => RpcError {
102 code: crate::rpc_core::error::ErrorCode::ServerError(20),
103 message: error_message.into(),
104 data: None,
105 },
106 ApiError::TransactionNotFound => RpcError {
107 code: crate::rpc_core::error::ErrorCode::ServerError(29),
108 message: error_message.into(),
109 data: None,
110 },
111 ApiError::InvalidTransactionIndexInBlock => RpcError {
112 code: crate::rpc_core::error::ErrorCode::ServerError(27),
113 message: error_message.into(),
114 data: None,
115 },
116 ApiError::ClassHashNotFound => RpcError {
117 code: crate::rpc_core::error::ErrorCode::ServerError(28),
118 message: error_message.into(),
119 data: None,
120 },
121 ApiError::ContractError(contract_execution_error) => RpcError {
122 code: crate::rpc_core::error::ErrorCode::ServerError(40),
123 message: error_message.into(),
124 data: Some(json!({
125 "revert_error": contract_execution_error
126 })),
127 },
128 ApiError::TransactionExecutionError { execution_error, failure_index } => RpcError {
129 code: crate::rpc_core::error::ErrorCode::ServerError(41),
130 message: error_message.into(),
131 data: Some(serde_json::json!({
132 "transaction_index": failure_index,
133 "execution_error": execution_error,
134 })),
135 },
136 ApiError::NoBlocks => RpcError {
137 code: crate::rpc_core::error::ErrorCode::ServerError(32),
138 message: error_message.into(),
139 data: None,
140 },
141 ApiError::RequestPageSizeTooBig => RpcError {
142 code: crate::rpc_core::error::ErrorCode::ServerError(31),
143 message: error_message.into(),
144 data: None,
145 },
146 ApiError::InvalidContinuationToken => RpcError {
147 code: crate::rpc_core::error::ErrorCode::ServerError(33),
148 message: error_message.into(),
149 data: None,
150 },
151 ApiError::TooManyKeysInFilter => RpcError {
152 code: crate::rpc_core::error::ErrorCode::ServerError(34),
153 message: error_message.into(),
154 data: None,
155 },
156 ApiError::ClassAlreadyDeclared => RpcError {
157 code: crate::rpc_core::error::ErrorCode::ServerError(51),
158 message: error_message.into(),
159 data: None,
160 },
161 ApiError::InvalidContractClass => RpcError {
162 code: crate::rpc_core::error::ErrorCode::ServerError(50),
163 message: error_message.into(),
164 data: None,
165 },
166 ApiError::TypesError(_) => RpcError {
167 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
168 message: error_message.into(),
169 data: None,
170 },
171 ApiError::UnsupportedAction { msg } => RpcError {
172 code: crate::rpc_core::error::ErrorCode::InvalidRequest,
173 message: msg.into(),
174 data: None,
175 },
176 ApiError::InsufficientResourcesForValidate => RpcError {
177 code: crate::rpc_core::error::ErrorCode::ServerError(53),
178 message: error_message.into(),
179 data: None,
180 },
181 ApiError::InvalidTransactionNonce { address, account_nonce, incoming_tx_nonce } => {
182 RpcError {
183 code: crate::rpc_core::error::ErrorCode::ServerError(52),
184 message: error_message.into(),
185 data: Some(json!(format!(
186 "Invalid transaction nonce of contract at address {address}. Account \
187 nonce: {account_nonce}; got: {incoming_tx_nonce}."
188 ))),
189 }
190 }
191 ApiError::InsufficientAccountBalance => RpcError {
192 code: crate::rpc_core::error::ErrorCode::ServerError(54),
193 message: error_message.into(),
194 data: None,
195 },
196 ApiError::ValidationFailure { reason } => RpcError {
197 code: crate::rpc_core::error::ErrorCode::ServerError(55),
198 message: error_message.into(),
199 data: Some(serde_json::Value::String(reason)),
200 },
201 ApiError::CompiledClassHashMismatch => RpcError {
202 code: crate::rpc_core::error::ErrorCode::ServerError(60),
203 message: error_message.into(),
204 data: None,
205 },
206 ApiError::StarknetDevnetError(
207 starknet_core::error::Error::TransactionValidationError(validation_error),
208 ) => {
209 let api_err = match validation_error {
210 TransactionValidationError::InsufficientResourcesForValidate => {
211 ApiError::InsufficientResourcesForValidate
212 }
213 TransactionValidationError::InvalidTransactionNonce {
214 address,
215 account_nonce,
216 incoming_tx_nonce,
217 } => ApiError::InvalidTransactionNonce {
218 address: address.into(),
219 account_nonce: *account_nonce,
220 incoming_tx_nonce: *incoming_tx_nonce,
221 },
222 TransactionValidationError::InsufficientAccountBalance => {
223 ApiError::InsufficientAccountBalance
224 }
225 TransactionValidationError::ValidationFailure { reason } => {
226 ApiError::ValidationFailure { reason }
227 }
228 };
229
230 api_err.api_error_to_rpc_error()
231 }
232 ApiError::StarknetDevnetError(error) => RpcError {
233 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
234 message: anyhow::format_err!(error).root_cause().to_string().into(),
235 data: None,
236 },
237 ApiError::NoTraceAvailable => RpcError {
238 code: crate::rpc_core::error::ErrorCode::ServerError(10),
239 message: error_message.into(),
240 data: None,
241 },
242 ApiError::NoStateAtBlock { .. } => RpcError {
243 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
244 message: error_message.into(),
245 data: None,
246 },
247 ApiError::EntrypointNotFound => RpcError {
248 code: crate::rpc_core::error::ErrorCode::ServerError(21),
249 message: error_message.into(),
250 data: None,
251 },
252 ApiError::TooManyBlocksBack => RpcError {
253 code: crate::rpc_core::error::ErrorCode::ServerError(68),
254 message: error_message.into(),
255 data: None,
256 },
257 ApiError::InvalidSubscriptionId => RpcError {
258 code: crate::rpc_core::error::ErrorCode::ServerError(66),
259 message: error_message.into(),
260 data: None,
261 },
262 ApiError::StorageProofNotSupported => RpcError {
263 code: crate::rpc_core::error::ErrorCode::ServerError(42),
264 message: error_message.into(),
265 data: None,
266 },
267 ApiError::ContractClassSizeIsTooLarge => RpcError {
268 code: crate::rpc_core::error::ErrorCode::ServerError(57),
269 message: error_message.into(),
270 data: None,
271 },
272 ApiError::MintingReverted { tx_hash, revert_reason: reason } => RpcError {
273 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
274 message: error_message.into(),
275 data: Some(serde_json::json!({ "tx_hash": tx_hash, "revert_reason": reason })),
276 },
277 ApiError::DumpError { msg } => RpcError {
278 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
279 message: msg.into(),
280 data: None,
281 },
282 ApiError::MessagingError { msg } => RpcError {
283 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
284 message: msg.into(),
285 data: None,
286 },
287 ApiError::InvalidAddress { msg } => RpcError {
288 code: crate::rpc_core::error::ErrorCode::ServerError(WILDCARD_RPC_ERROR_CODE),
289 message: msg.into(),
290 data: None,
291 },
292 }
293 }
294
295 pub(crate) fn is_forwardable_to_origin(&self) -> bool {
296 #[warn(clippy::wildcard_enum_match_arm)]
297 match self {
298 Self::BlockNotFound
299 | Self::TransactionNotFound
300 | Self::NoStateAtBlock { .. }
301 | Self::ClassHashNotFound => true,
302 Self::StarknetDevnetError(_)
303 | Self::NoTraceAvailable
304 | Self::TypesError(_)
305 | Self::RpcError(_)
306 | Self::ContractNotFound | Self::InvalidTransactionIndexInBlock
308 | Self::ContractError { .. }
309 | Self::NoBlocks
310 | Self::RequestPageSizeTooBig
311 | Self::InvalidContinuationToken
312 | Self::TooManyKeysInFilter
313 | Self::ClassAlreadyDeclared
314 | Self::InvalidContractClass
315 | Self::UnsupportedAction { .. }
316 | Self::InvalidTransactionNonce { .. }
317 | Self::InsufficientAccountBalance
318 | Self::ValidationFailure { .. }
319 | Self::EntrypointNotFound
320 | Self::TransactionExecutionError { .. }
321 | Self::TooManyBlocksBack
322 | Self::InvalidSubscriptionId
323 | Self::InsufficientResourcesForValidate
324 | Self::StorageProofNotSupported
325 | Self::ContractClassSizeIsTooLarge
326 | Self::MintingReverted { .. }
327 | Self::CompiledClassHashMismatch
328 | Self::DumpError { .. }
329 | Self::MessagingError { .. }
330 | Self::InvalidAddress { .. } => false,
331 }
332 }
333}
334
335pub type StrictRpcResult = Result<JsonRpcResponse, ApiError>;
336
337#[cfg(test)]
338mod tests {
339 use serde_json::json;
340 use starknet_core::error::ContractExecutionError;
341 use starknet_rs_core::types::Felt;
342 use starknet_types::contract_address::ContractAddress;
343 use starknet_types::starknet_api::core::Nonce;
344
345 use super::StrictRpcResult;
346 use crate::api::error::ApiError;
347 use crate::api::models::ToRpcResponseResult;
348 use crate::rpc_core::error::{ErrorCode, RpcError};
349
350 #[test]
351 fn contract_not_found_error() {
352 error_expected_code_and_message(ApiError::ContractNotFound, 20, "Contract not found");
353 }
354
355 #[test]
356 fn block_not_found_error() {
357 error_expected_code_and_message(ApiError::BlockNotFound, 24, "Block not found");
358 }
359
360 #[test]
361 fn transaction_not_found_error() {
362 error_expected_code_and_message(
363 ApiError::TransactionNotFound,
364 29,
365 "Transaction hash not found",
366 );
367 }
368
369 #[test]
370 fn invalid_transaction_index_error() {
371 error_expected_code_and_message(
372 ApiError::InvalidTransactionIndexInBlock,
373 27,
374 "Invalid transaction index in a block",
375 );
376 }
377
378 #[test]
379 fn class_hash_not_found_error() {
380 error_expected_code_and_message(ApiError::ClassHashNotFound, 28, "Class hash not found");
381 }
382
383 #[test]
384 fn page_size_too_big_error() {
385 error_expected_code_and_message(
386 ApiError::RequestPageSizeTooBig,
387 31,
388 "Requested page size is too big",
389 );
390 }
391
392 #[test]
393 fn no_blocks_error() {
394 error_expected_code_and_message(ApiError::NoBlocks, 32, "There are no blocks");
395 }
396
397 #[test]
398 fn invalid_continuation_token_error() {
399 error_expected_code_and_message(
400 ApiError::InvalidContinuationToken,
401 33,
402 "The supplied continuation token is invalid or unknown",
403 );
404 }
405
406 #[test]
407 fn too_many_keys_in_filter_error() {
408 error_expected_code_and_message(
409 ApiError::TooManyKeysInFilter,
410 34,
411 "Too many keys provided in a filter",
412 );
413 }
414
415 #[test]
416 fn contract_error() {
417 let api_error =
418 ApiError::ContractError(ContractExecutionError::Message("some_reason".to_string()));
419
420 error_expected_code_and_message(api_error, 40, "Contract error");
421
422 let error =
424 ApiError::ContractError(ContractExecutionError::Message("some_reason".to_string()))
425 .api_error_to_rpc_error();
426
427 let error_data = error.data.unwrap();
428 assert_eq!(error_data["revert_error"].as_str().unwrap(), "some_reason");
429 }
430
431 #[test]
432 fn transaction_execution_error() {
433 error_expected_code_and_message(
434 ApiError::TransactionExecutionError {
435 failure_index: 0,
436 execution_error: ContractExecutionError::Message("anything".to_string()),
437 },
438 41,
439 "Transaction execution error",
440 );
441
442 error_expected_code_and_data(
443 ApiError::TransactionExecutionError {
444 failure_index: 1,
445 execution_error: ContractExecutionError::Message("anything".to_string()),
446 },
447 41,
448 &serde_json::json!({ "transaction_index": 1, "execution_error": "anything" }),
449 );
450 }
451
452 #[test]
453 fn invalid_transaction_nonce_error() {
454 let devnet_error =
455 ApiError::StarknetDevnetError(starknet_core::error::Error::TransactionValidationError(
456 starknet_core::error::TransactionValidationError::InvalidTransactionNonce {
457 address: ContractAddress::zero(),
458 account_nonce: Nonce(Felt::ONE),
459 incoming_tx_nonce: Nonce(Felt::TWO),
460 },
461 ));
462
463 assert_eq!(
464 devnet_error.api_error_to_rpc_error(),
465 RpcError {
466 code: ErrorCode::ServerError(52),
467 message: "Invalid transaction nonce".into(),
468 data: Some(json!(
469 "Invalid transaction nonce of contract at address \
470 0x0000000000000000000000000000000000000000000000000000000000000000. Account \
471 nonce: 1; got: 2."
472 ))
473 }
474 );
475 }
476
477 #[test]
478 fn insufficient_resources_error() {
479 let devnet_error =
480 ApiError::StarknetDevnetError(starknet_core::error::Error::TransactionValidationError(
481 starknet_core::error::TransactionValidationError::InsufficientResourcesForValidate,
482 ));
483
484 assert_eq!(
485 devnet_error.api_error_to_rpc_error(),
486 ApiError::InsufficientResourcesForValidate.api_error_to_rpc_error()
487 );
488 error_expected_code_and_message(
489 ApiError::InsufficientResourcesForValidate,
490 53,
491 "The transaction's resources don't cover validation or the minimal transaction fee",
492 );
493 }
494
495 #[test]
496 fn insufficient_account_balance_error() {
497 let devnet_error =
498 ApiError::StarknetDevnetError(starknet_core::error::Error::TransactionValidationError(
499 starknet_core::error::TransactionValidationError::InsufficientAccountBalance,
500 ));
501
502 assert_eq!(
503 devnet_error.api_error_to_rpc_error(),
504 ApiError::InsufficientAccountBalance.api_error_to_rpc_error()
505 );
506 error_expected_code_and_message(
507 ApiError::InsufficientAccountBalance,
508 54,
509 "Account balance is smaller than the transaction's maximal fee (calculated as the sum \
510 of each resource's limit x max price)",
511 );
512 }
513
514 #[test]
515 fn account_validation_error() {
516 let reason = String::from("some reason");
517 let devnet_error =
518 ApiError::StarknetDevnetError(starknet_core::error::Error::TransactionValidationError(
519 starknet_core::error::TransactionValidationError::ValidationFailure {
520 reason: reason.clone(),
521 },
522 ));
523
524 assert_eq!(
525 devnet_error.api_error_to_rpc_error(),
526 ApiError::ValidationFailure { reason: reason.clone() }.api_error_to_rpc_error()
527 );
528 error_expected_code_and_message(
529 ApiError::ValidationFailure { reason: reason.clone() },
530 55,
531 "Account validation failed",
532 );
533
534 error_expected_code_and_data(
535 ApiError::ValidationFailure { reason: reason.clone() },
536 55,
537 &serde_json::json!(reason),
538 );
539 }
540
541 #[test]
542 fn minting_reverted_error() {
543 let revert_reason = String::from("some kind of reason");
544 let devnet_error = ApiError::MintingReverted {
545 tx_hash: Felt::ONE,
546 revert_reason: Some(revert_reason.clone()),
547 };
548
549 error_expected_code_and_data(
550 devnet_error,
551 -1,
552 &serde_json::json!({
553 "tx_hash": "0x1",
554 "revert_reason": revert_reason,
555 }),
556 );
557 }
558
559 fn error_expected_code_and_message(err: ApiError, expected_code: i64, expected_message: &str) {
560 let error_result = StrictRpcResult::Err(err).to_rpc_result();
561 match error_result {
562 crate::rpc_core::response::ResponseResult::Success(_) => panic!("Expected error"),
563 crate::rpc_core::response::ResponseResult::Error(err) => {
564 assert_eq!(err.message, expected_message);
565 assert_eq!(err.code, crate::rpc_core::error::ErrorCode::ServerError(expected_code))
566 }
567 }
568 }
569
570 fn error_expected_code_and_data(
571 err: ApiError,
572 expected_code: i64,
573 expected_data: &serde_json::Value,
574 ) {
575 let error_result = StrictRpcResult::Err(err).to_rpc_result();
576 match error_result {
577 crate::rpc_core::response::ResponseResult::Success(_) => panic!("Expected error"),
578 crate::rpc_core::response::ResponseResult::Error(err) => {
579 assert_eq!(&err.data.unwrap(), expected_data);
580 assert_eq!(err.code, crate::rpc_core::error::ErrorCode::ServerError(expected_code))
581 }
582 }
583 }
584}