From f7b6660817cebc6de4533fac86ec8c182f3acc34 Mon Sep 17 00:00:00 2001 From: jimboj Date: Mon, 3 Nov 2025 16:18:03 -0700 Subject: [PATCH 1/4] add initial fork cli --- crates/anvil-polkadot/src/cmd.rs | 27 ++++++++++++++++++- crates/anvil-polkadot/src/config.rs | 41 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/src/cmd.rs b/crates/anvil-polkadot/src/cmd.rs index bab435dc64aaa..e415a7e8383be 100644 --- a/crates/anvil-polkadot/src/cmd.rs +++ b/crates/anvil-polkadot/src/cmd.rs @@ -8,6 +8,7 @@ use anvil_server::ServerConfig; use clap::Parser; use foundry_common::shell; use foundry_config::Chain; +use polkadot_sdk::sp_core::H256; use rand_08::{SeedableRng, rngs::StdRng}; use std::{net::IpAddr, path::PathBuf, time::Duration}; @@ -134,7 +135,11 @@ impl NodeArgs { .with_code_size_limit(self.evm.code_size_limit) .disable_code_size_limit(self.evm.disable_code_size_limit) .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer) - .with_memory_limit(self.evm.memory_limit); + .with_memory_limit(self.evm.memory_limit) + .with_fork_url(self.fork.fork_url) + .with_fork_block_hash(self.fork.fork_block_hash) + .with_fork_delay(self.fork.fork_delay) + .with_fork_retries(self.fork.fork_retries); let substrate_node_config = SubstrateNodeConfig::new(&anvil_config); @@ -245,6 +250,26 @@ pub struct AnvilEvmArgs { pub memory_limit: Option, } +#[derive(Clone, Debug, Parser)] +#[command(next_help_heading = "Fork options")] +pub struct ForkArgs { + /// Fetch state over a remote endpoint instead of starting from an empty state. + #[arg(long = "fork-url", short = 'f', value_name = "URL")] + pub fork_url: Option, + + /// Fetch state from a specific block hash over a remote endpoint. + #[arg(long, value_name = "BLOCK")] + pub fork_block_hash: Option, + + /// Delay between RPC requests in milliseconds to avoid rate limiting. + #[arg(long, default_value = "0", value_name = "MS")] + pub fork_delay: u32, + + /// Maximum number of retries per RPC request. + #[arg(long, default_value = "3", value_name = "NUM")] + pub fork_retries: u32, +} + /// Clap's value parser for genesis. Loads a genesis.json file. fn read_genesis_file(path: &str) -> Result { foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string()) diff --git a/crates/anvil-polkadot/src/config.rs b/crates/anvil-polkadot/src/config.rs index bc994c22978a0..366dc9e61962b 100644 --- a/crates/anvil-polkadot/src/config.rs +++ b/crates/anvil-polkadot/src/config.rs @@ -21,6 +21,7 @@ use polkadot_sdk::{ RPC_DEFAULT_MAX_SUBS_PER_CONN, RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN, }, sc_service, + sp_core::H256, }; use rand_08::thread_rng; use serde_json::{Value, json}; @@ -331,6 +332,14 @@ pub struct AnvilNodeConfig { pub memory_limit: Option, /// Do not print log messages. pub silent: bool, + /// Fetch state over a remote endpoint instead of starting from an empty state. + pub fork_url: Option, + /// Fetch state from a specific block hash over a remote endpoint. + pub fork_block_hash: Option, + /// Delay between RPC requests in milliseconds when forking. + pub fork_delay: u32, + /// Maximum number of retries per RPC request when forking. + pub fork_retries: u32, } impl AnvilNodeConfig { @@ -554,6 +563,10 @@ impl Default for AnvilNodeConfig { disable_default_create2_deployer: false, memory_limit: None, silent: false, + fork_url: None, + fork_block_hash: None, + fork_delay: 0, + fork_retries: 3, } } } @@ -857,6 +870,34 @@ impl AnvilNodeConfig { self.silent = silent; self } + + /// Sets the fork url + #[must_use] + pub fn with_fork_url(mut self, fork_url: Option) -> Self { + self.fork_url = fork_url; + self + } + + /// Sets the fork block + #[must_use] + pub fn with_fork_block_hash(mut self, fork_block_hash: Option) -> Self { + self.fork_block_hash = fork_block_hash; + self + } + + /// Sets the fork delay between RPC requests + #[must_use] + pub fn with_fork_delay(mut self, fork_delay: u32) -> Self { + self.fork_delay = fork_delay; + self + } + + /// Sets the fork max retries per RPC request + #[must_use] + pub fn with_fork_retries(mut self, fork_retries: u32) -> Self { + self.fork_retries = fork_retries; + self + } } /// Can create dev accounts From 38b7e689c845138b7e43e6e60b2f46b032130642 Mon Sep 17 00:00:00 2001 From: jimboj Date: Fri, 7 Nov 2025 16:08:06 -0300 Subject: [PATCH 2/4] format config --- crates/anvil-polkadot/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/src/config.rs b/crates/anvil-polkadot/src/config.rs index 366dc9e61962b..27802d70ee2a5 100644 --- a/crates/anvil-polkadot/src/config.rs +++ b/crates/anvil-polkadot/src/config.rs @@ -885,7 +885,7 @@ impl AnvilNodeConfig { self } - /// Sets the fork delay between RPC requests + /// Sets the fork delay between RPC requests #[must_use] pub fn with_fork_delay(mut self, fork_delay: u32) -> Self { self.fork_delay = fork_delay; From 6a6754e37bcd1f7d51729eda108edec68ad1ac3f Mon Sep 17 00:00:00 2001 From: jimboj Date: Mon, 10 Nov 2025 16:37:31 -0300 Subject: [PATCH 3/4] refactor to mirror anvil ethereum --- crates/anvil-polkadot/src/cmd.rs | 127 ++++++++++++++++++++++------ crates/anvil-polkadot/src/config.rs | 114 +++++++++++++++++++------ 2 files changed, 191 insertions(+), 50 deletions(-) diff --git a/crates/anvil-polkadot/src/cmd.rs b/crates/anvil-polkadot/src/cmd.rs index e415a7e8383be..b76ba0d54b5d1 100644 --- a/crates/anvil-polkadot/src/cmd.rs +++ b/crates/anvil-polkadot/src/cmd.rs @@ -1,16 +1,16 @@ use crate::config::{ - AccountGenerator, AnvilNodeConfig, CHAIN_ID, DEFAULT_MNEMONIC, SubstrateNodeConfig, + AccountGenerator, AnvilNodeConfig, CHAIN_ID, DEFAULT_MNEMONIC, ForkChoice, SubstrateNodeConfig, }; use alloy_genesis::Genesis; -use alloy_primitives::{U256, utils::Unit}; +use alloy_primitives::{B256, U256, utils::Unit}; use alloy_signer_local::coins_bip39::{English, Mnemonic}; use anvil_server::ServerConfig; use clap::Parser; +use core::fmt; use foundry_common::shell; use foundry_config::Chain; -use polkadot_sdk::sp_core::H256; use rand_08::{SeedableRng, rngs::StdRng}; -use std::{net::IpAddr, path::PathBuf, time::Duration}; +use std::{net::IpAddr, path::PathBuf, str::FromStr, time::Duration}; #[derive(Clone, Debug, Parser)] pub struct NodeArgs { @@ -136,10 +136,19 @@ impl NodeArgs { .disable_code_size_limit(self.evm.disable_code_size_limit) .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer) .with_memory_limit(self.evm.memory_limit) - .with_fork_url(self.fork.fork_url) - .with_fork_block_hash(self.fork.fork_block_hash) - .with_fork_delay(self.fork.fork_delay) - .with_fork_retries(self.fork.fork_retries); + .with_fork_choice(match (self.evm.fork_block_number, self.evm.fork_transaction_hash) { + (Some(block), None) => Some(ForkChoice::Block(block)), + (None, Some(hash)) => Some(ForkChoice::Transaction(hash)), + _ => self + .evm + .fork_url + .as_ref() + .and_then(|f| f.block) + .map(|num| ForkChoice::Block(num as i128)), + }) + .with_eth_rpc_url(self.evm.fork_url.map(|fork| fork.url)) + .fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis)) + .fork_request_retries(self.evm.fork_request_retries); let substrate_node_config = SubstrateNodeConfig::new(&anvil_config); @@ -175,6 +184,56 @@ impl NodeArgs { #[derive(Clone, Debug, Parser)] #[command(next_help_heading = "EVM options")] pub struct AnvilEvmArgs { + /// Fetch state over a remote endpoint instead of starting from an empty state. + /// + /// If you want to fetch state from a specific block number, add a block number like `http://localhost:8545@1400000` or use the `--fork-block-number` argument. + #[arg( + long, + short, + visible_alias = "rpc-url", + value_name = "URL", + help_heading = "Fork config" + )] + pub fork_url: Option, + + /// Fetch state from a specific block number over a remote endpoint. + /// + /// If negative, the given value is subtracted from the `latest` block number. + /// + /// See --fork-url. + #[arg( + long, + requires = "fork_url", + value_name = "BLOCK", + help_heading = "Fork config", + allow_hyphen_values = true + )] + pub fork_block_number: Option, + + /// Fetch state from a specific transaction hash over a remote endpoint. + /// + /// See --fork-url. + #[arg( + long, + requires = "fork_url", + value_name = "TRANSACTION", + help_heading = "Fork config", + conflicts_with = "fork_block_number" + )] + pub fork_transaction_hash: Option, + + /// Timeout in ms for requests sent to remote JSON-RPC server in forking mode. + /// + /// Default value 45000 + #[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")] + pub fork_request_timeout: Option, + + /// Number of retry requests for spurious networks (timed out requests) + /// + /// Default value 5 + #[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")] + pub fork_request_retries: Option, + /// The block gas limit. #[arg(long, alias = "block-gas-limit", help_heading = "Environment config")] pub gas_limit: Option, @@ -250,24 +309,44 @@ pub struct AnvilEvmArgs { pub memory_limit: Option, } -#[derive(Clone, Debug, Parser)] -#[command(next_help_heading = "Fork options")] -pub struct ForkArgs { - /// Fetch state over a remote endpoint instead of starting from an empty state. - #[arg(long = "fork-url", short = 'f', value_name = "URL")] - pub fork_url: Option, - - /// Fetch state from a specific block hash over a remote endpoint. - #[arg(long, value_name = "BLOCK")] - pub fork_block_hash: Option, +/// Represents the input URL for a fork with an optional trailing block number: +/// `http://localhost:8545@1000000` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ForkUrl { + /// The endpoint url + pub url: String, + /// Optional trailing block + pub block: Option, +} - /// Delay between RPC requests in milliseconds to avoid rate limiting. - #[arg(long, default_value = "0", value_name = "MS")] - pub fork_delay: u32, +impl fmt::Display for ForkUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.url.fmt(f)?; + if let Some(block) = self.block { + write!(f, "@{block}")?; + } + Ok(()) + } +} - /// Maximum number of retries per RPC request. - #[arg(long, default_value = "3", value_name = "NUM")] - pub fork_retries: u32, +impl FromStr for ForkUrl { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Some((url, block)) = s.rsplit_once('@') { + if block == "latest" { + return Ok(Self { url: url.to_string(), block: None }); + } + // this will prevent false positives for auths `user:password@example.com` + if !block.is_empty() && !block.contains(':') && !block.contains('.') { + let block: u64 = block + .parse() + .map_err(|_| format!("Failed to parse block number: `{block}`"))?; + return Ok(Self { url: url.to_string(), block: Some(block) }); + } + } + Ok(Self { url: s.to_string(), block: None }) + } } /// Clap's value parser for genesis. Loads a genesis.json file. diff --git a/crates/anvil-polkadot/src/config.rs b/crates/anvil-polkadot/src/config.rs index 27802d70ee2a5..9b917120eecba 100644 --- a/crates/anvil-polkadot/src/config.rs +++ b/crates/anvil-polkadot/src/config.rs @@ -3,7 +3,7 @@ use crate::{ substrate_node::chain_spec::keypairs_from_private_keys, }; use alloy_genesis::Genesis; -use alloy_primitives::{Address, U256, hex, map::HashMap, utils::Unit}; +use alloy_primitives::{Address, TxHash, U256, hex, map::HashMap, utils::Unit}; use alloy_signer::Signer; use alloy_signer_local::{ MnemonicBuilder, PrivateKeySigner, @@ -11,7 +11,7 @@ use alloy_signer_local::{ }; use anvil_server::ServerConfig; use eyre::{Context, Result}; -use foundry_common::{duration_since_unix_epoch, sh_println}; +use foundry_common::{REQUEST_TIMEOUT, duration_since_unix_epoch, sh_println}; use polkadot_sdk::{ pallet_revive::evm::Account, sc_cli::{ @@ -332,14 +332,14 @@ pub struct AnvilNodeConfig { pub memory_limit: Option, /// Do not print log messages. pub silent: bool, - /// Fetch state over a remote endpoint instead of starting from an empty state. - pub fork_url: Option, - /// Fetch state from a specific block hash over a remote endpoint. - pub fork_block_hash: Option, - /// Delay between RPC requests in milliseconds when forking. - pub fork_delay: u32, - /// Maximum number of retries per RPC request when forking. - pub fork_retries: u32, + /// url of the rpc server that should be used for any rpc calls + pub eth_rpc_url: Option, + /// pins the block number or transaction hash for the state fork + pub fork_choice: Option, + /// Timeout in for requests sent to remote JSON-RPC server in forking mode + pub fork_request_timeout: Duration, + /// Number of request retries for spurious networks + pub fork_request_retries: u32, } impl AnvilNodeConfig { @@ -563,10 +563,10 @@ impl Default for AnvilNodeConfig { disable_default_create2_deployer: false, memory_limit: None, silent: false, - fork_url: None, - fork_block_hash: None, - fork_delay: 0, - fork_retries: 3, + eth_rpc_url: None, + fork_choice: None, + fork_request_timeout: REQUEST_TIMEOUT, + fork_request_retries: 5, } } } @@ -871,35 +871,97 @@ impl AnvilNodeConfig { self } - /// Sets the fork url + /// Sets the `eth_rpc_url` to use when forking #[must_use] - pub fn with_fork_url(mut self, fork_url: Option) -> Self { - self.fork_url = fork_url; + pub fn with_eth_rpc_url>(mut self, eth_rpc_url: Option) -> Self { + self.eth_rpc_url = eth_rpc_url.map(Into::into); self } - /// Sets the fork block + /// Sets the `fork_choice` to use to fork off from based on a block number #[must_use] - pub fn with_fork_block_hash(mut self, fork_block_hash: Option) -> Self { - self.fork_block_hash = fork_block_hash; + pub fn with_fork_block_number>(self, fork_block_number: Option) -> Self { + self.with_fork_choice(fork_block_number.map(Into::into)) + } + + /// Sets the `fork_choice` to use to fork off from based on a transaction hash + #[must_use] + pub fn with_fork_transaction_hash>( + self, + fork_transaction_hash: Option, + ) -> Self { + self.with_fork_choice(fork_transaction_hash.map(Into::into)) + } + + /// Sets the `fork_choice` to use to fork off from + #[must_use] + pub fn with_fork_choice>(mut self, fork_choice: Option) -> Self { + self.fork_choice = fork_choice.map(Into::into); self } - /// Sets the fork delay between RPC requests + /// Sets the `fork_request_timeout` to use for requests #[must_use] - pub fn with_fork_delay(mut self, fork_delay: u32) -> Self { - self.fork_delay = fork_delay; + pub fn fork_request_timeout(mut self, fork_request_timeout: Option) -> Self { + if let Some(fork_request_timeout) = fork_request_timeout { + self.fork_request_timeout = fork_request_timeout; + } self } - /// Sets the fork max retries per RPC request + /// Sets the `fork_request_retries` to use for spurious networks #[must_use] - pub fn with_fork_retries(mut self, fork_retries: u32) -> Self { - self.fork_retries = fork_retries; + pub fn fork_request_retries(mut self, fork_request_retries: Option) -> Self { + if let Some(fork_request_retries) = fork_request_retries { + self.fork_request_retries = fork_request_retries; + } self } } +/// Fork delimiter used to specify which block or transaction to fork from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ForkChoice { + /// Block number to fork from. + /// + /// If negative, the given value is subtracted from the `latest` block number. + Block(i128), + /// Transaction hash to fork from. + Transaction(TxHash), +} + +impl ForkChoice { + /// Returns the block number to fork from + pub fn block_number(&self) -> Option { + match self { + Self::Block(block_number) => Some(*block_number), + Self::Transaction(_) => None, + } + } + + /// Returns the transaction hash to fork from + pub fn transaction_hash(&self) -> Option { + match self { + Self::Block(_) => None, + Self::Transaction(transaction_hash) => Some(*transaction_hash), + } + } +} + +/// Convert a transaction hash into a ForkChoice +impl From for ForkChoice { + fn from(tx_hash: TxHash) -> Self { + Self::Transaction(tx_hash) + } +} + +/// Convert a decimal block number into a ForkChoice +impl From for ForkChoice { + fn from(block: u64) -> Self { + Self::Block(block as i128) + } +} + /// Can create dev accounts #[derive(Clone, Debug)] pub struct AccountGenerator { From 8889aa2efae651f1d9f10dfe06230f1ba2a78b8f Mon Sep 17 00:00:00 2001 From: JimboJ <40345116+jimjbrettj@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:46:30 -0300 Subject: [PATCH 4/4] Update crates/anvil-polkadot/src/config.rs Co-authored-by: Alin Dima --- crates/anvil-polkadot/src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/anvil-polkadot/src/config.rs b/crates/anvil-polkadot/src/config.rs index 9b917120eecba..b0fc99058cd7f 100644 --- a/crates/anvil-polkadot/src/config.rs +++ b/crates/anvil-polkadot/src/config.rs @@ -21,7 +21,6 @@ use polkadot_sdk::{ RPC_DEFAULT_MAX_SUBS_PER_CONN, RPC_DEFAULT_MESSAGE_CAPACITY_PER_CONN, }, sc_service, - sp_core::H256, }; use rand_08::thread_rng; use serde_json::{Value, json};