Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ethabi Contract methods #195

Merged
merged 19 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ syn-solidity = { version = "0.2.0", path = "crates/syn-solidity", default-featur

# serde
serde = { version = "1.0", default-features = false, features = ["alloc"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc", "std"] }
prestwich marked this conversation as resolved.
Show resolved Hide resolved

# macros
proc-macro2 = "1.0"
Expand Down
1 change: 1 addition & 0 deletions crates/json-abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exclude.workspace = true
alloy-primitives = { workspace = true, features = ["serde"] }
alloy-sol-type-parser.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

[dev-dependencies]
serde_json.workspace = true
Expand Down
64 changes: 60 additions & 4 deletions crates/json-abi/src/abi.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use crate::{AbiItem, Constructor, Error, Event, Fallback, Function, Receive};
use alloc::{collections::btree_map, string::String, vec::Vec};
use alloc::{
collections::{btree_map, btree_map::Values},
string::String,
vec::Vec,
};
use alloy_primitives::Bytes;
use btree_map::BTreeMap;
use core::{fmt, iter};
use core::{fmt, iter, iter::Flatten};
use serde_json as _;
DaniPopes marked this conversation as resolved.
Show resolved Hide resolved
use serde::{
de::{MapAccess, SeqAccess, Visitor},
ser::SerializeSeq,
Deserialize, Deserializer, Serialize,
};
#[cfg(feature = "std")]
use std::io;

/// The JSON contract ABI, as specified in the [Solidity ABI spec][ref].
///
Expand Down Expand Up @@ -72,6 +79,55 @@ impl JsonAbi {
errors: self.errors.into_values().flatten(),
}
}

/// Creates constructor call builder.
pub const fn constructor(&self) -> Option<&Constructor> {
self.constructor.as_ref()
}

/// Loads contract from json
#[cfg(feature = "std")]
pub fn load<T: io::Read>(mut reader: T) -> Result<Self, serde_json::Error> {
// https://docs.rs/serde_json/latest/serde_json/fn.from_reader.html
// serde_json docs recommend buffering the whole reader to a string
// This also prevents a borrowing issue when deserializing from a reader
let mut json = String::with_capacity(1000);
prestwich marked this conversation as resolved.
Show resolved Hide resolved
reader
.read_to_string(&mut json)
.map_err(serde_json::Error::io)?;

serde_json::from_str(&json)
}

/// Gets all the functions with the given name.
pub fn function(&self, name: &str) -> Option<&[Function]> {
self.functions.get(name).map(Vec::as_slice)
}

/// Gets all the events with the given name.
pub fn event(&self, name: &str) -> Option<&[Event]> {
self.events.get(name).map(Vec::as_slice)
}

/// Gets all the errors with the given name.
pub fn error(&self, name: &str) -> Option<&[Error]> {
self.errors.get(name).map(Vec::as_slice)
}

/// Iterates over all the functions of the contract in arbitrary order.
pub fn functions(&self) -> Flatten<Values<'_, String, Vec<Function>>> {
self.functions.values().flatten()
}

/// Iterates over all the events of the contract in arbitrary order.
pub fn events(&self) -> Flatten<Values<'_, String, Vec<Event>>> {
self.events.values().flatten()
}

/// Iterates over all the errors of the contract in arbitrary order.
pub fn errors(&self) -> Flatten<Values<'_, String, Vec<Error>>> {
self.errors.values().flatten()
}
}

macro_rules! next_item {
Expand Down Expand Up @@ -144,7 +200,7 @@ macro_rules! iter_impl {
};
}

type FlattenValues<'a, V> = iter::Flatten<btree_map::Values<'a, String, Vec<V>>>;
type FlattenValues<'a, V> = Flatten<btree_map::Values<'a, String, Vec<V>>>;

/// An iterator over all of the items in the ABI.
///
Expand All @@ -169,7 +225,7 @@ impl<'a> Iterator for Items<'a> {

iter_impl!(traits Items<'_>);

type FlattenIntoValues<V> = iter::Flatten<btree_map::IntoValues<String, Vec<V>>>;
type FlattenIntoValues<V> = Flatten<btree_map::IntoValues<String, Vec<V>>>;

/// An iterator over all of the items in the ABI.
///
Expand Down
12 changes: 12 additions & 0 deletions crates/json-abi/src/internal_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,18 @@ impl<'de> Visitor<'de> for ItVisitor {
E::invalid_value(serde::de::Unexpected::Str(v), &"a valid internal type")
})
}

fn visit_str<E>(self, _v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
// `from_reader` copies the bytes into a Vec before calling this
// method. Because the lifetime is unspecified, we can't borrow from it.
// As a result, we don't support `from_reader`.
Err(serde::de::Error::custom(
"Using serde_json::from_reader is not supported. Instead, buffer the reader contents into a string, as in alloy_json_abi::JsonAbi::load.",
))
}
}

#[cfg(test)]
Expand Down
111 changes: 107 additions & 4 deletions crates/json-abi/tests/test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloy_json_abi::{AbiItem, Error, EventParam, JsonAbi, Param};
use std::{collections::HashMap, fs::File, io::BufReader};

#[test]
fn complex_error() {
Expand Down Expand Up @@ -35,7 +36,7 @@ macro_rules! abi_parse_tests {
($($name:ident($path:literal, $len:literal))*) => {$(
#[test]
fn $name() {
parse_test(include_str!($path), $len);
parse_test(include_str!($path), $len, $path);
}
)*};
}
Expand All @@ -52,23 +53,55 @@ abi_parse_tests! {
udvts("abi/Udvts.json", 1)
}

fn parse_test(s: &str, len: usize) {
#[test]
fn test_constructor() {
// Parse the ABI JSON file
let abi_items_wo_constructor = include_str!("abi/Abiencoderv2Test.json");
let abi_items_w_constructor = include_str!("abi/Seaport.json");

let abi_wo_constructor: JsonAbi =
serde_json::from_str(abi_items_wo_constructor).expect("Failed to parse ABI JSON string");
let abi_w_constructor: JsonAbi =
serde_json::from_str(abi_items_w_constructor).expect("Failed to parse ABI JSON string");

// Check that the ABI JSON file has no constructor
assert!(abi_wo_constructor.constructor().is_none());

// Check that the ABI JSON file has a constructor
assert!(abi_w_constructor.constructor().is_some());
}

#[cfg(feature = "std")]
fn load_test(path: &str, abi: &JsonAbi) {
let file_path: String = format!("tests/{}", path);
let file: File = File::open(file_path).unwrap();
let buffer: BufReader<File> = BufReader::new(file);
let loaded_abi: JsonAbi = JsonAbi::load(buffer).unwrap();

assert_eq!(*abi, loaded_abi);
}

fn parse_test(s: &str, len: usize, path: &str) {
let abi_items: Vec<AbiItem<'_>> = serde_json::from_str(s).unwrap();
assert_eq!(abi_items.len(), len);

let json = serde_json::to_string(&abi_items).unwrap();
let json: String = serde_json::to_string(&abi_items).unwrap();
let abi1: JsonAbi = serde_json::from_str(&json).unwrap();

let abi2: JsonAbi = serde_json::from_str(s).unwrap();

assert_eq!(len, abi2.len());
assert_eq!(abi1, abi2);

let json = serde_json::to_string(&abi2).unwrap();
#[cfg(feature = "std")]
load_test(path, &abi1);

let json: String = serde_json::to_string(&abi2).unwrap();
let abi3: JsonAbi = serde_json::from_str(&json).unwrap();
assert_eq!(abi2, abi3);

param_tests(&abi1);
method_tests(&abi1);

iterator_test(abi1.items(), abi1.items().rev(), len);
iterator_test(abi1.items().skip(1), abi1.items().skip(1).rev(), len - 1);
Expand Down Expand Up @@ -102,6 +135,62 @@ fn param_tests(abi: &JsonAbi) {
})
}

fn method_tests(abi: &JsonAbi) {
test_functions(abi);
test_events(abi);
test_errors(abi);
}

fn test_functions(abi: &JsonAbi) {
abi.functions().for_each(|f| {
f.inputs.iter().for_each(test_param);
f.outputs.iter().for_each(test_param);
});

abi.functions()
.map(|f| f.name.clone())
.fold(HashMap::new(), |mut freq_count, name| {
*freq_count.entry(name).or_insert(0) += 1;
freq_count
})
.into_iter()
.for_each(|(name, freq)| {
assert_eq!(abi.function(&name).unwrap().len(), freq);
});
}

fn test_errors(abi: &JsonAbi) {
abi.errors()
.for_each(|e| e.inputs.iter().for_each(test_param));

abi.errors()
.map(|e| e.name.clone())
.fold(HashMap::new(), |mut freq_count, name| {
*freq_count.entry(name).or_insert(0) += 1;
freq_count
})
.into_iter()
.for_each(|(name, freq)| {
assert_eq!(abi.error(&name).unwrap().len(), freq);
});
}

fn test_events(abi: &JsonAbi) {
abi.events()
.for_each(|e| e.inputs.iter().for_each(test_event_param));

abi.events()
.map(|e| e.name.clone())
.fold(HashMap::new(), |mut freq_count, name| {
*freq_count.entry(name).or_insert(0) += 1;
freq_count
})
.into_iter()
.for_each(|(name, freq)| {
assert_eq!(abi.event(&name).unwrap().len(), freq);
});
}

fn test_event_param(param: &EventParam) {
if param.components.is_empty() {
assert!(!param.ty.contains("tuple"));
Expand Down Expand Up @@ -131,3 +220,17 @@ fn test_param(param: &Param) {

param.components.iter().for_each(test_param);
}

#[test]
fn no_from_reader() {
let path = "abi/Abiencoderv2Test.json";
let file_path: String = format!("tests/{}", path);
let file: File = File::open(file_path).unwrap();
let buffer: BufReader<File> = BufReader::new(file);

let res = serde_json::from_reader::<_, JsonAbi>(buffer);
assert!(res.is_err());
assert!(
format!("{}", res.unwrap_err()).contains("Using serde_json::from_reader is not supported.")
);
}