starknet_devnet_server/api/
error.rs

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")] // slightly modified spec message
78    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 // Doesn't require forwarding, handled at state reader level
307            | 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        // check contract error data property
423        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}