Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 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
42 changes: 41 additions & 1 deletion crates/config/src/fork_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,48 @@ use std::{collections::HashMap, ops::Deref};
pub struct ForkConfigs(pub HashMap<String, ForkChainConfig>);

impl ForkConfigs {
/// Normalize fork config chain keys and resolve environment variables in all configured fields.
pub fn normalize_and_resolve(&mut self) -> Result<(), ExtractConfigError> {
self.normalize_keys()?;
self.resolve_env_vars()
}

/// Normalize fork config chains, so that all have `alloy_chain::NamedChain` compatible names.
fn normalize_keys(&mut self) -> Result<(), ExtractConfigError> {
let mut normalized = HashMap::new();

for (key, config) in std::mem::take(&mut self.0) {
// Determine the canonical key for this entry
let canonical_key = if let Ok(chain_id) = key.parse::<u64>() {
if let Some(named) = alloy_chains::Chain::from_id(chain_id).named() {
named.as_str().to_string()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we can't fall back to just integers if they are not supported in alloy_chains?

Copy link
Contributor Author

@0xrusowsky 0xrusowsky Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, that's what we were discussing above.

addressed with 7ccb5d9

} else {
return Err(ExtractConfigError::new(figment::Error::from(format!(
"chain id '{key}' is not supported. Check 'https://github.com/alloy-rs/chains' and consider opening a PR.",
))));
}
} else if let Ok(named) = key.parse::<alloy_chains::NamedChain>() {
named.as_str().to_string()
} else {
return Err(ExtractConfigError::new(figment::Error::from(format!(
"chain name '{key}' is not supported. Check 'https://github.com/alloy-rs/chains' and consider opening a PR.",
))));
};

// Insert and check for conflicts
if normalized.insert(canonical_key, config).is_some() {
return Err(ExtractConfigError::new(figment::Error::from(
"duplicate fork configuration.",
)));
}
}

self.0 = normalized;
Ok(())
}

/// Resolve environment variables in all fork config fields
pub fn resolve_env_vars(&mut self) -> Result<(), ExtractConfigError> {
fn resolve_env_vars(&mut self) -> Result<(), ExtractConfigError> {
for (name, fork_config) in &mut self.0 {
// Take temporary ownership of the config, so that it can be consumed.
let config = std::mem::take(fork_config);
Expand Down
164 changes: 141 additions & 23 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ impl Config {
add_profile(&config.profile);

config.normalize_optimizer_settings();
config.forks.resolve_env_vars()?;
config.forks.normalize_and_resolve()?;

Ok(config)
}
Expand Down Expand Up @@ -5159,10 +5159,10 @@ mod tests {
[forks]

[forks.mainnet]
rpc_endpoint = "${_MAINNET_RPC}"
rpc_endpoint = "${MAINNET_RPC}"

[forks.mainnet.vars]
weth = "${_WETH_ADDRESS}"
weth = "${WETH_MAINNET}"
usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
pool_name = "USDC-ETH"
pool_fee = 3000
Expand All @@ -5173,7 +5173,7 @@ mod tests {
int_array = [-100, 200, -300]
uint_array = [100, 200, 300]
addr_array = [
"${_ADDR1}",
"${ADDR_1}",
"0x2222222222222222222222222222222222222222"
]
bytes32_array = [
Expand All @@ -5182,24 +5182,33 @@ mod tests {
]
bytes_array = ["0x1234", "0x5678", "0xabcd"]
string_array = ["hello", "world", "test"]

[forks.10]
rpc_endpoint = "${OPTIMISM_RPC}"

[forks.10.vars]
weth = "${WETH_OPTIMISM}"
"#,
)?;

// Now set the environment variables
jail.set_env("_MAINNET_RPC", "mainnet-rpc");
jail.set_env("_WETH_ADDRESS", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
jail.set_env("_ADDR1", "0x1111111111111111111111111111111111111111");
jail.set_env("MAINNET_RPC", "mainnet-rpc");
jail.set_env("OPTIMISM_RPC", "optimism-rpc");
jail.set_env("WETH_MAINNET", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
jail.set_env("WETH_OPTIMISM", "0x4200000000000000000000000000000000000006");
jail.set_env("ADDR_1", "0x1111111111111111111111111111111111111111");

// Reload the config with env vars set
let config = Config::load().unwrap();

let expected: HashMap<String, ForkChainConfig> = vec![(
"mainnet".to_string(),
ForkChainConfig {
rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url(
"mainnet-rpc".to_string(),
))),
vars: vec![
let expected: HashMap<String, ForkChainConfig> = vec![
(
"mainnet".to_string(),
ForkChainConfig {
rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url(
"mainnet-rpc".to_string(),
))),
vars: vec![
("weth".into(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".into()),
("usdc".into(), "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into()),
("pool_name".into(), "USDC-ETH".into()),
Expand All @@ -5219,10 +5228,25 @@ mod tests {
("bytes_array".into(), vec!["0x1234", "0x5678", "0xabcd"].into()),
("string_array".into(), vec!["hello", "world", "test"].into()),
]
.into_iter()
.collect(),
},
)]
.into_iter()
.collect(),
},
),
(
"optimism".to_string(),
ForkChainConfig {
rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url(
"optimism-rpc".to_string(),
))),
vars: vec![(
"weth".into(),
"0x4200000000000000000000000000000000000006".into(),
)]
.into_iter()
.collect(),
},
),
]
.into_iter()
.collect();
assert_eq!(
Expand All @@ -5231,18 +5255,29 @@ mod tests {
);

let expected_mainnet = expected.get("mainnet").unwrap();
let expected_optimism = expected.get("optimism").unwrap();
let mainnet = config.forks.get("mainnet").unwrap();
let optimism = config.forks.get("optimism").unwrap();

// Verify that rpc_endpoint is now resolved to the actual value
// Verify that rpc_endpoints are resolved to their actual value
if let Some(rpc) = &mainnet.rpc_endpoint {
// The rpc endpoint should be resolved after env var is set
let resolved_url = rpc.to_owned().resolve().url().unwrap();
assert_eq!(resolved_url, "mainnet-rpc");
}
if let Some(rpc) = &optimism.rpc_endpoint {
let resolved_url = rpc.to_owned().resolve().url().unwrap();
assert_eq!(resolved_url, "optimism-rpc");
}

// Verify that weth is now resolved to the actual address
let weth_after = mainnet.vars.get("weth").unwrap();
assert_eq!(weth_after.as_str().unwrap(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
// Verify that weth placeholders are resolved to their actual addresses
assert_eq!(
mainnet.vars.get("weth").unwrap().as_str().unwrap(),
expected_mainnet.vars.get("weth").unwrap().as_str().unwrap(),
);
assert_eq!(
optimism.vars.get("weth").unwrap().as_str().unwrap(),
expected_optimism.vars.get("weth").unwrap().as_str().unwrap(),
);

// Check all other vars match expected values
for (k, v) in &expected_mainnet.vars {
Expand Down Expand Up @@ -5292,6 +5327,89 @@ mod tests {
});
}

#[test]
fn test_fork_config_invalid_chain_fails() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[forks]

[forks.randomchain]
rpc_endpoint = "random-chain-rpc"
[forks.randomchain.vars]
some_value = "some_value"
"#,
)?;
let result = Config::load();
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();

// Check the error message
assert!(err_str.contains(
"foundry config error: chain name 'randomchain' is not supported. Check 'https://github.com/alloy-rs/chains' and consider opening a PR."
));

Ok(())
});

figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[forks]

[forks.0]
rpc_endpoint = "random-chain-rpc"
[forks.0.vars]
some_value = "some_value"
"#,
)?;
let result = Config::load();
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();

// Check the error message
assert!(
err_str.contains(
"foundry config error: chain id '0' is not supported. Check 'https://github.com/alloy-rs/chains' and consider opening a PR."
)
);

Ok(())
});
}

#[test]
fn test_fork_config_duplicate_key_fails() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[forks]

[forks.mainnet]
rpc_endpoint = "mainnet-rpc"
[forks.mainnet.vars]
some_value = "some_value"

[forks.1]
rpc_endpoint = "mainnet-rpc"
[forks.1.vars]
some_value = "some_value"
"#,
)?;
let result = Config::load();
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();

// Check the error message
assert!(err_str.contains("duplicate fork configuration."));

Ok(())
});
}

#[test]
fn test_fork_config_missing_env_var_fails() {
figment::Jail::expect_with(|jail| {
Expand Down
2 changes: 0 additions & 2 deletions crates/forge/tests/cli/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3489,7 +3489,6 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| {
("addr".into(), "0xdeadbeef00000000000000000000000000000000".into()),
("bytes".into(), "0x00000000000f00".into()),
("str".into(), "bar".into()),
// Array configurations for testing new array cheatcodes
("bool_array".into(), vec![true, false, true].into()),
("int_array".into(), vec!["-100", "200", "-300"].into()),
("uint_array".into(), vec!["100", "200", "300"].into()),
Expand Down Expand Up @@ -3524,7 +3523,6 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| {
("addr".into(), "0x00000000000000000000000000000000deadbeef".into()),
("bytes".into(), "0x00f00000000000".into()),
("str".into(), "bazz".into()),
// Array configurations for testing new array cheatcodes
("bool_array".into(), vec![false, true, false].into()),
("int_array".into(), vec!["-400", "500", "-600"].into()),
("uint_array".into(), vec!["400", "500", "600"].into()),
Expand Down
Loading