Skip to content

Commit

Permalink
feat(json-abi): support legacy JSON ABIs (#596)
Browse files Browse the repository at this point in the history
feat(json-abi): support legacy JSON ABI
  • Loading branch information
DaniPopes authored Apr 9, 2024
1 parent c903cdf commit 2723294
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 57 deletions.
6 changes: 0 additions & 6 deletions crates/json-abi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,3 @@ for item in abi.items() {
println!("{item:?}");
}
```

Resolve a `Function`'s input type with [`alloy-dyn-abi`](../dyn-abi):

```rust,ignore
todo!()
```
12 changes: 5 additions & 7 deletions crates/json-abi/src/item.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{param::Param, utils::*, EventParam, StateMutability};
use crate::{param::Param, serde_state_mutability_compat, utils::*, EventParam, StateMutability};
use alloc::{borrow::Cow, string::String, vec::Vec};
use alloy_primitives::{keccak256, Selector, B256};
use core::str::FromStr;
Expand Down Expand Up @@ -66,23 +66,23 @@ abi_items! {
/// The input types of the constructor. May be empty.
pub inputs: Vec<Param>,
/// The state mutability of the constructor.
#[serde(default)]
#[serde(default, flatten, with = "serde_state_mutability_compat")]
pub state_mutability: StateMutability,
}

/// A JSON ABI fallback function.
#[derive(Copy)]
pub struct Fallback: "fallback" {
/// The state mutability of the fallback function.
#[serde(default)]
#[serde(default, flatten, with = "serde_state_mutability_compat")]
pub state_mutability: StateMutability,
}

/// A JSON ABI receive function.
#[derive(Copy)]
pub struct Receive: "receive" {
/// The state mutability of the receive function.
#[serde(default)]
#[serde(default, flatten, with = "serde_state_mutability_compat")]
pub state_mutability: StateMutability,
}

Expand All @@ -96,9 +96,7 @@ abi_items! {
/// The output types of the function. May be empty.
pub outputs: Vec<Param>,
/// The state mutability of the function.
///
/// By default this is [StateMutability::NonPayable] which is reflected in Solidity by not specifying a state mutability modifier at all. This field was introduced in 0.4.16: <https://github.com/ethereum/solidity/releases/tag/v0.4.16>
#[serde(default)]
#[serde(default, flatten, with = "serde_state_mutability_compat")]
pub state_mutability: StateMutability,
}

Expand Down
40 changes: 3 additions & 37 deletions crates/json-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ extern crate alloc;

pub extern crate alloy_sol_type_parser as parser;

use serde::{Deserialize, Serialize};

mod abi;
pub use abi::{ContractObject, IntoItems, Items, JsonAbi};

Expand All @@ -43,45 +41,13 @@ 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};

mod internal_type;
pub use internal_type::InternalType;

mod to_sol;
pub use to_sol::ToSolConfig;

pub(crate) mod utils;

/// A JSON ABI function's state mutability.
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum StateMutability {
/// Pure functions promise not to read from or modify the state.
Pure,
/// View functions promise not to modify the state.
View,
/// Nonpayable functions promise not to receive Ether.
///
/// This is the solidity default: <https://docs.soliditylang.org/en/latest/abi-spec.html#json>
///
/// The state mutability nonpayable is reflected in Solidity by not specifying a state
/// mutability modifier at all.
#[default]
NonPayable,
/// Payable functions make no promises.
Payable,
}

impl StateMutability {
/// Returns the string representation of the state mutability.
#[inline]
pub const fn as_str(self) -> Option<&'static str> {
match self {
Self::Pure => Some("pure"),
Self::View => Some("view"),
Self::Payable => Some("payable"),
Self::NonPayable => None,
}
}
}
183 changes: 183 additions & 0 deletions crates/json-abi/src/state_mutability.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use serde::{Deserialize, Serialize};

const COMPAT_ERROR: &str = "state mutability cannot be both `payable` and `constant`";

/// A JSON ABI function's state mutability.
///
/// This will serialize/deserialize as the `stateMutability` JSON ABI field's value, see
/// [`as_json_str`](Self::as_json_str).
/// For backwards compatible deserialization, see [`serde_state_mutability_compat`].
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum StateMutability {
/// Pure functions promise not to read from or modify the state.
Pure,
/// View functions promise not to modify the state.
View,
/// Nonpayable functions promise not to receive Ether.
///
/// This is the solidity default: <https://docs.soliditylang.org/en/latest/abi-spec.html#json>
///
/// The state mutability nonpayable is reflected in Solidity by not specifying a state
/// mutability modifier at all.
#[default]
NonPayable,
/// Payable functions make no promises.
Payable,
}

impl StateMutability {
/// Returns the string representation of the state mutability.
#[inline]
pub const fn as_str(self) -> Option<&'static str> {
if let Self::NonPayable = self {
None
} else {
Some(self.as_json_str())
}
}

/// Returns the string representation of the state mutability when serialized to JSON.
#[inline]
pub const fn as_json_str(self) -> &'static str {
match self {
Self::Pure => "pure",
Self::View => "view",
Self::NonPayable => "nonpayable",
Self::Payable => "payable",
}
}
}

/// [`serde`] implementation for [`StateMutability`] for backwards compatibility with older
/// versions of the JSON ABI.
///
/// In particular, this will deserialize the `stateMutability` field if it is present,
/// and otherwise fall back to the deprecated `constant` and `payable` fields.
///
/// Since it must be used in combination with `#[serde(flatten)]`, a `serialize` implementation
/// is also provided, which will always serialize the `stateMutability` field.
///
/// # Examples
///
/// Usage: `#[serde(default, flatten, with = "serde_state_mutability_compat")]` on a
/// [`StateMutability`] struct field.
///
/// ```rust
/// use alloy_json_abi::{serde_state_mutability_compat, StateMutability};
/// use serde::{Deserialize, Serialize};
///
/// #[derive(Serialize, Deserialize)]
/// #[serde(rename_all = "camelCase")]
/// struct MyStruct {
/// #[serde(default, flatten, with = "serde_state_mutability_compat")]
/// state_mutability: StateMutability,
/// }
///
/// let json = r#"{"constant":true,"payable":false}"#;
/// let ms = serde_json::from_str::<MyStruct>(json).expect("failed deserializing");
/// assert_eq!(ms.state_mutability, StateMutability::View);
///
/// let reserialized = serde_json::to_string(&ms).expect("failed reserializing");
/// assert_eq!(reserialized, r#"{"stateMutability":"view"}"#);
/// ```
pub mod serde_state_mutability_compat {
use super::*;
use serde::ser::SerializeStruct;

/// Deserializes a [`StateMutability`], compatibile with older JSON ABI versions.
///
/// See [the module-level documentation](self) for more information.
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<StateMutability, D::Error> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct StateMutabilityCompat {
#[serde(default)]
state_mutability: Option<StateMutability>,
#[serde(default)]
payable: Option<bool>,
#[serde(default)]
constant: Option<bool>,
}

impl StateMutabilityCompat {
fn flatten(self) -> Option<StateMutability> {
let Self { state_mutability, payable, constant } = self;
if state_mutability.is_some() {
return state_mutability;
}
match (payable.unwrap_or(false), constant.unwrap_or(false)) {
(false, false) => Some(StateMutability::default()),
(true, false) => Some(StateMutability::Payable),
(false, true) => Some(StateMutability::View),
(true, true) => None,
}
}
}

StateMutabilityCompat::deserialize(deserializer).and_then(|compat| {
compat.flatten().ok_or_else(|| serde::de::Error::custom(COMPAT_ERROR))
})
}

/// Serializes a [`StateMutability`] as a single-field struct (`stateMutability`).
///
/// See [the module-level documentation](self) for more information.
pub fn serialize<S: serde::Serializer>(
state_mutability: &StateMutability,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("StateMutability", 1)?;
s.serialize_field("stateMutability", state_mutability)?;
s.end()
}
}

#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;

#[derive(Debug, Serialize, Deserialize)]
struct CompatTest {
#[serde(default, flatten, with = "serde_state_mutability_compat")]
sm: StateMutability,
}

#[test]
fn test_compat() {
let test = |expect: StateMutability, json: &str| {
let compat = serde_json::from_str::<CompatTest>(json).expect(json);
assert_eq!(compat.sm, expect, "{json:?}");

let re_ser = serde_json::to_string(&compat).expect(json);
let expect = format!(r#"{{"stateMutability":"{}"}}"#, expect.as_json_str());
assert_eq!(re_ser, expect, "{json:?}");
};

test(StateMutability::Pure, r#"{"stateMutability":"pure"}"#);
test(
StateMutability::Pure,
r#"{"stateMutability":"pure","constant":false,"payable":false}"#,
);

test(StateMutability::View, r#"{"constant":true}"#);
test(StateMutability::View, r#"{"constant":true,"payable":false}"#);

test(StateMutability::Payable, r#"{"payable":true}"#);
test(StateMutability::Payable, r#"{"constant":false,"payable":true}"#);

test(StateMutability::NonPayable, r#"{}"#);
test(StateMutability::NonPayable, r#"{"constant":false}"#);
test(StateMutability::NonPayable, r#"{"payable":false}"#);
test(StateMutability::NonPayable, r#"{"constant":false,"payable":false}"#);

let json = r#"{"constant":true,"payable":true}"#;
let e = serde_json::from_str::<CompatTest>(json).unwrap_err().to_string();
assert!(e.contains(COMPAT_ERROR), "{e:?}");
}
}
4 changes: 3 additions & 1 deletion crates/json-abi/tests/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ fn to_sol_test(path: &str, abi: &JsonAbi, run_solc: bool) {
if matches!(
name,
// https://github.com/alloy-rs/core/issues/349
"ZeroXExchange" | "GaugeController" | "DoubleExponentInterestSetter" | "NamelessParams"
|"ZeroXExchange"| "GaugeController" | "DoubleExponentInterestSetter" | "NamelessParams"
// UniswapV1Exchange has return values with the same name.
| "UniswapV1Exchange"
) {
return;
}
Expand Down
Loading

0 comments on commit 2723294

Please sign in to comment.