diff --git a/starknet-accounts/tests/single_owner_account.rs b/starknet-accounts/tests/single_owner_account.rs index 658ca150..7cd56941 100644 --- a/starknet-accounts/tests/single_owner_account.rs +++ b/starknet-accounts/tests/single_owner_account.rs @@ -1,5 +1,7 @@ use rand::RngCore; -use starknet_accounts::{Account, Call, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; +use starknet_accounts::{ + Account, AccountError, Call, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount, +}; use starknet_core::{ chain_id, types::{ @@ -7,13 +9,13 @@ use starknet_core::{ legacy::{LegacyContractClass, RawLegacyAbiEntry, RawLegacyFunction}, SierraClass, }, - BlockId, BlockTag, FieldElement, + BlockId, BlockTag, FieldElement, StarknetError, }, utils::get_selector_from_name, }; use starknet_providers::{ jsonrpc::{HttpTransport, JsonRpcClient}, - Provider, SequencerGatewayProvider, + Provider, ProviderError, SequencerGatewayProvider, }; use starknet_signers::{LocalWallet, SigningKey}; use std::sync::Arc; @@ -66,6 +68,15 @@ async fn can_estimate_fee_with_jsonrpc() { .await } +#[tokio::test] +async fn can_parse_fee_estimation_error_with_jsonrpc() { + can_parse_fee_estimation_error_inner( + create_jsonrpc_client(), + "0x44c3c30803ea9c4e063ae052e6b7ef537284fca6b93849dae9a093e42aa1574", + ) + .await +} + // The `simulate`-related test cases are temporarily removed until it's supported in [Provider] // TODO: add `simulate` test cases back once transaction simulation in supported @@ -197,6 +208,54 @@ async fn can_estimate_fee_inner(provider: P, address: assert!(fee_estimate.overall_fee > 0); } +async fn can_parse_fee_estimation_error_inner( + provider: P, + address: &str, +) { + let signer = LocalWallet::from(SigningKey::from_secret_scalar( + FieldElement::from_hex_be( + "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ) + .unwrap(), + )); + let address = FieldElement::from_hex_be(address).unwrap(); + let eth_token_address = FieldElement::from_hex_be( + "049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + ) + .unwrap(); + + let mut account = SingleOwnerAccount::new( + provider, + signer, + address, + chain_id::TESTNET, + ExecutionEncoding::Legacy, + ); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + match account + .execute(vec![Call { + to: eth_token_address, + selector: get_selector_from_name("transfer").unwrap(), + calldata: vec![ + address, + FieldElement::from_dec_str("1000000000000000000000").unwrap(), + FieldElement::ZERO, + ], + }]) + .estimate_fee() + .await + { + Ok(_) => panic!("unexpected successful fee estimation"), + Err(AccountError::Provider(ProviderError::StarknetError( + StarknetError::ContractError(err_data), + ))) => { + assert!(!err_data.revert_error.is_empty()); + } + _ => panic!("unexpected error type"), + } +} + async fn can_execute_tst_mint_inner(provider: P, address: &str) { // This test case is not very useful as the sequencer will always respond with // `TransactionReceived` even if the transaction will eventually fail, just like how diff --git a/starknet-core/src/types/codegen.rs b/starknet-core/src/types/codegen.rs index 15201af4..fb2fb69e 100644 --- a/starknet-core/src/types/codegen.rs +++ b/starknet-core/src/types/codegen.rs @@ -3,7 +3,7 @@ // https://github.com/xJonathanLEI/starknet-jsonrpc-codegen // Code generated with version: -// https://github.com/xJonathanLEI/starknet-jsonrpc-codegen#51260963a0723fdbc715598efb7198ce5a1d49b9 +// https://github.com/xJonathanLEI/starknet-jsonrpc-codegen#bddc1b829c33b14440d22a85bc937e3d16e32ed1 // Code generation requested but not implemented for these types: // - `BLOCK_ID` @@ -241,6 +241,14 @@ pub struct CompressedLegacyContractClass { pub abi: Option>, } +/// More data about the execution failure. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))] +pub struct ContractErrorData { + /// A string encoding the execution trace up to the point of failure + pub revert_error: String, +} + /// Contract storage diff item. #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -1007,6 +1015,14 @@ pub struct MsgToL1 { pub payload: Vec, } +/// Extra information on why trace is not available. Either it wasn't executed yet (received), or +/// the transaction failed (rejected). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))] +pub struct NoTraceAvailableErrorData { + pub status: SequencerTransactionStatus, +} + /// Nonce update. /// /// The updated nonce per contract address. @@ -1305,7 +1321,7 @@ pub enum SimulationFlag { } /// JSON-RPC error codes -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum StarknetError { /// Failed to write transaction FailedToReceiveTransaction, @@ -1328,7 +1344,7 @@ pub enum StarknetError { /// Too many keys provided in a filter TooManyKeysInFilter, /// Contract error - ContractError, + ContractError(ContractErrorData), /// Class already declared ClassAlreadyDeclared, /// Invalid transaction nonce @@ -1354,9 +1370,9 @@ pub enum StarknetError { /// the contract class version is not supported UnsupportedContractClassVersion, /// An unexpected error occurred - UnexpectedError, + UnexpectedError(String), /// No trace available for transaction - NoTraceAvailable, + NoTraceAvailable(NoTraceAvailableErrorData), /// Invalid transaction hash InvalidTransactionHash, } @@ -1377,7 +1393,7 @@ impl core::fmt::Display for StarknetError { Self::NoBlocks => write!(f, "NoBlocks"), Self::InvalidContinuationToken => write!(f, "InvalidContinuationToken"), Self::TooManyKeysInFilter => write!(f, "TooManyKeysInFilter"), - Self::ContractError => write!(f, "ContractError"), + Self::ContractError(_) => write!(f, "ContractError"), Self::ClassAlreadyDeclared => write!(f, "ClassAlreadyDeclared"), Self::InvalidTransactionNonce => write!(f, "InvalidTransactionNonce"), Self::InsufficientMaxFee => write!(f, "InsufficientMaxFee"), @@ -1390,8 +1406,8 @@ impl core::fmt::Display for StarknetError { Self::CompiledClassHashMismatch => write!(f, "CompiledClassHashMismatch"), Self::UnsupportedTxVersion => write!(f, "UnsupportedTxVersion"), Self::UnsupportedContractClassVersion => write!(f, "UnsupportedContractClassVersion"), - Self::UnexpectedError => write!(f, "UnexpectedError"), - Self::NoTraceAvailable => write!(f, "NoTraceAvailable"), + Self::UnexpectedError(_) => write!(f, "UnexpectedError"), + Self::NoTraceAvailable(_) => write!(f, "NoTraceAvailable"), Self::InvalidTransactionHash => write!(f, "InvalidTransactionHash"), } } @@ -1410,7 +1426,7 @@ impl StarknetError { Self::NoBlocks => "There are no blocks", Self::InvalidContinuationToken => "The supplied continuation token is invalid or unknown", Self::TooManyKeysInFilter => "Too many keys provided in a filter", - Self::ContractError => "Contract error", + Self::ContractError(_) => "Contract error", Self::ClassAlreadyDeclared => "Class already declared", Self::InvalidTransactionNonce => "Invalid transaction nonce", Self::InsufficientMaxFee => "Max fee is smaller than the minimal transaction cost (validation plus fee transfer)", @@ -1423,8 +1439,8 @@ impl StarknetError { Self::CompiledClassHashMismatch => "the compiled class hash did not match the one supplied in the transaction", Self::UnsupportedTxVersion => "the transaction version is not supported", Self::UnsupportedContractClassVersion => "the contract class version is not supported", - Self::UnexpectedError => "An unexpected error occurred", - Self::NoTraceAvailable => "No trace available for transaction", + Self::UnexpectedError(_) => "An unexpected error occurred", + Self::NoTraceAvailable(_) => "No trace available for transaction", Self::InvalidTransactionHash => "Invalid transaction hash", } } diff --git a/starknet-core/src/types/mod.rs b/starknet-core/src/types/mod.rs index d6c95d5d..7c3e4eba 100644 --- a/starknet-core/src/types/mod.rs +++ b/starknet-core/src/types/mod.rs @@ -16,18 +16,19 @@ mod codegen; pub use codegen::{ BlockStatus, BlockTag, BlockWithTxHashes, BlockWithTxs, BroadcastedDeclareTransactionV1, BroadcastedDeclareTransactionV2, BroadcastedDeployAccountTransaction, - BroadcastedInvokeTransaction, CallType, CompressedLegacyContractClass, ContractStorageDiffItem, - DataAvailabilityMode, DeclareTransactionReceipt, DeclareTransactionTrace, DeclareTransactionV0, - DeclareTransactionV1, DeclareTransactionV2, DeclaredClassItem, DeployAccountTransaction, - DeployAccountTransactionReceipt, DeployAccountTransactionTrace, DeployTransaction, - DeployTransactionReceipt, DeployedContractItem, EmittedEvent, EntryPointType, - EntryPointsByType, Event, EventFilter, EventFilterWithPage, EventsChunk, ExecutionResources, - FeeEstimate, FlattenedSierraClass, FunctionCall, FunctionInvocation, FunctionStateMutability, - InvokeTransactionReceipt, InvokeTransactionTrace, InvokeTransactionV0, InvokeTransactionV1, - L1HandlerTransaction, L1HandlerTransactionReceipt, L1HandlerTransactionTrace, - LegacyContractEntryPoint, LegacyEntryPointsByType, LegacyEventAbiEntry, LegacyEventAbiType, - LegacyFunctionAbiEntry, LegacyFunctionAbiType, LegacyStructAbiEntry, LegacyStructAbiType, - LegacyStructMember, LegacyTypedParameter, MsgFromL1, MsgToL1, NonceUpdate, OrderedEvent, + BroadcastedInvokeTransaction, CallType, CompressedLegacyContractClass, ContractErrorData, + ContractStorageDiffItem, DataAvailabilityMode, DeclareTransactionReceipt, + DeclareTransactionTrace, DeclareTransactionV0, DeclareTransactionV1, DeclareTransactionV2, + DeclaredClassItem, DeployAccountTransaction, DeployAccountTransactionReceipt, + DeployAccountTransactionTrace, DeployTransaction, DeployTransactionReceipt, + DeployedContractItem, EmittedEvent, EntryPointType, EntryPointsByType, Event, EventFilter, + EventFilterWithPage, EventsChunk, ExecutionResources, FeeEstimate, FlattenedSierraClass, + FunctionCall, FunctionInvocation, FunctionStateMutability, InvokeTransactionReceipt, + InvokeTransactionTrace, InvokeTransactionV0, InvokeTransactionV1, L1HandlerTransaction, + L1HandlerTransactionReceipt, L1HandlerTransactionTrace, LegacyContractEntryPoint, + LegacyEntryPointsByType, LegacyEventAbiEntry, LegacyEventAbiType, LegacyFunctionAbiEntry, + LegacyFunctionAbiType, LegacyStructAbiEntry, LegacyStructAbiType, LegacyStructMember, + LegacyTypedParameter, MsgFromL1, MsgToL1, NoTraceAvailableErrorData, NonceUpdate, OrderedEvent, OrderedMessage, PendingBlockWithTxHashes, PendingBlockWithTxs, PendingDeclareTransactionReceipt, PendingDeployAccountTransactionReceipt, PendingInvokeTransactionReceipt, PendingL1HandlerTransactionReceipt, PendingStateUpdate, @@ -516,42 +517,6 @@ impl TryFrom<&L1HandlerTransaction> for MsgToL2 { } } -impl TryFrom for StarknetError { - type Error = (); - - fn try_from(value: i64) -> Result { - Ok(match value { - 1 => StarknetError::FailedToReceiveTransaction, - 20 => StarknetError::ContractNotFound, - 24 => StarknetError::BlockNotFound, - 27 => StarknetError::InvalidTransactionIndex, - 28 => StarknetError::ClassHashNotFound, - 29 => StarknetError::TransactionHashNotFound, - 31 => StarknetError::PageSizeTooBig, - 32 => StarknetError::NoBlocks, - 33 => StarknetError::InvalidContinuationToken, - 34 => StarknetError::TooManyKeysInFilter, - 40 => StarknetError::ContractError, - 51 => StarknetError::ClassAlreadyDeclared, - 52 => StarknetError::InvalidTransactionNonce, - 53 => StarknetError::InsufficientMaxFee, - 54 => StarknetError::InsufficientAccountBalance, - 55 => StarknetError::ValidationFailure, - 56 => StarknetError::CompilationFailed, - 57 => StarknetError::ContractClassSizeIsTooLarge, - 58 => StarknetError::NonAccount, - 59 => StarknetError::DuplicateTx, - 60 => StarknetError::CompiledClassHashMismatch, - 61 => StarknetError::UnsupportedTxVersion, - 62 => StarknetError::UnsupportedContractClassVersion, - 63 => StarknetError::UnexpectedError, - 10 => StarknetError::NoTraceAvailable, - 25 => StarknetError::InvalidTransactionHash, - _ => return Err(()), - }) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/starknet-providers/src/jsonrpc/mod.rs b/starknet-providers/src/jsonrpc/mod.rs index 0d5e9e76..e67507be 100644 --- a/starknet-providers/src/jsonrpc/mod.rs +++ b/starknet-providers/src/jsonrpc/mod.rs @@ -1,4 +1,4 @@ -use std::{any::Any, error::Error}; +use std::{any::Any, error::Error, fmt::Display}; use async_trait::async_trait; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -8,12 +8,13 @@ use starknet_core::{ types::{ requests::*, BlockHashAndNumber, BlockId, BroadcastedDeclareTransaction, BroadcastedDeployAccountTransaction, BroadcastedInvokeTransaction, BroadcastedTransaction, - ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilter, - EventFilterWithPage, EventsPage, FeeEstimate, FieldElement, FunctionCall, + ContractClass, ContractErrorData, DeclareTransactionResult, DeployAccountTransactionResult, + EventFilter, EventFilterWithPage, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, - MaybePendingStateUpdate, MaybePendingTransactionReceipt, MsgFromL1, ResultPageRequest, - SimulatedTransaction, SimulationFlag, SyncStatusType, Transaction, TransactionStatus, - TransactionTrace, TransactionTraceWithHash, + MaybePendingStateUpdate, MaybePendingTransactionReceipt, MsgFromL1, + NoTraceAvailableErrorData, ResultPageRequest, SimulatedTransaction, SimulationFlag, + StarknetError, SyncStatusType, Transaction, TransactionStatus, TransactionTrace, + TransactionTraceWithHash, }, }; @@ -135,11 +136,12 @@ pub enum JsonRpcClientError { JsonRpcError(JsonRpcError), } -#[derive(Debug, thiserror::Error, Deserialize)] -#[error("JSON-RPC error: code={code}, message=\"{message}\"")] +#[derive(Debug, Deserialize)] pub struct JsonRpcError { pub code: i64, pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, } #[derive(Debug, Deserialize)] @@ -149,6 +151,17 @@ pub enum JsonRpcResponse { Error { id: u64, error: JsonRpcError }, } +/// Failures trying to parse a [JsonRpcError] into [StarknetError]. +#[derive(Debug, thiserror::Error)] +pub enum JsonRpcErrorConversionError { + #[error("unknown error code")] + UnknownCode, + #[error("missing data field")] + MissingData, + #[error("unable to parse the data field")] + DataParsingFailure, +} + #[serde_as] #[derive(Serialize, Deserialize)] struct Felt(#[serde_as(as = "UfeHex")] pub FieldElement); @@ -179,10 +192,12 @@ where .map_err(JsonRpcClientError::TransportError)? { JsonRpcResponse::Success { result, .. } => Ok(result), - JsonRpcResponse::Error { error, .. } => Err(match error.code.try_into() { - Ok(code) => ProviderError::StarknetError(code), - Err(_) => JsonRpcClientError::::JsonRpcError(error).into(), - }), + JsonRpcResponse::Error { error, .. } => { + Err(match TryInto::::try_into(&error) { + Ok(error) => ProviderError::StarknetError(error), + Err(_) => JsonRpcClientError::::JsonRpcError(error).into(), + }) + } } } } @@ -831,3 +846,91 @@ impl From for JsonRpcClientError { Self::JsonError(value) } } + +impl TryFrom<&JsonRpcError> for StarknetError { + type Error = JsonRpcErrorConversionError; + + fn try_from(value: &JsonRpcError) -> Result { + match value.code { + 1 => Ok(StarknetError::FailedToReceiveTransaction), + 20 => Ok(StarknetError::ContractNotFound), + 24 => Ok(StarknetError::BlockNotFound), + 27 => Ok(StarknetError::InvalidTransactionIndex), + 28 => Ok(StarknetError::ClassHashNotFound), + 29 => Ok(StarknetError::TransactionHashNotFound), + 31 => Ok(StarknetError::PageSizeTooBig), + 32 => Ok(StarknetError::NoBlocks), + 33 => Ok(StarknetError::InvalidContinuationToken), + 34 => Ok(StarknetError::TooManyKeysInFilter), + 40 => { + let data = ContractErrorData::deserialize( + value + .data + .as_ref() + .ok_or(JsonRpcErrorConversionError::MissingData)?, + ) + .map_err(|_| JsonRpcErrorConversionError::DataParsingFailure)?; + Ok(StarknetError::ContractError(data)) + } + 51 => Ok(StarknetError::ClassAlreadyDeclared), + 52 => Ok(StarknetError::InvalidTransactionNonce), + 53 => Ok(StarknetError::InsufficientMaxFee), + 54 => Ok(StarknetError::InsufficientAccountBalance), + 55 => Ok(StarknetError::ValidationFailure), + 56 => Ok(StarknetError::CompilationFailed), + 57 => Ok(StarknetError::ContractClassSizeIsTooLarge), + 58 => Ok(StarknetError::NonAccount), + 59 => Ok(StarknetError::DuplicateTx), + 60 => Ok(StarknetError::CompiledClassHashMismatch), + 61 => Ok(StarknetError::UnsupportedTxVersion), + 62 => Ok(StarknetError::UnsupportedContractClassVersion), + 63 => { + let data = String::deserialize( + value + .data + .as_ref() + .ok_or(JsonRpcErrorConversionError::MissingData)?, + ) + .map_err(|_| JsonRpcErrorConversionError::DataParsingFailure)?; + Ok(StarknetError::UnexpectedError(data)) + } + 10 => { + let data = NoTraceAvailableErrorData::deserialize( + value + .data + .as_ref() + .ok_or(JsonRpcErrorConversionError::MissingData)?, + ) + .map_err(|_| JsonRpcErrorConversionError::DataParsingFailure)?; + Ok(StarknetError::NoTraceAvailable(data)) + } + 25 => Ok(StarknetError::InvalidTransactionHash), + _ => Err(JsonRpcErrorConversionError::UnknownCode), + } + } +} + +impl Error for JsonRpcError {} + +impl Display for JsonRpcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.data { + Some(data) => { + write!( + f, + "JSON-RPC error: code={}, message=\"{}\", data={}", + self.code, + self.message, + serde_json::to_string(data).map_err(|_| std::fmt::Error)? + ) + } + None => { + write!( + f, + "JSON-RPC error: code={}, message=\"{}\"", + self.code, self.message + ) + } + } + } +} diff --git a/starknet-providers/src/sequencer/mod.rs b/starknet-providers/src/sequencer/mod.rs index aacc17ea..9ab81acc 100644 --- a/starknet-providers/src/sequencer/mod.rs +++ b/starknet-providers/src/sequencer/mod.rs @@ -715,7 +715,7 @@ impl From for ProviderError { let matching_code = match value.code { ErrorCode::BlockNotFound => Some(StarknetError::BlockNotFound), ErrorCode::EntryPointNotFoundInContract => None, - ErrorCode::InvalidProgram => Some(StarknetError::ContractError), + ErrorCode::InvalidProgram => None, ErrorCode::TransactionFailed => Some(StarknetError::ValidationFailure), ErrorCode::TransactionNotFound => Some(StarknetError::ContractNotFound), ErrorCode::UninitializedContract => Some(StarknetError::ContractNotFound), @@ -726,7 +726,7 @@ impl From for ProviderError { ErrorCode::CompilationFailed => Some(StarknetError::CompilationFailed), ErrorCode::InvalidCompiledClassHash => Some(StarknetError::CompiledClassHashMismatch), ErrorCode::DuplicatedTransaction => Some(StarknetError::DuplicateTx), - ErrorCode::InvalidContractClass => Some(StarknetError::ContractError), + ErrorCode::InvalidContractClass => None, }; match matching_code {