Skip to content

Commit

Permalink
feat(sol-macro): support namespaces (#694)
Browse files Browse the repository at this point in the history
* wip

* wip

* feat: implement namespaces on `sol!` side (#693)

* respect namespaces

* un-ignore test

* clippy

* clippy

* fix doc

* rm debug prints

* chore: cleanup

* fix: correctly expand derives for namespaced JSON outputs (#704)

* fix

* newline

* chore: clippy

* chore: use mem::replace

* fix: respect namespaces when resolving overloads

* test: add regression test

* chore: link the test

---------

Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
  • Loading branch information
DaniPopes and klkvr authored Aug 12, 2024
1 parent 66efe6e commit b4ca4fe
Show file tree
Hide file tree
Showing 21 changed files with 532 additions and 340 deletions.
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

0 comments on commit b4ca4fe

Please sign in to comment.