diff --git a/starknet-providers/src/any.rs b/starknet-providers/src/any.rs index b2d09416..d2c3382b 100644 --- a/starknet-providers/src/any.rs +++ b/starknet-providers/src/any.rs @@ -5,7 +5,8 @@ use starknet_core::types::{ ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilter, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingStateUpdate, - MaybePendingTransactionReceipt, MsgFromL1, SyncStatusType, Transaction, + MaybePendingTransactionReceipt, MsgFromL1, SimulatedTransaction, SimulationFlag, + SyncStatusType, Transaction, TransactionTrace, TransactionTraceWithHash, }; use crate::{ @@ -551,6 +552,82 @@ impl Provider for AnyProvider { ), } } + + async fn trace_transaction( + &self, + transaction_hash: H, + ) -> Result> + where + H: AsRef + Send + Sync, + { + match self { + Self::JsonRpcHttp(inner) => Ok( + as Provider>::trace_transaction( + inner, + transaction_hash, + ) + .await?, + ), + Self::SequencerGateway(inner) => Ok( + ::trace_transaction(inner, transaction_hash) + .await?, + ), + } + } + + async fn simulate_transactions( + &self, + block_id: B, + transactions: T, + simulation_flags: S, + ) -> Result, ProviderError> + where + B: AsRef + Send + Sync, + T: AsRef<[BroadcastedTransaction]> + Send + Sync, + S: AsRef<[SimulationFlag]> + Send + Sync, + { + match self { + Self::JsonRpcHttp(inner) => Ok( + as Provider>::simulate_transactions( + inner, + block_id, + transactions, + simulation_flags, + ) + .await?, + ), + Self::SequencerGateway(inner) => Ok( + ::simulate_transactions( + inner, + block_id, + transactions, + simulation_flags, + ) + .await?, + ), + } + } + + async fn trace_block_transactions( + &self, + block_hash: H, + ) -> Result, ProviderError> + where + H: AsRef + Send + Sync, + { + match self { + Self::JsonRpcHttp(inner) => Ok( + as Provider>::trace_block_transactions( + inner, block_hash, + ) + .await?, + ), + Self::SequencerGateway(inner) => Ok( + ::trace_block_transactions(inner, block_hash) + .await?, + ), + } + } } impl From as Provider>::Error>> diff --git a/starknet-providers/src/jsonrpc/mod.rs b/starknet-providers/src/jsonrpc/mod.rs index 6aab0bc3..0daedb1a 100644 --- a/starknet-providers/src/jsonrpc/mod.rs +++ b/starknet-providers/src/jsonrpc/mod.rs @@ -10,7 +10,8 @@ use starknet_core::{ EventFilterWithPage, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingStateUpdate, MaybePendingTransactionReceipt, MsgFromL1, ResultPageRequest, - StarknetError, SyncStatusType, Transaction, + SimulatedTransaction, SimulationFlag, StarknetError, SyncStatusType, Transaction, + TransactionTrace, TransactionTraceWithHash, }, }; @@ -77,6 +78,12 @@ pub enum JsonRpcMethod { AddDeclareTransaction, #[serde(rename = "starknet_addDeployAccountTransaction")] AddDeployAccountTransaction, + #[serde(rename = "starknet_traceTransaction")] + TraceTransaction, + #[serde(rename = "starknet_simulateTransactions")] + SimulateTransactions, + #[serde(rename = "starknet_traceBlockTransactions")] + TraceBlockTransactions, } #[derive(Debug, Clone)] @@ -111,6 +118,9 @@ pub enum JsonRpcRequestData { AddInvokeTransaction(AddInvokeTransactionRequest), AddDeclareTransaction(AddDeclareTransactionRequest), AddDeployAccountTransaction(AddDeployAccountTransactionRequest), + TraceTransaction(TraceTransactionRequest), + SimulateTransactions(SimulateTransactionsRequest), + TraceBlockTransactions(TraceBlockTransactionsRequest), } #[derive(Debug, thiserror::Error)] @@ -601,6 +611,65 @@ where ) .await } + + /// For a given executed transaction, return the trace of its execution, including internal + /// calls + async fn trace_transaction( + &self, + transaction_hash: H, + ) -> Result> + where + H: AsRef + Send + Sync, + { + self.send_request( + JsonRpcMethod::TraceTransaction, + TraceTransactionRequestRef { + transaction_hash: transaction_hash.as_ref(), + }, + ) + .await + } + + /// Simulate a given sequence of transactions on the requested state, and generate the execution + /// traces. If one of the transactions is reverted, raises CONTRACT_ERROR. + async fn simulate_transactions( + &self, + block_id: B, + transactions: TX, + simulation_flags: S, + ) -> Result, ProviderError> + where + B: AsRef + Send + Sync, + TX: AsRef<[BroadcastedTransaction]> + Send + Sync, + S: AsRef<[SimulationFlag]> + Send + Sync, + { + self.send_request( + JsonRpcMethod::SimulateTransactions, + SimulateTransactionsRequestRef { + block_id: block_id.as_ref(), + transactions: transactions.as_ref(), + simulation_flags: simulation_flags.as_ref(), + }, + ) + .await + } + + /// Retrieve traces for all transactions in the given block. + async fn trace_block_transactions( + &self, + block_hash: H, + ) -> Result, ProviderError> + where + H: AsRef + Send + Sync, + { + self.send_request( + JsonRpcMethod::TraceBlockTransactions, + TraceBlockTransactionsRequestRef { + block_hash: block_hash.as_ref(), + }, + ) + .await + } } impl<'de> Deserialize<'de> for JsonRpcRequest { @@ -725,6 +794,18 @@ impl<'de> Deserialize<'de> for JsonRpcRequest { .map_err(error_mapper)?, ) } + JsonRpcMethod::TraceTransaction => JsonRpcRequestData::TraceTransaction( + serde_json::from_value::(raw_request.params) + .map_err(error_mapper)?, + ), + JsonRpcMethod::SimulateTransactions => JsonRpcRequestData::SimulateTransactions( + serde_json::from_value::(raw_request.params) + .map_err(error_mapper)?, + ), + JsonRpcMethod::TraceBlockTransactions => JsonRpcRequestData::TraceBlockTransactions( + serde_json::from_value::(raw_request.params) + .map_err(error_mapper)?, + ), }; Ok(Self { diff --git a/starknet-providers/src/provider.rs b/starknet-providers/src/provider.rs index 979d97cc..75d4d875 100644 --- a/starknet-providers/src/provider.rs +++ b/starknet-providers/src/provider.rs @@ -6,7 +6,8 @@ use starknet_core::types::{ ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilter, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingStateUpdate, - MaybePendingTransactionReceipt, MsgFromL1, StarknetError, SyncStatusType, Transaction, + MaybePendingTransactionReceipt, MsgFromL1, SimulatedTransaction, SimulationFlag, StarknetError, + SyncStatusType, Transaction, TransactionTrace, TransactionTraceWithHash, }; use std::error::Error; @@ -202,6 +203,36 @@ pub trait Provider { where D: AsRef + Send + Sync; + /// For a given executed transaction, return the trace of its execution, including internal + /// calls + async fn trace_transaction( + &self, + transaction_hash: H, + ) -> Result> + where + H: AsRef + Send + Sync; + + /// Simulate a given sequence of transactions on the requested state, and generate the execution + /// traces. If one of the transactions is reverted, raises CONTRACT_ERROR. + async fn simulate_transactions( + &self, + block_id: B, + transactions: T, + simulation_flags: S, + ) -> Result, ProviderError> + where + B: AsRef + Send + Sync, + T: AsRef<[BroadcastedTransaction]> + Send + Sync, + S: AsRef<[SimulationFlag]> + Send + Sync; + + /// Retrieve traces for all transactions in the given block. + async fn trace_block_transactions( + &self, + block_hash: H, + ) -> Result, ProviderError> + where + H: AsRef + Send + Sync; + /// Same as [estimate_fee], but only with one estimate. async fn estimate_fee_single( &self, @@ -223,6 +254,34 @@ pub trait Provider { Err(ProviderError::ArrayLengthMismatch) } } + + /// Same as [simulate_transactions], but only with one simulation. + async fn simulate_transaction( + &self, + block_id: B, + transaction: T, + simulation_flags: S, + ) -> Result> + where + B: AsRef + Send + Sync, + T: AsRef + Send + Sync, + S: AsRef<[SimulationFlag]> + Send + Sync, + { + let mut result = self + .simulate_transactions( + block_id, + [transaction.as_ref().to_owned()], + simulation_flags, + ) + .await?; + + if result.len() == 1 { + // Unwrapping here is safe becuase we already checked length + Ok(result.pop().unwrap()) + } else { + Err(ProviderError::ArrayLengthMismatch) + } + } } #[derive(Debug, thiserror::Error)] diff --git a/starknet-providers/src/sequencer/mod.rs b/starknet-providers/src/sequencer/mod.rs index 3f3ede15..abe0cbdf 100644 --- a/starknet-providers/src/sequencer/mod.rs +++ b/starknet-providers/src/sequencer/mod.rs @@ -48,6 +48,12 @@ pub enum GatewayClientError { /// Model conversion error (only when using as [Provider]) #[error("unable to convert gateway models to jsonrpc types")] ModelConversionError, + /// Simulating multiple transactions is not supported (only when using as [Provider]) + #[error("simulating multiple transactions not supported")] + BulkSimulationNotSupported, + /// At least one of the simulation flags is not supported (only when using as [Provider]) + #[error("unsupported simulation flag")] + UnsupportedSimulationFlag, } #[derive(Debug, thiserror::Error, Deserialize)] diff --git a/starknet-providers/src/sequencer/models/conversions.rs b/starknet-providers/src/sequencer/models/conversions.rs index d0630f3c..5b0d22ed 100644 --- a/starknet-providers/src/sequencer/models/conversions.rs +++ b/starknet-providers/src/sequencer/models/conversions.rs @@ -4,6 +4,10 @@ use starknet_core::types::{self as core, contract::legacy as contract_legacy, Fi use super::{ state_update::{DeclaredContract, DeployedContract, StateDiff, StorageDiff}, + trace::{ + CallType, FunctionInvocation, OrderedEventResponse, OrderedL2ToL1MessageResponse, + TransactionTraceWithHash, + }, *, }; @@ -16,6 +20,11 @@ pub(crate) struct TransactionWithReceipt { pub receipt: TransactionReceipt, } +pub(crate) struct OrderedL2ToL1MessageResponseWithFromAddress { + pub message: OrderedL2ToL1MessageResponse, + pub from: FieldElement, +} + impl From for BlockId { fn from(value: core::BlockId) -> Self { match value { @@ -883,6 +892,151 @@ impl TryFrom for core::ContractClass { } } +impl TryFrom for core::TransactionTrace { + type Error = ConversionError; + + fn try_from(value: TransactionTrace) -> Result { + // Unlike JSON-RPC, which names fields from different variants differently, there's no way + // to distinguish between Invoke, DeployAccount, and L1Handler traces. (The only exception + // is when the Invoke execution reverts, which makes it definitely an Invoke variant.) + // + // For these variants, we simply always resolve to Invoke. This is suboptimal but still + // better than just failing the conversion. This is yet another reason to avoid using the + // sequencer gateway provider. + + let validate_invocation = match value.validate_invocation { + Some(invocation) => Some(invocation.try_into()?), + None => None, + }; + + let fee_transfer_invocation = match value.fee_transfer_invocation { + Some(invocation) => Some(invocation.try_into()?), + None => None, + }; + + match value.function_invocation { + Some(invocation) => Ok(Self::Invoke(core::InvokeTransactionTrace { + validate_invocation, + + execute_invocation: match value.revert_error { + Some(revert_error) => { + core::ExecuteInvocation::Reverted(core::RevertedInvocation { + revert_reason: revert_error, + }) + } + None => core::ExecuteInvocation::Success(invocation.try_into()?), + }, + fee_transfer_invocation, + })), + None => { + // Only DECLARE transactions do not have `function_invocation` + Ok(Self::Declare(core::DeclareTransactionTrace { + validate_invocation, + fee_transfer_invocation, + })) + } + } + } +} + +impl TryFrom for core::TransactionTraceWithHash { + type Error = ConversionError; + + fn try_from(value: TransactionTraceWithHash) -> Result { + Ok(Self { + transaction_hash: value.transaction_hash, + trace_root: value.trace.try_into()?, + }) + } +} + +impl TryFrom for core::SimulatedTransaction { + type Error = ConversionError; + + fn try_from(value: TransactionSimulationInfo) -> Result { + Ok(Self { + transaction_trace: value.trace.try_into()?, + fee_estimation: value.fee_estimation.into(), + }) + } +} + +impl TryFrom for core::FunctionInvocation { + type Error = ConversionError; + + fn try_from(value: FunctionInvocation) -> Result { + Ok(Self { + contract_address: value.contract_address, + entry_point_selector: value.selector.ok_or(ConversionError)?, + calldata: value.calldata, + caller_address: value.caller_address, + class_hash: value.class_hash.ok_or(ConversionError)?, + entry_point_type: value.entry_point_type.ok_or(ConversionError)?.into(), + call_type: value.call_type.ok_or(ConversionError)?.into(), + result: value.result, + calls: value + .internal_calls + .into_iter() + .map(|call| call.try_into()) + .collect::, _>>()?, + events: value.events.into_iter().map(|event| event.into()).collect(), + messages: value + .messages + .into_iter() + .map(|message| { + OrderedL2ToL1MessageResponseWithFromAddress { + message, + from: value.contract_address, + } + .into() + }) + .collect(), + }) + } +} + +impl From for core::EntryPointType { + fn from(value: EntryPointType) -> Self { + match value { + EntryPointType::External => Self::External, + EntryPointType::L1Handler => Self::L1Handler, + EntryPointType::Constructor => Self::Constructor, + } + } +} + +impl From for core::CallType { + fn from(value: CallType) -> Self { + match value { + CallType::Call => Self::Call, + CallType::Delegate => Self::LibraryCall, + } + } +} + +impl From for core::EventContent { + fn from(value: OrderedEventResponse) -> Self { + Self { + keys: value.keys, + data: value.data, + } + } +} + +impl From for core::MsgToL1 { + fn from(value: OrderedL2ToL1MessageResponseWithFromAddress) -> Self { + Self { + from_address: value.from, + // Unwrapping is safe here as H160 is only 20 bytes + to_address: FieldElement::from_byte_slice_be( + &value.message.to_address.to_fixed_bytes(), + ) + .unwrap(), + payload: value.message.payload, + } + } +} + fn convert_execution_result( status: TransactionStatus, execution_status: Option, diff --git a/starknet-providers/src/sequencer/models/trace.rs b/starknet-providers/src/sequencer/models/trace.rs index 922f68b3..04d77084 100644 --- a/starknet-providers/src/sequencer/models/trace.rs +++ b/starknet-providers/src/sequencer/models/trace.rs @@ -16,6 +16,8 @@ pub struct BlockTraces { #[derive(Debug, Deserialize)] #[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))] pub struct TransactionTrace { + #[serde(default)] + pub revert_error: Option, /// An object describing the invocation of a specific function. #[serde(default)] pub function_invocation: Option, diff --git a/starknet-providers/src/sequencer/provider.rs b/starknet-providers/src/sequencer/provider.rs index 1b2d713d..5901c23b 100644 --- a/starknet-providers/src/sequencer/provider.rs +++ b/starknet-providers/src/sequencer/provider.rs @@ -7,7 +7,8 @@ use starknet_core::types::{ ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilter, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingStateUpdate, - MaybePendingTransactionReceipt, MsgFromL1, StarknetError, SyncStatusType, Transaction, + MaybePendingTransactionReceipt, MsgFromL1, SimulatedTransaction, SimulationFlag, StarknetError, + SyncStatusType, Transaction, TransactionTrace, TransactionTraceWithHash, }; use crate::{ @@ -392,4 +393,78 @@ impl Provider for SequencerGatewayProvider { contract_address: result.address.ok_or(ConversionError)?, }) } + + async fn trace_transaction( + &self, + transaction_hash: H, + ) -> Result> + where + H: AsRef + Send + Sync, + { + Ok(self + .get_transaction_trace(*transaction_hash.as_ref()) + .await? + .try_into()?) + } + + async fn simulate_transactions( + &self, + block_id: B, + transactions: T, + simulation_flags: S, + ) -> Result, ProviderError> + where + B: AsRef + Send + Sync, + T: AsRef<[BroadcastedTransaction]> + Send + Sync, + S: AsRef<[SimulationFlag]> + Send + Sync, + { + let transactions = transactions.as_ref(); + if transactions.len() != 1 { + return Err(ProviderError::Other( + GatewayClientError::BulkSimulationNotSupported, + )); + } + + let transaction = transactions[0].to_owned(); + + let mut skip_validate = false; + for flag in simulation_flags.as_ref().iter() { + match flag { + SimulationFlag::SkipValidate => { + skip_validate = true; + } + SimulationFlag::SkipFeeCharge => { + return Err(ProviderError::Other( + GatewayClientError::UnsupportedSimulationFlag, + )); + } + } + } + + let simulation = self + .simulate_transaction( + transaction.try_into()?, + block_id.as_ref().to_owned().into(), + skip_validate, + ) + .await?; + + Ok(vec![simulation.try_into()?]) + } + + async fn trace_block_transactions( + &self, + block_hash: H, + ) -> Result, ProviderError> + where + H: AsRef + Send + Sync, + { + Ok(self + .get_block_traces(super::models::BlockId::Hash(*block_hash.as_ref())) + .await? + .traces + .into_iter() + .map(|est| est.try_into()) + .collect::, _>>()?) + } } diff --git a/starknet-providers/tests/jsonrpc.rs b/starknet-providers/tests/jsonrpc.rs index 41407fe6..d2063187 100644 --- a/starknet-providers/tests/jsonrpc.rs +++ b/starknet-providers/tests/jsonrpc.rs @@ -1,10 +1,11 @@ use starknet_core::{ types::{ BlockId, BlockTag, BroadcastedInvokeTransaction, BroadcastedTransaction, ContractClass, - DeclareTransaction, EthAddress, EventFilter, ExecutionResult, FieldElement, FunctionCall, - InvokeTransaction, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, - MaybePendingStateUpdate, MaybePendingTransactionReceipt, MsgFromL1, StarknetError, - SyncStatusType, Transaction, TransactionReceipt, + DeclareTransaction, EthAddress, EventFilter, ExecuteInvocation, ExecutionResult, + FieldElement, FunctionCall, InvokeTransaction, MaybePendingBlockWithTxHashes, + MaybePendingBlockWithTxs, MaybePendingStateUpdate, MaybePendingTransactionReceipt, + MsgFromL1, StarknetError, SyncStatusType, Transaction, TransactionReceipt, + TransactionTrace, }, utils::{get_selector_from_name, get_storage_var_address}, }; @@ -761,6 +762,118 @@ async fn jsonrpc_get_nonce() { assert_eq!(nonce, FieldElement::ZERO); } +#[tokio::test] +async fn jsonrpc_trace_invoke() { + let rpc_client = create_jsonrpc_client(); + + let trace = rpc_client + .trace_transaction( + FieldElement::from_hex_be( + "06d2ea57520318e577328ee0da9c609344ed77c86375a6764acc0c5854ebf258", + ) + .unwrap(), + ) + .await + .unwrap(); + + let trace = match trace { + TransactionTrace::Invoke(trace) => trace, + _ => panic!("unexpected trace type"), + }; + + match trace.execute_invocation { + ExecuteInvocation::Success(_) => {} + _ => panic!("unexpected execution result"), + } +} + +#[tokio::test] +async fn jsonrpc_trace_invoke_reverted() { + let rpc_client = create_jsonrpc_client(); + + let trace = rpc_client + .trace_transaction( + FieldElement::from_hex_be( + "0555c9392299727de9d3d6c85dd5db94f63a0994e698386d85c12b16f71fbfd0", + ) + .unwrap(), + ) + .await + .unwrap(); + + let trace = match trace { + TransactionTrace::Invoke(trace) => trace, + _ => panic!("unexpected trace type"), + }; + + match trace.execute_invocation { + ExecuteInvocation::Reverted(_) => {} + _ => panic!("unexpected execution result"), + } +} + +#[tokio::test] +async fn jsonrpc_trace_l1_handler() { + let rpc_client = create_jsonrpc_client(); + + let trace = rpc_client + .trace_transaction( + FieldElement::from_hex_be( + "0374286ae28f201e61ffbc5b022cc9701208640b405ea34ea9799f97d5d2d23c", + ) + .unwrap(), + ) + .await + .unwrap(); + + match trace { + TransactionTrace::L1Handler(_) => {} + _ => panic!("unexpected trace type"), + } +} + +#[tokio::test] +async fn jsonrpc_trace_declare() { + let rpc_client = create_jsonrpc_client(); + + let trace = rpc_client + .trace_transaction( + FieldElement::from_hex_be( + "021933cb48e59c74caa4575a78e89e6046d043505e5600fd88af7f051d3610ca", + ) + .unwrap(), + ) + .await + .unwrap(); + + match trace { + TransactionTrace::Declare(_) => {} + _ => panic!("unexpected trace type"), + } +} + +// DEPLOY transactions cannot be traced + +#[tokio::test] +async fn jsonrpc_trace_deploy_account() { + let rpc_client = create_jsonrpc_client(); + + let trace = rpc_client + .trace_transaction( + FieldElement::from_hex_be( + "058ba7cdaf437d3a3b9680e6cbb4169811cddfa693875812bd98a8b1d61278de", + ) + .unwrap(), + ) + .await + .unwrap(); + + match trace { + TransactionTrace::DeployAccount(_) => {} + _ => panic!("unexpected trace type"), + } +} + // NOTE: `addXxxxTransaction` methods are harder to test here since they require signatures. These // are integration tests anyways, so we might as well just leave the job to th tests in // `starknet-accounts`.