From cb45c95a5cfa55ac8cca95839f0c964edb486109 Mon Sep 17 00:00:00 2001 From: Alan Sapede Date: Mon, 20 May 2024 21:45:38 +0200 Subject: [PATCH] Adds debug_traceCall RPC method (#2796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds debug_traceCall method * test debug_traceCall rpc method --------- Co-authored-by: Rodrigo Quelhas Co-authored-by: Éloïs --- Cargo.lock | 3 + Cargo.toml | 2 +- client/rpc-core/debug/Cargo.toml | 2 + client/rpc-core/debug/src/lib.rs | 40 ++- client/rpc/debug/Cargo.toml | 1 + client/rpc/debug/src/lib.rs | 275 ++++++++++++++++++- primitives/rpc/debug/Cargo.toml | 2 +- primitives/rpc/debug/src/lib.rs | 17 +- runtime/common/src/apis.rs | 85 ++++++ runtime/moonbase/tests/evm_tracing.rs | 35 ++- runtime/moonbeam/tests/evm_tracing.rs | 35 ++- runtime/moonriver/tests/evm_tracing.rs | 35 ++- test/suites/tracing-tests/test-trace-call.ts | 39 +++ 13 files changed, 562 insertions(+), 9 deletions(-) create mode 100644 test/suites/tracing-tests/test-trace-call.ts diff --git a/Cargo.lock b/Cargo.lock index 7b8a2cf561..a610ea160a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6464,7 +6464,9 @@ dependencies = [ name = "moonbeam-rpc-core-debug" version = "0.1.0" dependencies = [ + "ethereum", "ethereum-types", + "fc-rpc-core", "futures 0.3.30", "jsonrpsee", "moonbeam-client-evm-tracing", @@ -6518,6 +6520,7 @@ dependencies = [ "fc-consensus", "fc-db", "fc-rpc", + "fc-rpc-core", "fc-storage", "fp-rpc", "futures 0.3.30", diff --git a/Cargo.toml b/Cargo.toml index dfe6c2dda3..81b37215f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ evm-tracing-events = { path = "primitives/rpc/evm-tracing-events", default-featu moonbeam-core-primitives = { path = "core-primitives", default-features = false } moonbeam-primitives-ext = { path = "primitives/ext", default-features = false } moonbeam-rpc-primitives-debug = { path = "primitives/rpc/debug", default-features = false, features = [ - "runtime-2900", + "runtime-3000", ] } moonbeam-rpc-primitives-txpool = { path = "primitives/rpc/txpool", default-features = false } storage-proof-primitives = { path = "primitives/storage-proof", default-features = false } diff --git a/client/rpc-core/debug/Cargo.toml b/client/rpc-core/debug/Cargo.toml index c693d636d4..99836a0232 100644 --- a/client/rpc-core/debug/Cargo.toml +++ b/client/rpc-core/debug/Cargo.toml @@ -8,6 +8,7 @@ repository = { workspace = true } version = "0.1.0" [dependencies] +ethereum = { workspace = true, features = [ "with-codec" ] } ethereum-types = { workspace = true, features = [ "std" ] } futures = { workspace = true, features = [ "compat" ] } jsonrpsee = { workspace = true, features = [ "macros", "server" ] } @@ -17,3 +18,4 @@ serde = { workspace = true, features = [ "derive" ] } serde_json = { workspace = true } sp-core = { workspace = true, features = [ "std" ] } +fc-rpc-core = { workspace = true } diff --git a/client/rpc-core/debug/src/lib.rs b/client/rpc-core/debug/src/lib.rs index 0c10a35fe2..5feff9dc98 100644 --- a/client/rpc-core/debug/src/lib.rs +++ b/client/rpc-core/debug/src/lib.rs @@ -13,7 +13,10 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . -use ethereum_types::H256; + +use ethereum::AccessListItem; +use ethereum_types::{H160, H256, U256}; +use fc_rpc_core::types::Bytes; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use moonbeam_client_evm_tracing::types::single; use moonbeam_rpc_core_types::RequestBlockId; @@ -30,6 +33,34 @@ pub struct TraceParams { pub timeout: Option, } +#[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TraceCallParams { + /// Sender + pub from: Option, + /// Recipient + pub to: H160, + /// Gas Price, legacy. + pub gas_price: Option, + /// Max BaseFeePerGas the user is willing to pay. + pub max_fee_per_gas: Option, + /// The miner's tip. + pub max_priority_fee_per_gas: Option, + /// Gas + pub gas: Option, + /// Value of transaction in wei + pub value: Option, + /// Additional data sent with transaction + pub data: Option, + /// Nonce + pub nonce: Option, + /// EIP-2930 access list + pub access_list: Option>, + /// EIP-2718 type + #[serde(rename = "type")] + pub transaction_type: Option, +} + #[rpc(server)] #[jsonrpsee::core::async_trait] pub trait Debug { @@ -39,6 +70,13 @@ pub trait Debug { transaction_hash: H256, params: Option, ) -> RpcResult; + #[method(name = "debug_traceCall")] + async fn trace_call( + &self, + call_params: TraceCallParams, + id: RequestBlockId, + params: Option, + ) -> RpcResult; #[method(name = "debug_traceBlockByNumber", aliases = ["debug_traceBlockByHash"])] async fn trace_block( &self, diff --git a/client/rpc/debug/Cargo.toml b/client/rpc/debug/Cargo.toml index e296fa8ecd..f4fbfcc7d5 100644 --- a/client/rpc/debug/Cargo.toml +++ b/client/rpc/debug/Cargo.toml @@ -36,5 +36,6 @@ fc-consensus = { workspace = true } fc-db = { workspace = true } fc-api = { workspace = true } fc-rpc = { workspace = true, features = [ "rpc-binary-search-estimate" ] } +fc-rpc-core = { workspace = true } fc-storage = { workspace = true } fp-rpc = { workspace = true, features = [ "std" ] } diff --git a/client/rpc/debug/src/lib.rs b/client/rpc/debug/src/lib.rs index b5008fe650..27d17ed3f8 100644 --- a/client/rpc/debug/src/lib.rs +++ b/client/rpc/debug/src/lib.rs @@ -15,7 +15,7 @@ // along with Moonbeam. If not, see . use futures::StreamExt; use jsonrpsee::core::{async_trait, RpcResult}; -pub use moonbeam_rpc_core_debug::{DebugServer, TraceParams}; +pub use moonbeam_rpc_core_debug::{DebugServer, TraceCallParams, TraceParams}; use tokio::{ self, @@ -42,6 +42,7 @@ use sp_runtime::{ use std::{future::Future, marker::PhantomData, sync::Arc}; pub enum RequesterInput { + Call((RequestBlockId, TraceCallParams)), Transaction(H256), Block(RequestBlockId), } @@ -122,6 +123,36 @@ impl DebugServer for Debug { _ => unreachable!(), }) } + + /// Handler for `debug_traceCall` request. Communicates with the service-defined task + /// using channels. + async fn trace_call( + &self, + call_params: TraceCallParams, + id: RequestBlockId, + params: Option, + ) -> RpcResult { + let requester = self.requester.clone(); + + let (tx, rx) = oneshot::channel(); + // Send a message from the rpc handler to the service level task. + requester + .unbounded_send(((RequesterInput::Call((id, call_params)), params), tx)) + .map_err(|err| { + internal_err(format!( + "failed to send request to debug service : {:?}", + err + )) + })?; + + // Receive a message from the service level task and send the rpc response. + rx.await + .map_err(|err| internal_err(format!("debug service dropped the channel : {:?}", err)))? + .map(|res| match res { + Response::Single(res) => res, + _ => unreachable!(), + }) + } } pub struct DebugHandler(PhantomData<(B, C, BE)>); @@ -193,6 +224,40 @@ where ); }); } + Some(( + (RequesterInput::Call((request_block_id, call_params)), params), + response_tx, + )) => { + let client = client.clone(); + let frontier_backend = frontier_backend.clone(); + let permit_pool = permit_pool.clone(); + + tokio::task::spawn(async move { + let _ = response_tx.send( + async { + let _permit = permit_pool.acquire().await; + tokio::task::spawn_blocking(move || { + Self::handle_call_request( + client.clone(), + frontier_backend.clone(), + request_block_id, + call_params, + params, + raw_max_memory_usage, + ) + }) + .await + .map_err(|e| { + internal_err(format!( + "Internal error on spawned task : {:?}", + e + )) + })? + } + .await, + ); + }); + } Some(((RequesterInput::Block(request_block_id), params), response_tx)) => { let client = client.clone(); let backend = backend.clone(); @@ -640,4 +705,212 @@ where } Err(internal_err("Runtime block call failed".to_string())) } + + fn handle_call_request( + client: Arc, + frontier_backend: Arc + Send + Sync>, + request_block_id: RequestBlockId, + call_params: TraceCallParams, + trace_params: Option, + raw_max_memory_usage: usize, + ) -> RpcResult { + let (tracer_input, trace_type) = Self::handle_params(trace_params)?; + + let reference_id: BlockId = match request_block_id { + RequestBlockId::Number(n) => Ok(BlockId::Number(n.unique_saturated_into())), + RequestBlockId::Tag(RequestBlockTag::Latest) => { + Ok(BlockId::Number(client.info().best_number)) + } + RequestBlockId::Tag(RequestBlockTag::Earliest) => { + Ok(BlockId::Number(0u32.unique_saturated_into())) + } + RequestBlockId::Tag(RequestBlockTag::Pending) => { + Err(internal_err("'pending' blocks are not supported")) + } + RequestBlockId::Hash(eth_hash) => { + match futures::executor::block_on(frontier_backend_client::load_hash::( + client.as_ref(), + frontier_backend.as_ref(), + eth_hash, + )) { + Ok(Some(hash)) => Ok(BlockId::Hash(hash)), + Ok(_) => Err(internal_err("Block hash not found".to_string())), + Err(e) => Err(e), + } + } + }?; + + // Get ApiRef. This handle allow to keep changes between txs in an internal buffer. + let api = client.runtime_api(); + // Get the header I want to work with. + let Ok(hash) = client.expect_block_hash_from_id(&reference_id) else { + return Err(internal_err("Block header not found")); + }; + let header = match client.header(hash) { + Ok(Some(h)) => h, + _ => return Err(internal_err("Block header not found")), + }; + // Get parent blockid. + let parent_block_hash = *header.parent_hash(); + + // Get DebugRuntimeApi version + let trace_api_version = if let Ok(Some(api_version)) = + api.api_version::>(parent_block_hash) + { + api_version + } else { + return Err(internal_err( + "Runtime api version call failed (trace)".to_string(), + )); + }; + + if trace_api_version <= 5 { + return Err(internal_err( + "debug_traceCall not supported with old runtimes".to_string(), + )); + } + + let TraceCallParams { + from, + to, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + gas, + value, + data, + nonce, + access_list, + .. + } = call_params; + + let (max_fee_per_gas, max_priority_fee_per_gas) = + match (gas_price, max_fee_per_gas, max_priority_fee_per_gas) { + (gas_price, None, None) => { + // Legacy request, all default to gas price. + // A zero-set gas price is None. + let gas_price = if gas_price.unwrap_or_default().is_zero() { + None + } else { + gas_price + }; + (gas_price, gas_price) + } + (_, max_fee, max_priority) => { + // eip-1559 + // A zero-set max fee is None. + let max_fee = if max_fee.unwrap_or_default().is_zero() { + None + } else { + max_fee + }; + // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`. + if let Some(max_priority) = max_priority { + let max_fee = max_fee.unwrap_or_default(); + if max_priority > max_fee { + return Err(internal_err( + "Invalid input: `max_priority_fee_per_gas` greater than `max_fee_per_gas`", + )); + } + } + (max_fee, max_priority) + } + }; + + let gas_limit = match gas { + Some(amount) => amount, + None => { + if let Some(block) = api + .current_block(parent_block_hash) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + { + block.header.gas_limit + } else { + return Err(internal_err( + "block unavailable, cannot query gas limit".to_string(), + )); + } + } + }; + let data = data.map(|d| d.0).unwrap_or_default(); + + let access_list = access_list.unwrap_or_default(); + + let f = || -> RpcResult<_> { + let _result = api + .trace_call( + parent_block_hash, + &header, + from.unwrap_or_default(), + to, + data, + value.unwrap_or_default(), + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + Some( + access_list + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect(), + ), + ) + .map_err(|e| internal_err(format!("Runtime api access error: {:?}", e)))? + .map_err(|e| internal_err(format!("DispatchError: {:?}", e)))?; + + Ok(moonbeam_rpc_primitives_debug::Response::Single) + }; + + return match trace_type { + single::TraceType::Raw { + disable_storage, + disable_memory, + disable_stack, + } => { + let mut proxy = moonbeam_client_evm_tracing::listeners::Raw::new( + disable_storage, + disable_memory, + disable_stack, + raw_max_memory_usage, + ); + proxy.using(f)?; + Ok(Response::Single( + moonbeam_client_evm_tracing::formatters::Raw::format(proxy).ok_or( + internal_err( + "replayed transaction generated too much data. \ + try disabling memory or storage?", + ), + )?, + )) + } + single::TraceType::CallList => { + let mut proxy = moonbeam_client_evm_tracing::listeners::CallList::default(); + proxy.using(f)?; + proxy.finish_transaction(); + let response = match tracer_input { + TracerInput::Blockscout => { + moonbeam_client_evm_tracing::formatters::Blockscout::format(proxy) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e))) + } + TracerInput::CallTracer => { + let mut res = + moonbeam_client_evm_tracing::formatters::CallTracer::format(proxy) + .ok_or("Trace result is empty.") + .map_err(|e| internal_err(format!("{:?}", e)))?; + Ok(res.pop().expect("Trace result is empty.")) + } + _ => Err(internal_err( + "Bug: failed to resolve the tracer format.".to_string(), + )), + }?; + Ok(Response::Single(response)) + } + not_supported => Err(internal_err(format!( + "Bug: `handle_call_request` does not support {:?}.", + not_supported + ))), + }; + } } diff --git a/primitives/rpc/debug/Cargo.toml b/primitives/rpc/debug/Cargo.toml index 4c27999b32..1d75a03184 100644 --- a/primitives/rpc/debug/Cargo.toml +++ b/primitives/rpc/debug/Cargo.toml @@ -37,4 +37,4 @@ std = [ "sp-runtime/std", "sp-std/std", ] -runtime-2900 = [] +runtime-3000 = [] diff --git a/primitives/rpc/debug/src/lib.rs b/primitives/rpc/debug/src/lib.rs index abc4c19314..401379bdd0 100644 --- a/primitives/rpc/debug/src/lib.rs +++ b/primitives/rpc/debug/src/lib.rs @@ -17,7 +17,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use ethereum::{TransactionV0 as LegacyTransaction, TransactionV2 as Transaction}; -use ethereum_types::H256; +use ethereum_types::{H160, H256, U256}; use parity_scale_codec::{Decode, Encode}; use sp_std::vec::Vec; @@ -30,7 +30,7 @@ sp_api::decl_runtime_apis! { // In order to be able to use ApiExt as part of the RPC handler logic we need to be always // above the version that exists on chain for this Api, even if this Api is only meant // to be used overridden. - #[api_version(5)] + #[api_version(6)] pub trait DebugRuntimeApi { #[changed_in(5)] fn trace_transaction( @@ -61,6 +61,19 @@ sp_api::decl_runtime_apis! { known_transactions: Vec, header: &Block::Header, ) -> Result<(), sp_runtime::DispatchError>; + + fn trace_call( + header: &Block::Header, + from: H160, + to: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + access_list: Option)>>, + ) -> Result<(), sp_runtime::DispatchError>; } } diff --git a/runtime/common/src/apis.rs b/runtime/common/src/apis.rs index b8893ede19..70876799f6 100644 --- a/runtime/common/src/apis.rs +++ b/runtime/common/src/apis.rs @@ -269,6 +269,91 @@ macro_rules! impl_runtime_apis_plus_common { "Missing `evm-tracing` compile time feature flag.", )) } + + fn trace_call( + header: &::Header, + from: H160, + to: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + access_list: Option)>>, + ) -> Result<(), sp_runtime::DispatchError> { + #[cfg(feature = "evm-tracing")] + { + use moonbeam_evm_tracer::tracer::EvmTracer; + + // Initialize block: calls the "on_initialize" hook on every pallet + // in AllPalletsWithSystem. + Executive::initialize_block(header); + + EvmTracer::new().trace(|| { + let is_transactional = false; + let validate = true; + let without_base_extrinsic_weight = true; + + + // Estimated encoded transaction size must be based on the heaviest transaction + // type (EIP1559Transaction) to be compatible with all transaction types. + let mut estimated_transaction_len = data.len() + + // pallet ethereum index: 1 + // transact call index: 1 + // Transaction enum variant: 1 + // chain_id 8 bytes + // nonce: 32 + // max_priority_fee_per_gas: 32 + // max_fee_per_gas: 32 + // gas_limit: 32 + // action: 21 (enum varianrt + call address) + // value: 32 + // access_list: 1 (empty vec size) + // 65 bytes signature + 258; + + if access_list.is_some() { + estimated_transaction_len += access_list.encoded_size(); + } + + let gas_limit = gas_limit.min(u64::MAX.into()).low_u64(); + + let (weight_limit, proof_size_base_cost) = + match ::GasWeightMapping::gas_to_weight( + gas_limit, + without_base_extrinsic_weight + ) { + weight_limit if weight_limit.proof_size() > 0 => { + (Some(weight_limit), Some(estimated_transaction_len as u64)) + } + _ => (None, None), + }; + + let _ = ::Runner::call( + from, + to, + data, + value, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + access_list.unwrap_or_default(), + is_transactional, + validate, + weight_limit, + proof_size_base_cost, + ::config(), + ); + }); + Ok(()) + } + #[cfg(not(feature = "evm-tracing"))] + Err(sp_runtime::DispatchError::Other( + "Missing `evm-tracing` compile time feature flag.", + )) + } } impl moonbeam_rpc_primitives_txpool::TxPoolRuntimeApi for Runtime { diff --git a/runtime/moonbase/tests/evm_tracing.rs b/runtime/moonbase/tests/evm_tracing.rs index 6c8b4c79c7..2fc821a28b 100644 --- a/runtime/moonbase/tests/evm_tracing.rs +++ b/runtime/moonbase/tests/evm_tracing.rs @@ -24,7 +24,7 @@ mod tests { use super::common::*; use pallet_evm::AddressMapping; - use sp_core::H160; + use sp_core::{H160, U256}; use moonbeam_rpc_primitives_debug::runtime_decl_for_debug_runtime_api::DebugRuntimeApi; use std::str::FromStr; @@ -107,4 +107,37 @@ mod tests { .is_ok()); }); } + + #[test] + fn debug_runtime_api_trace_call() { + let block = Header { + digest: Default::default(), + extrinsics_root: Default::default(), + number: 1, + parent_hash: Default::default(), + state_root: Default::default(), + }; + let alith = H160::from_str("6be02d1d3665660d22ff9624b7be0551ee1ac91b") + .expect("internal H160 is valid; qed"); + let alith_account_id = + ::AddressMapping::into_account_id(alith); + ExtBuilder::default() + .with_balances(vec![(alith_account_id, 100 * UNIT)]) + .build() + .execute_with(|| { + assert!(Runtime::trace_call( + &block, + alith, + H160::random(), + Vec::new(), + U256::from(99), + U256::max_value(), + Some(U256::one()), + Some(U256::one()), + None, + None, + ) + .is_ok()); + }); + } } diff --git a/runtime/moonbeam/tests/evm_tracing.rs b/runtime/moonbeam/tests/evm_tracing.rs index 144dae000e..aa2b050547 100644 --- a/runtime/moonbeam/tests/evm_tracing.rs +++ b/runtime/moonbeam/tests/evm_tracing.rs @@ -24,7 +24,7 @@ mod tests { use super::common::*; use pallet_evm::AddressMapping; - use sp_core::H160; + use sp_core::{H160, U256}; use moonbeam_rpc_primitives_debug::runtime_decl_for_debug_runtime_api::DebugRuntimeApi; use std::str::FromStr; @@ -107,4 +107,37 @@ mod tests { .is_ok()); }); } + + #[test] + fn debug_runtime_api_trace_call() { + let block = Header { + digest: Default::default(), + extrinsics_root: Default::default(), + number: 1, + parent_hash: Default::default(), + state_root: Default::default(), + }; + let alith = H160::from_str("6be02d1d3665660d22ff9624b7be0551ee1ac91b") + .expect("internal H160 is valid; qed"); + let alith_account_id = + ::AddressMapping::into_account_id(alith); + ExtBuilder::default() + .with_balances(vec![(alith_account_id, 100 * GLMR)]) + .build() + .execute_with(|| { + assert!(Runtime::trace_call( + &block, + alith, + H160::random(), + Vec::new(), + U256::from(99), + U256::max_value(), + Some(U256::one()), + Some(U256::one()), + None, + None, + ) + .is_ok()); + }); + } } diff --git a/runtime/moonriver/tests/evm_tracing.rs b/runtime/moonriver/tests/evm_tracing.rs index 86aaab3707..fcd3fcfef7 100644 --- a/runtime/moonriver/tests/evm_tracing.rs +++ b/runtime/moonriver/tests/evm_tracing.rs @@ -24,7 +24,7 @@ mod tests { use super::common::*; use pallet_evm::AddressMapping; - use sp_core::H160; + use sp_core::{H160, U256}; use moonbeam_rpc_primitives_debug::runtime_decl_for_debug_runtime_api::DebugRuntimeApi; use std::str::FromStr; @@ -107,4 +107,37 @@ mod tests { .is_ok()); }); } + + #[test] + fn debug_runtime_api_trace_call() { + let block = Header { + digest: Default::default(), + extrinsics_root: Default::default(), + number: 1, + parent_hash: Default::default(), + state_root: Default::default(), + }; + let alith = H160::from_str("6be02d1d3665660d22ff9624b7be0551ee1ac91b") + .expect("internal H160 is valid; qed"); + let alith_account_id = + ::AddressMapping::into_account_id(alith); + ExtBuilder::default() + .with_balances(vec![(alith_account_id, 100 * MOVR)]) + .build() + .execute_with(|| { + assert!(Runtime::trace_call( + &block, + alith, + H160::random(), + Vec::new(), + U256::from(99), + U256::max_value(), + Some(U256::one()), + Some(U256::one()), + None, + None, + ) + .is_ok()); + }); + } } diff --git a/test/suites/tracing-tests/test-trace-call.ts b/test/suites/tracing-tests/test-trace-call.ts new file mode 100644 index 0000000000..3e06b8df5c --- /dev/null +++ b/test/suites/tracing-tests/test-trace-call.ts @@ -0,0 +1,39 @@ +import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { encodeFunctionData } from "viem"; +import { createContracts } from "../../helpers"; + +describeSuite({ + id: "T16", + title: "Test 'debug_traceCall'", + foundationMethods: "dev", + testCases: ({ context, it }) => { + it({ + id: "T01", + title: "should trace nested contract calls", + test: async function () { + const contracts = await createContracts(context); + const callParams = { + to: contracts.callerAddr, + data: encodeFunctionData({ + abi: contracts.abiCaller, + functionName: "someAction", + args: [contracts.calleeAddr, 6], + }), + }; + const traceTx = await customDevRpcRequest("debug_traceCall", [callParams, "latest"]); + const logs: any[] = []; + for (const log of traceTx.structLogs) { + if (logs.length == 1) { + logs.push(log); + } + if (log.op == "RETURN") { + logs.push(log); + } + } + expect(logs).to.be.lengthOf(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }, + }); + }, +});