Skip to content

Commit 8ce931c

Browse files
authored
feat(cheats): resolveEnv + getChainId + writeXXXUpsert (#11414)
1 parent cce84fd commit 8ce931c

File tree

11 files changed

+269
-5
lines changed

11 files changed

+269
-5
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ strum = "0.27"
341341
tempfile = "3.20"
342342
tokio = "1"
343343
toml = "0.9"
344+
toml_edit = "0.23"
344345
tower = "0.5"
345346
tower-http = "0.6"
346347
tracing = "0.1"

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,13 @@ interface Vm {
463463

464464
// -------- Block and Transaction Properties --------
465465

466+
/// Gets the current `block.chainid` of the currently selected environment.
467+
/// You should use this instead of `block.chainid` if you use `vm.selectFork` or `vm.createSelectFork`, as `block.chainid` could be assumed
468+
/// to be constant across a transaction, and as a result will get optimized out by the compiler.
469+
/// See https://github.com/foundry-rs/foundry/issues/6180
470+
#[cheatcode(group = Evm, safety = Safe)]
471+
function getChainId() external view returns (uint256 blockChainId);
472+
466473
/// Sets `block.chainid`.
467474
#[cheatcode(group = Evm, safety = Unsafe)]
468475
function chainId(uint256 newChainId) external;
@@ -2027,6 +2034,10 @@ interface Vm {
20272034

20282035
// ======== Environment Variables ========
20292036

2037+
/// Resolves the env variable placeholders of a given input string.
2038+
#[cheatcode(group = Environment)]
2039+
function resolveEnv(string calldata input) external returns (string memory);
2040+
20302041
/// Sets environment variables.
20312042
#[cheatcode(group = Environment)]
20322043
function setEnv(string calldata name, string calldata value) external;
@@ -2542,6 +2553,12 @@ interface Vm {
25422553
#[cheatcode(group = Json)]
25432554
function writeJson(string calldata json, string calldata path, string calldata valueKey) external;
25442555

2556+
/// Write a serialized JSON object to an **existing** JSON file, replacing a value with key = <value_key.>
2557+
/// This is useful to replace a specific value of a JSON file, without having to parse the entire thing.
2558+
/// Unlike `writeJson`, this cheatcode will create new keys if they didn't previously exist.
2559+
#[cheatcode(group = Toml)]
2560+
function writeJsonUpsert(string calldata json, string calldata path, string calldata valueKey) external;
2561+
25452562
// ======== TOML Parsing and Manipulation ========
25462563

25472564
// -------- Reading --------
@@ -2647,6 +2664,12 @@ interface Vm {
26472664
#[cheatcode(group = Toml)]
26482665
function writeToml(string calldata json, string calldata path, string calldata valueKey) external;
26492666

2667+
/// Takes serialized JSON, converts to TOML and write a serialized TOML table to an **existing** TOML file, replacing a value with key = <value_key.>
2668+
/// This is useful to replace a specific value of a TOML file, without having to parse the entire thing.
2669+
/// Unlike `writeToml`, this cheatcode will create new keys if they didn't previously exist.
2670+
#[cheatcode(group = Toml)]
2671+
function writeTomlUpsert(string calldata json, string calldata path, string calldata valueKey) external;
2672+
26502673
// ======== Cryptography ========
26512674

26522675
// -------- Key Management --------

crates/cheatcodes/src/env.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use alloy_sol_types::SolValue;
66
use std::{env, sync::OnceLock};
77

88
/// Stores the forge execution context for the duration of the program.
9-
static FORGE_CONTEXT: OnceLock<ForgeContext> = OnceLock::new();
9+
pub static FORGE_CONTEXT: OnceLock<ForgeContext> = OnceLock::new();
1010

1111
impl Cheatcode for setEnvCall {
1212
fn apply(&self, _state: &mut Cheatcodes) -> Result {
@@ -28,6 +28,15 @@ impl Cheatcode for setEnvCall {
2828
}
2929
}
3030

31+
impl Cheatcode for resolveEnvCall {
32+
fn apply(&self, _state: &mut Cheatcodes) -> Result {
33+
let Self { input } = self;
34+
let resolved = foundry_config::resolve::interpolate(input)
35+
.map_err(|e| fmt_err!("failed to resolve env var: {e}"))?;
36+
Ok(resolved.abi_encode())
37+
}
38+
}
39+
3140
impl Cheatcode for envExistsCall {
3241
fn apply(&self, _state: &mut Cheatcodes) -> Result {
3342
let Self { name } = self;

crates/cheatcodes/src/evm.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ impl Cheatcode for lastCallGasCall {
469469
}
470470
}
471471

472+
impl Cheatcode for getChainIdCall {
473+
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
474+
let Self {} = self;
475+
Ok(U256::from(ccx.ecx.cfg.chain_id).abi_encode())
476+
}
477+
}
478+
472479
impl Cheatcode for chainIdCall {
473480
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
474481
let Self { newChainId } = self;

crates/cheatcodes/src/json.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,23 @@ impl Cheatcode for writeJson_1Call {
361361
}
362362
}
363363

364+
impl Cheatcode for writeJsonUpsertCall {
365+
fn apply(&self, state: &mut Cheatcodes) -> Result {
366+
let Self { json: value, path, valueKey } = self;
367+
368+
// Read, parse, and update the JSON object
369+
let data_path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
370+
let data_string = fs::read_to_string(&data_path)?;
371+
let mut data =
372+
serde_json::from_str(&data_string).unwrap_or_else(|_| Value::String(data_string));
373+
upsert_json_value(&mut data, value, valueKey)?;
374+
375+
// Write the updated content back to the file
376+
let json_string = serde_json::to_string_pretty(&data)?;
377+
super::fs::write_file(state, path.as_ref(), json_string.as_bytes())
378+
}
379+
}
380+
364381
pub(super) fn check_json_key_exists(json: &str, key: &str) -> Result {
365382
let json = parse_json_str(json)?;
366383
let values = select(&json, key)?;
@@ -643,11 +660,68 @@ pub(super) fn resolve_type(type_description: &str) -> Result<DynSolType> {
643660
bail!("type description should be a valid Solidity type or a EIP712 `encodeType` string")
644661
}
645662

663+
/// Upserts a value into a JSON object based on a dot-separated key.
664+
///
665+
/// This function navigates through a mutable `serde_json::Value` object using a
666+
/// path-like key. It creates nested JSON objects if they do not exist along the path.
667+
/// The value is inserted at the final key in the path.
668+
///
669+
/// # Arguments
670+
///
671+
/// * `data` - A mutable reference to the `serde_json::Value` to be modified.
672+
/// * `value` - The string representation of the value to upsert. This string is first parsed as
673+
/// JSON, and if that fails, it's treated as a plain JSON string.
674+
/// * `key` - A dot-separated string representing the path to the location for upserting.
675+
pub(super) fn upsert_json_value(data: &mut Value, value: &str, key: &str) -> Result<()> {
676+
// Parse the path key into segments.
677+
let canonical_key = canonicalize_json_path(key);
678+
let parts: Vec<&str> = canonical_key
679+
.strip_prefix("$.")
680+
.unwrap_or(key)
681+
.split('.')
682+
.filter(|s| !s.is_empty())
683+
.collect();
684+
685+
if parts.is_empty() {
686+
return Err(fmt_err!("'valueKey' cannot be empty or just '$'"));
687+
}
688+
689+
// Separate the final key from the path.
690+
// Traverse the objects, creating intermediary ones if necessary.
691+
if let Some((key_to_insert, path_to_parent)) = parts.split_last() {
692+
let mut current_level = data;
693+
694+
for segment in path_to_parent {
695+
if !current_level.is_object() {
696+
return Err(fmt_err!("path segment '{segment}' does not resolve to an object."));
697+
}
698+
current_level = current_level
699+
.as_object_mut()
700+
.unwrap()
701+
.entry(segment.to_string())
702+
.or_insert(Value::Object(Map::new()));
703+
}
704+
705+
// Upsert the new value
706+
if let Some(parent_obj) = current_level.as_object_mut() {
707+
parent_obj.insert(
708+
key_to_insert.to_string(),
709+
serde_json::from_str(value).unwrap_or_else(|_| Value::String(value.to_owned())),
710+
);
711+
} else {
712+
return Err(fmt_err!("final destination is not an object, cannot insert key."));
713+
}
714+
}
715+
716+
Ok(())
717+
}
718+
646719
#[cfg(test)]
647720
mod tests {
648721
use super::*;
649722
use alloy_primitives::FixedBytes;
650723
use proptest::strategy::Strategy;
724+
use serde_json::json;
651725

652726
fn contains_tuple(value: &DynSolValue) -> bool {
653727
match value {
@@ -717,4 +791,51 @@ mod tests {
717791
assert_eq!(value, v);
718792
}
719793
}
794+
795+
#[test]
796+
fn test_upsert_json_value() {
797+
// Tuples of: (initial_json, key, value_to_upsert, expected)
798+
let scenarios = vec![
799+
// Simple key-value insert with a plain string
800+
(json!({}), "foo", r#""bar""#, json!({"foo": "bar"})),
801+
// Overwrite existing value with a number
802+
(json!({"foo": "bar"}), "foo", "123", json!({"foo": 123})),
803+
// Create nested objects
804+
(json!({}), "a.b.c", r#""baz""#, json!({"a": {"b": {"c": "baz"}}})),
805+
// Upsert into existing nested object with a boolean
806+
(json!({"a": {"b": {}}}), "a.b.c", "true", json!({"a": {"b": {"c": true}}})),
807+
// Upsert a JSON object as a value
808+
(json!({}), "a.b", r#"{"d": "e"}"#, json!({"a": {"b": {"d": "e"}}})),
809+
// Upsert a JSON array as a value
810+
(json!({}), "myArray", r#"[1, "test", null]"#, json!({"myArray": [1, "test", null]})),
811+
];
812+
813+
for (mut initial, key, value_str, expected) in scenarios {
814+
upsert_json_value(&mut initial, value_str, key).unwrap();
815+
assert_eq!(initial, expected);
816+
}
817+
818+
let error_scenarios = vec![
819+
// Path traverses a non-object value
820+
(
821+
json!({"a": "a string value"}),
822+
"a.b",
823+
r#""bar""#,
824+
"final destination is not an object, cannot insert key.",
825+
),
826+
// Empty key should fail
827+
(json!({}), "", r#""bar""#, "'valueKey' cannot be empty or just '$'"),
828+
// Root path with a trailing dot should fail
829+
(json!({}), "$.", r#""bar""#, "'valueKey' cannot be empty or just '$'"),
830+
];
831+
832+
for (mut initial, key, value_str, error_msg) in error_scenarios {
833+
let result = upsert_json_value(&mut initial, value_str, key);
834+
assert!(result.is_err(), "Expected an error for key: '{key}' but got Ok");
835+
assert!(
836+
result.unwrap_err().to_string().contains(error_msg),
837+
"Error message for key '{key}' did not contain '{error_msg}'"
838+
);
839+
}
840+
}
720841
}

crates/cheatcodes/src/toml.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
Vm::*,
66
json::{
77
canonicalize_json_path, check_json_key_exists, parse_json, parse_json_coerce,
8-
parse_json_keys, resolve_type,
8+
parse_json_keys, resolve_type, upsert_json_value,
99
},
1010
};
1111
use alloy_dyn_abi::DynSolType;
@@ -194,6 +194,25 @@ impl Cheatcode for writeToml_1Call {
194194
}
195195
}
196196

197+
impl Cheatcode for writeTomlUpsertCall {
198+
fn apply(&self, state: &mut Cheatcodes) -> Result {
199+
let Self { json: value, path, valueKey } = self;
200+
201+
// Read and parse the TOML file
202+
let data_path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
203+
let toml_data = fs::read_to_string(&data_path)?;
204+
205+
// Convert to JSON and update the object
206+
let mut json_data: JsonValue =
207+
toml::from_str(&toml_data).map_err(|e| fmt_err!("failed parsing TOML: {e}"))?;
208+
upsert_json_value(&mut json_data, value, valueKey)?;
209+
210+
// Serialize back to TOML and write the updated content back to the file
211+
let toml_string = format_json_to_toml(json_data)?;
212+
super::fs::write_file(state, path.as_ref(), toml_string.as_bytes())
213+
}
214+
}
215+
197216
/// Parse
198217
fn parse_toml_str(toml: &str) -> Result<TomlValue> {
199218
toml::from_str(toml).map_err(|e| fmt_err!("failed parsing TOML: {e}"))

crates/config/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ serde.workspace = true
4343
soldeer-core.workspace = true
4444
thiserror.workspace = true
4545
toml = { workspace = true, features = ["preserve_order"] }
46-
toml_edit = "0.23"
46+
toml_edit.workspace = true
4747
tracing.workspace = true
4848
walkdir.workspace = true
4949
yansi.workspace = true

0 commit comments

Comments
 (0)