Skip to content

Commit 655a69f

Browse files
odyslammattsse
andauthored
Feat: parseJson (#2293)
* refactor get_env * feat: test all possible types * chore: add jsonpath * feat: parse JSON paths to abi-encoded bytes * feat: flat nested json into a vec of <Value, abi_type> * fix: support nested json objects as tuples * chore: add test for nested object * feat: function overload to load entire json * fix: minor improvements * chore: add comments * chore: forge fmt * feat: writeJson(), without tests * fix: remove commented-out test * fix: improve error handling * fix: address Matt's comments * fix: bool * chore: remove writeJson code * fix: cherry pick shenanigan * chore: format, lint, remove old tests * fix: cargo clippy * fix: json file test Co-authored-by: Matthias Seitz <[email protected]>
1 parent 5e3de8a commit 655a69f

File tree

9 files changed

+286
-16
lines changed

9 files changed

+286
-16
lines changed

Cargo.lock

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

evm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ serde_json = "1.0.67"
1717
serde = "1.0.130"
1818
hex = "0.4.3"
1919
ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "abigen"] }
20+
jsonpath-rust = "0.1.5"
2021

2122
# Error handling
2223
eyre = "0.6.5"

evm/src/executor/abi/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ ethers::contract::abigen!(
106106
rollFork(uint256,uint256)
107107
rpcUrl(string)(string)
108108
rpcUrls()(string[2][])
109+
parseJson(string, string)(bytes)
110+
parseJson(string)(bytes)
109111
]"#,
110112
);
111113
pub use hevm::{HEVMCalls, HEVM_ABI};

evm/src/executor/inspector/cheatcodes/ext.rs

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
use crate::{
22
abi::HEVMCalls,
3-
executor::inspector::{cheatcodes::util, Cheatcodes},
3+
executor::inspector::{
4+
cheatcodes::util::{self},
5+
Cheatcodes,
6+
},
47
};
58
use bytes::Bytes;
69
use ethers::{
710
abi::{self, AbiEncode, ParamType, Token},
811
prelude::{artifacts::CompactContractBytecode, ProjectPathsConfig},
9-
types::{Address, I256, U256},
12+
types::*,
1013
utils::hex::FromHex,
1114
};
1215
use foundry_common::fs;
16+
use jsonpath_rust::JsonPathFinder;
1317
use serde::Deserialize;
18+
use serde_json::Value;
1419
use std::{
1520
env,
1621
io::{BufRead, BufReader, Write},
@@ -127,15 +132,7 @@ fn set_env(key: &str, val: &str) -> Result<Bytes, Bytes> {
127132
Ok(Bytes::new())
128133
}
129134
}
130-
131-
fn get_env(key: &str, r#type: ParamType, delim: Option<&str>) -> Result<Bytes, Bytes> {
132-
let val = env::var(key).map_err::<Bytes, _>(|e| e.to_string().encode().into())?;
133-
let val = if let Some(d) = delim {
134-
val.split(d).map(|v| v.trim()).collect()
135-
} else {
136-
vec![val.as_str()]
137-
};
138-
135+
fn value_to_abi(val: Vec<String>, r#type: ParamType, is_array: bool) -> Result<Bytes, Bytes> {
139136
let parse_bool = |v: &str| v.to_lowercase().parse::<bool>();
140137
let parse_uint = |v: &str| {
141138
if v.starts_with("0x") {
@@ -172,15 +169,26 @@ fn get_env(key: &str, r#type: ParamType, delim: Option<&str>) -> Result<Bytes, B
172169
})
173170
.collect::<Result<Vec<Token>, String>>()
174171
.map(|mut tokens| {
175-
if delim.is_none() {
176-
abi::encode(&[tokens.remove(0)]).into()
177-
} else {
172+
if is_array {
178173
abi::encode(&[Token::Array(tokens)]).into()
174+
} else {
175+
abi::encode(&[tokens.remove(0)]).into()
179176
}
180177
})
181178
.map_err(|e| e.into())
182179
}
183180

181+
fn get_env(key: &str, r#type: ParamType, delim: Option<&str>) -> Result<Bytes, Bytes> {
182+
let val = env::var(key).map_err::<Bytes, _>(|e| e.to_string().encode().into())?;
183+
let val = if let Some(d) = delim {
184+
val.split(d).map(|v| v.trim().to_string()).collect()
185+
} else {
186+
vec![val]
187+
};
188+
let is_array: bool = delim.is_some();
189+
value_to_abi(val, r#type, is_array)
190+
}
191+
184192
fn full_path(state: &Cheatcodes, path: impl AsRef<Path>) -> PathBuf {
185193
state.config.root.join(path)
186194
}
@@ -262,6 +270,60 @@ fn remove_file(state: &mut Cheatcodes, path: impl AsRef<Path>) -> Result<Bytes,
262270
Ok(Bytes::new())
263271
}
264272

273+
/// Converts a serde_json::Value to an abi::Token
274+
/// The function is designed to run recursively, so that in case of an object
275+
/// it will call itself to convert each of it's value and encode the whole as a
276+
/// Tuple
277+
fn value_to_token(value: &Value) -> Result<Token, Token> {
278+
if value.is_boolean() {
279+
Ok(Token::Bool(value.as_bool().unwrap()))
280+
} else if value.is_string() {
281+
let val = value.as_str().unwrap();
282+
// If it can decoded as an address, it's an address
283+
if let Ok(addr) = H160::from_str(val) {
284+
Ok(Token::Address(addr))
285+
} else {
286+
Ok(Token::String(val.to_owned()))
287+
}
288+
} else if value.is_u64() {
289+
Ok(Token::Uint(value.as_u64().unwrap().into()))
290+
} else if value.is_i64() {
291+
Ok(Token::Int(value.as_i64().unwrap().into()))
292+
} else if value.is_array() {
293+
let arr = value.as_array().unwrap();
294+
Ok(Token::Array(arr.iter().map(|val| value_to_token(val).unwrap()).collect::<Vec<Token>>()))
295+
} else if value.is_object() {
296+
let values = value
297+
.as_object()
298+
.unwrap()
299+
.values()
300+
.map(|val| value_to_token(val).unwrap())
301+
.collect::<Vec<Token>>();
302+
Ok(Token::Tuple(values))
303+
} else {
304+
Err(Token::String("Could not decode field".to_string()))
305+
}
306+
}
307+
/// Parses a JSON and returns a single value, an array or an entire JSON object encoded as tuple.
308+
/// As the JSON object is parsed serially, with the keys ordered alphabetically, they must be
309+
/// deserialized in the same order. That means that the solidity `struct` should order it's fields
310+
/// alphabetically and not by efficient packing or some other taxonomy.
311+
fn parse_json(_state: &mut Cheatcodes, json: &str, key: &str) -> Result<Bytes, Bytes> {
312+
let values: Value = JsonPathFinder::from_str(json, key)?.find();
313+
// values is an array of items. Depending on the JsonPath key, they
314+
// can be many or a single item. An item can be a single value or
315+
// an entire JSON object.
316+
let res = values
317+
.as_array()
318+
.ok_or_else(|| util::encode_error("JsonPath did not return an array"))?
319+
.iter()
320+
.map(|inner| value_to_token(inner).map_err(util::encode_error))
321+
.collect::<Result<Vec<Token>, Bytes>>();
322+
// encode the bytes as the 'bytes' solidity type
323+
let abi_encoded = abi::encode(&[Token::Bytes(abi::encode(&res?))]);
324+
Ok(abi_encoded.into())
325+
}
326+
265327
pub fn apply(
266328
state: &mut Cheatcodes,
267329
ffi_enabled: bool,
@@ -299,6 +361,10 @@ pub fn apply(
299361
HEVMCalls::WriteLine(inner) => write_line(state, &inner.0, &inner.1),
300362
HEVMCalls::CloseFile(inner) => close_file(state, &inner.0),
301363
HEVMCalls::RemoveFile(inner) => remove_file(state, &inner.0),
364+
// If no key argument is passed, return the whole JSON object.
365+
// "$" is the JSONPath key for the root of the object
366+
HEVMCalls::ParseJson0(inner) => parse_json(state, &inner.0, "$"),
367+
HEVMCalls::ParseJson1(inner) => parse_json(state, &inner.0, &inner.1),
302368
_ => return None,
303369
})
304370
}

forge/tests/it/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub fn base_runner() -> MultiContractRunnerBuilder {
2626
pub fn runner() -> MultiContractRunner {
2727
let mut config = Config::with_root(PROJECT.root());
2828
config.rpc_endpoints = rpc_endpoints();
29+
config.allow_paths.push(env!("CARGO_MANIFEST_DIR").into());
2930

3031
base_runner()
3132
.with_cheats_config(CheatsConfig::new(&config, &EVM_OPTS))

testdata/cheats/Cheats.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,6 @@ interface Cheats {
180180
function rpcUrl(string calldata) external returns(string memory);
181181
/// Returns all rpc urls and their aliases `[alias, url][]`
182182
function rpcUrls() external returns(string[2][] memory);
183+
function parseJson(string calldata, string calldata) external returns(bytes memory);
184+
function parseJson(string calldata) external returns(bytes memory);
183185
}

0 commit comments

Comments
 (0)