diff --git a/crates/katana/executor/src/implementation/blockifier/utils.rs b/crates/katana/executor/src/implementation/blockifier/utils.rs index 4568a387f6..544b14c0f8 100644 --- a/crates/katana/executor/src/implementation/blockifier/utils.rs +++ b/crates/katana/executor/src/implementation/blockifier/utils.rs @@ -49,7 +49,7 @@ use katana_primitives::fee::TxFeeInfo; use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; use katana_primitives::trace::{L1Gas, TxExecInfo, TxResources}; use katana_primitives::transaction::{ - DeclareTx, DeployAccountTx, ExecutableTx, ExecutableTxWithHash, InvokeTx, + DeclareTx, DeployAccountTx, ExecutableTx, ExecutableTxWithHash, InvokeTx, TxType, }; use katana_primitives::{class, event, message, trace, Felt}; use katana_provider::traits::contract::ContractClassProvider; @@ -126,7 +126,7 @@ pub fn transact( match transact_inner(state, block_context, simulation_flags, to_executor_tx(tx.clone())) { Ok((info, fee)) => { // get the trace and receipt from the execution info - let trace = to_exec_info(info); + let trace = to_exec_info(info, tx.r#type()); let receipt = build_receipt(tx.tx_ref(), fee, &trace); ExecutionResult::new_success(receipt, trace) } @@ -563,8 +563,9 @@ fn starknet_api_ethaddr_to_felt(value: katana_cairo::starknet_api::core::EthAddr Felt::from_bytes_be(&bytes) } -pub fn to_exec_info(exec_info: TransactionExecutionInfo) -> TxExecInfo { +pub fn to_exec_info(exec_info: TransactionExecutionInfo, r#type: TxType) -> TxExecInfo { TxExecInfo { + r#type, validate_call_info: exec_info.validate_call_info.map(to_call_info), execute_call_info: exec_info.execute_call_info.map(to_call_info), fee_transfer_call_info: exec_info.fee_transfer_call_info.map(to_call_info), diff --git a/crates/katana/primitives/src/trace.rs b/crates/katana/primitives/src/trace.rs index 3530f5e251..26ddee3b99 100644 --- a/crates/katana/primitives/src/trace.rs +++ b/crates/katana/primitives/src/trace.rs @@ -1,11 +1,13 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use katana_cairo::cairo_vm::types::builtin_name::BuiltinName; use katana_cairo::cairo_vm::vm; use crate::class::ClassHash; use crate::contract::ContractAddress; use crate::event::OrderedEvent; use crate::message::OrderedL2ToL1Message; +use crate::transaction::TxType; use crate::Felt; pub type ExecutionResources = vm::runners::cairo_runner::ExecutionResources; @@ -26,6 +28,8 @@ pub struct TxExecInfo { pub actual_resources: TxResources, /// Error string for reverted transactions; [None] if transaction execution was successful. pub revert_error: Option, + /// The transaction type of this execution info. + pub r#type: TxType, } #[derive(Debug, Clone, PartialEq, Eq, Default)] @@ -107,3 +111,81 @@ pub struct CallInfo { /// True if the execution has failed, false otherwise. pub failed: bool, } + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BuiltinCounters(HashMap); + +impl BuiltinCounters { + /// Returns the number of instances of the `output` builtin, if any. + pub fn output(&self) -> Option { + self.builtin(BuiltinName::output) + } + + /// Returns the number of instances of the `range_check` builtin, if any. + pub fn range_check(&self) -> Option { + self.builtin(BuiltinName::range_check) + } + + /// Returns the number of instances of the `pedersen` builtin, if any. + pub fn pedersen(&self) -> Option { + self.builtin(BuiltinName::pedersen) + } + + /// Returns the number of instances of the `ecdsa` builtin, if any. + pub fn ecdsa(&self) -> Option { + self.builtin(BuiltinName::ecdsa) + } + + /// Returns the number of instances of the `keccak` builtin, if any. + pub fn keccak(&self) -> Option { + self.builtin(BuiltinName::keccak) + } + + /// Returns the number of instances of the `bitwise` builtin, if any. + pub fn bitwise(&self) -> Option { + self.builtin(BuiltinName::bitwise) + } + + /// Returns the number of instances of the `ec_op` builtin, if any. + pub fn ec_op(&self) -> Option { + self.builtin(BuiltinName::ec_op) + } + + /// Returns the number of instances of the `poseidon` builtin, if any. + pub fn poseidon(&self) -> Option { + self.builtin(BuiltinName::poseidon) + } + + /// Returns the number of instances of the `segment_arena` builtin, if any. + pub fn segment_arena(&self) -> Option { + self.builtin(BuiltinName::segment_arena) + } + + /// Returns the number of instances of the `range_check96` builtin, if any. + pub fn range_check96(&self) -> Option { + self.builtin(BuiltinName::range_check96) + } + + /// Returns the number of instances of the `add_mod` builtin, if any. + pub fn add_mod(&self) -> Option { + self.builtin(BuiltinName::add_mod) + } + + /// Returns the number of instances of the `mul_mod` builtin, if any. + pub fn mul_mod(&self) -> Option { + self.builtin(BuiltinName::mul_mod) + } + + fn builtin(&self, builtin: BuiltinName) -> Option { + self.0.get(&builtin).map(|&x| x as u64) + } +} + +impl From> for BuiltinCounters { + fn from(map: HashMap) -> Self { + // Filter out the builtins with 0 count. + let filtered = map.into_iter().filter(|builtin| builtin.1 != 0).collect(); + BuiltinCounters(filtered) + } +} diff --git a/crates/katana/primitives/src/transaction.rs b/crates/katana/primitives/src/transaction.rs index 0fe976d77b..54f039c05b 100644 --- a/crates/katana/primitives/src/transaction.rs +++ b/crates/katana/primitives/src/transaction.rs @@ -17,6 +17,29 @@ pub type TxHash = Felt; /// The sequential number for all the transactions. pub type TxNumber = u64; +/// The transaction types as defined by the [Starknet API]. +/// +/// [Starknet API]: https://github.com/starkware-libs/starknet-specs/blob/b5c43955b1868b8e19af6d1736178e02ec84e678/api/starknet_api_openrpc.json +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TxType { + /// Invokes a function of a contract. + #[default] + Invoke, + + /// Declares new contract class. + Declare, + + /// Deploys new account contracts. + DeployAccount, + + /// Function invocation that is instantiated from the L1. + /// + /// It is only used internally for handling messages sent from L1. Therefore, it is not a + /// transaction that can be broadcasted like the other transaction types. + L1Handler, +} + #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Tx { @@ -63,6 +86,15 @@ impl ExecutableTx { ExecutableTx::DeployAccount(tx) => TxRef::DeployAccount(tx), } } + + pub fn r#type(&self) -> TxType { + match self { + ExecutableTx::Invoke(_) => TxType::Invoke, + ExecutableTx::Declare(_) => TxType::Declare, + ExecutableTx::L1Handler(_) => TxType::L1Handler, + ExecutableTx::DeployAccount(_) => TxType::DeployAccount, + } + } } #[derive(Debug, Clone, AsRef, Deref)] diff --git a/crates/katana/rpc/rpc/src/starknet/trace.rs b/crates/katana/rpc/rpc/src/starknet/trace.rs index dade31c98e..e37cada0b5 100644 --- a/crates/katana/rpc/rpc/src/starknet/trace.rs +++ b/crates/katana/rpc/rpc/src/starknet/trace.rs @@ -1,32 +1,178 @@ -use jsonrpsee::core::{async_trait, Error, RpcResult}; -use jsonrpsee::types::error::{CallError, METHOD_NOT_FOUND_CODE}; -use jsonrpsee::types::ErrorObject; +use jsonrpsee::core::{async_trait, RpcResult}; use katana_executor::{ExecutionResult, ExecutorFactory, ResultAndStates}; -use katana_primitives::block::BlockIdOrTag; -use katana_primitives::receipt::Receipt; -use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; +use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag}; +use katana_primitives::fee::TxFeeInfo; +use katana_primitives::trace::{BuiltinCounters, TxExecInfo}; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash, TxType}; +use katana_provider::traits::block::{BlockHashProvider, BlockNumberProvider, BlockProvider}; +use katana_provider::traits::transaction::{TransactionTraceProvider, TransactionsProviderExt}; use katana_rpc_api::starknet::StarknetTraceApiServer; use katana_rpc_types::error::starknet::StarknetApiError; use katana_rpc_types::trace::FunctionInvocation; use katana_rpc_types::transaction::BroadcastedTx; use katana_rpc_types::{FeeEstimate, SimulationFlag}; use starknet::core::types::{ - ComputationResources, DataAvailabilityResources, DataResources, DeclareTransactionTrace, - DeployAccountTransactionTrace, ExecuteInvocation, ExecutionResources, InvokeTransactionTrace, - L1HandlerTransactionTrace, RevertedInvocation, SimulatedTransaction, TransactionTrace, - TransactionTraceWithHash, + BlockTag, ComputationResources, DataAvailabilityResources, DataResources, + DeclareTransactionTrace, DeployAccountTransactionTrace, ExecuteInvocation, ExecutionResources, + InvokeTransactionTrace, L1HandlerTransactionTrace, RevertedInvocation, SimulatedTransaction, + TransactionTrace, TransactionTraceWithHash, }; use super::StarknetApi; +impl StarknetApi { + pub fn simulate_txs( + &self, + block_id: BlockIdOrTag, + transactions: Vec, + simulation_flags: Vec, + ) -> Result, StarknetApiError> { + let chain_id = self.inner.backend.chain_id; + + let executables = transactions + .into_iter() + .map(|tx| { + let tx = match tx { + BroadcastedTx::Invoke(tx) => { + let is_query = tx.is_query(); + ExecutableTxWithHash::new_query( + ExecutableTx::Invoke(tx.into_tx_with_chain_id(chain_id)), + is_query, + ) + } + BroadcastedTx::Declare(tx) => { + let is_query = tx.is_query(); + ExecutableTxWithHash::new_query( + ExecutableTx::Declare( + tx.try_into_tx_with_chain_id(chain_id) + .map_err(|_| StarknetApiError::InvalidContractClass)?, + ), + is_query, + ) + } + BroadcastedTx::DeployAccount(tx) => { + let is_query = tx.is_query(); + ExecutableTxWithHash::new_query( + ExecutableTx::DeployAccount(tx.into_tx_with_chain_id(chain_id)), + is_query, + ) + } + }; + Result::::Ok(tx) + }) + .collect::, _>>()?; + + // If the node is run with transaction validation disabled, then we should not validate + // even if the `SKIP_VALIDATE` flag is not set. + #[allow(deprecated)] + let should_validate = !(simulation_flags.contains(&SimulationFlag::SkipValidate) + || self.inner.backend.config.disable_validate); + + // If the node is run with fee charge disabled, then we should disable charing fees even + // if the `SKIP_FEE_CHARGE` flag is not set. + #[allow(deprecated)] + let should_skip_fee = !(simulation_flags.contains(&SimulationFlag::SkipFeeCharge) + || self.inner.backend.config.disable_fee); + + let flags = katana_executor::SimulationFlag { + skip_validate: !should_validate, + skip_fee_transfer: !should_skip_fee, + ..Default::default() + }; + + // get the state and block env at the specified block for execution + let state = self.state(&block_id)?; + let env = self.block_env_at(&block_id)?; + + // create the executor + let executor = self.inner.backend.executor_factory.with_state_and_block_env(state, env); + let results = executor.simulate(executables, flags); + + let mut simulated = Vec::with_capacity(results.len()); + for (i, ResultAndStates { result, .. }) in results.into_iter().enumerate() { + match result { + ExecutionResult::Success { trace, receipt } => { + let transaction_trace = to_rpc_trace(trace); + let fee_estimation = to_rpc_fee_estimate(receipt.fee().clone()); + let value = SimulatedTransaction { transaction_trace, fee_estimation }; + simulated.push(value) + } + + ExecutionResult::Failed { error } => { + let error = StarknetApiError::TransactionExecutionError { + transaction_index: i, + execution_error: error.to_string(), + }; + return Err(error); + } + } + } + + Ok(simulated) + } + + pub fn block_traces( + &self, + block_id: BlockIdOrTag, + ) -> Result, StarknetApiError> { + use StarknetApiError::BlockNotFound; + + let provider = self.inner.backend.blockchain.provider(); + + let block_id: BlockHashOrNumber = match block_id { + BlockIdOrTag::Tag(BlockTag::Pending) => match self.pending_executor() { + Some(state) => { + let pending_block = state.read(); + + // extract the txs from the pending block + let traces = pending_block.transactions().iter().filter_map(|(t, r)| { + if let Some(trace) = r.trace() { + let transaction_hash = t.hash; + let trace_root = to_rpc_trace(trace.clone()); + Some(TransactionTraceWithHash { transaction_hash, trace_root }) + } else { + None + } + }); + + return Ok(traces.collect::>()); + } + + None => provider.latest_hash()?.into(), + }, + BlockIdOrTag::Tag(BlockTag::Latest) => provider.latest_number()?.into(), + BlockIdOrTag::Number(num) => num.into(), + BlockIdOrTag::Hash(hash) => hash.into(), + }; + + // TODO: this could probably be reduced to a single query + let indices = provider.block_body_indices(block_id)?.ok_or(BlockNotFound)?; + let hashes = provider.transaction_hashes_in_range(indices.into())?; + let traces = provider.transaction_executions_by_block(block_id)?.ok_or(BlockNotFound)?; + + // convert to rpc types + let traces = traces.into_iter().map(to_rpc_trace); + let result = hashes + .into_iter() + .zip(traces) + .map(|(h, r)| TransactionTraceWithHash { transaction_hash: h, trace_root: r }) + .collect::>(); + + Ok(result) + } + + pub fn trace(&self, tx_hash: TxHash) -> Result { + use StarknetApiError::TxnHashNotFound; + let provider = self.inner.backend.blockchain.provider(); + let trace = provider.transaction_execution(tx_hash)?.ok_or(TxnHashNotFound)?; + Ok(to_rpc_trace(trace)) + } +} + #[async_trait] impl StarknetTraceApiServer for StarknetApi { - async fn trace_transaction(&self, _: TxHash) -> RpcResult { - Err(Error::Call(CallError::Custom(ErrorObject::owned( - METHOD_NOT_FOUND_CODE, - "Unsupported method - starknet_traceTransaction".to_string(), - None::<()>, - )))) + async fn trace_transaction(&self, transaction_hash: TxHash) -> RpcResult { + self.on_io_blocking_task(move |this| Ok(this.trace(transaction_hash)?)).await } async fn simulate_transactions( @@ -36,188 +182,111 @@ impl StarknetTraceApiServer for StarknetApi { simulation_flags: Vec, ) -> RpcResult> { self.on_cpu_blocking_task(move |this| { - let chain_id = this.inner.backend.chain_id; - - let executables = transactions - .into_iter() - .map(|tx| { - let tx = match tx { - BroadcastedTx::Invoke(tx) => { - let is_query = tx.is_query(); - ExecutableTxWithHash::new_query( - ExecutableTx::Invoke(tx.into_tx_with_chain_id(chain_id)), - is_query, - ) - } - BroadcastedTx::Declare(tx) => { - let is_query = tx.is_query(); - ExecutableTxWithHash::new_query( - ExecutableTx::Declare( - tx.try_into_tx_with_chain_id(chain_id) - .map_err(|_| StarknetApiError::InvalidContractClass)?, - ), - is_query, - ) - } - BroadcastedTx::DeployAccount(tx) => { - let is_query = tx.is_query(); - ExecutableTxWithHash::new_query( - ExecutableTx::DeployAccount(tx.into_tx_with_chain_id(chain_id)), - is_query, - ) - } - }; - Result::::Ok(tx) - }) - .collect::, _>>()?; - - // If the node is run with transaction validation disabled, then we should not validate - // even if the `SKIP_VALIDATE` flag is not set. - #[allow(deprecated)] - let should_validate = !(simulation_flags.contains(&SimulationFlag::SkipValidate) - || this.inner.backend.config.disable_validate); - - // If the node is run with fee charge disabled, then we should disable charing fees even - // if the `SKIP_FEE_CHARGE` flag is not set. - #[allow(deprecated)] - let should_skip_fee = !(simulation_flags.contains(&SimulationFlag::SkipFeeCharge) - || this.inner.backend.config.disable_fee); - - let flags = katana_executor::SimulationFlag { - skip_validate: !should_validate, - skip_fee_transfer: !should_skip_fee, - ..Default::default() - }; - - // get the state and block env at the specified block for execution - let state = this.state(&block_id)?; - let env = this.block_env_at(&block_id)?; - - // create the executor - let executor = this.inner.backend.executor_factory.with_state_and_block_env(state, env); - let results = executor.simulate(executables, flags); - - let mut simulated = Vec::with_capacity(results.len()); - for (i, ResultAndStates { result, .. }) in results.into_iter().enumerate() { - match result { - ExecutionResult::Success { trace, receipt } => { - let fee_transfer_invocation = - trace.fee_transfer_call_info.map(|f| FunctionInvocation::from(f).0); - let validate_invocation = - trace.validate_call_info.map(|f| FunctionInvocation::from(f).0); - let execute_invocation = - trace.execute_call_info.map(|f| FunctionInvocation::from(f).0); - let revert_reason = trace.revert_error; - // TODO: compute the state diff - let state_diff = None; - - let execution_resources = ExecutionResources { - computation_resources: ComputationResources { - steps: 0, - memory_holes: None, - segment_arena_builtin: None, - ecdsa_builtin_applications: None, - ec_op_builtin_applications: None, - keccak_builtin_applications: None, - bitwise_builtin_applications: None, - pedersen_builtin_applications: None, - poseidon_builtin_applications: None, - range_check_builtin_applications: None, - }, - data_resources: DataResources { - data_availability: DataAvailabilityResources { - l1_gas: 0, - l1_data_gas: 0, - }, - }, - }; - - let transaction_trace = match receipt { - Receipt::Invoke(_) => { - TransactionTrace::Invoke(InvokeTransactionTrace { - fee_transfer_invocation, - validate_invocation, - state_diff, - execute_invocation: if let Some(revert_reason) = revert_reason { - ExecuteInvocation::Reverted(RevertedInvocation { - revert_reason, - }) - } else { - ExecuteInvocation::Success( - execute_invocation - .expect("should exist if not reverted"), - ) - }, - execution_resources: execution_resources.clone(), - }) - } - - Receipt::Declare(_) => { - TransactionTrace::Declare(DeclareTransactionTrace { - fee_transfer_invocation, - validate_invocation, - state_diff, - execution_resources: execution_resources.clone(), - }) - } - - Receipt::DeployAccount(_) => { - TransactionTrace::DeployAccount(DeployAccountTransactionTrace { - fee_transfer_invocation, - validate_invocation, - state_diff, - constructor_invocation: execute_invocation - .expect("should exist bcs tx succeed"), - execution_resources: execution_resources.clone(), - }) - } - - Receipt::L1Handler(_) => { - TransactionTrace::L1Handler(L1HandlerTransactionTrace { - state_diff, - function_invocation: execute_invocation - .expect("should exist bcs tx succeed"), - execution_resources, - }) - } - }; - - let fee = receipt.fee(); - simulated.push(SimulatedTransaction { - transaction_trace, - fee_estimation: FeeEstimate { - unit: fee.unit, - gas_price: fee.gas_price.into(), - overall_fee: fee.overall_fee.into(), - gas_consumed: fee.gas_consumed.into(), - data_gas_price: Default::default(), - data_gas_consumed: Default::default(), - }, - }) - } - - ExecutionResult::Failed { error } => { - return Err(Error::from(StarknetApiError::TransactionExecutionError { - transaction_index: i, - execution_error: error.to_string(), - })); - } - } - } - - Ok(simulated) + Ok(this.simulate_txs(block_id, transactions, simulation_flags)?) }) .await } async fn trace_block_transactions( &self, - _: BlockIdOrTag, + block_id: BlockIdOrTag, ) -> RpcResult> { - Err(Error::Call(CallError::Custom(ErrorObject::owned( - METHOD_NOT_FOUND_CODE, - "Unsupported method - starknet_traceBlockTransactions".to_string(), - None::<()>, - )))) + self.on_io_blocking_task(move |this| Ok(this.block_traces(block_id)?)).await + } +} + +// TODO: move this conversion to katana_rpc_types + +fn to_rpc_trace(trace: TxExecInfo) -> TransactionTrace { + let fee_transfer_invocation = + trace.fee_transfer_call_info.map(|f| FunctionInvocation::from(f).0); + let validate_invocation = trace.validate_call_info.map(|f| FunctionInvocation::from(f).0); + let execute_invocation = trace.execute_call_info.map(|f| FunctionInvocation::from(f).0); + let revert_reason = trace.revert_error; + // TODO: compute the state diff + let state_diff = None; + + let execution_resources = to_rpc_resources(trace.actual_resources.vm_resources); + + match trace.r#type { + TxType::Invoke => { + let execute_invocation = if let Some(revert_reason) = revert_reason { + let invocation = RevertedInvocation { revert_reason }; + ExecuteInvocation::Reverted(invocation) + } else { + let invocation = execute_invocation.expect("should exist if not reverted"); + ExecuteInvocation::Success(invocation) + }; + + TransactionTrace::Invoke(InvokeTransactionTrace { + execution_resources: execution_resources.clone(), + fee_transfer_invocation, + validate_invocation, + execute_invocation, + state_diff, + }) + } + + TxType::Declare => TransactionTrace::Declare(DeclareTransactionTrace { + execution_resources: execution_resources.clone(), + fee_transfer_invocation, + validate_invocation, + state_diff, + }), + + TxType::DeployAccount => { + let constructor_invocation = execute_invocation.expect("should exist if not reverted"); + TransactionTrace::DeployAccount(DeployAccountTransactionTrace { + execution_resources: execution_resources.clone(), + fee_transfer_invocation, + constructor_invocation, + validate_invocation, + state_diff, + }) + } + + TxType::L1Handler => { + let function_invocation = execute_invocation.expect("should exist if not reverted"); + TransactionTrace::L1Handler(L1HandlerTransactionTrace { + execution_resources, + function_invocation, + state_diff, + }) + } + } +} + +fn to_rpc_resources(resources: katana_primitives::trace::ExecutionResources) -> ExecutionResources { + let steps = resources.n_steps as u64; + let memory_holes = resources.n_memory_holes as u64; + let builtins = BuiltinCounters::from(resources.builtin_instance_counter); + + let data_availability = DataAvailabilityResources { l1_gas: 0, l1_data_gas: 0 }; + let data_resources = DataResources { data_availability }; + + let computation_resources = ComputationResources { + steps, + memory_holes: Some(memory_holes), + ecdsa_builtin_applications: builtins.ecdsa(), + ec_op_builtin_applications: builtins.ec_op(), + keccak_builtin_applications: builtins.keccak(), + segment_arena_builtin: builtins.segment_arena(), + bitwise_builtin_applications: builtins.bitwise(), + pedersen_builtin_applications: builtins.pedersen(), + poseidon_builtin_applications: builtins.poseidon(), + range_check_builtin_applications: builtins.range_check(), + }; + + ExecutionResources { data_resources, computation_resources } +} + +fn to_rpc_fee_estimate(fee: TxFeeInfo) -> FeeEstimate { + FeeEstimate { + unit: fee.unit, + gas_price: fee.gas_price.into(), + overall_fee: fee.overall_fee.into(), + gas_consumed: fee.gas_consumed.into(), + data_gas_price: Default::default(), + data_gas_consumed: Default::default(), } } diff --git a/crates/katana/rpc/rpc/tests/starknet.rs b/crates/katana/rpc/rpc/tests/starknet.rs index 281cadde81..39ddbe1c6d 100644 --- a/crates/katana/rpc/rpc/tests/starknet.rs +++ b/crates/katana/rpc/rpc/tests/starknet.rs @@ -26,7 +26,7 @@ use starknet::core::types::contract::legacy::LegacyContractClass; use starknet::core::types::{ BlockId, BlockTag, Call, DeclareTransactionReceipt, DeployAccountTransactionReceipt, EventFilter, EventsPage, ExecutionResult, Felt, StarknetError, TransactionFinalityStatus, - TransactionReceipt, + TransactionReceipt, TransactionTrace, }; use starknet::core::utils::get_contract_address; use starknet::macros::{felt, selector}; @@ -756,3 +756,69 @@ async fn get_events_with_pending() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn trace() -> Result<()> { + let sequencer = + TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; + + let provider = sequencer.provider(); + let account = sequencer.account(); + + // setup contract to interact with (can be any existing contract that can be interacted with) + let contract = Erc20Contract::new(DEFAULT_FEE_TOKEN_ADDRESS.into(), &account); + + // setup contract function params + let recipient = felt!("0x1"); + let amount = Uint256 { low: felt!("0x1"), high: Felt::ZERO }; + + let res = contract.transfer(&recipient, &amount).send().await?; + dojo_utils::TransactionWaiter::new(res.transaction_hash, &provider).await?; + + let trace = provider.trace_transaction(res.transaction_hash).await?; + assert_matches!(trace, TransactionTrace::Invoke(_)); + + Ok(()) +} + +#[tokio::test] +async fn block_traces() -> Result<()> { + let sequencer = TestSequencer::start( + SequencerConfig { no_mining: true, ..Default::default() }, + get_default_test_starknet_config(), + ) + .await; + + let provider = sequencer.provider(); + let account = sequencer.account(); + + // setup contract to interact with (can be any existing contract that can be interacted with) + let contract = Erc20Contract::new(DEFAULT_FEE_TOKEN_ADDRESS.into(), &account); + + // setup contract function params + let recipient = felt!("0x1"); + let amount = Uint256 { low: felt!("0x1"), high: Felt::ZERO }; + + let mut hashes = Vec::new(); + for _ in 0..5 { + let res = contract.transfer(&recipient, &amount).send().await?; + dojo_utils::TransactionWaiter::new(res.transaction_hash, &provider).await?; + hashes.push(res.transaction_hash); + } + + let client = HttpClientBuilder::default().build(sequencer.url())?; + + // Generate a block to include the transactions. The generated block will have block number 1. + client.generate_block().await?; + + // Get the traces of the transactions in block 1. + let traces = provider.trace_block_transactions(BlockId::Number(1)).await?; + assert_eq!(traces.len(), 5); + + for i in 0..5 { + assert_eq!(traces[i].transaction_hash, hashes[i]); + assert_matches!(&traces[i].trace_root, TransactionTrace::Invoke(_)); + } + + Ok(()) +}