Skip to content

Conversation

@odyslam
Copy link
Contributor

@odyslam odyslam commented Jul 13, 2022

Motivation

I am opening a draft PR to gather feedback and improve code quality.

Checklist

  • Poc
  • Support arbitrarily nested JSON objects
  • Add Solidity tests
  • Improve error handling & code quality
  • Rebase master
  • Merge forge-std library
  • Merge docs

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

I think that should work

If I understand correctly the ABI spec, structs are encoded as tuples.

exactly

for objects we should be able to convert them into Vec<ParamType> which will encode/decode as tuples, which would mean value_to_type is recursive over Value::Object

}
fn parse_json(state: &mut Cheatcodes, json: &str, key: &str) -> Result<Bytes, Bytes> {
// let v: Value = serde_json::from_str(json).map_err(util::encode_error)?;
let values: Value = jsonpath_rust::JsonPathFinder::from_str(json, key)?.find();
Copy link
Member

Choose a reason for hiding this comment

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

what's the benefit if JsonPathFinder here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the crate you mean? So that I don't have to implement the jsonPath syntax

@odyslam
Copy link
Contributor Author

odyslam commented Jul 13, 2022

I think that should work

If I understand correctly the ABI spec, structs are encoded as tuples.

exactly

for objects we should be able to convert them into Vec<ParamType> which will encode/decode as tuples, which would mean value_to_type is recursive over Value::Object

@mattsse What do you think of the fact that json objects are not ordered, thus it's possible that the same JSON will be serialized differently with every read of the file?

Comment on lines 16 to 21
function test_uintArray() public {
bytes memory data = cheats.parseJson(json, ".uintArray");
uint[] memory decodedData = abi.decode(data, (uint[]));
assertEq(42, decodedData[0]);
assertEq(43, decodedData[1]);
}
Copy link
Member

Choose a reason for hiding this comment

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

This is nice, what if I want to query a JSON object multiple times? e.g. say I'd like to do bytes memory data = cheats.parseJson(json) and then data.getUintArray(".uintArray") followed by data.getString(".str")? I guess we'd want a parseJson variant which does not take a second argument and reads the entire thing, and then a JSON library (which we discussed before) which gives nice utils that handle the abi decoding (in forge-std probably)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parseJson variant which does not take a second argument and reads the entire thing

Reads it into what? We have the JSON in stringified via the readFile. So what we could do is basically a library that reads the JSON via readFile, stores the string locally, and then does parseJson(key) on the stored inner value.

Agreed on the library for forge-std.

use crate::{
abi::HEVMCalls,
executor::inspector::{cheatcodes::util, Cheatcodes},
executor::inspector::{
Copy link
Collaborator

@mds1 mds1 Jul 13, 2022

Choose a reason for hiding this comment

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

(arbitrary comment location to start a thread)

Have not take a close look at this yet, but just wanted to comment on this:

Address inference from a string is not great. Could it misinterpret a hex-encoded bytes string of the same length? Is this an assumption we are willing to make?

I think ethers.js has a good convention for distinguishing hex strings that are numbers from hex strings that are bytes. From their docs:

For example, the string "0x1234" is 6 characters long (and in this case 6 bytes long). This is not equivalent to the array [ 0x12, 0x34 ], which is 2 bytes long.

So we can use the same convention for json parsing / script output writing. I quickly sanity checked based on that portion of their docs, but would love someone to double check that / confirm it's a good approach here

@mattsse
Copy link
Member

mattsse commented Jul 13, 2022

@mattsse What do you think of the fact that json objects are not ordered, thus it's possible that the same JSON will be serialized differently with every read of the file?

iirc serde_json::Value::Object is a sorted BTreeMap.

I think we want sorted keys, and hence sorted encoded tuples so order is consistent?

@odyslam
Copy link
Contributor Author

odyslam commented Jul 14, 2022

@mattsse What do you think of the fact that json objects are not ordered, thus it's possible that the same JSON will be serialized differently with every read of the file?

iirc serde_json::Value::Object is a sorted BTreeMap.

I think we want sorted keys, and hence sorted encoded tuples so order is consistent?

Yup, that sounds reasonable. Just underlining it cause that affects the order the user would have to define the attributes in the struct. While they would intuitively do it via some taxonomical or tight-packing order, they would need to know they they have to list the attributes in alphabetical order in order for the tuple to be translated into the struct correctly

@mattsse
Copy link
Member

mattsse commented Jul 14, 2022

they would need to know they they have to list the attributes in alphabetical

I think this is reasonable, otherwise it will cause problems if you want to parse e.g. a "transaction" json and different nodes may return a different order in the RPC response

@odyslam
Copy link
Contributor Author

odyslam commented Jul 26, 2022

@mattsse finally made it work with arbitrarily nested objects. Switched to handling them via Tokens and it became much simpler. I would like your suggestion on:

  • Better error handling, especially inside the closure to reduce unwrap()
  • parseAddress: I tried your suggestion, but didn't make it work. I think serde follows the same logic under the hood, trying different types in-order unless one decodes successfully.
  • Solidity tests pass. To run them easily, you can use this one-liner: ./target/debug/forge test --root testdata --contracts cheats -vvv --match-contract ParseJson

Copy link
Member

@gakonst gakonst left a comment

Choose a reason for hiding this comment

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

there's a commented out test + needs a test for writeJSON, not sure i exactly get how it's supposed to be used!

the parseJSON flow looks good

@odyslam
Copy link
Contributor Author

odyslam commented Jul 31, 2022

@gakonst I added a new comment on the issue on a proposed API for writeJson. I haven't found something that I love, so I want to get some alignment before I get full steam ahead implementing.

The one I have implemented right now, is only good for writing simple values (not arrays, not struct) at the first "level" of a json (so not being able to write in a nested object). I proposed a new API, but it's meh. Any idea is more than welcome.

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

couple nits, otherwise ,lg

@OliverNChalk
Copy link
Contributor

Hey, just testing this out locally and haven't had any issues parsing JSON files yet. I merged master in and it was fine, just one compile issue fixed here: proximacapital@e6867e1

Was wondering if there was an eta? I'll continue building out my json usage locally but struggling to see a practical (i.e. not compiling from source) way to get access to this feature in CI before it merges?

@gakonst
Copy link
Member

gakonst commented Aug 1, 2022

foundryup -P 2293 will get this working for you I think?

@OliverNChalk
Copy link
Contributor

Build currently failing but ill def use that flag in the future, for now running off of a fork

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

a general usage is limited due to some assumptions and limitations regarding encoding, but it is definitely useful and we should add that

needs rebase though

@mds1
Copy link
Collaborator

mds1 commented Aug 10, 2022

a general usage is limited due to some assumptions and limitations regarding encoding, but it is definitely useful and we should add that

What limitations are these?

Also @odyslam should this be taken out of draft now?

@odyslam
Copy link
Contributor Author

odyslam commented Aug 10, 2022

Tomorrow will do the following:

  • remote writeJson code (will be replaced eitherway)
  • rebase
  • put out of draft and ping for review. Goal to merge tomorrow

Then, I will create a new branch to start working on @mattsse suggestion for writeJson.

Sorry for delaying the feature for so long.

@gakonst
Copy link
Member

gakonst commented Aug 11, 2022

SGTM. Let's get this over the line and iterate on the writeJSON separately.

@odyslam odyslam marked this pull request as ready for review August 11, 2022 05:28
@mattsse
Copy link
Member

mattsse commented Aug 11, 2022

a general usage is limited due to some assumptions and limitations regarding encoding, but it is definitely useful and we should add that

What limitations are these?

for structs, we currently have no way of knowing how the target struct looks like so current assumption is that the sol type matches the json object 1:1, right @odyslam?

@odyslam
Copy link
Contributor Author

odyslam commented Aug 11, 2022 via email

@mattsse mattsse merged commit 655a69f into foundry-rs:master Aug 11, 2022
@odyslam
Copy link
Contributor Author

odyslam commented Aug 11, 2022

@mattsse will proceed with forge-std and docs update shortly.

Thanks for the JSON file test fix.

@0xPhaze
Copy link

0xPhaze commented Aug 21, 2022

Does this work with bytes32 and bytes? I am getting some weird results there.
Edit: I guess these are always encoded as string.

@OliverNChalk
Copy link
Contributor

@0xPhaze I am able to parse this:
image

Into this:
image

@0xPhaze
Copy link

0xPhaze commented Aug 21, 2022

Hm.. are you able to parse something like this?

image

I have to decode this into string and then parse the string to get the bytes32.

@odyslam
Copy link
Contributor Author

odyslam commented Aug 21, 2022

Hey,

so currently the above will be returned as a type string. I have added a fix for that and will pr now, but they will be returned as type bytes.

@odyslam
Copy link
Contributor Author

odyslam commented Aug 21, 2022

@0xPhaze that should do it

#2866

iFrostizz pushed a commit to iFrostizz/foundry that referenced this pull request Nov 9, 2022
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-feature Type: feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: JSON parsing cheatcodes

7 participants