diff --git a/.github/workflows/edr-ci.yml b/.github/workflows/edr-ci.yml index 2b253f4cc..431254da3 100644 --- a/.github/workflows/edr-ci.yml +++ b/.github/workflows/edr-ci.yml @@ -10,7 +10,6 @@ on: workflow_dispatch: env: - RUSTFLAGS: -Dwarnings RUSTDOCFLAGS: -Dwarnings concurrency: @@ -37,6 +36,8 @@ jobs: - name: Cargo hack uses: actions-rs/cargo@v1 + env: + RUSTFLAGS: -Dwarnings with: command: hack args: check --feature-powerset --no-dev-deps diff --git a/Cargo.lock b/Cargo.lock index 1004ada2f..876cc91f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,15 +1185,14 @@ dependencies = [ name = "edr_napi" version = "0.3.5" dependencies = [ - "ansi_term", "derive-where", "edr_eth", "edr_evm", "edr_generic", "edr_napi_core", + "edr_optimism", "edr_provider", "edr_rpc_client", - "itertools 0.12.1", "mimalloc", "napi", "napi-build", @@ -1202,7 +1201,6 @@ dependencies = [ "rand", "serde", "serde_json", - "thiserror", "tracing", "tracing-flame", "tracing-subscriber", @@ -1212,10 +1210,19 @@ dependencies = [ name = "edr_napi_core" version = "0.3.5" dependencies = [ + "ansi_term", + "derive-where", "edr_defaults", "edr_eth", + "edr_evm", + "edr_generic", "edr_provider", + "edr_rpc_client", + "itertools 0.12.1", + "napi", "serde", + "serde_json", + "thiserror", ] [[package]] @@ -1229,6 +1236,8 @@ dependencies = [ "edr_defaults", "edr_eth", "edr_evm", + "edr_generic", + "edr_napi_core", "edr_provider", "edr_rpc_eth", "edr_test_utils", diff --git a/crates/edr_napi/Cargo.toml b/crates/edr_napi/Cargo.toml index 0b03fe552..cae707b2a 100644 --- a/crates/edr_napi/Cargo.toml +++ b/crates/edr_napi/Cargo.toml @@ -7,15 +7,14 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -ansi_term = { version = "0.12.1", default-features = false } derive-where = { version = "1.2.7", default-features = false } edr_eth = { path = "../edr_eth" } edr_evm = { path = "../edr_evm" } edr_generic = { path = "../edr_generic" } edr_napi_core = { path = "../edr_napi_core" } +edr_optimism = { path = "../edr_optimism", optional = true } edr_provider = { path = "../edr_provider" } edr_rpc_client = { path = "../edr_rpc_client" } -itertools = { version = "0.12.0", default-features = false } mimalloc = { version = "0.1.39", default-features = false, features = ["local_dynamic_tls"] } # when napi is pinned, be sure to pin napi-derive to the same version # The `async` feature ensures that a tokio runtime is available @@ -24,7 +23,6 @@ napi-derive = "2.16.0" rand = { version = "0.8.4", optional = true } serde = { version = "1.0.209", features = ["derive"] } serde_json = { version = "1.0.127" } -thiserror = { version = "1.0.37", default-features = false } tracing = { version = "0.1.37", default-features = false, features = ["std"] } tracing-flame = { version = "0.2.0", default-features = false, features = ["smallvec"] } tracing-subscriber = { version = "0.3.18", default-features = false, features = ["ansi", "env-filter", "fmt", "parking_lot", "smallvec", "std"] } @@ -45,11 +43,9 @@ openssl-sys = { version = "0.9.93", features = ["vendored"] } napi-build = "2.0.1" [features] -tracing = ["edr_evm/tracing", "edr_napi_core/tracing", "edr_provider/tracing"] +optimism = ["dep:edr_optimism"] scenarios = ["rand"] - -[profile.release] -lto = true +tracing = ["edr_evm/tracing", "edr_napi_core/tracing", "edr_provider/tracing"] [lints] workspace = true diff --git a/crates/edr_napi/index.d.ts b/crates/edr_napi/index.d.ts index 064a09567..c97cb11c6 100644 --- a/crates/edr_napi/index.d.ts +++ b/crates/edr_napi/index.d.ts @@ -63,6 +63,72 @@ export interface CallOverrideResult { result: Buffer shouldRevert: boolean } +export const GENERIC_CHAIN_TYPE: string +export function genericChainProviderFactory(): ProviderFactory +export const L1_CHAIN_TYPE: string +export function l1ProviderFactory(): ProviderFactory +/** Identifier for the Ethereum spec. */ +export const enum SpecId { + /** Frontier */ + Frontier = 0, + /** Frontier Thawing */ + FrontierThawing = 1, + /** Homestead */ + Homestead = 2, + /** DAO Fork */ + DaoFork = 3, + /** Tangerine */ + Tangerine = 4, + /** Spurious Dragon */ + SpuriousDragon = 5, + /** Byzantium */ + Byzantium = 6, + /** Constantinople */ + Constantinople = 7, + /** Petersburg */ + Petersburg = 8, + /** Istanbul */ + Istanbul = 9, + /** Muir Glacier */ + MuirGlacier = 10, + /** Berlin */ + Berlin = 11, + /** London */ + London = 12, + /** Arrow Glacier */ + ArrowGlacier = 13, + /** Gray Glacier */ + GrayGlacier = 14, + /** Merge */ + Merge = 15, + /** Shanghai */ + Shanghai = 16, + /** Cancun */ + Cancun = 17, + /** Latest */ + Latest = 18 +} +export const FRONTIER: string +export const FRONTIER_THAWING: string +export const HOMESTEAD: string +export const DAO_FORK: string +export const TANGERINE: string +export const SPURIOUS_DRAGON: string +export const BYZANTIUM: string +export const CONSTANTINOPLE: string +export const PETERSBURG: string +export const ISTANBUL: string +export const MUIR_GLACIER: string +export const BERLIN: string +export const LONDON: string +export const ARROW_GLACIER: string +export const GRAY_GLACIER: string +export const MERGE: string +export const SHANGHAI: string +export const CANCUN: string +export const PRAGUE: string +export const PRAGUE_EOF: string +export const LATEST: string /** Configuration for a chain */ export interface ChainConfig { /** The chain ID */ @@ -195,72 +261,6 @@ export interface DebugTraceLogItem { /** Map of all stored values with keys and values encoded as hex strings. */ storage?: Record } -export const GENERIC_CHAIN_TYPE: string -export function genericChainProviderFactory(): ProviderFactory -export const L1_CHAIN_TYPE: string -export function l1ProviderFactory(): ProviderFactory -/** Identifier for the Ethereum spec. */ -export const enum SpecId { - /** Frontier */ - Frontier = 0, - /** Frontier Thawing */ - FrontierThawing = 1, - /** Homestead */ - Homestead = 2, - /** DAO Fork */ - DaoFork = 3, - /** Tangerine */ - Tangerine = 4, - /** Spurious Dragon */ - SpuriousDragon = 5, - /** Byzantium */ - Byzantium = 6, - /** Constantinople */ - Constantinople = 7, - /** Petersburg */ - Petersburg = 8, - /** Istanbul */ - Istanbul = 9, - /** Muir Glacier */ - MuirGlacier = 10, - /** Berlin */ - Berlin = 11, - /** London */ - London = 12, - /** Arrow Glacier */ - ArrowGlacier = 13, - /** Gray Glacier */ - GrayGlacier = 14, - /** Merge */ - Merge = 15, - /** Shanghai */ - Shanghai = 16, - /** Cancun */ - Cancun = 17, - /** Latest */ - Latest = 18 -} -export const FRONTIER: string -export const FRONTIER_THAWING: string -export const HOMESTEAD: string -export const DAO_FORK: string -export const TANGERINE: string -export const SPURIOUS_DRAGON: string -export const BYZANTIUM: string -export const CONSTANTINOPLE: string -export const PETERSBURG: string -export const ISTANBUL: string -export const MUIR_GLACIER: string -export const BERLIN: string -export const LONDON: string -export const ARROW_GLACIER: string -export const GRAY_GLACIER: string -export const MERGE: string -export const SHANGHAI: string -export const CANCUN: string -export const PRAGUE: string -export const PRAGUE_EOF: string -export const LATEST: string /** Ethereum execution log. */ export interface ExecutionLog { address: Buffer @@ -437,6 +437,14 @@ export class EdrContext { registerProviderFactory(chainType: string, factory: ProviderFactory): Promise } export class ProviderFactory { } +export class Response { + /**Returns the response data as a JSON string or a JSON object. */ + get data(): string | any + /**Returns the Solidity trace of the transaction that failed to execute, if any. */ + get solidityTrace(): RawTrace | null + /**Returns the raw traces of executed contracts. This maybe contain zero or more traces. */ + get traces(): Array +} /** A JSON-RPC provider for Ethereum. */ export class Provider { /**Handles a JSON-RPC request and returns a JSON-RPC response. */ @@ -450,14 +458,6 @@ export class Provider { */ setVerboseTracing(verboseTracing: boolean): Promise } -export class Response { - /**Returns the response data as a JSON string or a JSON object. */ - get data(): string | any - /**Returns the Solidity trace of the transaction that failed to execute, if any. */ - get solidityTrace(): RawTrace | null - /**Returns the raw traces of executed contracts. This maybe contain zero or more traces. */ - get traces(): Array -} export class RawTrace { trace(): Array } diff --git a/crates/edr_napi/index.js b/crates/edr_napi/index.js index 01f5c1d03..7b981ee05 100644 --- a/crates/edr_napi/index.js +++ b/crates/edr_napi/index.js @@ -310,10 +310,8 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { MineOrdering, EdrContext, GENERIC_CHAIN_TYPE, genericChainProviderFactory, L1_CHAIN_TYPE, l1ProviderFactory, SpecId, FRONTIER, FRONTIER_THAWING, HOMESTEAD, DAO_FORK, TANGERINE, SPURIOUS_DRAGON, BYZANTIUM, CONSTANTINOPLE, PETERSBURG, ISTANBUL, MUIR_GLACIER, BERLIN, LONDON, ARROW_GLACIER, GRAY_GLACIER, MERGE, SHANGHAI, CANCUN, PRAGUE, PRAGUE_EOF, LATEST, ProviderFactory, Provider, SuccessReason, ExceptionalHalt, Response, RawTrace } = nativeBinding +const { GENERIC_CHAIN_TYPE, genericChainProviderFactory, L1_CHAIN_TYPE, l1ProviderFactory, SpecId, FRONTIER, FRONTIER_THAWING, HOMESTEAD, DAO_FORK, TANGERINE, SPURIOUS_DRAGON, BYZANTIUM, CONSTANTINOPLE, PETERSBURG, ISTANBUL, MUIR_GLACIER, BERLIN, LONDON, ARROW_GLACIER, GRAY_GLACIER, MERGE, SHANGHAI, CANCUN, PRAGUE, PRAGUE_EOF, LATEST, MineOrdering, EdrContext, ProviderFactory, Response, Provider, SuccessReason, ExceptionalHalt, RawTrace } = nativeBinding -module.exports.MineOrdering = MineOrdering -module.exports.EdrContext = EdrContext module.exports.GENERIC_CHAIN_TYPE = GENERIC_CHAIN_TYPE module.exports.genericChainProviderFactory = genericChainProviderFactory module.exports.L1_CHAIN_TYPE = L1_CHAIN_TYPE @@ -340,9 +338,11 @@ module.exports.CANCUN = CANCUN module.exports.PRAGUE = PRAGUE module.exports.PRAGUE_EOF = PRAGUE_EOF module.exports.LATEST = LATEST +module.exports.MineOrdering = MineOrdering +module.exports.EdrContext = EdrContext module.exports.ProviderFactory = ProviderFactory +module.exports.Response = Response module.exports.Provider = Provider module.exports.SuccessReason = SuccessReason module.exports.ExceptionalHalt = ExceptionalHalt -module.exports.Response = Response module.exports.RawTrace = RawTrace diff --git a/crates/edr_napi/package.json b/crates/edr_napi/package.json index 89cedc17f..7e5f2a985 100644 --- a/crates/edr_napi/package.json +++ b/crates/edr_napi/package.json @@ -32,6 +32,7 @@ "license": "MIT", "devDependencies": { "@napi-rs/cli": "^2.18.1", + "@nomicfoundation/ethereumjs-util": "^9.0.4", "@types/chai": "^4.2.0", "@types/chai-as-promised": "^7.1.8", "@types/mocha": ">=9.1.0", @@ -50,14 +51,15 @@ "artifacts": "napi artifacts", "build": "napi build --platform --release", "build:debug": "napi build --platform", + "build:optimism": "napi build --platform --release --features optimism", "build:tracing": "napi build --platform --release --features tracing", "build:scenarios": "napi build --platform --release --features scenarios", - "prepublishOnly": "bash scripts/prepublish.sh", + "prepublishOnly": "bash ../../scripts/prepublish.sh", "universal": "napi universal", "version": "napi version", - "pretest": "pnpm build", + "pretest": "pnpm build:optimism", "test": "pnpm tsc && node --max-old-space-size=8192 node_modules/mocha/bin/_mocha --recursive \"test/**/*.ts\"", - "testNoBuild": "pnpm tsc && node --max-old-space-size=8192 node_modules/mocha/bin/_mocha --recursive \"test/**/*.ts\"", + "testNoBuild": "pnpm tsc && node --max-old-space-size=8192 node_modules/mocha/bin/_mocha --recursive \"test/**/{,!(optimism)}.ts\"", "clean": "rm -rf @nomicfoundation/edr.node" } } diff --git a/crates/edr_napi/src/chains.rs b/crates/edr_napi/src/chains.rs new file mode 100644 index 000000000..e9d9b4c3d --- /dev/null +++ b/crates/edr_napi/src/chains.rs @@ -0,0 +1,7 @@ +/// Types for the generic L1 Ethereum implementation. +pub mod generic; +/// Types for L1 Ethereum implementation. +pub mod l1; +/// Types for Optimism implementation. +#[cfg(feature = "optimism")] +pub mod optimism; diff --git a/crates/edr_napi/src/generic_chain.rs b/crates/edr_napi/src/chains/generic.rs similarity index 64% rename from crates/edr_napi/src/generic_chain.rs rename to crates/edr_napi/src/chains/generic.rs index 2eb0c1350..a5219dcbe 100644 --- a/crates/edr_napi/src/generic_chain.rs +++ b/crates/edr_napi/src/chains/generic.rs @@ -1,14 +1,15 @@ use std::sync::Arc; use edr_generic::GenericChainSpec; +use edr_napi_core::{ + logger::{self, Logger}, + provider::{self, ProviderBuilder, SyncProviderFactory}, + spec::SyncNapiSpec as _, + subscription, +}; use napi_derive::napi; -use crate::{ - logger::{Logger, LoggerConfig}, - provider::{self, factory::SyncProviderFactory, ProviderBuilder, ProviderFactory}, - spec::SyncNapiSpec, - subscription::{SubscriptionCallback, SubscriptionConfig}, -}; +use crate::provider::ProviderFactory; pub struct GenericChainProviderFactory; @@ -17,19 +18,19 @@ impl SyncProviderFactory for GenericChainProviderFactory { &self, env: &napi::Env, provider_config: edr_napi_core::provider::Config, - logger_config: LoggerConfig, - subscription_config: SubscriptionConfig, + logger_config: logger::Config, + subscription_config: subscription::Config, ) -> napi::Result> { - let logger = Logger::::new(env, logger_config)?; + let logger = Logger::::new(logger_config)?; let provider_config = edr_provider::ProviderConfig::::from(provider_config); let subscription_callback = - SubscriptionCallback::new(env, subscription_config.subscription_callback)?; + subscription::Callback::new(env, subscription_config.subscription_callback)?; Ok(Box::new(ProviderBuilder::new( - logger, + Box::new(logger), provider_config, subscription_callback, ))) diff --git a/crates/edr_napi/src/l1.rs b/crates/edr_napi/src/chains/l1.rs similarity index 87% rename from crates/edr_napi/src/l1.rs rename to crates/edr_napi/src/chains/l1.rs index 2aa0251a8..61726ede6 100644 --- a/crates/edr_napi/src/l1.rs +++ b/crates/edr_napi/src/chains/l1.rs @@ -1,14 +1,15 @@ use std::sync::Arc; use edr_eth::{chain_spec::L1ChainSpec, specification}; +use edr_napi_core::{ + logger::Logger, + provider::{self, ProviderBuilder, SyncProviderFactory}, + spec::SyncNapiSpec as _, + subscription, +}; use napi_derive::napi; -use crate::{ - logger::{Logger, LoggerConfig}, - provider::{self, factory::SyncProviderFactory, ProviderBuilder, ProviderFactory}, - spec::SyncNapiSpec, - subscription::{SubscriptionCallback, SubscriptionConfig}, -}; +use crate::provider::ProviderFactory; pub struct L1ProviderFactory; @@ -17,18 +18,18 @@ impl SyncProviderFactory for L1ProviderFactory { &self, env: &napi::Env, provider_config: edr_napi_core::provider::Config, - logger_config: LoggerConfig, - subscription_config: SubscriptionConfig, + logger_config: edr_napi_core::logger::Config, + subscription_config: edr_napi_core::subscription::Config, ) -> napi::Result> { - let logger = Logger::::new(env, logger_config)?; + let logger = Logger::::new(logger_config)?; let provider_config = edr_provider::ProviderConfig::::from(provider_config); let subscription_callback = - SubscriptionCallback::new(env, subscription_config.subscription_callback)?; + subscription::Callback::new(env, subscription_config.subscription_callback)?; Ok(Box::new(ProviderBuilder::new( - logger, + Box::new(logger), provider_config, subscription_callback, ))) diff --git a/crates/edr_napi/src/chains/optimism.rs b/crates/edr_napi/src/chains/optimism.rs new file mode 100644 index 000000000..6efe0634b --- /dev/null +++ b/crates/edr_napi/src/chains/optimism.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use edr_napi_core::{ + logger::{self, Logger}, + provider::{self, ProviderBuilder, SyncProviderFactory}, + spec::SyncNapiSpec as _, + subscription, +}; +use edr_optimism::OptimismChainSpec; +use napi_derive::napi; + +use crate::provider::ProviderFactory; + +pub struct OptimismProviderFactory; + +impl SyncProviderFactory for OptimismProviderFactory { + fn create_provider_builder( + &self, + env: &napi::Env, + provider_config: provider::Config, + logger_config: logger::Config, + subscription_config: subscription::Config, + ) -> napi::Result> { + let logger = Logger::::new(logger_config)?; + + let provider_config = + edr_provider::ProviderConfig::::from(provider_config); + + let subscription_callback = + subscription::Callback::new(env, subscription_config.subscription_callback)?; + + Ok(Box::new(ProviderBuilder::new( + Box::new(logger), + provider_config, + subscription_callback, + ))) + } +} + +#[napi] +pub const OPTIMISM_CHAIN_TYPE: &str = OptimismChainSpec::CHAIN_TYPE; + +#[napi] +pub fn optimism_provider_factory() -> ProviderFactory { + let factory: Arc = Arc::new(OptimismProviderFactory); + factory.into() +} diff --git a/crates/edr_napi/src/context.rs b/crates/edr_napi/src/context.rs index 9104b878e..525f7df34 100644 --- a/crates/edr_napi/src/context.rs +++ b/crates/edr_napi/src/context.rs @@ -1,6 +1,7 @@ use std::{io, sync::Arc}; use edr_eth::HashMap; +use edr_napi_core::provider::{self, SyncProviderFactory}; use napi::{ tokio::{runtime, sync::Mutex as AsyncMutex}, Env, JsObject, Status, @@ -11,7 +12,7 @@ use tracing_subscriber::{prelude::*, EnvFilter, Registry}; use crate::{ config::ProviderConfig, logger::LoggerConfig, - provider::{factory::SyncProviderFactory, Provider, ProviderFactory}, + provider::{Provider, ProviderFactory}, subscription::SubscriptionConfig, }; @@ -44,6 +45,7 @@ impl EdrContext { subscription_config: SubscriptionConfig, ) -> napi::Result { let provider_config = edr_napi_core::provider::Config::try_from(provider_config)?; + let logger_config = logger_config.resolve(&env)?; #[cfg(feature = "scenarios")] let scenario_file = @@ -61,7 +63,7 @@ impl EdrContext { &chain_type, provider_config, logger_config, - subscription_config, + subscription_config.into(), )? }; @@ -153,17 +155,10 @@ impl Context { env: &napi::Env, chain_type: &str, provider_config: edr_napi_core::provider::Config, - logger_config: LoggerConfig, - subscription_config: SubscriptionConfig, - ) -> napi::Result> { + logger_config: edr_napi_core::logger::Config, + subscription_config: edr_napi_core::subscription::Config, + ) -> napi::Result> { if let Some(factory) = self.provider_factories.get(chain_type) { - // #[cfg(feature = "scenarios")] - // let scenario_file = crate::scenarios::scenario_file( - // &config, - // edr_provider::Logger::is_enabled(&*logger), - // ) - // .await?; - factory.create_provider_builder( env, provider_config, diff --git a/crates/edr_napi/src/lib.rs b/crates/edr_napi/src/lib.rs index d460cc274..780bc2be9 100644 --- a/crates/edr_napi/src/lib.rs +++ b/crates/edr_napi/src/lib.rs @@ -11,16 +11,13 @@ mod block; pub mod call_override; /// Types for casting N-API types to Rust types. pub mod cast; +/// Supported chain types. +pub mod chains; /// Types for configuration. pub mod config; /// Types related to an EDR N-API context. pub mod context; mod debug_trace; -/// Types for the generic L1 Ethereum implementation. -pub mod generic_chain; - -/// Types for L1 Ethereum implementation. -pub mod l1; /// Types for EVM execution logs. pub mod log; /// Types for an RPC request logger. @@ -32,8 +29,6 @@ pub mod result; /// Types relating to benchmark scenarios. #[cfg(feature = "scenarios")] pub mod scenarios; -/// Types for N-API-related chain specification. -pub mod spec; /// Types for subscribing to events. pub mod subscription; /// Types for EVM traces. diff --git a/crates/edr_napi/src/logger.rs b/crates/edr_napi/src/logger.rs index 527bab261..db28f5f62 100644 --- a/crates/edr_napi/src/logger.rs +++ b/crates/edr_napi/src/logger.rs @@ -1,26 +1,12 @@ -use core::fmt::Debug; -use std::{fmt::Display, marker::PhantomData, sync::mpsc::channel}; +use std::sync::{mpsc::channel, Arc}; -use ansi_term::{Color, Style}; -use derive_where::derive_where; -use edr_eth::{result::ExecutionResult, transaction::ExecutableTransaction, Bytes, B256, U256}; -use edr_evm::{ - blockchain::BlockchainError, - precompile::{self, Precompiles}, - trace::{AfterMessage, Trace, TraceMessage}, - transaction::Transaction as _, - SyncBlock, -}; -use edr_provider::{ - time::CurrentTime, CallResult, DebugMineBlockResult, EstimateGasFailure, ProviderError, - ProviderSpec, TransactionFailure, -}; -use itertools::izip; +use edr_eth::Bytes; +use edr_napi_core::logger::LoggerError; use napi::{ threadsafe_function::{ ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, }, - Env, JsFunction, Status, + JsFunction, Status, }; use napi_derive::napi; @@ -34,6 +20,12 @@ pub struct ContractAndFunctionName { pub function_name: Option, } +struct ContractAndFunctionNameCall { + code: Bytes, + /// Only present for calls. + calldata: Option, +} + impl TryCast<(String, Option)> for ContractAndFunctionName { type Error = napi::Error; @@ -42,12 +34,6 @@ impl TryCast<(String, Option)> for ContractAndFunctionName { } } -struct ContractAndFunctionNameCall { - code: Bytes, - /// Only present for calls. - calldata: Option, -} - #[napi(object)] pub struct LoggerConfig { /// Whether to enable the logger. @@ -60,217 +46,15 @@ pub struct LoggerConfig { pub print_line_callback: JsFunction, } -#[derive(Clone)] -pub enum LoggingState { - CollapsingMethod(CollapsedMethod), - HardhatMinining { - empty_blocks_range_start: Option, - }, - IntervalMining { - empty_blocks_range_start: Option, - }, - Empty, -} - -impl LoggingState { - /// Converts the state into a hardhat mining state. - pub fn into_hardhat_mining(self) -> Option { - match self { - Self::HardhatMinining { - empty_blocks_range_start, - } => empty_blocks_range_start, - _ => None, - } - } - - /// Converts the state into an interval mining state. - pub fn into_interval_mining(self) -> Option { - match self { - Self::IntervalMining { - empty_blocks_range_start, - } => empty_blocks_range_start, - _ => None, - } - } -} - -impl Default for LoggingState { - fn default() -> Self { - Self::Empty - } -} - -#[derive(Clone)] -enum LogLine { - Single(String), - WithTitle(String, String), -} - -#[derive(Debug, thiserror::Error)] -pub enum LoggerError { - #[error("Failed to print line")] - PrintLine, -} - -#[derive_where(Clone)] -pub struct Logger> { - collector: LogCollector, -} - -impl> Logger { - pub fn new(env: &Env, config: LoggerConfig) -> napi::Result { - Ok(Self { - collector: LogCollector::new(env, config)?, - }) - } -} - -impl edr_provider::Logger for Logger -where - ChainSpecT: ProviderSpec, -{ - type BlockchainError = BlockchainError; - - fn is_enabled(&self) -> bool { - self.collector.is_enabled - } - - fn set_is_enabled(&mut self, is_enabled: bool) { - self.collector.is_enabled = is_enabled; - } - - fn log_call( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - result: &CallResult, - ) -> Result<(), Box> { - self.collector.log_call(hardfork, transaction, result); - - Ok(()) - } - - fn log_estimate_gas_failure( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - failure: &EstimateGasFailure, - ) -> Result<(), Box> { - self.collector - .log_estimate_gas(hardfork, transaction, failure); - - Ok(()) - } - - fn log_interval_mined( - &mut self, - hardfork: ChainSpecT::Hardfork, - mining_result: &DebugMineBlockResult, - ) -> Result<(), Box> { - self.collector - .log_interval_mined(hardfork, mining_result) - .map_err(Box::new)?; - - Ok(()) - } - - fn log_mined_block( - &mut self, - hardfork: ChainSpecT::Hardfork, - mining_results: &[DebugMineBlockResult], - ) -> Result<(), Box> { - self.collector.log_mined_blocks(hardfork, mining_results); - - Ok(()) - } - - fn log_send_transaction( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - mining_results: &[DebugMineBlockResult], - ) -> Result<(), Box> { - self.collector - .log_send_transaction(hardfork, transaction, mining_results); - - Ok(()) - } - - fn print_method_logs( - &mut self, - method: &str, - error: Option<&ProviderError>, - ) -> Result<(), Box> { - if let Some(error) = error { - self.collector.state = LoggingState::Empty; - - if matches!(error, ProviderError::UnsupportedMethod { .. }) { - self.collector - .print::(Color::Red.paint(error.to_string()))?; - } else { - self.collector.print::(Color::Red.paint(method))?; - self.collector.print_logs()?; - - if !matches!(error, ProviderError::TransactionFailed(_)) { - self.collector.print_empty_line()?; - - let error_message = error.to_string(); - self.collector - .try_indented(|logger| logger.print::(&error_message))?; - - if matches!(error, ProviderError::InvalidEip155TransactionChainId) { - self.collector.try_indented(|logger| { - logger.print::(Color::Yellow.paint( - "If you are using MetaMask, you can learn how to fix this error here: https://hardhat.org/metamask-issue" - )) - })?; - } - } - - self.collector.print_empty_line()?; - } - } else { - self.collector.print_method(method)?; - - let printed = self.collector.print_logs()?; - if printed { - self.collector.print_empty_line()?; - } - } - - Ok(()) - } -} - -#[derive(Clone)] -pub struct CollapsedMethod { - count: usize, - method: String, -} - -#[derive_where(Clone)] -struct LogCollector> { - decode_console_log_inputs_fn: ThreadsafeFunction, ErrorStrategy::Fatal>, - get_contract_and_function_name_fn: - ThreadsafeFunction, - indentation: usize, - is_enabled: bool, - logs: Vec, - print_line_fn: ThreadsafeFunction<(String, bool), ErrorStrategy::Fatal>, - state: LoggingState, - title_length: usize, - phantom: PhantomData, -} - -impl> LogCollector { - pub fn new(env: &Env, config: LoggerConfig) -> napi::Result { - let mut decode_console_log_inputs_fn = config - .decode_console_log_inputs_callback - .create_threadsafe_function(0, |ctx: ThreadSafeCallContext>| { - let inputs = - ctx.env - .create_array_with_length(ctx.value.len()) - .and_then(|mut inputs| { +impl LoggerConfig { + /// Resolves the logger config, converting it to a + /// `edr_napi_core::logger::Config`. + pub fn resolve(self, env: &napi::Env) -> napi::Result { + let mut decode_console_log_inputs_callback: ThreadsafeFunction<_, ErrorStrategy::Fatal> = + self.decode_console_log_inputs_callback + .create_threadsafe_function(0, |ctx: ThreadSafeCallContext>| { + let inputs = ctx.env.create_array_with_length(ctx.value.len()).and_then( + |mut inputs| { for (idx, input) in ctx.value.into_iter().enumerate() { ctx.env.create_buffer_with_data(input.to_vec()).and_then( |input| inputs.set_element(idx as u32, input.into_raw()), @@ -278,16 +62,40 @@ impl> LogCollector { } Ok(inputs) - })?; + }, + )?; - Ok(vec![inputs]) - })?; + Ok(vec![inputs]) + })?; // Maintain a weak reference to the function to avoid the event loop from // exiting. - decode_console_log_inputs_fn.unref(env)?; + decode_console_log_inputs_callback.unref(env)?; + + let decode_console_log_inputs_fn = Arc::new(move |console_log_inputs| { + let (sender, receiver) = channel(); - let mut get_contract_and_function_name_fn = config + let status = decode_console_log_inputs_callback.call_with_return_value( + console_log_inputs, + ThreadsafeFunctionCallMode::Blocking, + move |decoded_inputs: Vec| { + sender.send(decoded_inputs).map_err(|_error| { + napi::Error::new( + Status::GenericFailure, + "Failed to send result from decode_console_log_inputs", + ) + }) + }, + ); + assert_eq!(status, Status::Ok); + + receiver.recv().unwrap() + }); + + let mut get_contract_and_function_name_callback: ThreadsafeFunction< + _, + ErrorStrategy::Fatal, + > = self .get_contract_and_function_name_callback .create_threadsafe_function( 0, @@ -313,269 +121,12 @@ impl> LogCollector { // Maintain a weak reference to the function to avoid the event loop from // exiting. - get_contract_and_function_name_fn.unref(env)?; - - let mut print_line_fn = config.print_line_callback.create_threadsafe_function( - 0, - |ctx: ThreadSafeCallContext<(String, bool)>| { - // String - let message = ctx.env.create_string_from_std(ctx.value.0)?; - - // bool - let replace = ctx.env.get_boolean(ctx.value.1)?; - - Ok(vec![message.into_unknown(), replace.into_unknown()]) - }, - )?; - - // Maintain a weak reference to the function to avoid the event loop from - // exiting. - print_line_fn.unref(env)?; - - Ok(Self { - decode_console_log_inputs_fn, - get_contract_and_function_name_fn, - indentation: 0, - is_enabled: config.enable, - logs: Vec::new(), - print_line_fn, - state: LoggingState::default(), - title_length: 0, - phantom: PhantomData, - }) - } - - pub fn log_call( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - result: &CallResult, - ) { - let CallResult { - console_log_inputs, - execution_result, - trace, - } = result; - - self.state = LoggingState::Empty; - - self.indented(|logger| { - logger.log_contract_and_function_name::(hardfork, trace); - - logger.log_with_title("From", format!("0x{:x}", transaction.caller())); - if let Some(to) = transaction.kind().to() { - logger.log_with_title("To", format!("0x{to:x}")); - } - if *transaction.value() > U256::ZERO { - logger.log_with_title("Value", wei_to_human_readable(transaction.value())); - } - - logger.log_console_log_messages(console_log_inputs); - - if let Some(transaction_failure) = - TransactionFailure::::from_execution_result::( - execution_result, - None, - trace, - ) - { - logger.log_transaction_failure(&transaction_failure); - } - }); - } - - pub fn log_estimate_gas( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - result: &EstimateGasFailure, - ) { - let EstimateGasFailure { - console_log_inputs, - transaction_failure, - } = result; - - self.state = LoggingState::Empty; - - self.indented(|logger| { - logger.log_contract_and_function_name::( - hardfork, - &transaction_failure.failure.solidity_trace, - ); - - logger.log_with_title("From", format!("0x{:x}", transaction.caller())); - if let Some(to) = transaction.kind().to() { - logger.log_with_title("To", format!("0x{to:x}")); - } - logger.log_with_title("Value", wei_to_human_readable(transaction.value())); - - logger.log_console_log_messages(console_log_inputs); - - logger.log_transaction_failure(&transaction_failure.failure); - }); - } - - fn log_transaction_failure(&mut self, failure: &edr_provider::TransactionFailure) { - let is_revert_error = matches!( - failure.reason, - edr_provider::TransactionFailureReason::Revert(_) - ); - - let error_type = if is_revert_error { - "Error" - } else { - "TransactionExecutionError" - }; - - self.log_empty_line(); - self.log(format!("{error_type}: {failure}")); - } - - pub fn log_mined_blocks( - &mut self, - hardfork: ChainSpecT::Hardfork, - mining_results: &[DebugMineBlockResult>], - ) { - let num_results = mining_results.len(); - for (idx, mining_result) in mining_results.iter().enumerate() { - let state = std::mem::take(&mut self.state); - let empty_blocks_range_start = state.into_hardhat_mining(); + get_contract_and_function_name_callback.unref(env)?; - if mining_result.block.transactions().is_empty() { - self.log_hardhat_mined_empty_block(&mining_result.block, empty_blocks_range_start); + let get_contract_and_function_name_fn = Arc::new(move |code, calldata| { + let (sender, receiver) = channel(); - let block_number = mining_result.block.header().number; - self.state = LoggingState::HardhatMinining { - empty_blocks_range_start: Some( - empty_blocks_range_start.unwrap_or(block_number), - ), - }; - } else { - self.log_hardhat_mined_block(hardfork, mining_result); - - if idx < num_results - 1 { - self.log_empty_line(); - } - } - } - } - - pub fn log_interval_mined( - &mut self, - hardfork: ChainSpecT::Hardfork, - mining_result: &DebugMineBlockResult>, - ) -> Result<(), LoggerError> { - let block_header = mining_result.block.header(); - let block_number = block_header.number; - - if mining_result.block.transactions().is_empty() { - let state = std::mem::take(&mut self.state); - let empty_blocks_range_start = state.into_interval_mining(); - - if let Some(empty_blocks_range_start) = empty_blocks_range_start { - self.print::(format!( - "Mined empty block range #{empty_blocks_range_start} to #{block_number}" - ))?; - } else { - let base_fee = if let Some(base_fee) = block_header.base_fee_per_gas.as_ref() { - format!(" with base fee {base_fee}") - } else { - String::new() - }; - - self.print::(format!("Mined empty block #{block_number}{base_fee}"))?; - } - - self.state = LoggingState::IntervalMining { - empty_blocks_range_start: Some( - empty_blocks_range_start.unwrap_or(block_header.number), - ), - }; - } else { - self.log_interval_mined_block(hardfork, mining_result); - - self.print::(format!("Mined block #{block_number}"))?; - - let printed = self.print_logs()?; - if printed { - self.print_empty_line()?; - } - } - - Ok(()) - } - - pub fn log_send_transaction( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - mining_results: &[DebugMineBlockResult>], - ) { - if !mining_results.is_empty() { - self.state = LoggingState::Empty; - - let (sent_block_result, sent_transaction_result, sent_trace) = mining_results - .iter() - .find_map(|result| { - izip!( - result.block.transactions(), - result.transaction_results.iter(), - result.transaction_traces.iter() - ) - .find(|(block_transaction, _, _)| { - *block_transaction.transaction_hash() == *transaction.transaction_hash() - }) - .map(|(_, transaction_result, trace)| (result, transaction_result, trace)) - }) - .expect("Transaction result not found"); - - if mining_results.len() > 1 { - self.log_multiple_blocks_warning(); - self.log_auto_mined_block_results( - hardfork, - mining_results, - transaction.transaction_hash(), - ); - self.log_currently_sent_transaction( - hardfork, - sent_block_result, - transaction, - sent_transaction_result, - sent_trace, - ); - } else if let Some(result) = mining_results.first() { - let transactions = result.block.transactions(); - if transactions.len() > 1 { - self.log_multiple_transactions_warning(); - self.log_auto_mined_block_results( - hardfork, - mining_results, - transaction.transaction_hash(), - ); - self.log_currently_sent_transaction( - hardfork, - sent_block_result, - transaction, - sent_transaction_result, - sent_trace, - ); - } else if let Some(transaction) = transactions.first() { - self.log_single_transaction_mining_result(hardfork, result, transaction); - } - } - } - } - - fn contract_and_function_name( - &self, - code: Bytes, - calldata: Option, - ) -> (String, Option) { - let (sender, receiver) = channel(); - - let status = self - .get_contract_and_function_name_fn - .call_with_return_value( + let status = get_contract_and_function_name_callback.call_with_return_value( ContractAndFunctionNameCall { code, calldata }, ThreadsafeFunctionCallMode::Blocking, move |result: ContractAndFunctionName| { @@ -588,688 +139,46 @@ impl> LogCollector { }) }, ); - assert_eq!(status, Status::Ok); - - receiver - .recv() - .unwrap() - .expect("Failed call to get_contract_and_function_name") - } - - fn format(&self, message: impl ToString) -> String { - let message = message.to_string(); - - if message.is_empty() { - message - } else { - message - .split('\n') - .map(|line| format!("{:indent$}{line}", "", indent = self.indentation)) - .collect::>() - .join("\n") - } - } - - fn indented(&mut self, display_fn: impl FnOnce(&mut Self)) { - self.indentation += 2; - display_fn(self); - self.indentation -= 2; - } - - fn try_indented( - &mut self, - display_fn: impl FnOnce(&mut Self) -> Result<(), LoggerError>, - ) -> Result<(), LoggerError> { - self.indentation += 2; - let result = display_fn(self); - self.indentation -= 2; - - result - } - - fn log(&mut self, message: impl ToString) { - let formatted = self.format(message); - - self.logs.push(LogLine::Single(formatted)); - } - - fn log_auto_mined_block_results( - &mut self, - hardfork: ChainSpecT::Hardfork, - results: &[DebugMineBlockResult>], - sent_transaction_hash: &B256, - ) { - for result in results { - self.log_block_from_auto_mine(hardfork, result, sent_transaction_hash); - } - } - - fn log_base_fee(&mut self, base_fee: Option<&U256>) { - if let Some(base_fee) = base_fee { - self.log(format!("Base fee: {base_fee}")); - } - } - - fn log_block_from_auto_mine( - &mut self, - hardfork: ChainSpecT::Hardfork, - result: &DebugMineBlockResult>, - transaction_hash_to_highlight: &edr_eth::B256, - ) { - let DebugMineBlockResult { - block, - transaction_results, - transaction_traces, - console_log_inputs, - } = result; - - let transactions = block.transactions(); - let num_transactions = transactions.len(); - - debug_assert_eq!(num_transactions, transaction_results.len()); - debug_assert_eq!(num_transactions, transaction_traces.len()); - - let block_header = block.header(); - - self.indented(|logger| { - logger.log_block_id(block); - - logger.indented(|logger| { - logger.log_base_fee(block_header.base_fee_per_gas.as_ref()); - - for (idx, transaction, result, trace) in izip!( - 0..num_transactions, - transactions, - transaction_results, - transaction_traces - ) { - let should_highlight_hash = - *transaction.transaction_hash() == *transaction_hash_to_highlight; - logger.log_block_transaction( - hardfork, - transaction, - result, - trace, - console_log_inputs, - should_highlight_hash, - ); - - logger.log_empty_line_between_transactions(idx, num_transactions); - } - }); - }); - - self.log_empty_line(); - } - - fn log_block_hash( - &mut self, - block: &dyn SyncBlock>, - ) { - let block_hash = block.hash(); - - self.log(format!("Block: {block_hash}")); - } - - fn log_block_id( - &mut self, - block: &dyn SyncBlock>, - ) { - let block_number = block.header().number; - let block_hash = block.hash(); - - self.log(format!("Block #{block_number}: {block_hash}")); - } - - fn log_block_number( - &mut self, - block: &dyn SyncBlock>, - ) { - let block_number = block.header().number; - - self.log(format!("Mined block #{block_number}")); - } - - /// Logs a transaction that's part of a block. - fn log_block_transaction( - &mut self, - hardfork: ChainSpecT::Hardfork, - transaction: &ChainSpecT::Transaction, - result: &ExecutionResult, - trace: &Trace, - console_log_inputs: &[Bytes], - should_highlight_hash: bool, - ) { - let transaction_hash = transaction.transaction_hash(); - if should_highlight_hash { - self.log_with_title( - "Transaction", - Style::new().bold().paint(transaction_hash.to_string()), - ); - } else { - self.log_with_title("Transaction", transaction_hash.to_string()); - } - - self.indented(|logger| { - logger.log_contract_and_function_name::(hardfork, trace); - logger.log_with_title("From", format!("0x{:x}", transaction.caller())); - if let Some(to) = transaction.kind().to() { - logger.log_with_title("To", format!("0x{to:x}")); - } - logger.log_with_title("Value", wei_to_human_readable(transaction.value())); - logger.log_with_title( - "Gas used", - format!( - "{gas_used} of {gas_limit}", - gas_used = result.gas_used(), - gas_limit = transaction.gas_limit() - ), - ); - - logger.log_console_log_messages(console_log_inputs); + assert_eq!(status, Status::Ok); - let transaction_failure = - edr_provider::TransactionFailure::::from_execution_result::< - ChainSpecT, - CurrentTime, - >(result, Some(transaction_hash), trace); - - if let Some(transaction_failure) = transaction_failure { - logger.log_transaction_failure(&transaction_failure); - } + receiver + .recv() + .unwrap() + .expect("Failed call to get_contract_and_function_name") }); - } - - fn log_console_log_messages(&mut self, console_log_inputs: &[Bytes]) { - let (sender, receiver) = channel(); - - let status = self.decode_console_log_inputs_fn.call_with_return_value( - console_log_inputs.to_vec(), - ThreadsafeFunctionCallMode::Blocking, - move |decoded_inputs: Vec| { - sender.send(decoded_inputs).map_err(|_error| { - napi::Error::new( - Status::GenericFailure, - "Failed to send result from decode_console_log_inputs", - ) - }) - }, - ); - assert_eq!(status, Status::Ok); - - let console_log_inputs = receiver.recv().unwrap(); - // This is a special case, as we always want to print the console.log messages. - // The difference is how. If we have a logger, we should use that, so that logs - // are printed in order. If we don't, we just print the messages here. - if self.is_enabled { - if !console_log_inputs.is_empty() { - self.log_empty_line(); - self.log("console.log:"); - - self.indented(|logger| { - for input in console_log_inputs { - logger.log(input); - } - }); - } - } else { - for input in console_log_inputs { - let status = self - .print_line_fn - .call((input, false), ThreadsafeFunctionCallMode::Blocking); - - assert_eq!(status, napi::Status::Ok); - } - } - } - - fn log_contract_and_function_name( - &mut self, - hardfork: ChainSpecT::Hardfork, - trace: &Trace, - ) { - if let Some(TraceMessage::Before(before_message)) = trace.messages.first() { - if let Some(to) = before_message.to { - // Call - let is_precompile = { - let precompiles = Precompiles::new(precompile::PrecompileSpecId::from_spec_id( - hardfork.into(), - )); - precompiles.contains(&to) - }; - - if is_precompile { - let precompile = u16::from_be_bytes([to[18], to[19]]); - self.log_with_title( - "Precompile call", - format!(""), - ); - } else { - let is_code_empty = before_message - .code - .as_ref() - .map_or(true, edr_eth::Bytecode::is_empty); - - if is_code_empty { - if PRINT_INVALID_CONTRACT_WARNING { - self.log("WARNING: Calling an account which is not a contract"); - } - } else { - let (contract_name, function_name) = self.contract_and_function_name( - before_message - .code - .as_ref() - .map(edr_eth::Bytecode::original_bytes) - .expect("Call must be defined"), - Some(before_message.data.clone()), - ); - - let function_name = function_name.expect("Function name must be defined"); - self.log_with_title( - "Contract call", - if function_name.is_empty() { - contract_name - } else { - format!("{contract_name}#{function_name}") - }, - ); - } - } - } else { - let result = if let Some(TraceMessage::After(AfterMessage { - execution_result, - .. - })) = trace.messages.last() - { - execution_result - } else { - unreachable!("Before messages must have an after message") - }; - - // Create - let (contract_name, _) = - self.contract_and_function_name(before_message.data.clone(), None); - - self.log_with_title("Contract deployment", contract_name); - - if let ExecutionResult::Success { output, .. } = result { - if let edr_eth::result::Output::Create(_, address) = output { - if let Some(deployed_address) = address { - self.log_with_title( - "Contract address", - format!("0x{deployed_address:x}"), - ); - } - } else { - unreachable!("Create calls must return a Create output") - } - } - } - } - } - - fn log_empty_block( - &mut self, - block: &dyn SyncBlock>, - ) { - let block_header = block.header(); - let block_number = block_header.number; - - let base_fee = if let Some(base_fee) = block_header.base_fee_per_gas.as_ref() { - format!(" with base fee {base_fee}") - } else { - String::new() - }; - - self.log(format!("Mined empty block #{block_number}{base_fee}",)); - } - - fn log_empty_line(&mut self) { - self.log(""); - } - - fn log_empty_line_between_transactions(&mut self, idx: usize, num_transactions: usize) { - if num_transactions > 1 && idx < num_transactions - 1 { - self.log_empty_line(); - } - } - - fn log_hardhat_mined_empty_block( - &mut self, - block: &dyn SyncBlock>, - empty_blocks_range_start: Option, - ) { - self.indented(|logger| { - if let Some(empty_blocks_range_start) = empty_blocks_range_start { - logger.replace_last_log_line(format!( - "Mined empty block range #{empty_blocks_range_start} to #{block_number}", - block_number = block.header().number - )); - } else { - logger.log_empty_block(block); - } - }); - } - - /// Logs the result of interval mining a block. - fn log_interval_mined_block( - &mut self, - hardfork: ChainSpecT::Hardfork, - result: &DebugMineBlockResult>, - ) { - let DebugMineBlockResult { - block, - transaction_results, - transaction_traces, - console_log_inputs, - } = result; - - let transactions = block.transactions(); - let num_transactions = transactions.len(); - - debug_assert_eq!(num_transactions, transaction_results.len()); - debug_assert_eq!(num_transactions, transaction_traces.len()); - - let block_header = block.header(); - - self.indented(|logger| { - logger.log_block_hash(block); - logger.indented(|logger| { - logger.log_base_fee(block_header.base_fee_per_gas.as_ref()); - - for (idx, transaction, result, trace) in izip!( - 0..num_transactions, - transactions, - transaction_results, - transaction_traces - ) { - logger.log_block_transaction( - hardfork, - transaction, - result, - trace, - console_log_inputs, - false, - ); + let mut print_line_callback: ThreadsafeFunction<_, ErrorStrategy::Fatal> = self + .print_line_callback + .create_threadsafe_function(0, |ctx: ThreadSafeCallContext<(String, bool)>| { + // String + let message = ctx.env.create_string_from_std(ctx.value.0)?; - logger.log_empty_line_between_transactions(idx, num_transactions); - } - }); - }); - } + // bool + let replace = ctx.env.get_boolean(ctx.value.1)?; - fn log_hardhat_mined_block( - &mut self, - hardfork: ChainSpecT::Hardfork, - result: &DebugMineBlockResult>, - ) { - let DebugMineBlockResult { - block, - transaction_results, - transaction_traces, - console_log_inputs, - } = result; + Ok(vec![message.into_unknown(), replace.into_unknown()]) + })?; - let transactions = block.transactions(); - let num_transactions = transactions.len(); + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + print_line_callback.unref(env)?; - debug_assert_eq!(num_transactions, transaction_results.len()); - debug_assert_eq!(num_transactions, transaction_traces.len()); + let print_line_fn = Arc::new(move |message, replace| { + let status = + print_line_callback.call((message, replace), ThreadsafeFunctionCallMode::Blocking); - self.indented(|logger| { - if transactions.is_empty() { - logger.log_empty_block(block); + if status == napi::Status::Ok { + Ok(()) } else { - logger.log_block_number(block); - - logger.indented(|logger| { - logger.log_block_hash(block); - - logger.indented(|logger| { - logger.log_base_fee(block.header().base_fee_per_gas.as_ref()); - - for (idx, transaction, result, trace) in izip!( - 0..num_transactions, - transactions, - transaction_results, - transaction_traces - ) { - logger.log_block_transaction( - hardfork, - transaction, - result, - trace, - console_log_inputs, - false, - ); - - logger.log_empty_line_between_transactions(idx, num_transactions); - } - }); - }); - } - }); - } - - /// Logs a warning about multiple blocks being mined. - fn log_multiple_blocks_warning(&mut self) { - self.indented(|logger| { - logger - .log("There were other pending transactions. More than one block had to be mined:"); - }); - self.log_empty_line(); - } - - /// Logs a warning about multiple transactions being mined. - fn log_multiple_transactions_warning(&mut self) { - self.indented(|logger| { - logger.log("There were other pending transactions mined in the same block:"); - }); - self.log_empty_line(); - } - - fn log_with_title(&mut self, title: impl Into, message: impl Display) { - // repeat whitespace self.indentation times and concatenate with title - let title = format!("{:indent$}{}", "", title.into(), indent = self.indentation); - if title.len() > self.title_length { - self.title_length = title.len(); - } - - let message = format!("{message}"); - self.logs.push(LogLine::WithTitle(title, message)); - } - - fn log_currently_sent_transaction( - &mut self, - hardfork: ChainSpecT::Hardfork, - block_result: &DebugMineBlockResult>, - transaction: &ChainSpecT::Transaction, - transaction_result: &ExecutionResult, - trace: &Trace, - ) { - self.indented(|logger| { - logger.log("Currently sent transaction:"); - logger.log(""); - }); - - self.log_transaction( - hardfork, - block_result, - transaction, - transaction_result, - trace, - ); - } - - fn log_single_transaction_mining_result( - &mut self, - hardfork: ChainSpecT::Hardfork, - result: &DebugMineBlockResult>, - transaction: &ChainSpecT::Transaction, - ) { - let trace = result - .transaction_traces - .first() - .expect("A transaction exists, so the trace must exist as well."); - - let transaction_result = result - .transaction_results - .first() - .expect("A transaction exists, so the result must exist as well."); - - self.log_transaction(hardfork, result, transaction, transaction_result, trace); - } - - fn log_transaction( - &mut self, - hardfork: ChainSpecT::Hardfork, - block_result: &DebugMineBlockResult>, - transaction: &ChainSpecT::Transaction, - transaction_result: &ExecutionResult, - trace: &Trace, - ) { - self.indented(|logger| { - logger.log_contract_and_function_name::(hardfork, trace); - - let transaction_hash = transaction.transaction_hash(); - logger.log_with_title("Transaction", transaction_hash); - - logger.log_with_title("From", format!("0x{:x}", transaction.caller())); - if let Some(to) = transaction.kind().to() { - logger.log_with_title("To", format!("0x{to:x}")); - } - logger.log_with_title("Value", wei_to_human_readable(transaction.value())); - logger.log_with_title( - "Gas used", - format!( - "{gas_used} of {gas_limit}", - gas_used = transaction_result.gas_used(), - gas_limit = transaction.gas_limit() - ), - ); - - let block_number = block_result.block.header().number; - logger.log_with_title(format!("Block #{block_number}"), block_result.block.hash()); - - logger.log_console_log_messages(&block_result.console_log_inputs); - - let transaction_failure = - edr_provider::TransactionFailure::::from_execution_result::< - ChainSpecT, - CurrentTime, - >(transaction_result, Some(transaction_hash), trace); - - if let Some(transaction_failure) = transaction_failure { - logger.log_transaction_failure(&transaction_failure); + Err(LoggerError::PrintLine) } }); - } - - fn print(&mut self, message: impl ToString) -> Result<(), LoggerError> { - if !self.is_enabled { - return Ok(()); - } - - let formatted = self.format(message); - - let status = self - .print_line_fn - .call((formatted, REPLACE), ThreadsafeFunctionCallMode::Blocking); - - if status == napi::Status::Ok { - Ok(()) - } else { - Err(LoggerError::PrintLine) - } - } - - fn print_empty_line(&mut self) -> Result<(), LoggerError> { - self.print::("") - } - - fn print_logs(&mut self) -> Result { - let logs = std::mem::take(&mut self.logs); - if logs.is_empty() { - return Ok(false); - } - - for log in logs { - let line = match log { - LogLine::Single(message) => message, - LogLine::WithTitle(title, message) => { - let title = format!("{title}:"); - format!("{title:indent$} {message}", indent = self.title_length + 1) - } - }; - - self.print::(line)?; - } - - Ok(true) - } - - fn print_method(&mut self, method: &str) -> Result<(), LoggerError> { - if let Some(collapsed_method) = self.collapsed_method(method) { - collapsed_method.count += 1; - - let line = format!("{method} ({count})", count = collapsed_method.count); - self.print::(Color::Green.paint(line)) - } else { - self.state = LoggingState::CollapsingMethod(CollapsedMethod { - count: 1, - method: method.to_string(), - }); - self.print::(Color::Green.paint(method)) - } - } - - /// Retrieves the collapsed method with the provided name, if it exists. - fn collapsed_method(&mut self, method: &str) -> Option<&mut CollapsedMethod> { - if let LoggingState::CollapsingMethod(collapsed_method) = &mut self.state { - if collapsed_method.method == method { - return Some(collapsed_method); - } - } - - None - } - - fn replace_last_log_line(&mut self, message: impl ToString) { - let formatted = self.format(message); - - *self.logs.last_mut().expect("There must be a log line") = LogLine::Single(formatted); - } -} - -fn wei_to_human_readable(wei: &U256) -> String { - if *wei == U256::ZERO { - "0 ETH".to_string() - } else if *wei < U256::from(100_000u64) { - format!("{wei} wei") - } else if *wei < U256::from(100_000_000_000_000u64) { - let mut decimal = to_decimal_string(wei, 9); - decimal.push_str(" gwei"); - decimal - } else { - let mut decimal = to_decimal_string(wei, 18); - decimal.push_str(" ETH"); - decimal + Ok(edr_napi_core::logger::Config { + enable: self.enable, + decode_console_log_inputs_fn, + get_contract_and_function_name_fn, + print_line_fn, + }) } } - -/// Converts the provided `value` to a decimal string after dividing it by -/// `10^exponent`. The returned string will have at most `MAX_DECIMALS` -/// decimals. -fn to_decimal_string(value: &U256, exponent: u8) -> String { - const MAX_DECIMALS: u8 = 4; - - let (integer, remainder) = value.div_rem(U256::from(10).pow(U256::from(exponent))); - let decimal = remainder / U256::from(10).pow(U256::from(exponent - MAX_DECIMALS)); - - // Remove trailing zeros - let decimal = decimal.to_string().trim_end_matches('0').to_string(); - - format!("{integer}.{decimal}") -} diff --git a/crates/edr_napi/src/provider.rs b/crates/edr_napi/src/provider.rs index 9695a55fa..0ae2dfcff 100644 --- a/crates/edr_napi/src/provider.rs +++ b/crates/edr_napi/src/provider.rs @@ -1,22 +1,16 @@ -mod builder; /// Types related to provider factories. pub mod factory; +mod response; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; -use edr_provider::InvalidRequestReason; -use edr_rpc_client::jsonrpc; +use edr_napi_core::provider::SyncProvider; use napi::{tokio::runtime, Env, JsFunction, JsObject, Status}; use napi_derive::napi; -pub use self::{ - builder::{Builder, ProviderBuilder}, - factory::ProviderFactory, -}; -use crate::{ - call_override::CallOverrideCallback, - spec::{Response, SyncNapiSpec}, -}; +pub use self::factory::ProviderFactory; +use self::response::Response; +use crate::call_override::CallOverrideCallback; /// A JSON-RPC provider for Ethereum. #[napi] @@ -57,10 +51,11 @@ impl Provider { crate::scenarios::write_request(scenario_file, &request).await?; } - runtime::Handle::current() + self.runtime .spawn_blocking(move || provider.handle_request(request)) .await .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string()))? + .map(Response::from) } #[napi(ts_return_type = "Promise")] @@ -75,6 +70,9 @@ impl Provider { let call_override_callback = CallOverrideCallback::new(&env, call_override_callback, self.runtime.clone())?; + let call_override_callback = + Arc::new(move |address, data| call_override_callback.call_override(address, data)); + let provider = self.provider.clone(); let (deferred, promise) = env.create_deferred()?; @@ -103,78 +101,3 @@ impl Provider { .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) } } - -/// Trait for a synchronous N-API provider that can be used for dynamic trait -/// objects. -pub trait SyncProvider: Send + Sync { - /// Blocking method to handle a request. - fn handle_request(&self, request: String) -> napi::Result; - - /// Set to `true` to make the traces returned with `eth_call`, - /// `eth_estimateGas`, `eth_sendRawTransaction`, `eth_sendTransaction`, - /// `evm_mine`, `hardhat_mine` include the full stack and memory. Set to - /// `false` to disable this. - fn set_call_override_callback(&self, call_override_callback: CallOverrideCallback); - - /// Set the verbose tracing flag to the provided value. - fn set_verbose_tracing(&self, enabled: bool); -} - -impl SyncProvider for edr_provider::Provider { - fn handle_request(&self, request: String) -> napi::Result { - let request = match serde_json::from_str(&request) { - Ok(request) => request, - Err(error) => { - let message = error.to_string(); - - let request = serde_json::Value::from_str(&request).ok(); - let method_name = request - .as_ref() - .and_then(|request| request.get("method")) - .and_then(serde_json::Value::as_str); - - let reason = InvalidRequestReason::new(method_name, &message); - - // HACK: We need to log failed deserialization attempts when they concern input - // validation. - if let Some((method_name, provider_error)) = reason.provider_error() { - // Ignore potential failure of logging, as returning the original error is more - // important - let _result = self.log_failed_deserialization(method_name, &provider_error); - } - - let response = jsonrpc::ResponseData::<()>::Error { - error: jsonrpc::Error { - code: reason.error_code(), - message: reason.error_message(), - data: request, - }, - }; - - return serde_json::to_string(&response) - .map_err(|error| { - napi::Error::new( - Status::Unknown, - format!("Failed to serialize response due to: {error}"), - ) - }) - .map(Response::from); - } - }; - - let response = edr_provider::Provider::handle_request(self, request); - - ChainSpecT::cast_response(response) - } - - fn set_call_override_callback(&self, call_override_callback: CallOverrideCallback) { - let call_override_callback = - Arc::new(move |address, data| call_override_callback.call_override(address, data)); - - self.set_call_override_callback(Some(call_override_callback)); - } - - fn set_verbose_tracing(&self, enabled: bool) { - self.set_verbose_tracing(enabled); - } -} diff --git a/crates/edr_napi/src/provider/factory.rs b/crates/edr_napi/src/provider/factory.rs index 52e2082ac..6a07c0991 100644 --- a/crates/edr_napi/src/provider/factory.rs +++ b/crates/edr_napi/src/provider/factory.rs @@ -1,9 +1,8 @@ use std::sync::Arc; +use edr_napi_core::provider::SyncProviderFactory; use napi_derive::napi; -use crate::{logger::LoggerConfig, provider, subscription::SubscriptionConfig}; - #[napi] pub struct ProviderFactory { inner: Arc, @@ -21,15 +20,3 @@ impl From> for ProviderFactory { Self { inner } } } - -/// Trait for creating a new provider using the builder pattern. -pub trait SyncProviderFactory: Send + Sync { - /// Creates a `ProviderBuilder` that. - fn create_provider_builder( - &self, - env: &napi::Env, - provider_config: edr_napi_core::provider::Config, - logger_config: LoggerConfig, - subscription_config: SubscriptionConfig, - ) -> napi::Result>; -} diff --git a/crates/edr_napi/src/provider/response.rs b/crates/edr_napi/src/provider/response.rs new file mode 100644 index 000000000..058f97b44 --- /dev/null +++ b/crates/edr_napi/src/provider/response.rs @@ -0,0 +1,44 @@ +use edr_generic::GenericChainSpec; +use napi::Either; +use napi_derive::napi; + +use crate::trace::RawTrace; + +#[napi] +pub struct Response { + inner: edr_napi_core::spec::Response, +} + +impl From> for Response { + fn from(value: edr_napi_core::spec::Response) -> Self { + Self { inner: value } + } +} + +#[napi] +impl Response { + #[doc = "Returns the response data as a JSON string or a JSON object."] + #[napi(getter)] + pub fn data(&self) -> Either { + self.inner.data.clone() + } + + #[doc = "Returns the Solidity trace of the transaction that failed to execute, if any."] + #[napi(getter)] + pub fn solidity_trace(&self) -> Option { + self.inner + .solidity_trace + .as_ref() + .map(|trace| RawTrace::from(trace.clone())) + } + + #[doc = "Returns the raw traces of executed contracts. This maybe contain zero or more traces."] + #[napi(getter)] + pub fn traces(&self) -> Vec { + self.inner + .traces + .iter() + .map(|trace| RawTrace::from(trace.clone())) + .collect() + } +} diff --git a/crates/edr_napi/src/subscription.rs b/crates/edr_napi/src/subscription.rs index 01e3930a4..eb6dacca0 100644 --- a/crates/edr_napi/src/subscription.rs +++ b/crates/edr_napi/src/subscription.rs @@ -1,63 +1,6 @@ -use derive_where::derive_where; -use edr_eth::B256; -use edr_provider::{time::CurrentTime, ProviderSpec}; -use napi::{ - bindgen_prelude::BigInt, - threadsafe_function::{ - ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, - }, - Env, JsFunction, -}; +use napi::{bindgen_prelude::BigInt, JsFunction}; use napi_derive::napi; -#[derive_where(Clone)] -pub struct SubscriptionCallback> { - inner: ThreadsafeFunction, ErrorStrategy::Fatal>, -} - -impl> SubscriptionCallback { - pub fn new(env: &Env, subscription_event_callback: JsFunction) -> napi::Result { - let mut callback = subscription_event_callback.create_threadsafe_function( - 0, - |ctx: ThreadSafeCallContext>| { - // SubscriptionEvent - let mut event = ctx.env.create_object()?; - - ctx.env - .create_bigint_from_words(false, ctx.value.filter_id.as_limbs().to_vec()) - .and_then(|filter_id| event.set_named_property("filterId", filter_id))?; - - let result = match ctx.value.result { - edr_provider::SubscriptionEventData::Logs(logs) => ctx.env.to_js_value(&logs), - edr_provider::SubscriptionEventData::NewHeads(block) => { - let block = ChainSpecT::RpcBlock::::from(block); - ctx.env.to_js_value(&block) - } - edr_provider::SubscriptionEventData::NewPendingTransactions(tx_hash) => { - ctx.env.to_js_value(&tx_hash) - } - }?; - - event.set_named_property("result", result)?; - - Ok(vec![event]) - }, - )?; - - // Maintain a weak reference to the function to avoid the event loop from - // exiting. - callback.unref(env)?; - - Ok(Self { inner: callback }) - } - - pub fn call(&self, event: edr_provider::SubscriptionEvent) { - // This is blocking because it's important that the subscription events are - // in-order - self.inner.call(event, ThreadsafeFunctionCallMode::Blocking); - } -} - /// Configuration for subscriptions. #[napi(object)] pub struct SubscriptionConfig { @@ -66,6 +9,22 @@ pub struct SubscriptionConfig { pub subscription_callback: JsFunction, } +impl From for SubscriptionConfig { + fn from(config: edr_napi_core::subscription::Config) -> Self { + Self { + subscription_callback: config.subscription_callback, + } + } +} + +impl From for edr_napi_core::subscription::Config { + fn from(config: SubscriptionConfig) -> Self { + Self { + subscription_callback: config.subscription_callback, + } + } +} + #[napi(object)] pub struct SubscriptionEvent { pub filter_id: BigInt, diff --git a/crates/edr_napi/src/trace.rs b/crates/edr_napi/src/trace.rs index dfd4333de..35fe3e452 100644 --- a/crates/edr_napi/src/trace.rs +++ b/crates/edr_napi/src/trace.rs @@ -156,11 +156,9 @@ pub struct RawTrace { inner: Arc>, } -impl From> for RawTrace { - fn from(value: edr_evm::trace::Trace) -> Self { - Self { - inner: Arc::new(value), - } +impl From>> for RawTrace { + fn from(value: Arc>) -> Self { + Self { inner: value } } } diff --git a/crates/edr_napi/test/helpers.ts b/crates/edr_napi/test/helpers.ts index ba2b1aa81..7053450af 100644 --- a/crates/edr_napi/test/helpers.ts +++ b/crates/edr_napi/test/helpers.ts @@ -1,3 +1,4 @@ +import { toBytes } from "@nomicfoundation/ethereumjs-util"; import { TracingMessage, TracingMessageResult, TracingStep } from ".."; function getEnv(key: string): string | undefined { @@ -36,3 +37,7 @@ export function collectMessages( (traceItem) => "isStaticCall" in traceItem, ) as TracingMessage[]; } + +export function toBuffer(x: Parameters[0]) { + return Buffer.from(toBytes(x)); +} diff --git a/crates/edr_napi/test/issues.ts b/crates/edr_napi/test/issues.ts index bfc30f5c8..785918319 100644 --- a/crates/edr_napi/test/issues.ts +++ b/crates/edr_napi/test/issues.ts @@ -78,7 +78,7 @@ describe("Provider", () => { } // This test is slow because the debug_traceTransaction is performed on a large transaction. - this.timeout(240_000); + this.timeout(1_800_000); const provider = await context.createProvider( GENERIC_CHAIN_TYPE, diff --git a/crates/edr_napi/test/optimism.ts b/crates/edr_napi/test/optimism.ts new file mode 100644 index 000000000..ab3243ce0 --- /dev/null +++ b/crates/edr_napi/test/optimism.ts @@ -0,0 +1,159 @@ +import chai, { assert } from "chai"; +import chaiAsPromised from "chai-as-promised"; + +import { + ContractAndFunctionName, + EdrContext, + L1_CHAIN_TYPE, + l1ProviderFactory, + MineOrdering, + SubscriptionEvent, + // HACK: There is no way to exclude tsc type checking for a file from the + // CLI, so we ignore the error here to allow `pnpm testNoBuild` to pass. + // @ts-ignore + OPTIMISM_CHAIN_TYPE, + // @ts-ignore + optimismProviderFactory, +} from ".."; +import { ALCHEMY_URL, toBuffer } from "./helpers"; + +chai.use(chaiAsPromised); + +describe("Multi-chain", () => { + const context = new EdrContext(); + + before(async () => { + await context.registerProviderFactory(L1_CHAIN_TYPE, l1ProviderFactory()); + await context.registerProviderFactory( + OPTIMISM_CHAIN_TYPE, + optimismProviderFactory(), + ); + }); + + const providerConfig = { + allowBlocksWithSameTimestamp: false, + allowUnlimitedContractSize: true, + bailOnCallFailure: false, + bailOnTransactionFailure: false, + blockGasLimit: 300_000_000n, + chainId: 123n, + chains: [], + coinbase: Buffer.from("0000000000000000000000000000000000000000", "hex"), + enableRip7212: false, + genesisAccounts: [ + { + secretKey: + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + balance: 1000n * 10n ** 18n, + }, + ], + hardfork: "Latest", + initialBlobGas: { + gasUsed: 0n, + excessGas: 0n, + }, + initialParentBeaconBlockRoot: Buffer.from( + "0000000000000000000000000000000000000000000000000000000000000000", + "hex", + ), + minGasPrice: 0n, + mining: { + autoMine: true, + memPool: { + order: MineOrdering.Priority, + }, + }, + networkId: 123n, + }; + + const loggerConfig = { + enable: false, + decodeConsoleLogInputsCallback: (inputs: Buffer[]): string[] => { + return []; + }, + getContractAndFunctionNameCallback: ( + _code: Buffer, + _calldata?: Buffer, + ): ContractAndFunctionName => { + return { + contractName: "", + }; + }, + printLineCallback: (message: string, replace: boolean) => {}, + }; + + it("initialize L1 provider", async function () { + const provider = context.createProvider( + L1_CHAIN_TYPE, + providerConfig, + loggerConfig, + { + subscriptionCallback: (_event: SubscriptionEvent) => {}, + }, + ); + + await assert.isFulfilled(provider); + }); + + it("initialize Optimism provider", async function () { + const provider = context.createProvider( + OPTIMISM_CHAIN_TYPE, + providerConfig, + loggerConfig, + { + subscriptionCallback: (_event: SubscriptionEvent) => {}, + }, + ); + + await assert.isFulfilled(provider); + }); + + it("initialize remote Optimism provider", async function () { + if (ALCHEMY_URL === undefined) { + this.skip(); + } + + const provider = context.createProvider( + OPTIMISM_CHAIN_TYPE, + { + fork: { + jsonRpcUrl: ALCHEMY_URL.replace("eth-", "opt-"), + }, + ...providerConfig, + }, + loggerConfig, + { + subscriptionCallback: (_event: SubscriptionEvent) => {}, + }, + ); + + await assert.isFulfilled(provider); + }); + + describe("Optimism", () => { + it("eth_getBlockByNumber", async function () { + // Block with Optimism-specific transaction type + const BLOCK_NUMBER = 117_156_000; + + const provider = await context.createProvider( + OPTIMISM_CHAIN_TYPE, + providerConfig, + loggerConfig, + { + subscriptionCallback: (_event: SubscriptionEvent) => {}, + }, + ); + + const block = provider.handleRequest( + JSON.stringify({ + id: 1, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: [toBuffer(BLOCK_NUMBER), false], + }), + ); + + await assert.isFulfilled(block); + }); + }); +}); diff --git a/crates/edr_napi_core/Cargo.toml b/crates/edr_napi_core/Cargo.toml index 9f14feba9..577559e86 100644 --- a/crates/edr_napi_core/Cargo.toml +++ b/crates/edr_napi_core/Cargo.toml @@ -4,10 +4,19 @@ version = "0.3.5" edition = "2021" [dependencies] +ansi_term = { version = "0.12.1", default-features = false } +derive-where = { version = "1.2.7", default-features = false } edr_defaults = { path = "../edr_defaults" } edr_eth = { path = "../edr_eth" } +edr_evm = { path = "../edr_evm" } +edr_generic = { path = "../edr_generic" } edr_provider = { path = "../edr_provider" } +edr_rpc_client = { path = "../edr_rpc_client" } +itertools = { version = "0.12.0", default-features = false } +napi = { version = "2.16.0", default-features = false, features = ["async", "error_anyhow", "napi8", "serde-json"] } serde = { version = "1.0.209", features = ["derive"] } +serde_json = { version = "1.0.127" } +thiserror = { version = "1.0.37", default-features = false } [features] tracing = ["edr_provider/tracing"] diff --git a/crates/edr_napi_core/src/lib.rs b/crates/edr_napi_core/src/lib.rs index 8336397f8..d0c2b7d7a 100644 --- a/crates/edr_napi_core/src/lib.rs +++ b/crates/edr_napi_core/src/lib.rs @@ -1 +1,7 @@ +pub mod logger; pub mod provider; +pub mod spec; +pub mod subscription; + +// Re-export the napi crate. +pub use napi; diff --git a/crates/edr_napi_core/src/logger.rs b/crates/edr_napi_core/src/logger.rs new file mode 100644 index 000000000..7978b2ce4 --- /dev/null +++ b/crates/edr_napi_core/src/logger.rs @@ -0,0 +1,1193 @@ +use core::fmt::Debug; +use std::{fmt::Display, marker::PhantomData, sync::Arc}; + +use ansi_term::{Color, Style}; +use derive_where::derive_where; +use edr_eth::{result::ExecutionResult, transaction::ExecutableTransaction, Bytes, B256, U256}; +use edr_evm::{ + blockchain::BlockchainError, + precompile::{self, Precompiles}, + trace::{AfterMessage, Trace, TraceMessage}, + transaction::Transaction as _, + SyncBlock, +}; +use edr_provider::{ + time::CurrentTime, CallResult, DebugMineBlockResult, EstimateGasFailure, ProviderError, + ProviderSpec, TransactionFailure, +}; +use itertools::izip; + +/// Trait for a function that decodes console log inputs. +pub trait DecodeConsoleLogInputsFn: Fn(Vec) -> Vec + Send + Sync {} + +impl DecodeConsoleLogInputsFn for FnT where FnT: Fn(Vec) -> Vec + Send + Sync {} + +/// Trait for a function that retrieves the contract and function name. +pub trait GetContractAndFunctionNameFn: + Fn(Bytes, Option) -> (String, Option) + Send + Sync +{ +} + +impl GetContractAndFunctionNameFn for FnT where + FnT: Fn(Bytes, Option) -> (String, Option) + Send + Sync +{ +} + +/// Trait for a function that prints a line or replaces the last printed line. +pub trait PrintLineFn: Fn(String, bool) -> Result<(), LoggerError> + Send + Sync {} + +impl PrintLineFn for FnT where FnT: Fn(String, bool) -> Result<(), LoggerError> + Send + Sync {} + +#[derive(Clone)] +pub struct Config { + /// Whether to enable the logger. + pub enable: bool, + pub decode_console_log_inputs_fn: Arc, + pub get_contract_and_function_name_fn: Arc, + pub print_line_fn: Arc, +} + +#[derive(Clone)] +pub enum LoggingState { + CollapsingMethod(CollapsedMethod), + HardhatMinining { + empty_blocks_range_start: Option, + }, + IntervalMining { + empty_blocks_range_start: Option, + }, + Empty, +} + +impl LoggingState { + /// Converts the state into a hardhat mining state. + pub fn into_hardhat_mining(self) -> Option { + match self { + Self::HardhatMinining { + empty_blocks_range_start, + } => empty_blocks_range_start, + _ => None, + } + } + + /// Converts the state into an interval mining state. + pub fn into_interval_mining(self) -> Option { + match self { + Self::IntervalMining { + empty_blocks_range_start, + } => empty_blocks_range_start, + _ => None, + } + } +} + +impl Default for LoggingState { + fn default() -> Self { + Self::Empty + } +} + +#[derive(Clone)] +enum LogLine { + Single(String), + WithTitle(String, String), +} + +#[derive(Debug, thiserror::Error)] +pub enum LoggerError { + #[error("Failed to print line")] + PrintLine, +} + +#[derive_where(Clone)] +pub struct Logger> { + collector: LogCollector, +} + +impl> Logger { + pub fn new(config: Config) -> napi::Result { + Ok(Self { + collector: LogCollector::new(config)?, + }) + } +} + +impl edr_provider::Logger for Logger +where + ChainSpecT: ProviderSpec, +{ + type BlockchainError = BlockchainError; + + fn is_enabled(&self) -> bool { + self.collector.config.enable + } + + fn set_is_enabled(&mut self, is_enabled: bool) { + self.collector.config.enable = is_enabled; + } + + fn log_call( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + result: &CallResult, + ) -> Result<(), Box> { + self.collector.log_call(hardfork, transaction, result)?; + + Ok(()) + } + + fn log_estimate_gas_failure( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + failure: &EstimateGasFailure, + ) -> Result<(), Box> { + self.collector + .log_estimate_gas(hardfork, transaction, failure)?; + + Ok(()) + } + + fn log_interval_mined( + &mut self, + hardfork: ChainSpecT::Hardfork, + mining_result: &DebugMineBlockResult, + ) -> Result<(), Box> { + self.collector + .log_interval_mined(hardfork, mining_result) + .map_err(Box::new)?; + + Ok(()) + } + + fn log_mined_block( + &mut self, + hardfork: ChainSpecT::Hardfork, + mining_results: &[DebugMineBlockResult], + ) -> Result<(), Box> { + self.collector.log_mined_blocks(hardfork, mining_results)?; + + Ok(()) + } + + fn log_send_transaction( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + mining_results: &[DebugMineBlockResult], + ) -> Result<(), Box> { + self.collector + .log_send_transaction(hardfork, transaction, mining_results)?; + + Ok(()) + } + + fn print_method_logs( + &mut self, + method: &str, + error: Option<&ProviderError>, + ) -> Result<(), Box> { + if let Some(error) = error { + self.collector.state = LoggingState::Empty; + + if matches!(error, ProviderError::UnsupportedMethod { .. }) { + self.collector + .print::(Color::Red.paint(error.to_string()))?; + } else { + self.collector.print::(Color::Red.paint(method))?; + self.collector.print_logs()?; + + if !matches!(error, ProviderError::TransactionFailed(_)) { + self.collector.print_empty_line()?; + + let error_message = error.to_string(); + self.collector + .try_indented(|logger| logger.print::(&error_message))?; + + if matches!(error, ProviderError::InvalidEip155TransactionChainId) { + self.collector.try_indented(|logger| { + logger.print::(Color::Yellow.paint( + "If you are using MetaMask, you can learn how to fix this error here: https://hardhat.org/metamask-issue" + )) + })?; + } + } + + self.collector.print_empty_line()?; + } + } else { + self.collector.print_method(method)?; + + let printed = self.collector.print_logs()?; + if printed { + self.collector.print_empty_line()?; + } + } + + Ok(()) + } +} + +#[derive(Clone)] +pub struct CollapsedMethod { + count: usize, + method: String, +} + +#[derive_where(Clone)] +struct LogCollector> { + config: Config, + indentation: usize, + logs: Vec, + state: LoggingState, + title_length: usize, + phantom: PhantomData, +} + +impl> LogCollector { + pub fn new(config: Config) -> napi::Result { + Ok(Self { + config, + indentation: 0, + logs: Vec::new(), + state: LoggingState::default(), + title_length: 0, + phantom: PhantomData, + }) + } + + pub fn log_call( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + result: &CallResult, + ) -> Result<(), LoggerError> { + let CallResult { + console_log_inputs, + execution_result, + trace, + } = result; + + self.state = LoggingState::Empty; + + self.indented(|logger| { + logger.log_contract_and_function_name::(hardfork, trace); + + logger.log_with_title("From", format!("0x{:x}", transaction.caller())); + if let Some(to) = transaction.kind().to() { + logger.log_with_title("To", format!("0x{to:x}")); + } + if *transaction.value() > U256::ZERO { + logger.log_with_title("Value", wei_to_human_readable(transaction.value())); + } + + logger.log_console_log_messages(console_log_inputs)?; + + if let Some(transaction_failure) = + TransactionFailure::::from_execution_result::( + execution_result, + None, + trace, + ) + { + logger.log_transaction_failure(&transaction_failure); + } + + Ok(()) + }) + } + + pub fn log_estimate_gas( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + result: &EstimateGasFailure, + ) -> Result<(), LoggerError> { + let EstimateGasFailure { + console_log_inputs, + transaction_failure, + } = result; + + self.state = LoggingState::Empty; + + self.indented(|logger| { + logger.log_contract_and_function_name::( + hardfork, + &transaction_failure.failure.solidity_trace, + ); + + logger.log_with_title("From", format!("0x{:x}", transaction.caller())); + if let Some(to) = transaction.kind().to() { + logger.log_with_title("To", format!("0x{to:x}")); + } + logger.log_with_title("Value", wei_to_human_readable(transaction.value())); + + logger.log_console_log_messages(console_log_inputs)?; + + logger.log_transaction_failure(&transaction_failure.failure); + + Ok(()) + }) + } + + fn log_transaction_failure(&mut self, failure: &edr_provider::TransactionFailure) { + let is_revert_error = matches!( + failure.reason, + edr_provider::TransactionFailureReason::Revert(_) + ); + + let error_type = if is_revert_error { + "Error" + } else { + "TransactionExecutionError" + }; + + self.log_empty_line(); + self.log(format!("{error_type}: {failure}")); + } + + pub fn log_mined_blocks( + &mut self, + hardfork: ChainSpecT::Hardfork, + mining_results: &[DebugMineBlockResult>], + ) -> Result<(), LoggerError> { + let num_results = mining_results.len(); + for (idx, mining_result) in mining_results.iter().enumerate() { + let state = std::mem::take(&mut self.state); + let empty_blocks_range_start = state.into_hardhat_mining(); + + if mining_result.block.transactions().is_empty() { + self.log_hardhat_mined_empty_block(&mining_result.block, empty_blocks_range_start)?; + + let block_number = mining_result.block.header().number; + self.state = LoggingState::HardhatMinining { + empty_blocks_range_start: Some( + empty_blocks_range_start.unwrap_or(block_number), + ), + }; + } else { + self.log_hardhat_mined_block(hardfork, mining_result)?; + + if idx < num_results - 1 { + self.log_empty_line(); + } + } + } + + Ok(()) + } + + pub fn log_interval_mined( + &mut self, + hardfork: ChainSpecT::Hardfork, + mining_result: &DebugMineBlockResult>, + ) -> Result<(), LoggerError> { + let block_header = mining_result.block.header(); + let block_number = block_header.number; + + if mining_result.block.transactions().is_empty() { + let state = std::mem::take(&mut self.state); + let empty_blocks_range_start = state.into_interval_mining(); + + if let Some(empty_blocks_range_start) = empty_blocks_range_start { + self.print::(format!( + "Mined empty block range #{empty_blocks_range_start} to #{block_number}" + ))?; + } else { + let base_fee = if let Some(base_fee) = block_header.base_fee_per_gas.as_ref() { + format!(" with base fee {base_fee}") + } else { + String::new() + }; + + self.print::(format!("Mined empty block #{block_number}{base_fee}"))?; + } + + self.state = LoggingState::IntervalMining { + empty_blocks_range_start: Some( + empty_blocks_range_start.unwrap_or(block_header.number), + ), + }; + } else { + self.log_interval_mined_block(hardfork, mining_result)?; + + self.print::(format!("Mined block #{block_number}"))?; + + let printed = self.print_logs()?; + if printed { + self.print_empty_line()?; + } + } + + Ok(()) + } + + pub fn log_send_transaction( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + mining_results: &[DebugMineBlockResult>], + ) -> Result<(), LoggerError> { + if !mining_results.is_empty() { + self.state = LoggingState::Empty; + + let (sent_block_result, sent_transaction_result, sent_trace) = mining_results + .iter() + .find_map(|result| { + izip!( + result.block.transactions(), + result.transaction_results.iter(), + result.transaction_traces.iter() + ) + .find(|(block_transaction, _, _)| { + *block_transaction.transaction_hash() == *transaction.transaction_hash() + }) + .map(|(_, transaction_result, trace)| (result, transaction_result, trace)) + }) + .expect("Transaction result not found"); + + if mining_results.len() > 1 { + self.log_multiple_blocks_warning()?; + self.log_auto_mined_block_results( + hardfork, + mining_results, + transaction.transaction_hash(), + )?; + self.log_currently_sent_transaction( + hardfork, + sent_block_result, + transaction, + sent_transaction_result, + sent_trace, + )?; + } else if let Some(result) = mining_results.first() { + let transactions = result.block.transactions(); + if transactions.len() > 1 { + self.log_multiple_transactions_warning()?; + self.log_auto_mined_block_results( + hardfork, + mining_results, + transaction.transaction_hash(), + )?; + self.log_currently_sent_transaction( + hardfork, + sent_block_result, + transaction, + sent_transaction_result, + sent_trace, + )?; + } else if let Some(transaction) = transactions.first() { + self.log_single_transaction_mining_result(hardfork, result, transaction)?; + } + } + } + + Ok(()) + } + + fn format(&self, message: impl ToString) -> String { + let message = message.to_string(); + + if message.is_empty() { + message + } else { + message + .split('\n') + .map(|line| format!("{:indent$}{line}", "", indent = self.indentation)) + .collect::>() + .join("\n") + } + } + + fn indented( + &mut self, + display_fn: impl FnOnce(&mut Self) -> Result<(), LoggerError>, + ) -> Result<(), LoggerError> { + self.indentation += 2; + let result = display_fn(self); + self.indentation -= 2; + + // We need to return the result of the inner function after resetting the + // indentation + result + } + + fn try_indented( + &mut self, + display_fn: impl FnOnce(&mut Self) -> Result<(), LoggerError>, + ) -> Result<(), LoggerError> { + self.indentation += 2; + let result = display_fn(self); + self.indentation -= 2; + + result + } + + fn log(&mut self, message: impl ToString) { + let formatted = self.format(message); + + self.logs.push(LogLine::Single(formatted)); + } + + fn log_auto_mined_block_results( + &mut self, + hardfork: ChainSpecT::Hardfork, + results: &[DebugMineBlockResult>], + sent_transaction_hash: &B256, + ) -> Result<(), LoggerError> { + for result in results { + self.log_block_from_auto_mine(hardfork, result, sent_transaction_hash)?; + } + + Ok(()) + } + + fn log_base_fee(&mut self, base_fee: Option<&U256>) { + if let Some(base_fee) = base_fee { + self.log(format!("Base fee: {base_fee}")); + } + } + + fn log_block_from_auto_mine( + &mut self, + hardfork: ChainSpecT::Hardfork, + result: &DebugMineBlockResult>, + transaction_hash_to_highlight: &edr_eth::B256, + ) -> Result<(), LoggerError> { + let DebugMineBlockResult { + block, + transaction_results, + transaction_traces, + console_log_inputs, + } = result; + + let transactions = block.transactions(); + let num_transactions = transactions.len(); + + debug_assert_eq!(num_transactions, transaction_results.len()); + debug_assert_eq!(num_transactions, transaction_traces.len()); + + let block_header = block.header(); + + self.indented(|logger| { + logger.log_block_id(block); + + logger.indented(|logger| { + logger.log_base_fee(block_header.base_fee_per_gas.as_ref()); + + for (idx, transaction, result, trace) in izip!( + 0..num_transactions, + transactions, + transaction_results, + transaction_traces + ) { + let should_highlight_hash = + *transaction.transaction_hash() == *transaction_hash_to_highlight; + logger.log_block_transaction( + hardfork, + transaction, + result, + trace, + console_log_inputs, + should_highlight_hash, + )?; + + logger.log_empty_line_between_transactions(idx, num_transactions); + } + + Ok(()) + })?; + + Ok(()) + })?; + + self.log_empty_line(); + + Ok(()) + } + + fn log_block_hash( + &mut self, + block: &dyn SyncBlock>, + ) { + let block_hash = block.hash(); + + self.log(format!("Block: {block_hash}")); + } + + fn log_block_id( + &mut self, + block: &dyn SyncBlock>, + ) { + let block_number = block.header().number; + let block_hash = block.hash(); + + self.log(format!("Block #{block_number}: {block_hash}")); + } + + fn log_block_number( + &mut self, + block: &dyn SyncBlock>, + ) { + let block_number = block.header().number; + + self.log(format!("Mined block #{block_number}")); + } + + /// Logs a transaction that's part of a block. + fn log_block_transaction( + &mut self, + hardfork: ChainSpecT::Hardfork, + transaction: &ChainSpecT::Transaction, + result: &ExecutionResult, + trace: &Trace, + console_log_inputs: &[Bytes], + should_highlight_hash: bool, + ) -> Result<(), LoggerError> { + let transaction_hash = transaction.transaction_hash(); + if should_highlight_hash { + self.log_with_title( + "Transaction", + Style::new().bold().paint(transaction_hash.to_string()), + ); + } else { + self.log_with_title("Transaction", transaction_hash.to_string()); + } + + self.indented(|logger| { + logger.log_contract_and_function_name::(hardfork, trace); + logger.log_with_title("From", format!("0x{:x}", transaction.caller())); + if let Some(to) = transaction.kind().to() { + logger.log_with_title("To", format!("0x{to:x}")); + } + logger.log_with_title("Value", wei_to_human_readable(transaction.value())); + logger.log_with_title( + "Gas used", + format!( + "{gas_used} of {gas_limit}", + gas_used = result.gas_used(), + gas_limit = transaction.gas_limit() + ), + ); + + logger.log_console_log_messages(console_log_inputs)?; + + let transaction_failure = + edr_provider::TransactionFailure::::from_execution_result::< + ChainSpecT, + CurrentTime, + >(result, Some(transaction_hash), trace); + + if let Some(transaction_failure) = transaction_failure { + logger.log_transaction_failure(&transaction_failure); + } + + Ok(()) + })?; + + Ok(()) + } + + fn log_console_log_messages( + &mut self, + console_log_inputs: &[Bytes], + ) -> Result<(), LoggerError> { + let console_log_inputs = + (self.config.decode_console_log_inputs_fn)(console_log_inputs.to_vec()); + + // This is a special case, as we always want to print the console.log messages. + // The difference is how. If we have a logger, we should use that, so that logs + // are printed in order. If we don't, we just print the messages here. + if self.config.enable { + if !console_log_inputs.is_empty() { + self.log_empty_line(); + self.log("console.log:"); + + self.indented(|logger| { + for input in console_log_inputs { + logger.log(input); + } + + Ok(()) + })?; + } + } else { + for input in console_log_inputs { + (self.config.print_line_fn)(input, false)?; + } + } + + Ok(()) + } + + fn log_contract_and_function_name( + &mut self, + hardfork: ChainSpecT::Hardfork, + trace: &Trace, + ) { + if let Some(TraceMessage::Before(before_message)) = trace.messages.first() { + if let Some(to) = before_message.to { + // Call + let is_precompile = { + let precompiles = Precompiles::new(precompile::PrecompileSpecId::from_spec_id( + hardfork.into(), + )); + precompiles.contains(&to) + }; + + if is_precompile { + let precompile = u16::from_be_bytes([to[18], to[19]]); + self.log_with_title( + "Precompile call", + format!(""), + ); + } else { + let is_code_empty = before_message + .code + .as_ref() + .map_or(true, edr_eth::Bytecode::is_empty); + + if is_code_empty { + if PRINT_INVALID_CONTRACT_WARNING { + self.log("WARNING: Calling an account which is not a contract"); + } + } else { + let (contract_name, function_name) = + (self.config.get_contract_and_function_name_fn)( + before_message + .code + .as_ref() + .map(edr_eth::Bytecode::original_bytes) + .expect("Call must be defined"), + Some(before_message.data.clone()), + ); + + let function_name = function_name.expect("Function name must be defined"); + self.log_with_title( + "Contract call", + if function_name.is_empty() { + contract_name + } else { + format!("{contract_name}#{function_name}") + }, + ); + } + } + } else { + let result = if let Some(TraceMessage::After(AfterMessage { + execution_result, + .. + })) = trace.messages.last() + { + execution_result + } else { + unreachable!("Before messages must have an after message") + }; + + // Create + let (contract_name, _) = (self.config.get_contract_and_function_name_fn)( + before_message.data.clone(), + None, + ); + + self.log_with_title("Contract deployment", contract_name); + + if let ExecutionResult::Success { output, .. } = result { + if let edr_eth::result::Output::Create(_, address) = output { + if let Some(deployed_address) = address { + self.log_with_title( + "Contract address", + format!("0x{deployed_address:x}"), + ); + } + } else { + unreachable!("Create calls must return a Create output") + } + } + } + } + } + + fn log_empty_block( + &mut self, + block: &dyn SyncBlock>, + ) { + let block_header = block.header(); + let block_number = block_header.number; + + let base_fee = if let Some(base_fee) = block_header.base_fee_per_gas.as_ref() { + format!(" with base fee {base_fee}") + } else { + String::new() + }; + + self.log(format!("Mined empty block #{block_number}{base_fee}",)); + } + + fn log_empty_line(&mut self) { + self.log(""); + } + + fn log_empty_line_between_transactions(&mut self, idx: usize, num_transactions: usize) { + if num_transactions > 1 && idx < num_transactions - 1 { + self.log_empty_line(); + } + } + + fn log_hardhat_mined_empty_block( + &mut self, + block: &dyn SyncBlock>, + empty_blocks_range_start: Option, + ) -> Result<(), LoggerError> { + self.indented(|logger| { + if let Some(empty_blocks_range_start) = empty_blocks_range_start { + logger.replace_last_log_line(format!( + "Mined empty block range #{empty_blocks_range_start} to #{block_number}", + block_number = block.header().number + )); + } else { + logger.log_empty_block(block); + } + + Ok(()) + })?; + + Ok(()) + } + + /// Logs the result of interval mining a block. + fn log_interval_mined_block( + &mut self, + hardfork: ChainSpecT::Hardfork, + result: &DebugMineBlockResult>, + ) -> Result<(), LoggerError> { + let DebugMineBlockResult { + block, + transaction_results, + transaction_traces, + console_log_inputs, + } = result; + + let transactions = block.transactions(); + let num_transactions = transactions.len(); + + debug_assert_eq!(num_transactions, transaction_results.len()); + debug_assert_eq!(num_transactions, transaction_traces.len()); + + let block_header = block.header(); + + self.indented(|logger| { + logger.log_block_hash(block); + + logger.indented(|logger| { + logger.log_base_fee(block_header.base_fee_per_gas.as_ref()); + + for (idx, transaction, result, trace) in izip!( + 0..num_transactions, + transactions, + transaction_results, + transaction_traces + ) { + logger.log_block_transaction( + hardfork, + transaction, + result, + trace, + console_log_inputs, + false, + )?; + + logger.log_empty_line_between_transactions(idx, num_transactions); + } + + Ok(()) + }) + }) + } + + fn log_hardhat_mined_block( + &mut self, + hardfork: ChainSpecT::Hardfork, + result: &DebugMineBlockResult>, + ) -> Result<(), LoggerError> { + let DebugMineBlockResult { + block, + transaction_results, + transaction_traces, + console_log_inputs, + } = result; + + let transactions = block.transactions(); + let num_transactions = transactions.len(); + + debug_assert_eq!(num_transactions, transaction_results.len()); + debug_assert_eq!(num_transactions, transaction_traces.len()); + + self.indented(|logger| { + if transactions.is_empty() { + logger.log_empty_block(block); + } else { + logger.log_block_number(block); + + logger.indented(|logger| { + logger.log_block_hash(block); + + logger.indented(|logger| { + logger.log_base_fee(block.header().base_fee_per_gas.as_ref()); + + for (idx, transaction, result, trace) in izip!( + 0..num_transactions, + transactions, + transaction_results, + transaction_traces + ) { + logger.log_block_transaction( + hardfork, + transaction, + result, + trace, + console_log_inputs, + false, + )?; + + logger.log_empty_line_between_transactions(idx, num_transactions); + } + + Ok(()) + }) + })?; + } + + Ok(()) + }) + } + + /// Logs a warning about multiple blocks being mined. + fn log_multiple_blocks_warning(&mut self) -> Result<(), LoggerError> { + self.indented(|logger| { + logger + .log("There were other pending transactions. More than one block had to be mined:"); + + Ok(()) + })?; + self.log_empty_line(); + + Ok(()) + } + + /// Logs a warning about multiple transactions being mined. + fn log_multiple_transactions_warning(&mut self) -> Result<(), LoggerError> { + self.indented(|logger| { + logger.log("There were other pending transactions mined in the same block:"); + + Ok(()) + })?; + self.log_empty_line(); + + Ok(()) + } + + fn log_with_title(&mut self, title: impl Into, message: impl Display) { + // repeat whitespace self.indentation times and concatenate with title + let title = format!("{:indent$}{}", "", title.into(), indent = self.indentation); + if title.len() > self.title_length { + self.title_length = title.len(); + } + + let message = format!("{message}"); + self.logs.push(LogLine::WithTitle(title, message)); + } + + fn log_currently_sent_transaction( + &mut self, + hardfork: ChainSpecT::Hardfork, + block_result: &DebugMineBlockResult>, + transaction: &ChainSpecT::Transaction, + transaction_result: &ExecutionResult, + trace: &Trace, + ) -> Result<(), LoggerError> { + self.indented(|logger| { + logger.log("Currently sent transaction:"); + logger.log(""); + + Ok(()) + })?; + + self.log_transaction( + hardfork, + block_result, + transaction, + transaction_result, + trace, + )?; + + Ok(()) + } + + fn log_single_transaction_mining_result( + &mut self, + hardfork: ChainSpecT::Hardfork, + result: &DebugMineBlockResult>, + transaction: &ChainSpecT::Transaction, + ) -> Result<(), LoggerError> { + let trace = result + .transaction_traces + .first() + .expect("A transaction exists, so the trace must exist as well."); + + let transaction_result = result + .transaction_results + .first() + .expect("A transaction exists, so the result must exist as well."); + + self.log_transaction(hardfork, result, transaction, transaction_result, trace)?; + + Ok(()) + } + + fn log_transaction( + &mut self, + hardfork: ChainSpecT::Hardfork, + block_result: &DebugMineBlockResult>, + transaction: &ChainSpecT::Transaction, + transaction_result: &ExecutionResult, + trace: &Trace, + ) -> Result<(), LoggerError> { + self.indented(|logger| { + logger.log_contract_and_function_name::(hardfork, trace); + + let transaction_hash = transaction.transaction_hash(); + logger.log_with_title("Transaction", transaction_hash); + + logger.log_with_title("From", format!("0x{:x}", transaction.caller())); + if let Some(to) = transaction.kind().to() { + logger.log_with_title("To", format!("0x{to:x}")); + } + logger.log_with_title("Value", wei_to_human_readable(transaction.value())); + logger.log_with_title( + "Gas used", + format!( + "{gas_used} of {gas_limit}", + gas_used = transaction_result.gas_used(), + gas_limit = transaction.gas_limit() + ), + ); + + let block_number = block_result.block.header().number; + logger.log_with_title(format!("Block #{block_number}"), block_result.block.hash()); + + logger.log_console_log_messages(&block_result.console_log_inputs)?; + + let transaction_failure = + edr_provider::TransactionFailure::::from_execution_result::< + ChainSpecT, + CurrentTime, + >(transaction_result, Some(transaction_hash), trace); + + if let Some(transaction_failure) = transaction_failure { + logger.log_transaction_failure(&transaction_failure); + } + + Ok(()) + }) + } + + fn print(&mut self, message: impl ToString) -> Result<(), LoggerError> { + if !self.config.enable { + return Ok(()); + } + + let formatted = self.format(message); + (self.config.print_line_fn)(formatted, REPLACE) + } + + fn print_empty_line(&mut self) -> Result<(), LoggerError> { + self.print::("") + } + + fn print_logs(&mut self) -> Result { + let logs = std::mem::take(&mut self.logs); + if logs.is_empty() { + return Ok(false); + } + + for log in logs { + let line = match log { + LogLine::Single(message) => message, + LogLine::WithTitle(title, message) => { + let title = format!("{title}:"); + format!("{title:indent$} {message}", indent = self.title_length + 1) + } + }; + + self.print::(line)?; + } + + Ok(true) + } + + fn print_method(&mut self, method: &str) -> Result<(), LoggerError> { + if let Some(collapsed_method) = self.collapsed_method(method) { + collapsed_method.count += 1; + + let line = format!("{method} ({count})", count = collapsed_method.count); + self.print::(Color::Green.paint(line)) + } else { + self.state = LoggingState::CollapsingMethod(CollapsedMethod { + count: 1, + method: method.to_string(), + }); + + self.print::(Color::Green.paint(method)) + } + } + + /// Retrieves the collapsed method with the provided name, if it exists. + fn collapsed_method(&mut self, method: &str) -> Option<&mut CollapsedMethod> { + if let LoggingState::CollapsingMethod(collapsed_method) = &mut self.state { + if collapsed_method.method == method { + return Some(collapsed_method); + } + } + + None + } + + fn replace_last_log_line(&mut self, message: impl ToString) { + let formatted = self.format(message); + + *self.logs.last_mut().expect("There must be a log line") = LogLine::Single(formatted); + } +} + +fn wei_to_human_readable(wei: &U256) -> String { + if *wei == U256::ZERO { + "0 ETH".to_string() + } else if *wei < U256::from(100_000u64) { + format!("{wei} wei") + } else if *wei < U256::from(100_000_000_000_000u64) { + let mut decimal = to_decimal_string(wei, 9); + decimal.push_str(" gwei"); + decimal + } else { + let mut decimal = to_decimal_string(wei, 18); + decimal.push_str(" ETH"); + decimal + } +} + +/// Converts the provided `value` to a decimal string after dividing it by +/// `10^exponent`. The returned string will have at most `MAX_DECIMALS` +/// decimals. +fn to_decimal_string(value: &U256, exponent: u8) -> String { + const MAX_DECIMALS: u8 = 4; + + let (integer, remainder) = value.div_rem(U256::from(10).pow(U256::from(exponent))); + let decimal = remainder / U256::from(10).pow(U256::from(exponent - MAX_DECIMALS)); + + // Remove trailing zeros + let decimal = decimal.to_string().trim_end_matches('0').to_string(); + + format!("{integer}.{decimal}") +} diff --git a/crates/edr_napi_core/src/provider.rs b/crates/edr_napi_core/src/provider.rs index 3b332ff0d..495bf2a4a 100644 --- a/crates/edr_napi_core/src/provider.rs +++ b/crates/edr_napi_core/src/provider.rs @@ -1,110 +1,88 @@ -use core::num::NonZeroU64; -use std::{path::PathBuf, time::SystemTime}; - -use edr_eth::{block::BlobGas, AccountInfo, Address, ChainId, HashMap, B256, U256}; -use edr_provider::{ - hardfork::{Activations, ForkCondition}, - hardhat_rpc_types::ForkConfig, - spec::ChainSpec, - AccountConfig, MiningConfig, +mod builder; +mod config; +mod factory; + +use std::{str::FromStr as _, sync::Arc}; + +use edr_generic::GenericChainSpec; +use edr_provider::{InvalidRequestReason, SyncCallOverride}; +use edr_rpc_client::jsonrpc; + +pub use self::{ + builder::{Builder, ProviderBuilder}, + config::{Config, HardforkActivation}, + factory::SyncProviderFactory, }; -use serde::{Deserialize, Serialize}; +use crate::spec::{Response, SyncNapiSpec}; -/// Chain-agnostic configuration for a hardfork activation. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct HardforkActivation { - pub block_number: u64, - pub hardfork: String, -} +/// Trait for a synchronous N-API provider that can be used for dynamic trait +/// objects. +pub trait SyncProvider: Send + Sync { + /// Blocking method to handle a request. + fn handle_request(&self, request: String) -> napi::Result>; -/// Chain-agnostic configuration for a provider. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Config { - pub allow_blocks_with_same_timestamp: bool, - pub allow_unlimited_contract_size: bool, - pub accounts: Vec, - /// Whether to return an `Err` when `eth_call` fails - pub bail_on_call_failure: bool, - /// Whether to return an `Err` when a `eth_sendTransaction` fails - pub bail_on_transaction_failure: bool, - pub block_gas_limit: NonZeroU64, - pub cache_dir: Option, - pub chain_id: ChainId, - pub chains: HashMap>, - pub coinbase: Address, - #[serde(default)] - pub enable_rip_7212: bool, - pub fork: Option, - // Genesis accounts in addition to accounts. Useful for adding impersonated accounts for tests. - pub genesis_accounts: HashMap, - pub hardfork: String, - pub initial_base_fee_per_gas: Option, - pub initial_blob_gas: Option, - pub initial_date: Option, - pub initial_parent_beacon_block_root: Option, - pub min_gas_price: U256, - pub mining: MiningConfig, - pub network_id: u64, + /// Set to `true` to make the traces returned with `eth_call`, + /// `eth_estimateGas`, `eth_sendRawTransaction`, `eth_sendTransaction`, + /// `evm_mine`, `hardhat_mine` include the full stack and memory. Set to + /// `false` to disable this. + fn set_call_override_callback(&self, call_override_callback: Arc); + + /// Set the verbose tracing flag to the provided value. + fn set_verbose_tracing(&self, enabled: bool); } -impl From for edr_provider::ProviderConfig -where - ChainSpecT: ChainSpec From<&'s str>>, -{ - fn from(value: Config) -> Self { - let cache_dir = PathBuf::from( - value - .cache_dir - .unwrap_or(String::from(edr_defaults::CACHE_DIR)), - ); - - let chains = value - .chains - .into_iter() - .map(|(chain_id, activations)| { - let activations = activations - .into_iter() - .map( - |HardforkActivation { - block_number, - hardfork, - }| { - let condition = ForkCondition::Block(block_number); - let hardfork = ChainSpecT::Hardfork::from(&hardfork); - - (condition, hardfork) - }, - ) - .collect(); - - (chain_id, Activations::new(activations)) - }) - .collect(); - - let hardfork = ChainSpecT::Hardfork::from(&value.hardfork); - - Self { - allow_blocks_with_same_timestamp: value.allow_blocks_with_same_timestamp, - allow_unlimited_contract_size: value.allow_unlimited_contract_size, - accounts: value.accounts, - bail_on_call_failure: value.bail_on_call_failure, - bail_on_transaction_failure: value.bail_on_transaction_failure, - block_gas_limit: value.block_gas_limit, - cache_dir, - chain_id: value.chain_id, - chains, - coinbase: value.coinbase, - enable_rip_7212: value.enable_rip_7212, - fork: value.fork, - genesis_accounts: value.genesis_accounts, - hardfork, - initial_base_fee_per_gas: value.initial_base_fee_per_gas, - initial_blob_gas: value.initial_blob_gas, - initial_date: value.initial_date, - initial_parent_beacon_block_root: value.initial_parent_beacon_block_root, - min_gas_price: value.min_gas_price, - mining: value.mining, - network_id: value.network_id, - } +impl SyncProvider for edr_provider::Provider { + fn handle_request(&self, request: String) -> napi::Result> { + let request = match serde_json::from_str(&request) { + Ok(request) => request, + Err(error) => { + let message = error.to_string(); + + let request = serde_json::Value::from_str(&request).ok(); + let method_name = request + .as_ref() + .and_then(|request| request.get("method")) + .and_then(serde_json::Value::as_str); + + let reason = InvalidRequestReason::new(method_name, &message); + + // HACK: We need to log failed deserialization attempts when they concern input + // validation. + if let Some((method_name, provider_error)) = reason.provider_error() { + // Ignore potential failure of logging, as returning the original error is more + // important + let _result = self.log_failed_deserialization(method_name, &provider_error); + } + + let response = jsonrpc::ResponseData::<()>::Error { + error: jsonrpc::Error { + code: reason.error_code(), + message: reason.error_message(), + data: request, + }, + }; + + return serde_json::to_string(&response) + .map_err(|error| { + napi::Error::new( + napi::Status::Unknown, + format!("Failed to serialize response due to: {error}"), + ) + }) + .map(Response::from); + } + }; + + let response = edr_provider::Provider::handle_request(self, request); + + ChainSpecT::cast_response(response) + } + + fn set_call_override_callback(&self, call_override_callback: Arc) { + self.set_call_override_callback(Some(call_override_callback)); + } + + fn set_verbose_tracing(&self, enabled: bool) { + self.set_verbose_tracing(enabled); } } diff --git a/crates/edr_napi/src/provider/builder.rs b/crates/edr_napi_core/src/provider/builder.rs similarity index 71% rename from crates/edr_napi/src/provider/builder.rs rename to crates/edr_napi_core/src/provider/builder.rs index 5183b2840..86a53b178 100644 --- a/crates/edr_napi/src/provider/builder.rs +++ b/crates/edr_napi_core/src/provider/builder.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use edr_provider::time::CurrentTime; +use edr_evm::blockchain::BlockchainError; +use edr_provider::{time::CurrentTime, SyncLogger}; use napi::tokio::runtime; -use crate::{ - logger::Logger, provider::SyncProvider, spec::SyncNapiSpec, subscription::SubscriptionCallback, -}; +use crate::{provider::SyncProvider, spec::SyncNapiSpec, subscription}; /// A builder for creating a new provider. pub trait Builder: Send { @@ -14,17 +13,17 @@ pub trait Builder: Send { } pub struct ProviderBuilder { - logger: Logger, + logger: Box>>, provider_config: edr_provider::ProviderConfig, - subscription_callback: SubscriptionCallback, + subscription_callback: subscription::Callback, } impl ProviderBuilder { /// Constructs a new instance. pub fn new( - logger: Logger, + logger: Box>>, provider_config: edr_provider::ProviderConfig, - subscription_callback: SubscriptionCallback, + subscription_callback: subscription::Callback, ) -> Self { Self { logger, @@ -40,7 +39,7 @@ impl Builder for ProviderBuilder { let provider = edr_provider::Provider::::new( runtime.clone(), - Box::new(builder.logger), + builder.logger, Box::new(move |event| builder.subscription_callback.call(event)), builder.provider_config, CurrentTime, diff --git a/crates/edr_napi_core/src/provider/config.rs b/crates/edr_napi_core/src/provider/config.rs new file mode 100644 index 000000000..3b332ff0d --- /dev/null +++ b/crates/edr_napi_core/src/provider/config.rs @@ -0,0 +1,110 @@ +use core::num::NonZeroU64; +use std::{path::PathBuf, time::SystemTime}; + +use edr_eth::{block::BlobGas, AccountInfo, Address, ChainId, HashMap, B256, U256}; +use edr_provider::{ + hardfork::{Activations, ForkCondition}, + hardhat_rpc_types::ForkConfig, + spec::ChainSpec, + AccountConfig, MiningConfig, +}; +use serde::{Deserialize, Serialize}; + +/// Chain-agnostic configuration for a hardfork activation. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HardforkActivation { + pub block_number: u64, + pub hardfork: String, +} + +/// Chain-agnostic configuration for a provider. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Config { + pub allow_blocks_with_same_timestamp: bool, + pub allow_unlimited_contract_size: bool, + pub accounts: Vec, + /// Whether to return an `Err` when `eth_call` fails + pub bail_on_call_failure: bool, + /// Whether to return an `Err` when a `eth_sendTransaction` fails + pub bail_on_transaction_failure: bool, + pub block_gas_limit: NonZeroU64, + pub cache_dir: Option, + pub chain_id: ChainId, + pub chains: HashMap>, + pub coinbase: Address, + #[serde(default)] + pub enable_rip_7212: bool, + pub fork: Option, + // Genesis accounts in addition to accounts. Useful for adding impersonated accounts for tests. + pub genesis_accounts: HashMap, + pub hardfork: String, + pub initial_base_fee_per_gas: Option, + pub initial_blob_gas: Option, + pub initial_date: Option, + pub initial_parent_beacon_block_root: Option, + pub min_gas_price: U256, + pub mining: MiningConfig, + pub network_id: u64, +} + +impl From for edr_provider::ProviderConfig +where + ChainSpecT: ChainSpec From<&'s str>>, +{ + fn from(value: Config) -> Self { + let cache_dir = PathBuf::from( + value + .cache_dir + .unwrap_or(String::from(edr_defaults::CACHE_DIR)), + ); + + let chains = value + .chains + .into_iter() + .map(|(chain_id, activations)| { + let activations = activations + .into_iter() + .map( + |HardforkActivation { + block_number, + hardfork, + }| { + let condition = ForkCondition::Block(block_number); + let hardfork = ChainSpecT::Hardfork::from(&hardfork); + + (condition, hardfork) + }, + ) + .collect(); + + (chain_id, Activations::new(activations)) + }) + .collect(); + + let hardfork = ChainSpecT::Hardfork::from(&value.hardfork); + + Self { + allow_blocks_with_same_timestamp: value.allow_blocks_with_same_timestamp, + allow_unlimited_contract_size: value.allow_unlimited_contract_size, + accounts: value.accounts, + bail_on_call_failure: value.bail_on_call_failure, + bail_on_transaction_failure: value.bail_on_transaction_failure, + block_gas_limit: value.block_gas_limit, + cache_dir, + chain_id: value.chain_id, + chains, + coinbase: value.coinbase, + enable_rip_7212: value.enable_rip_7212, + fork: value.fork, + genesis_accounts: value.genesis_accounts, + hardfork, + initial_base_fee_per_gas: value.initial_base_fee_per_gas, + initial_blob_gas: value.initial_blob_gas, + initial_date: value.initial_date, + initial_parent_beacon_block_root: value.initial_parent_beacon_block_root, + min_gas_price: value.min_gas_price, + mining: value.mining, + network_id: value.network_id, + } + } +} diff --git a/crates/edr_napi_core/src/provider/factory.rs b/crates/edr_napi_core/src/provider/factory.rs new file mode 100644 index 000000000..1d843bf82 --- /dev/null +++ b/crates/edr_napi_core/src/provider/factory.rs @@ -0,0 +1,13 @@ +use crate::{logger, provider, subscription}; + +/// Trait for creating a new provider using the builder pattern. +pub trait SyncProviderFactory: Send + Sync { + /// Creates a `ProviderBuilder` that. + fn create_provider_builder( + &self, + env: &napi::Env, + provider_config: provider::Config, + logger_config: logger::Config, + subscription_config: subscription::Config, + ) -> napi::Result>; +} diff --git a/crates/edr_napi/src/spec.rs b/crates/edr_napi_core/src/spec.rs similarity index 53% rename from crates/edr_napi/src/spec.rs rename to crates/edr_napi_core/src/spec.rs index f537470e5..1b1eb8cfb 100644 --- a/crates/edr_napi/src/spec.rs +++ b/crates/edr_napi_core/src/spec.rs @@ -1,55 +1,35 @@ +use std::sync::Arc; + use edr_eth::{ chain_spec::L1ChainSpec, result::InvalidTransaction, transaction::{IsEip155, IsEip4844, TransactionMut, TransactionType, TransactionValidation}, }; +use edr_evm::trace::Trace; use edr_generic::GenericChainSpec; use edr_provider::{time::CurrentTime, ProviderError, ResponseWithTraces, SyncProviderSpec}; use edr_rpc_client::jsonrpc; use napi::{Either, Status}; -use napi_derive::napi; -use crate::trace::RawTrace; +pub type ResponseData = Either; -#[napi] -pub struct Response { +pub struct Response { // N-API is known to be slow when marshalling `serde_json::Value`s, so we try to return a // `String`. If the object is too large to be represented as a `String`, we return a `Buffer` // instead. - data: Either, + pub data: ResponseData, /// When a transaction fails to execute, the provider returns a trace of the /// transaction. /// /// Only present for L1 Ethereum chains. - solidity_trace: Option, + pub solidity_trace: Option>>, /// This may contain zero or more traces, depending on the (batch) request /// /// Always empty for non-L1 Ethereum chains. - traces: Vec, + pub traces: Vec>>, } -#[napi] -impl Response { - #[doc = "Returns the response data as a JSON string or a JSON object."] - #[napi(getter)] - pub fn data(&self) -> Either { - self.data.clone() - } - - #[doc = "Returns the Solidity trace of the transaction that failed to execute, if any."] - #[napi(getter)] - pub fn solidity_trace(&self) -> Option { - self.solidity_trace.clone() - } - - #[doc = "Returns the raw traces of executed contracts. This maybe contain zero or more traces."] - #[napi(getter)] - pub fn traces(&self) -> Vec { - self.traces.clone() - } -} - -impl From for Response { +impl From for Response { fn from(value: String) -> Self { Response { solidity_trace: None, @@ -80,7 +60,7 @@ pub trait SyncNapiSpec: /// implementing type conversions for third-party types. fn cast_response( response: Result, ProviderError>, - ) -> napi::Result; + ) -> napi::Result>; } impl SyncNapiSpec for L1ChainSpec { @@ -88,31 +68,14 @@ impl SyncNapiSpec for L1ChainSpec { fn cast_response( response: Result, ProviderError>, - ) -> napi::Result { + ) -> napi::Result> { let response = jsonrpc::ResponseData::from(response.map(|response| response.result)); - serde_json::to_string(&response) - .and_then(|json| { - // We experimentally determined that 500_000_000 was the maximum string length - // that can be returned without causing the error: - // - // > Failed to convert rust `String` into napi `string` - // - // To be safe, we're limiting string lengths to half of that. - const MAX_STRING_LENGTH: usize = 250_000_000; - - if json.len() <= MAX_STRING_LENGTH { - Ok(Either::A(json)) - } else { - serde_json::to_value(response).map(Either::B) - } - }) - .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) - .map(|data| Response { - solidity_trace: None, - data, - traces: Vec::new(), - }) + marshal_response_data(response).map(|data| Response { + solidity_trace: None, + data, + traces: Vec::new(), + }) } } @@ -121,7 +84,7 @@ impl SyncNapiSpec for GenericChainSpec { fn cast_response( mut response: Result, ProviderError>, - ) -> napi::Result { + ) -> napi::Result> { // We can take the solidity trace as it won't be used for anything else let solidity_trace = response.as_mut().err().and_then(|error| { if let edr_provider::ProviderError::TransactionFailed(failure) = error { @@ -131,7 +94,7 @@ impl SyncNapiSpec for GenericChainSpec { ) { None } else { - Some(RawTrace::from(std::mem::take( + Some(Arc::new(std::mem::take( &mut failure.failure.solidity_trace, ))) } @@ -151,27 +114,34 @@ impl SyncNapiSpec for GenericChainSpec { let response = jsonrpc::ResponseData::from(response.map(|response| response.result)); - serde_json::to_string(&response) - .and_then(|json| { - // We experimentally determined that 500_000_000 was the maximum string length - // that can be returned without causing the error: - // - // > Failed to convert rust `String` into napi `string` - // - // To be safe, we're limiting string lengths to half of that. - const MAX_STRING_LENGTH: usize = 250_000_000; - - if json.len() <= MAX_STRING_LENGTH { - Ok(Either::A(json)) - } else { - serde_json::to_value(response).map(Either::B) - } - }) - .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) - .map(|data| Response { - solidity_trace, - data, - traces: traces.into_iter().map(RawTrace::from).collect(), - }) + marshal_response_data(response).map(|data| Response { + solidity_trace, + data, + traces: traces.into_iter().map(Arc::new).collect(), + }) } } + +/// Marshals a JSON-RPC response data into a `ResponseData`, taking into account +/// large responses. +pub fn marshal_response_data( + response: jsonrpc::ResponseData, +) -> napi::Result { + serde_json::to_string(&response) + .and_then(|json| { + // We experimentally determined that 500_000_000 was the maximum string length + // that can be returned without causing the error: + // + // > Failed to convert rust `String` into napi `string` + // + // To be safe, we're limiting string lengths to half of that. + const MAX_STRING_LENGTH: usize = 250_000_000; + + if json.len() <= MAX_STRING_LENGTH { + Ok(Either::A(json)) + } else { + serde_json::to_value(response).map(Either::B) + } + }) + .map_err(|error| napi::Error::new(Status::GenericFailure, error.to_string())) +} diff --git a/crates/edr_napi_core/src/subscription.rs b/crates/edr_napi_core/src/subscription.rs new file mode 100644 index 000000000..a695ec6f7 --- /dev/null +++ b/crates/edr_napi_core/src/subscription.rs @@ -0,0 +1,63 @@ +use derive_where::derive_where; +use edr_eth::B256; +use edr_provider::{time::CurrentTime, ProviderSpec, SubscriptionEvent}; +use napi::{ + threadsafe_function::{ + ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }, + JsFunction, +}; + +#[derive_where(Clone)] +pub struct Callback> { + inner: ThreadsafeFunction, ErrorStrategy::Fatal>, +} + +impl> Callback { + pub fn new(env: &napi::Env, subscription_event_callback: JsFunction) -> napi::Result { + let mut callback = subscription_event_callback.create_threadsafe_function( + 0, + |ctx: ThreadSafeCallContext>| { + // SubscriptionEvent + let mut event = ctx.env.create_object()?; + + ctx.env + .create_bigint_from_words(false, ctx.value.filter_id.as_limbs().to_vec()) + .and_then(|filter_id| event.set_named_property("filterId", filter_id))?; + + let result = match ctx.value.result { + edr_provider::SubscriptionEventData::Logs(logs) => ctx.env.to_js_value(&logs), + edr_provider::SubscriptionEventData::NewHeads(block) => { + let block = ChainSpecT::RpcBlock::::from(block); + ctx.env.to_js_value(&block) + } + edr_provider::SubscriptionEventData::NewPendingTransactions(tx_hash) => { + ctx.env.to_js_value(&tx_hash) + } + }?; + + event.set_named_property("result", result)?; + + Ok(vec![event]) + }, + )?; + + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + callback.unref(env)?; + + Ok(Self { inner: callback }) + } + + pub fn call(&self, event: SubscriptionEvent) { + // This is blocking because it's important that the subscription events are + // in-order + self.inner.call(event, ThreadsafeFunctionCallMode::Blocking); + } +} + +/// Configuration for subscriptions. +pub struct Config { + /// Callback to be called when a new event is received. + pub subscription_callback: JsFunction, +} diff --git a/crates/edr_optimism/Cargo.toml b/crates/edr_optimism/Cargo.toml index afee8ae21..8462e34ec 100644 --- a/crates/edr_optimism/Cargo.toml +++ b/crates/edr_optimism/Cargo.toml @@ -9,6 +9,8 @@ alloy-rlp = { version = "0.3", default-features = false, features = ["derive"] } alloy-serde = { version = "0.2.0", default-features = false, features = ["std"] } edr_eth = { path = "../edr_eth", features = ["serde", "std"] } edr_evm = { path = "../edr_evm" } +edr_generic = { path = "../edr_generic" } +edr_napi_core = { path = "../edr_napi_core" } edr_provider = { path = "../edr_provider" } edr_rpc_eth = { path = "../edr_rpc_eth" } log = { version = "0.4.17", default-features = false } diff --git a/crates/edr_optimism/src/spec.rs b/crates/edr_optimism/src/spec.rs index 35d3d48d7..6c36283ad 100644 --- a/crates/edr_optimism/src/spec.rs +++ b/crates/edr_optimism/src/spec.rs @@ -12,8 +12,13 @@ use edr_evm::{ transaction::{TransactionError, TransactionValidation}, RemoteBlockConversionError, }; +use edr_generic::GenericChainSpec; +use edr_napi_core::{ + napi, + spec::{marshal_response_data, Response, SyncNapiSpec}, +}; use edr_provider::{time::TimeSinceEpoch, ProviderSpec, TransactionFailureReason}; -use edr_rpc_eth::spec::RpcSpec; +use edr_rpc_eth::{jsonrpc, spec::RpcSpec}; use revm::{ handler::register::HandleRegisters, optimism::{OptimismHaltReason, OptimismInvalidTransaction, OptimismSpecId}, @@ -143,6 +148,22 @@ impl EthHeaderConstants for OptimismChainSpec { const MIN_ETHASH_DIFFICULTY: u64 = 0; } +impl SyncNapiSpec for OptimismChainSpec { + const CHAIN_TYPE: &'static str = "Optimism"; + + fn cast_response( + response: Result, edr_provider::ProviderError>, + ) -> napi::Result> { + let response = jsonrpc::ResponseData::from(response.map(|response| response.result)); + + marshal_response_data(response).map(|data| Response { + solidity_trace: None, + data, + traces: Vec::new(), + }) + } +} + impl ProviderSpec for OptimismChainSpec { type PooledTransaction = transaction::Pooled; type TransactionRequest = transaction::Request; diff --git a/crates/edr_optimism/src/transaction/pooled.rs b/crates/edr_optimism/src/transaction/pooled.rs index dfb6c8612..23eacc0c2 100644 --- a/crates/edr_optimism/src/transaction/pooled.rs +++ b/crates/edr_optimism/src/transaction/pooled.rs @@ -2,7 +2,7 @@ pub use edr_eth::transaction::pooled::{Eip155, Eip1559, Eip2930, Eip4844, Legacy use edr_eth::{ env::AuthorizationList, transaction::{ - signed::PreOrPostEip155, ExecutableTransaction, Transaction, TxKind, + signed::PreOrPostEip155, ExecutableTransaction, IsEip155, Transaction, TxKind, INVALID_TX_TYPE_ERROR_MESSAGE, }, utils::enveloped, @@ -211,6 +211,12 @@ impl HardforkValidationData for Pooled { } } +impl IsEip155 for Pooled { + fn is_eip155(&self) -> bool { + matches!(self, Pooled::PostEip155Legacy(_)) + } +} + impl Transaction for Pooled { fn caller(&self) -> &Address { match self { diff --git a/crates/edr_provider/src/lib.rs b/crates/edr_provider/src/lib.rs index 623095220..e75850bd7 100644 --- a/crates/edr_provider/src/lib.rs +++ b/crates/edr_provider/src/lib.rs @@ -35,8 +35,8 @@ pub use self::{ data::{CallResult, ProviderData}, debug_mine::DebugMineBlockResult, error::{EstimateGasFailure, ProviderError, TransactionFailure, TransactionFailureReason}, - logger::{Logger, NoopLogger}, - mock::CallOverrideResult, + logger::{Logger, NoopLogger, SyncLogger}, + mock::{CallOverrideResult, SyncCallOverride}, provider::Provider, requests::{ hardhat::rpc_types as hardhat_rpc_types, IntervalConfig as IntervalConfigRequest, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a2c73c99..dc89eabe8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@napi-rs/cli': specifier: ^2.18.1 version: 2.18.1 + '@nomicfoundation/ethereumjs-util': + specifier: ^9.0.4 + version: 9.0.4 '@types/chai': specifier: ^4.2.0 version: 4.3.12 @@ -222,10 +225,10 @@ importers: devDependencies: '@defi-wonderland/smock': specifier: ^2.4.0 - version: 2.4.0(patch_hash=zemhiof4b3pqw5bzdzeiyb46dm)(@ethersproject/abi@5.7.0)(@ethersproject/abstract-provider@5.7.0)(@ethersproject/abstract-signer@5.7.0)(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4)))(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4)) + version: 2.4.0(patch_hash=zemhiof4b3pqw5bzdzeiyb46dm)(@ethersproject/abi@5.7.0)(@ethersproject/abstract-provider@5.7.0)(@ethersproject/abstract-signer@5.7.0)(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4)))(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4)) '@nomiclabs/hardhat-ethers': specifier: ^2.2.3 - version: 2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4)) + version: 2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4)) chai: specifier: ^4.3.6 version: 4.4.1 @@ -3098,13 +3101,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@defi-wonderland/smock@2.4.0(patch_hash=zemhiof4b3pqw5bzdzeiyb46dm)(@ethersproject/abi@5.7.0)(@ethersproject/abstract-provider@5.7.0)(@ethersproject/abstract-signer@5.7.0)(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4)))(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4))': + '@defi-wonderland/smock@2.4.0(patch_hash=zemhiof4b3pqw5bzdzeiyb46dm)(@ethersproject/abi@5.7.0)(@ethersproject/abstract-provider@5.7.0)(@ethersproject/abstract-signer@5.7.0)(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4)))(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4))': dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/abstract-provider': 5.7.0 '@ethersproject/abstract-signer': 5.7.0 '@nomicfoundation/ethereumjs-util': 9.0.4 - '@nomiclabs/hardhat-ethers': 2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4)) + '@nomiclabs/hardhat-ethers': 2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4)) diff: 5.0.0 ethers: 5.7.2 hardhat: 2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4) @@ -3618,7 +3621,7 @@ snapshots: '@nomicfoundation/solidity-analyzer-win32-ia32-msvc': 0.1.1 '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.1 - '@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(typescript@5.0.4))(typescript@5.0.4))': + '@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2)(hardhat@2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4))': dependencies: ethers: 5.7.2 hardhat: 2.22.9(patch_hash=vkqvlc66s2ufa4tdcctiaivtte)(ts-node@10.9.2(@types/node@18.15.13)(typescript@5.0.4))(typescript@5.0.4) diff --git a/crates/edr_napi/scripts/prepublish.sh b/scripts/prepublish.sh similarity index 100% rename from crates/edr_napi/scripts/prepublish.sh rename to scripts/prepublish.sh