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(sol-macro): support namespaces #694

Merged
merged 12 commits into from
Aug 12, 2024
4 changes: 2 additions & 2 deletions crates/dyn-abi/src/eip712/typed_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ mod tests {

let typed_data: TypedData = serde_json::from_value(json).unwrap();

assert_eq!(typed_data.eip712_signing_hash(), Err(Error::CircularDependency("Mail".into())),);
assert_eq!(typed_data.eip712_signing_hash(), Err(Error::CircularDependency("Mail".into())));
}

#[test]
Expand Down Expand Up @@ -677,7 +677,7 @@ mod tests {
let s = MyStruct { name: "hello".to_string(), otherThing: "world".to_string() };

let typed_data = TypedData::from_struct(&s, None);
assert_eq!(typed_data.encode_type().unwrap(), "MyStruct(string name,string otherThing)",);
assert_eq!(typed_data.encode_type().unwrap(), "MyStruct(string name,string otherThing)");

assert!(typed_data.resolver.contains_type_name("EIP712Domain"));
}
Expand Down
16 changes: 2 additions & 14 deletions crates/json-abi/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,20 +208,8 @@ impl JsonAbi {
///
/// See [`to_sol`](JsonAbi::to_sol) for more information.
pub fn to_sol_raw(&self, name: &str, out: &mut String, config: Option<ToSolConfig>) {
let len = self.len();
out.reserve(len * 128);

out.push_str("interface ");
if !name.is_empty() {
out.push_str(name);
out.push(' ');
}
out.push('{');
if len > 0 {
out.push('\n');
SolPrinter::new(out, config.unwrap_or_default()).print(self);
}
out.push('}');
out.reserve(self.len() * 128);
SolPrinter::new(out, name, config.unwrap_or_default()).print(self);
}

/// Deduplicates all functions, errors, and events which have the same name and inputs.
Expand Down
146 changes: 108 additions & 38 deletions crates/json-abi/src/to_sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use crate::{
item::{Constructor, Error, Event, Fallback, Function, Receive},
EventParam, InternalType, JsonAbi, Param, StateMutability,
};
use alloc::{collections::BTreeSet, string::String, vec::Vec};
use alloc::{
collections::{BTreeMap, BTreeSet},
string::String,
vec::Vec,
};
use core::{
cmp::Ordering,
ops::{Deref, DerefMut},
Expand All @@ -13,6 +17,7 @@ use core::{
#[allow(missing_copy_implementations)] // Future-proofing
pub struct ToSolConfig {
print_constructors: bool,
enums_as_udvt: bool,
}

impl Default for ToSolConfig {
Expand All @@ -26,7 +31,7 @@ impl ToSolConfig {
/// Creates a new configuration with default settings.
#[inline]
pub const fn new() -> Self {
Self { print_constructors: false }
Self { print_constructors: false, enums_as_udvt: true }
}

/// Sets whether to print constructors. Default: `false`.
Expand All @@ -35,6 +40,14 @@ impl ToSolConfig {
self.print_constructors = yes;
self
}

/// Sets whether to print `enum`s as user-defined value types (UDVTs) instead of `uint8`.
/// Default: `true`.
#[inline]
pub const fn enums_as_udvt(mut self, yes: bool) -> Self {
self.enums_as_udvt = yes;
self
}
}

pub(crate) trait ToSol {
Expand All @@ -45,9 +58,12 @@ pub(crate) struct SolPrinter<'a> {
/// The buffer to write to.
s: &'a mut String,

/// The name of the current library/interface being printed.
name: &'a str,

/// Whether to emit `memory` when printing parameters.
/// This is set to `true` when printing functions so that we emit valid Solidity.
emit_param_location: bool,
print_param_location: bool,

/// Configuration.
config: ToSolConfig,
Expand All @@ -70,26 +86,22 @@ impl DerefMut for SolPrinter<'_> {
}

impl<'a> SolPrinter<'a> {
#[inline]
pub(crate) fn new(s: &'a mut String, config: ToSolConfig) -> Self {
Self { s, emit_param_location: false, config }
pub(crate) fn new(s: &'a mut String, name: &'a str, config: ToSolConfig) -> Self {
Self { s, name, print_param_location: false, config }
}

#[inline]
pub(crate) fn print<T: ToSol>(&mut self, value: &T) {
value.to_sol(self);
pub(crate) fn print(&mut self, abi: &'a JsonAbi) {
abi.to_sol_root(self);
}

#[inline]
fn indent(&mut self) {
self.push_str(" ");
}
}

impl ToSol for JsonAbi {
impl JsonAbi {
#[allow(unknown_lints, for_loops_over_fallibles)]
#[inline]
fn to_sol(&self, out: &mut SolPrinter<'_>) {
fn to_sol_root<'a>(&'a self, out: &mut SolPrinter<'a>) {
macro_rules! fmt {
($iter:expr) => {
let mut any = false;
Expand All @@ -105,9 +117,35 @@ impl ToSol for JsonAbi {
};
}

let mut its = InternalTypes::new();
let mut its = InternalTypes::new(out.name, out.config.enums_as_udvt);
its.visit_abi(self);
fmt!(its.0);

for (name, its) in &its.other {
if its.is_empty() {
continue;
}
out.push_str("library ");
out.push_str(name);
out.push_str(" {\n");
let prev = core::mem::replace(&mut out.name, name);
for it in its {
out.indent();
it.to_sol(out);
out.push('\n');
}
out.name = prev;
out.push_str("}\n\n");
}

out.push_str("interface ");
if !out.name.is_empty() {
out.s.push_str(out.name);
out.push(' ');
}
out.push('{');
out.push('\n');

fmt!(its.this_its);
fmt!(self.errors());
fmt!(self.events());
if out.config.print_constructors {
Expand All @@ -117,17 +155,23 @@ impl ToSol for JsonAbi {
fmt!(self.receive);
fmt!(self.functions());
out.pop(); // trailing newline

out.push('}');
}
}

/// Recursively collects internal structs, enums, and UDVTs from an ABI's items.
struct InternalTypes<'a>(BTreeSet<It<'a>>);
struct InternalTypes<'a> {
name: &'a str,
this_its: BTreeSet<It<'a>>,
other: BTreeMap<&'a String, BTreeSet<It<'a>>>,
enums_as_udvt: bool,
}

impl<'a> InternalTypes<'a> {
#[allow(clippy::missing_const_for_fn)]
#[inline]
fn new() -> Self {
Self(BTreeSet::new())
fn new(name: &'a str, enums_as_udvt: bool) -> Self {
Self { name, this_its: BTreeSet::new(), other: BTreeMap::new(), enums_as_udvt }
}

fn visit_abi(&mut self, abi: &'a JsonAbi) {
Expand Down Expand Up @@ -176,22 +220,37 @@ impl<'a> InternalTypes<'a> {
) {
match internal_type {
None | Some(InternalType::AddressPayable(_) | InternalType::Contract(_)) => {}
Some(InternalType::Struct { contract: _, ty }) => {
self.0.insert(It::new(ty, ItKind::Struct(components)));
Some(InternalType::Struct { contract, ty }) => {
self.extend_one(contract, It::new(ty, ItKind::Struct(components)));
}
Some(InternalType::Enum { contract: _, ty }) => {
self.0.insert(It::new(ty, ItKind::Enum));
Some(InternalType::Enum { contract, ty }) => {
if self.enums_as_udvt {
self.extend_one(contract, It::new(ty, ItKind::Enum));
}
}
Some(it @ InternalType::Other { contract: _, ty }) => {
Some(it @ InternalType::Other { contract, ty }) => {
// `Other` is a UDVT if it's not a basic Solidity type and not an array
if let Some(it) = it.other_specifier() {
if it.try_basic_solidity().is_err() && !it.is_array() {
self.0.insert(It::new(ty, ItKind::Udvt(real_ty)));
self.extend_one(contract, It::new(ty, ItKind::Udvt(real_ty)));
}
}
}
}
}

fn extend_one(&mut self, contract: &'a Option<String>, it: It<'a>) {
let contract = contract.as_ref();
if let Some(contract) = contract {
if contract == self.name {
self.this_its.insert(it);
} else {
self.other.entry(contract).or_default().insert(it);
}
} else {
self.this_its.insert(it);
}
}
}

/// An internal ABI type.
Expand Down Expand Up @@ -419,7 +478,7 @@ impl<IN: ToSol> ToSol for AbiFunction<'_, IN> {
self.kw,
AbiFunctionKw::Function | AbiFunctionKw::Fallback | AbiFunctionKw::Receive
) {
out.emit_param_location = true;
out.print_param_location = true;
}

out.push_str(self.kw.as_str());
Expand Down Expand Up @@ -466,7 +525,7 @@ impl<IN: ToSol> ToSol for AbiFunction<'_, IN> {

out.push(';');

out.emit_param_location = false;
out.print_param_location = false;
}
}

Expand Down Expand Up @@ -497,22 +556,25 @@ fn param(
components: &[Param],
out: &mut SolPrinter<'_>,
) {
let mut contract_name = None::<&str>;
let mut type_name = type_name;
let storage;
if let Some(it) = internal_type {
type_name = match it {
(contract_name, type_name) = match it {
InternalType::Contract(s) => {
if let Some(start) = s.find('[') {
let ty = if let Some(start) = s.find('[') {
storage = format!("address{}", &s[start..]);
&storage
} else {
"address"
}
};
(None, ty)
}
InternalType::AddressPayable(ty)
| InternalType::Struct { ty, .. }
| InternalType::Enum { ty, .. }
| InternalType::Other { ty, .. } => ty,
InternalType::Enum { .. } if !out.config.enums_as_udvt => (None, "uint8"),
InternalType::AddressPayable(ty) => (None, &ty[..]),
InternalType::Struct { contract, ty }
| InternalType::Enum { contract, ty }
| InternalType::Other { contract, ty } => (contract.as_deref(), &ty[..]),
};
};

Expand All @@ -525,7 +587,7 @@ fn param(
// tuple types `(T, U, V, ...)`, but it's valid for `sol!`.
out.push('(');
// Don't emit `memory` for tuple components because `sol!` can't parse them.
let prev = core::mem::replace(&mut out.emit_param_location, false);
let prev = core::mem::replace(&mut out.print_param_location, false);
for (i, component) in components.iter().enumerate() {
if i > 0 {
out.push_str(", ");
Expand All @@ -539,7 +601,7 @@ fn param(
out,
);
}
out.emit_param_location = prev;
out.print_param_location = prev;
// trailing comma for single-element tuples
if components.len() == 1 {
out.push(',');
Expand All @@ -549,7 +611,15 @@ fn param(
out.push_str(rest);
}
// primitive type
_ => out.push_str(type_name),
_ => {
if let Some(contract_name) = contract_name {
if contract_name != out.name {
out.push_str(contract_name);
out.push('.');
}
}
out.push_str(type_name);
}
}

// add `memory` if required (functions)
Expand All @@ -558,7 +628,7 @@ fn param(
"bytes" | "string" => true,
s => s.ends_with(']') || !components.is_empty(),
};
if out.emit_param_location && is_memory {
if out.print_param_location && is_memory {
out.push_str(" memory");
}

Expand Down
18 changes: 11 additions & 7 deletions crates/json-abi/tests/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,25 @@ fn to_sol_test(path: &str, abi: &JsonAbi, run_solc: bool) {
}

if run_solc {
let out = Command::new("solc").arg("--abi").arg(&sol_path).output().unwrap();
let out = Command::new("solc").arg("--combined-json=abi").arg(&sol_path).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let panik = |s| -> ! { panic!("{s}\n\nstdout:\n{stdout}\n\nstderr:\n{stderr}") };
if !out.status.success() {
panik("solc failed");
}
let Some(json_str_start) = stdout.find("[{") else {
panik("no JSON");
};
let json_str = &stdout[json_str_start..];
let solc_abi = match serde_json::from_str::<JsonAbi>(json_str) {
Ok(solc_abi) => solc_abi,
let combined_json = match serde_json::from_str::<serde_json::Value>(stdout.trim()) {
Ok(j) => j,
Err(e) => panik(&format!("invalid JSON: {e}")),
};
let (_, contract) = combined_json["contracts"]
.as_object()
.unwrap()
.iter()
.find(|(k, _)| k.contains(&format!(":{name}")))
.unwrap();
let solc_abi_str = serde_json::to_string(&contract["abi"]).unwrap();
let solc_abi: JsonAbi = serde_json::from_str(&solc_abi_str).unwrap();

// Note that we don't compare the ABIs directly since the conversion is lossy, e.g.
// `internalType` fields change.
Expand Down
6 changes: 4 additions & 2 deletions crates/json-abi/tests/abi/Abiencoderv2Test.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
interface Abiencoderv2Test {
library Hello {
struct Person {
string name;
uint256 age;
}
}

function defaultPerson() external pure returns (Person memory);
interface Abiencoderv2Test {
function defaultPerson() external pure returns (Hello.Person memory);
}
Loading