Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cd633dd
feat(cheats): add fork cheats to load array vars
0xrusowsky Aug 15, 2025
fda1b89
add more tests
0xrusowsky Aug 15, 2025
15b206a
Merge branch 'master' into rusowsky/extend-fork-cheats
grandizzy Aug 15, 2025
83e83ee
refactor to reduce duplicate code
0xrusowsky Aug 15, 2025
339ba1f
Merge branch 'rusowsky/extend-fork-cheats' of github.com:foundry-rs/f…
0xrusowsky Aug 15, 2025
3acf9e1
fix: test spacing
0xrusowsky Aug 15, 2025
d0c121a
style: clippy
0xrusowsky Aug 15, 2025
3c4af4e
Merge branch 'rusowsky/extend-fork-cheats' of github.com:foundry-rs/f…
0xrusowsky Aug 15, 2025
32bbd76
fix: test spacing
0xrusowsky Aug 15, 2025
d2cf99a
style: rename to `readFork..`
0xrusowsky Aug 15, 2025
202d3b9
docs: improve cheat comments
0xrusowsky Aug 15, 2025
da65c90
style: rename missing cheats
0xrusowsky Aug 15, 2025
e2b6c5d
docs: update cmnts
0xrusowsky Aug 15, 2025
503b4fe
fix: use existing toml and json parsing helper fns
0xrusowsky Aug 15, 2025
9bc005f
feat: resolve env vars in 'fork' config
0xrusowsky Aug 16, 2025
e3ad11f
fix: test spacing
0xrusowsky Aug 16, 2025
535fdc5
fix: expected test logs
0xrusowsky Aug 16, 2025
655384b
fix: use `let Self { .. } = self` syntax
0xrusowsky Aug 18, 2025
4be3d53
Merge branch 'master' into rusowsky/extend-fork-cheats
0xrusowsky Aug 18, 2025
2ce8cc5
feat: support numeric chain keys
0xrusowsky Aug 19, 2025
9ba7d51
Merge branch 'master' of github.com:foundry-rs/foundry into rusowsky/…
0xrusowsky Aug 19, 2025
95c9adc
style
0xrusowsky Aug 19, 2025
6a3d63e
add unit tests
0xrusowsky Aug 19, 2025
598f73a
better error msg
0xrusowsky Aug 19, 2025
7ccb5d9
use `alloy_chains::Chain` as config key
0xrusowsky Aug 19, 2025
44c67f4
fix: test spacing
0xrusowsky Aug 20, 2025
a4a6a2a
Merge branch 'master' into rusowsky/support-num-keys
0xrusowsky Aug 20, 2025
c7865eb
Merge branch 'master' into rusowsky/support-num-keys
grandizzy Aug 20, 2025
33c3550
Merge branch 'master' into rusowsky/support-num-keys
0xrusowsky Aug 20, 2025
e2f4d30
style: std error msg
0xrusowsky Aug 20, 2025
01e0ee3
Merge branch 'master' into rusowsky/support-num-keys
0xrusowsky Aug 20, 2025
25806c2
fix: tests
0xrusowsky Aug 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 59 additions & 57 deletions crates/cheatcodes/src/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,35 @@ use crate::{
Cheatcode, CheatsCtxt, DatabaseExt, Result, Vm::*, json::parse_json_as,
toml::toml_to_json_value,
};
use alloy_chains::Chain;
use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_sol_types::SolValue;
use eyre::OptionExt;
use foundry_evm_core::ContextExt;
use std::borrow::Cow;

impl Cheatcode for readForkChainIdsCall {
fn apply(&self, state: &mut crate::Cheatcodes) -> Result {
let Self {} = self;
Ok(state
.config
.forks
.keys()
.map(|name| alloy_chains::Chain::from_named(name.parse().unwrap()).id())
.collect::<Vec<_>>()
.abi_encode())
Ok(state.config.forks.keys().map(|chain| chain.id()).collect::<Vec<_>>().abi_encode())
}
}

impl Cheatcode for readForkChainsCall {
fn apply(&self, state: &mut crate::Cheatcodes) -> Result {
let Self {} = self;
Ok(state.config.forks.keys().collect::<Vec<_>>().abi_encode())
Ok(state
.config
.forks
.keys()
.map(|chain| get_chain_name(chain).to_string())
.collect::<Vec<_>>()
.abi_encode())
}
}

impl Cheatcode for readForkChainCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
Ok(get_active_fork_chain_name(ccx)?.abi_encode())
Ok(get_chain_name(&get_active_fork_chain(ccx)?).to_string().abi_encode())
}
}

Expand All @@ -41,42 +42,38 @@ impl Cheatcode for readForkChainIdCall {
}
}

fn resolve_rpc_url(name: &'static str, state: &mut crate::Cheatcodes) -> Result {
// Get the chain ID from the chain_configs
if let Some(config) = state.config.forks.get(name) {
fn resolve_rpc_url(chain: Chain, state: &mut crate::Cheatcodes) -> Result {
if let Some(config) = state.config.forks.get(&chain) {
let rpc = match config.rpc_endpoint {
Some(ref url) => url.clone().resolve(),
None => state.config.rpc_endpoint(name)?,
None => state.config.rpc_endpoint(&get_chain_name(&chain))?,
};

return Ok(rpc.url()?.abi_encode());
}

bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'")
bail!(
"'rpc_endpoint' not found in '[fork.<chain_id: {chain}>]' subsection of 'foundry.toml'",
chain = chain.id()
)
}

impl Cheatcode for readForkChainRpcUrlCall {
fn apply(&self, state: &mut crate::Cheatcodes) -> Result {
let Self { id } = self;
let name = get_chain_name(id.to::<u64>())?;
resolve_rpc_url(name, state)
resolve_rpc_url(Chain::from_id(id.to::<u64>()), state)
}
}

impl Cheatcode for readForkRpcUrlCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let name = get_active_fork_chain_name(ccx)?;
resolve_rpc_url(name, ccx.state)
resolve_rpc_url(get_active_fork_chain(ccx)?, ccx.state)
}
}

/// Gets the alloy chain name for a given chain id.
fn get_chain_name(id: u64) -> Result<&'static str> {
let chain = alloy_chains::Chain::from_id(id)
.named()
.ok_or_eyre("unknown name for active forked chain")?;

Ok(chain.as_str())
fn get_chain_name(chain: &Chain) -> Cow<'static, str> {
chain.named().map_or(Cow::Owned(chain.id().to_string()), |name| Cow::Borrowed(name.as_str()))
}

/// Gets the chain id of the active fork. Panics if no fork is selected.
Expand All @@ -88,9 +85,9 @@ fn get_active_fork_chain_id(ccx: &mut CheatsCtxt) -> Result<u64> {
Ok(env.cfg.chain_id)
}

/// Gets the alloy chain name for the active fork. Panics if no fork is selected.
fn get_active_fork_chain_name(ccx: &mut CheatsCtxt) -> Result<&'static str> {
get_chain_name(get_active_fork_chain_id(ccx)?)
/// Gets the alloy chain for the active fork. Panics if no fork is selected.
fn get_active_fork_chain(ccx: &mut CheatsCtxt) -> Result<Chain> {
get_active_fork_chain_id(ccx).map(Chain::from_id)
}

// Helper macros to generate cheatcode implementations
Expand All @@ -99,7 +96,7 @@ macro_rules! impl_get_value_cheatcode {
impl Cheatcode for $struct {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { key } = self;
let chain = get_active_fork_chain_id(ccx)?;
let chain = get_active_fork_chain(ccx)?;
get_value(chain, key, $sol_type, ccx.state)
}
}
Expand All @@ -108,7 +105,7 @@ macro_rules! impl_get_value_cheatcode {
impl Cheatcode for $struct {
fn apply(&self, state: &mut crate::Cheatcodes) -> Result {
let Self { chain, key } = self;
get_value(chain.to::<u64>(), key, $sol_type, state)
get_value(Chain::from_id(chain.to::<u64>()), key, $sol_type, state)
}
}
};
Expand All @@ -119,16 +116,15 @@ macro_rules! impl_get_array_cheatcode {
impl Cheatcode for $struct {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { key } = self;
let chain = get_active_fork_chain_id(ccx)?;
get_array(chain, key, $sol_type, ccx.state)
get_array(get_active_fork_chain(ccx)?, key, $sol_type, ccx.state)
}
}
};
($struct:ident, $sol_type:expr) => {
impl Cheatcode for $struct {
fn apply(&self, state: &mut crate::Cheatcodes) -> Result {
let Self { chain, key } = self;
get_array(chain.to::<u64>(), key, $sol_type, state)
get_array(Chain::from(chain.to::<u64>()), key, $sol_type, state)
}
}
};
Expand Down Expand Up @@ -178,44 +174,49 @@ impl_get_array_cheatcode!(readForkStringArrayCall, &DynSolType::String, stateful

/// Generic helper to get any value from the TOML config.
fn get_toml_value<'a>(
name: &'a str,
chain: Chain,
key: &'a str,
state: &'a crate::Cheatcodes,
) -> Result<&'a toml::Value> {
let config = state.config.forks.get(name).ok_or_else(|| {
fmt_err!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'")
let config = state.config.forks.get(&chain).ok_or_else(|| {
fmt_err!(
"'[fork.<chain_id: {chain}>]' subsection not found in 'foundry.toml'",
chain = chain.id()
)
})?;
let value = config.vars.get(key).ok_or_else(|| {
fmt_err!("variable '{key}' not found in '[fork.<chain_id: {chain}>]'", chain = chain.id())
})?;
let value = config
.vars
.get(key)
.ok_or_else(|| fmt_err!("Variable '{key}' not found in [fork.{name}] configuration"))?;

Ok(value)
}

/// Generic helper to get any single value from the TOML config.
fn get_value(chain: u64, key: &str, ty: &DynSolType, state: &crate::Cheatcodes) -> Result {
let name = get_chain_name(chain)?;
let value = get_toml_value(name, key, state)?;
let sol_value = parse_toml_element(value, ty, key, name)?;
fn get_value(chain: Chain, key: &str, ty: &DynSolType, state: &crate::Cheatcodes) -> Result {
let value = get_toml_value(chain, key, state)?;
let sol_value = parse_toml_element(value, ty, key, chain)?;
Ok(sol_value.abi_encode())
}

/// Generic helper to get an array of values from the TOML config.
fn get_array(chain: u64, key: &str, element_ty: &DynSolType, state: &crate::Cheatcodes) -> Result {
let name = get_chain_name(chain)?;
let value = get_toml_value(name, key, state)?;

let arr = value
.as_array()
.ok_or_else(|| fmt_err!("Variable '{key}' in [fork.{name}] must be an array"))?;

fn get_array(
chain: Chain,
key: &str,
element_ty: &DynSolType,
state: &crate::Cheatcodes,
) -> Result {
let arr = get_toml_value(chain, key, state)?.as_array().ok_or_else(|| {
fmt_err!(
"variable '{key}' in '[fork.<chain_id: {chain}>]' must be an array",
chain = chain.id()
)
})?;
let result: Result<Vec<_>> = arr
.iter()
.enumerate()
.map(|(i, elem)| {
let context = format!("{key}[{i}]");
parse_toml_element(elem, element_ty, &context, name)
parse_toml_element(elem, element_ty, &context, chain)
})
.collect();

Expand All @@ -226,10 +227,11 @@ fn get_array(chain: u64, key: &str, element_ty: &DynSolType, state: &crate::Chea
fn parse_toml_element(
elem: &toml::Value,
element_ty: &DynSolType,
context: &str,
fork_name: &str,
key: &str,
chain: Chain,
) -> Result<DynSolValue> {
// Convert TOML value to JSON value and use existing JSON parsing logic
parse_json_as(&toml_to_json_value(elem.to_owned()), element_ty)
.map_err(|e| fmt_err!("Failed to parse '{context}' in [fork.{fork_name}]: {e}"))
parse_json_as(&toml_to_json_value(elem.to_owned()), element_ty).map_err(|e| {
fmt_err!("failed to parse '{key}' in '[fork.<chain_id: {chain}>]': {e}", chain = chain.id())
})
}
5 changes: 3 additions & 2 deletions crates/config/src/fork_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use crate::{
resolve::{RE_PLACEHOLDER, UnresolvedEnvVarError, interpolate},
};

use alloy_chains::Chain;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, ops::Deref};

/// Fork-scoped config for tests and scripts.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
pub struct ForkConfigs(pub HashMap<String, ForkChainConfig>);
pub struct ForkConfigs(pub HashMap<Chain, ForkChainConfig>);

impl ForkConfigs {
/// Resolve environment variables in all fork config fields
Expand All @@ -36,7 +37,7 @@ impl ForkConfigs {
}

impl Deref for ForkConfigs {
type Target = HashMap<String, ForkChainConfig>;
type Target = HashMap<Chain, ForkChainConfig>;

fn deref(&self) -> &Self::Target {
&self.0
Expand Down
Loading
Loading