Skip to content

Commit

Permalink
feat: add support for parsing visibility and state mutability (#682)
Browse files Browse the repository at this point in the history
* feat: add support for state mutability parsing

* feat: finish the implementation

* docs

* doc

* more tests

* features

---------

Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
  • Loading branch information
mattsse and DaniPopes authored Jun 27, 2024
1 parent 69db111 commit 56de57a
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 86 deletions.
9 changes: 7 additions & 2 deletions crates/json-abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ exclude.workspace = true

[dependencies]
alloy-primitives = { workspace = true, features = ["serde"] }
alloy-sol-type-parser.workspace = true
alloy-sol-type-parser = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }

Expand All @@ -27,7 +27,12 @@ serde_json.workspace = true

[features]
default = ["std"]
std = ["serde/std", "alloy-primitives/std", "alloy-sol-type-parser/std", "serde_json?/std"]
std = [
"serde/std",
"alloy-primitives/std",
"alloy-sol-type-parser/std",
"serde_json?/std",
]
serde_json = ["dep:serde_json"]

[[bench]]
Expand Down
76 changes: 58 additions & 18 deletions crates/json-abi/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{param::Param, serde_state_mutability_compat, utils::*, EventParam, S
use alloc::{borrow::Cow, string::String, vec::Vec};
use alloy_primitives::{keccak256, Selector, B256};
use core::str::FromStr;
use parser::utils::ParsedSignature;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Declares all JSON ABI items.
Expand Down Expand Up @@ -328,12 +329,12 @@ impl FromStr for Constructor {
}

impl Constructor {
/// Parses a Solidity constructor string: `constructor($($inputs),*)`
/// Parses a Solidity constructor string:
/// `constructor($($inputs),*) [visibility] [s_mutability]`
///
/// Note:
/// - the name must always be `constructor`
/// - [`state_mutability`](Self::state_mutability) is currently not parsed from the input and is
/// always set to [`StateMutability::NonPayable`]
/// - visibility is ignored
///
/// # Examples
///
Expand All @@ -352,7 +353,8 @@ impl Constructor {
parse_sig::<false>(s).and_then(Self::parsed)
}

fn parsed((name, inputs, outputs, anonymous): ParseSigTuple<Param>) -> parser::Result<Self> {
fn parsed(sig: ParsedSignature<Param>) -> parser::Result<Self> {
let ParsedSignature { name, inputs, outputs, anonymous, state_mutability } = sig;
if name != "constructor" {
return Err(parser::Error::new("constructors' name must be exactly \"constructor\""));
}
Expand All @@ -362,7 +364,7 @@ impl Constructor {
if anonymous {
return Err(parser::Error::new("constructors cannot be anonymous"));
}
Ok(Self { inputs, state_mutability: StateMutability::NonPayable })
Ok(Self { inputs, state_mutability: state_mutability.unwrap_or_default() })
}
}

Expand Down Expand Up @@ -398,13 +400,17 @@ impl Error {
parse_maybe_prefixed(s, "error", parse_sig::<false>).and_then(Self::parsed)
}

fn parsed((name, inputs, outputs, anonymous): ParseSigTuple<Param>) -> parser::Result<Self> {
fn parsed(sig: ParsedSignature<Param>) -> parser::Result<Self> {
let ParsedSignature { name, inputs, outputs, anonymous, state_mutability } = sig;
if !outputs.is_empty() {
return Err(parser::Error::new("errors cannot have outputs"));
}
if anonymous {
return Err(parser::Error::new("errors cannot be anonymous"));
}
if state_mutability.is_some() {
return Err(parser::Error::new("errors cannot have mutability"));
}
Ok(Self { name, inputs })
}

Expand Down Expand Up @@ -434,12 +440,10 @@ impl FromStr for Function {

impl Function {
/// Parses a Solidity function signature string:
/// `$(function)? $name($($inputs),*) $(returns ($($outputs),+))?`
/// `$(function)? $name($($inputs),*) [visibility] [s_mutability] $(returns ($($outputs),+))?`
///
/// Note:
/// - [`state_mutability`](Self::state_mutability) is currently not parsed from the input and is
/// always set to [`StateMutability::NonPayable`]
/// - visibility is rejected
/// - visibility is ignored
///
/// If you want to parse a generic [Human-Readable ABI] string, use [`AbiItem::parse`].
///
Expand Down Expand Up @@ -467,12 +471,12 @@ impl Function {
/// ```
/// # use alloy_json_abi::{Function, Param, StateMutability};
/// assert_eq!(
/// Function::parse("function toString(uint number) returns (string s)"),
/// Function::parse("function toString(uint number) external view returns (string s)"),
/// Ok(Function {
/// name: "toString".to_string(),
/// inputs: vec![Param::parse("uint number").unwrap()],
/// outputs: vec![Param::parse("string s").unwrap()],
/// state_mutability: StateMutability::NonPayable,
/// state_mutability: StateMutability::View,
/// }),
/// );
/// ```
Expand All @@ -481,11 +485,12 @@ impl Function {
parse_maybe_prefixed(s, "function", parse_sig::<true>).and_then(Self::parsed)
}

fn parsed((name, inputs, outputs, anonymous): ParseSigTuple<Param>) -> parser::Result<Self> {
fn parsed(sig: ParsedSignature<Param>) -> parser::Result<Self> {
let ParsedSignature { name, inputs, outputs, anonymous, state_mutability } = sig;
if anonymous {
return Err(parser::Error::new("function cannot be anonymous"));
return Err(parser::Error::new("functions cannot be anonymous"));
}
Ok(Self { name, inputs, outputs, state_mutability: StateMutability::NonPayable })
Ok(Self { name, inputs, outputs, state_mutability: state_mutability.unwrap_or_default() })
}

/// Returns this function's signature: `$name($($inputs),*)`.
Expand Down Expand Up @@ -561,12 +566,14 @@ impl Event {
parse_maybe_prefixed(s, "event", parse_event_sig).and_then(Self::parsed)
}

fn parsed(
(name, inputs, outputs, anonymous): ParseSigTuple<EventParam>,
) -> parser::Result<Self> {
fn parsed(sig: ParsedSignature<EventParam>) -> parser::Result<Self> {
let ParsedSignature { name, inputs, outputs, anonymous, state_mutability } = sig;
if !outputs.is_empty() {
return Err(parser::Error::new("events cannot have outputs"));
}
if state_mutability.is_some() {
return Err(parser::Error::new("events cannot have state mutability"));
}
Ok(Self { name, inputs, anonymous })
}

Expand Down Expand Up @@ -606,6 +613,14 @@ impl Event {
mod tests {
use super::*;

// fn param(kind: &str) -> Param {
// param2(kind, "param")
// }

fn param2(kind: &str, name: &str) -> Param {
Param { ty: kind.into(), name: name.into(), internal_type: None, components: vec![] }
}

#[test]
fn parse_prefixes() {
for prefix in ["function", "error", "event"] {
Expand Down Expand Up @@ -648,4 +663,29 @@ mod tests {
assert_eq!(Error::parse("errorfoo()"), Ok(new("errorfoo")));
assert_eq!(Error::parse("error errorfoo()"), Ok(new("errorfoo")));
}

#[test]
fn parse_full() {
// https://github.com/alloy-rs/core/issues/389
assert_eq!(
Function::parse("function foo(uint256 a, uint256 b) external returns (uint256)"),
Ok(Function {
name: "foo".into(),
inputs: vec![param2("uint256", "a"), param2("uint256", "b")],
outputs: vec![param2("uint256", "")],
state_mutability: StateMutability::NonPayable,
})
);

// https://github.com/alloy-rs/core/issues/681
assert_eq!(
Function::parse("function balanceOf(address owner) view returns (uint256 balance)"),
Ok(Function {
name: "balanceOf".into(),
inputs: vec![param2("address", "owner")],
outputs: vec![param2("uint256", "balance")],
state_mutability: StateMutability::View,
})
);
}
}
3 changes: 1 addition & 2 deletions crates/json-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ pub use item::{AbiItem, Constructor, Error, Event, Fallback, Function, Receive};
mod param;
pub use param::{EventParam, Param};

mod state_mutability;
pub use state_mutability::{serde_state_mutability_compat, StateMutability};
pub use parser::{serde_state_mutability_compat, StateMutability};

mod internal_type;
pub use internal_type::InternalType;
Expand Down
93 changes: 47 additions & 46 deletions crates/json-abi/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use crate::{EventParam, Param, StateMutability};
use alloc::{
string::{String, ToString},
vec::Vec,
};
use alloc::string::{String, ToString};
use alloy_primitives::Selector;
use core::{fmt::Write, num::NonZeroUsize};
use parser::{ParameterSpecifier, TypeSpecifier, TypeStem};
use parser::{utils::ParsedSignature, ParameterSpecifier, TypeSpecifier, TypeStem};

/// Capacity to allocate per [Param].
const PARAM: usize = 32;
Expand Down Expand Up @@ -160,9 +157,6 @@ pub(crate) fn selector(preimage: &str) -> Selector {
}
}

pub(crate) type ParseSigTuple<T> = (String, Vec<T>, Vec<T>, bool);
pub(crate) type ParseSigResult<T> = parser::Result<ParseSigTuple<T>>;

/// Strips `prefix` from `s` before parsing with `parser`. `prefix` must be followed by whitespace.
pub(crate) fn parse_maybe_prefixed<F: FnOnce(&str) -> R, R>(
mut s: &str,
Expand All @@ -178,12 +172,12 @@ pub(crate) fn parse_maybe_prefixed<F: FnOnce(&str) -> R, R>(
}

#[inline]
pub(crate) fn parse_sig<const O: bool>(s: &str) -> ParseSigResult<Param> {
pub(crate) fn parse_sig<const O: bool>(s: &str) -> parser::Result<ParsedSignature<Param>> {
parser::utils::parse_signature::<O, _, _>(s, |p| mk_param(p.name, p.ty))
}

#[inline]
pub(crate) fn parse_event_sig(s: &str) -> ParseSigResult<EventParam> {
pub(crate) fn parse_event_sig(s: &str) -> parser::Result<ParsedSignature<EventParam>> {
parser::utils::parse_signature::<false, _, _>(s, mk_eparam)
}

Expand Down Expand Up @@ -424,65 +418,72 @@ mod tests {
}

#[test]
fn test_item_parse() {
assert_eq!(parse_sig::<true>("foo()"), Ok(("foo".into(), vec![], vec![], false)));
assert_eq!(parse_sig::<true>("foo()()"), Ok(("foo".into(), vec![], vec![], false)));
assert_eq!(parse_sig::<true>("foo() \t ()"), Ok(("foo".into(), vec![], vec![], false)));
assert_eq!(parse_sig::<true>("foo() ()"), Ok(("foo".into(), vec![], vec![], false)));
fn test_parse_sig() {
let empty_sig = |name: &str, anonymous| ParsedSignature::<Param> {
name: name.into(),
inputs: vec![],
outputs: vec![],
anonymous,
state_mutability: None,
};
let sig = |name: &str, inputs, outputs| ParsedSignature::<Param> {
name: name.into(),
inputs,
outputs,
anonymous: false,
state_mutability: None,
};

assert_eq!(parse_sig::<false>("foo()"), Ok(("foo".into(), vec![], vec![], false)));
assert_eq!(parse_sig::<true>("foo()"), Ok(empty_sig("foo", false)));
assert_eq!(parse_sig::<true>("foo()()"), Ok(empty_sig("foo", false)));
assert_eq!(parse_sig::<true>("foo()external()"), Ok(empty_sig("foo", false)));
assert_eq!(parse_sig::<true>("foo() \t ()"), Ok(empty_sig("foo", false)));
assert_eq!(parse_sig::<true>("foo() ()"), Ok(empty_sig("foo", false)));

assert_eq!(parse_sig::<false>("foo()"), Ok(empty_sig("foo", false)));
parse_sig::<false>("foo()()").unwrap_err();
parse_sig::<false>("foo()view external()").unwrap_err();
parse_sig::<false>("foo(,)()").unwrap_err();
parse_sig::<false>("foo(,)(,)").unwrap_err();

assert_eq!(parse_sig::<false>("foo()anonymous"), Ok(("foo".into(), vec![], vec![], true)));
assert_eq!(
parse_sig::<false>("foo()\t anonymous"),
Ok(("foo".into(), vec![], vec![], true))
);
assert_eq!(parse_sig::<false>("foo()anonymous"), Ok(empty_sig("foo", true)));
assert_eq!(parse_sig::<false>("foo()\t anonymous"), Ok(empty_sig("foo", true)));

assert_eq!(parse_sig::<true>("foo()anonymous"), Ok(("foo".into(), vec![], vec![], true)));
assert_eq!(
parse_sig::<true>("foo()\t anonymous"),
Ok(("foo".into(), vec![], vec![], true))
);
assert_eq!(parse_sig::<true>("foo()anonymous"), Ok(empty_sig("foo", true)));
assert_eq!(parse_sig::<true>("foo()\t anonymous"), Ok(empty_sig("foo", true)));

assert_eq!(
parse_sig::<true>("foo() \t ()anonymous"),
Ok(("foo".into(), vec![], vec![], true))
);
assert_eq!(parse_sig::<true>("foo()()anonymous"), Ok(("foo".into(), vec![], vec![], true)));
assert_eq!(
parse_sig::<true>("foo()()\t anonymous"),
Ok(("foo".into(), vec![], vec![], true))
);
assert_eq!(parse_sig::<true>("foo() \t ()anonymous"), Ok(empty_sig("foo", true)));
assert_eq!(parse_sig::<true>("foo()()anonymous"), Ok(empty_sig("foo", true)));
assert_eq!(parse_sig::<true>("foo()()\t anonymous"), Ok(empty_sig("foo", true)));

assert_eq!(
parse_sig::<false>("foo(uint256 param)"),
Ok(("foo".into(), vec![param("uint256")], vec![], false))
Ok(sig("foo", vec![param("uint256")], vec![]))
);
assert_eq!(
parse_sig::<false>("bar(uint256 param)"),
Ok(("bar".into(), vec![param("uint256")], vec![], false))
Ok(sig("bar", vec![param("uint256")], vec![]))
);
assert_eq!(
parse_sig::<false>("baz(uint256 param, bool param)"),
Ok(("baz".into(), vec![param("uint256"), param("bool")], vec![], false))
Ok(sig("baz", vec![param("uint256"), param("bool")], vec![]))
);

assert_eq!(
parse_sig::<true>("f(a b)(c d)"),
Ok(("f".into(), vec![param2("a", "b")], vec![param2("c", "d")], false))
Ok(sig("f", vec![param2("a", "b")], vec![param2("c", "d")]))
);

assert_eq!(
parse_sig::<true>("toString(uint number)(string s)"),
Ok((
"toString".into(),
vec![param2("uint256", "number")],
vec![param2("string", "s")],
false
))
)
Ok(sig("toString", vec![param2("uint256", "number")], vec![param2("string", "s")]))
);

let mut sig_full = sig("toString", vec![param("uint256")], vec![param("string")]);
sig_full.state_mutability = Some(StateMutability::View);
assert_eq!(
parse_sig::<true>("toString(uint param) external view returns(string param)"),
Ok(sig_full)
);
}
}
5 changes: 5 additions & 0 deletions crates/sol-type-parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ workspace = true

[dependencies]
winnow.workspace = true
serde = { workspace = true, optional = true, features = ["derive"] }

[dev-dependencies]
serde_json.workspace = true

[features]
default = ["std"]
std = ["winnow/std"]
serde = ["dep:serde"]
debug = ["std"] # intentionally doesn't enable `winnow/debug`
5 changes: 5 additions & 0 deletions crates/sol-type-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ pub use parameter::{ParameterSpecifier, Parameters, Storage};

/// Generic [`winnow`] parsing utilities.
pub mod utils;

mod state_mutability;
#[cfg(feature = "serde")]
pub use state_mutability::serde_state_mutability_compat;
pub use state_mutability::StateMutability;
Loading

0 comments on commit 56de57a

Please sign in to comment.