diff --git a/.github/workflows/nextest.yml b/.github/workflows/nextest.yml index da862c657abf..6fc1a6703901 100644 --- a/.github/workflows/nextest.yml +++ b/.github/workflows/nextest.yml @@ -91,4 +91,6 @@ jobs: - name: Test env: SVM_TARGET_PLATFORM: ${{ matrix.svm_target_platform }} + HTTP_ARCHIVE_URLS: ${{ secrets.HTTP_ARCHIVE_URLS }} + WS_ARCHIVE_URLS: ${{ secrets.WS_ARCHIVE_URLS }} run: cargo nextest run ${{ matrix.flags }} diff --git a/Cargo.lock b/Cargo.lock index 81adf0f4b1e1..144c6e07800c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3998,6 +3998,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tracing", + "url", ] [[package]] diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index a35d7c267d62..821395417fa3 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -1040,6 +1040,7 @@ async fn can_impersonate_in_fork() { // #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn test_total_difficulty_fork() { let (api, handle) = spawn(fork_config()).await; diff --git a/crates/evm/core/Cargo.toml b/crates/evm/core/Cargo.toml index 46441201664e..c9f5131d7f34 100644 --- a/crates/evm/core/Cargo.toml +++ b/crates/evm/core/Cargo.toml @@ -59,6 +59,7 @@ serde_json.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["time", "macros"] } tracing.workspace = true +url.workspace = true [dev-dependencies] foundry-test-utils.workspace = true diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 17ecbcca170e..99cc8dabaf07 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -8,6 +8,7 @@ use foundry_common::{provider::ProviderBuilder, ALCHEMY_FREE_TIER_CUPS}; use foundry_config::{Chain, Config}; use revm::primitives::{BlockEnv, CfgEnv, TxEnv}; use serde::{Deserialize, Deserializer, Serialize}; +use url::Url; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct EvmOpts { @@ -102,7 +103,13 @@ impl EvmOpts { ) .await .wrap_err_with(|| { - format!("Could not instantiate forked environment with fork url: {fork_url}") + let mut err_msg = "Could not instantiate forked environment".to_string(); + if let Ok(url) = Url::parse(fork_url) { + if let Some(provider) = url.host() { + err_msg.push_str(&format!(" with provider {provider}")); + } + } + err_msg }) } diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index f51cf6703a17..8e064c63c9fd 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -575,7 +575,7 @@ Compiler run successful! Ran 1 test for test/Contract.t.sol:USDTCallingTest [PASS] test() ([GAS]) Traces: - [9406] USDTCallingTest::test() + [..] USDTCallingTest::test() ├─ [0] VM::createSelectFork("[..]") │ └─ ← [Return] 0 ├─ [3110] 0xdAC17F958D2ee523a2206206994597C13D831ec7::name() [staticcall] @@ -2639,3 +2639,29 @@ contract ScrollForkTest is Test { cmd.args(["test", "--mt", "test_roll_scroll_fork_to_tx", "--evm-version", "cancun"]) .assert_success(); }); + +// Test that only provider is included in failed fork error. +forgetest_init!(test_display_provider_on_error, |prj, cmd| { + prj.add_test( + "ForkTest.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract ForkTest is Test { + function test_fork_err_message() public { + vm.createSelectFork("https://eth-mainnet.g.alchemy.com/v2/DUMMY_KEY"); + } +} + "#, + ) + .unwrap(); + + cmd.args(["test", "--mt", "test_fork_err_message"]).assert_failure().stdout_eq(str![[r#" +... +Ran 1 test for test/ForkTest.t.sol:ForkTest +[FAIL: vm.createSelectFork: Could not instantiate forked environment with provider eth-mainnet.g.alchemy.com;] test_fork_err_message() ([GAS]) +Suite result: FAILED. 0 passed; 1 failed; 0 skipped; [ELAPSED] +... + +"#]]); +}); diff --git a/crates/test-utils/src/rpc.rs b/crates/test-utils/src/rpc.rs index 44deda60cddf..ba27f381cb10 100644 --- a/crates/test-utils/src/rpc.rs +++ b/crates/test-utils/src/rpc.rs @@ -2,11 +2,19 @@ use foundry_config::{NamedChain, NamedChain::Optimism}; use rand::seq::SliceRandom; -use std::sync::{ - atomic::{AtomicUsize, Ordering}, - LazyLock, +use std::{ + env, + sync::{ + atomic::{AtomicUsize, Ordering}, + LazyLock, + }, }; +/// Env var key for ws archive endpoints. +const ENV_WS_ARCHIVE_ENDPOINTS: &str = "WS_ARCHIVE_URLS"; +/// Env var key for http archive endpoints. +const ENV_HTTP_ARCHIVE_ENDPOINTS: &str = "HTTP_ARCHIVE_URLS"; + // List of general purpose infura keys to rotate through static INFURA_KEYS: LazyLock> = LazyLock::new(|| { let mut keys = vec![ @@ -118,14 +126,30 @@ pub fn next_ws_endpoint(chain: NamedChain) -> String { /// Returns endpoint that has access to archive state pub fn next_http_archive_rpc_endpoint() -> String { - let idx = next() % ALCHEMY_KEYS.len(); - format!("https://eth-mainnet.g.alchemy.com/v2/{}", ALCHEMY_KEYS[idx]) + next_archive_endpoint(false) } /// Returns endpoint that has access to archive state pub fn next_ws_archive_rpc_endpoint() -> String { - let idx = next() % ALCHEMY_KEYS.len(); - format!("wss://eth-mainnet.g.alchemy.com/v2/{}", ALCHEMY_KEYS[idx]) + next_archive_endpoint(true) +} + +/// Returns endpoint that has access to archive state, http or ws. +/// Use env vars (comma separated urls) or default inline keys (Alchemy for ws, Infura for http). +fn next_archive_endpoint(is_ws: bool) -> String { + let env_urls = if is_ws { ENV_WS_ARCHIVE_ENDPOINTS } else { ENV_HTTP_ARCHIVE_ENDPOINTS }; + + let rpc_env_vars = env::var(env_urls).unwrap_or_default(); + if !rpc_env_vars.is_empty() { + let urls = rpc_env_vars.split(',').collect::>(); + urls.choose(&mut rand::thread_rng()).unwrap().to_string() + } else if is_ws { + let idx = next() % ALCHEMY_KEYS.len(); + format!("wss://eth-mainnet.g.alchemy.com/v2/{}", ALCHEMY_KEYS[idx]) + } else { + let idx = next() % INFURA_KEYS.len(); + format!("https://mainnet.infura.io/v3/{}", INFURA_KEYS[idx]) + } } /// Returns the next etherscan api key