From 1541e22a6a47af9131b1d52e67fa0f144469b981 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 13 Aug 2023 09:20:56 +0200 Subject: [PATCH] feat: implement abi2sol --- crates/json-abi/src/abi.rs | 90 ++++-- crates/json-abi/src/lib.rs | 2 + crates/json-abi/src/to_sol.rs | 374 ++++++++++++++++++++++++ crates/sol-macro/src/json.rs | 312 ++------------------ crates/sol-type-parser/src/type_spec.rs | 6 + 5 files changed, 476 insertions(+), 308 deletions(-) create mode 100644 crates/json-abi/src/to_sol.rs diff --git a/crates/json-abi/src/abi.rs b/crates/json-abi/src/abi.rs index 1b7d43c39b..e08de2ab97 100644 --- a/crates/json-abi/src/abi.rs +++ b/crates/json-abi/src/abi.rs @@ -33,6 +33,33 @@ pub struct JsonAbi { } impl JsonAbi { + /// Creates an empty ABI object. + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Parse the ABI json from a `str`. This is a convenience wrapper around + /// [`serde_json::from_str`]. + #[cfg(feature = "serde_json")] + pub fn from_json_str(json: &str) -> Result { + serde_json::from_str(json) + } + + /// Loads contract from json + #[cfg(all(feature = "std", feature = "serde_json"))] + pub fn load(mut reader: T) -> Result { + // 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(1024); + reader + .read_to_string(&mut json) + .map_err(serde_json::Error::io)?; + + Self::from_json_str(&json) + } + /// Returns the total number of items (of any type). pub fn len(&self) -> usize { self.constructor.is_some() as usize @@ -77,30 +104,53 @@ impl JsonAbi { } } - /// Creates constructor call builder. - pub const fn constructor(&self) -> Option<&Constructor> { - self.constructor.as_ref() + /// Formats this JSON ABI as a Solidity interface. + /// + /// The order of the definitions is not guaranteed. + /// + /// Generates: + /// + /// ```solidity + /// interface { + /// ... + /// ... + /// ... + /// ... + /// ... + /// + /// + /// ... + /// } + /// ``` + /// + /// Note that enums are going to be identical to a `uint8` UDVT, since no + /// other information about the enum is present in the ABI itself. + #[inline] + pub fn to_sol(&self, name: &str) -> String { + let mut out = String::new(); + self.to_sol_raw(name, &mut out); + out } - /// Parse the ABI json from a `str`. This is a convenience wrapper around - /// [`serde_json::from_str`]. - #[cfg(feature = "serde_json")] - pub fn from_json_str(json: &str) -> Result { - serde_json::from_str(json) + /// Formats this JSON ABI as a Solidity interface into the given string. + /// + /// See [`to_sol`](JsonAbi::to_sol) for more information. + pub fn to_sol_raw(&self, name: &str, out: &mut String) { + let len = self.len(); + out.reserve((len + 1) * 128); + + out.push_str("interface "); + out.push_str(name); + out.push('{'); + if len != 0 { + crate::to_sol::ToSol::to_sol(self, out); + } + out.push('}'); } - /// Loads contract from json - #[cfg(all(feature = "std", feature = "serde_json"))] - pub fn load(mut reader: T) -> Result { - // 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(1024); - reader - .read_to_string(&mut json) - .map_err(serde_json::Error::io)?; - - Self::from_json_str(&json) + /// Creates constructor call builder. + pub const fn constructor(&self) -> Option<&Constructor> { + self.constructor.as_ref() } /// Gets all the functions with the given name. diff --git a/crates/json-abi/src/lib.rs b/crates/json-abi/src/lib.rs index dec3cd50b8..ece39aeed8 100644 --- a/crates/json-abi/src/lib.rs +++ b/crates/json-abi/src/lib.rs @@ -43,6 +43,8 @@ pub use param::{EventParam, Param}; mod internal_type; pub use internal_type::InternalType; +mod to_sol; + pub(crate) mod utils; pub use alloy_sol_type_parser as parser; diff --git a/crates/json-abi/src/to_sol.rs b/crates/json-abi/src/to_sol.rs new file mode 100644 index 0000000000..8f284161b8 --- /dev/null +++ b/crates/json-abi/src/to_sol.rs @@ -0,0 +1,374 @@ +use crate::{ + item::{Error, Event, Fallback, Function, Receive}, + EventParam, InternalType, JsonAbi, Param, StateMutability, +}; +use alloc::{collections::BTreeSet, string::String, vec::Vec}; +use core::cmp::Ordering; + +const INDENT: &str = " "; + +pub(crate) trait ToSol { + fn to_sol(&self, out: &mut String); +} + +impl ToSol for JsonAbi { + #[allow(for_loops_over_fallibles, unknown_lints)] + #[inline] + fn to_sol(&self, out: &mut String) { + macro_rules! fmt { + ($e:expr) => { + fmt!($e, true) + }; + ($iter:expr, $sep:expr) => { + let mut any = false; + for x in $iter { + any = true; + out.push_str(INDENT); + x.to_sol(out); + out.push('\n'); + } + if $sep && any { + out.push('\n'); + } + }; + } + + let mut its = InternalTypes::new(); + its.visit_abi(self); + fmt!(its.0); + fmt!(self.errors()); + fmt!(self.events()); + fmt!(self.fallback); + fmt!(self.receive); + fmt!(self.functions(), false); + } +} + +/// Recursively collects internal structs, enums, and udvts from an ABI's items. +struct InternalTypes<'a>(BTreeSet>); + +impl<'a> InternalTypes<'a> { + #[allow(clippy::missing_const_for_fn)] + #[inline] + fn new() -> Self { + Self(BTreeSet::new()) + } + + fn visit_abi(&mut self, abi: &'a JsonAbi) { + if let Some(constructor) = &abi.constructor { + self.visit_params(&constructor.inputs); + } + for function in abi.functions() { + self.visit_params(&function.inputs); + self.visit_params(&function.outputs); + } + for error in abi.errors() { + self.visit_params(&error.inputs); + } + for event in abi.events() { + self.visit_event_params(&event.inputs); + } + } + + fn visit_params(&mut self, params: &'a [Param]) { + for param in params { + self.visit_param(param); + } + } + + fn visit_param(&mut self, param: &'a Param) { + self.extend(param.internal_type.as_ref(), ¶m.components, ¶m.ty); + self.visit_params(¶m.components); + } + + fn visit_event_params(&mut self, params: &'a [EventParam]) { + for param in params { + self.visit_event_param(param); + } + } + + fn visit_event_param(&mut self, param: &'a EventParam) { + self.extend(param.internal_type.as_ref(), ¶m.components, ¶m.ty); + self.visit_params(¶m.components); + } + + fn extend( + &mut self, + internal_type: Option<&'a InternalType>, + components: &'a Vec, + real_ty: &'a String, + ) { + 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::Enum { contract: _, ty }) => { + self.0.insert(It::new(ty, ItKind::Enum)); + } + 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))); + } + } + } + } + } +} + +/// An internal ABI type. +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct It<'a> { + // kind must come before name for `Ord` + kind: ItKind<'a>, + name: &'a str, +} + +#[derive(PartialEq, Eq)] +enum ItKind<'a> { + Enum, + Udvt(&'a String), + Struct(&'a Vec), +} + +// implemented manually because `Param: !Ord` +impl PartialOrd for ItKind<'_> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ItKind<'_> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (Self::Enum, Self::Enum) => Ordering::Equal, + (Self::Enum, _) => Ordering::Less, + (_, Self::Enum) => Ordering::Greater, + + (Self::Udvt(_), Self::Udvt(_)) => Ordering::Equal, + (Self::Udvt(_), _) => Ordering::Less, + (_, Self::Udvt(_)) => Ordering::Greater, + + (Self::Struct(_), Self::Struct(_)) => Ordering::Equal, + } + } +} + +impl<'a> It<'a> { + #[inline] + fn new(ty_name: &'a str, kind: ItKind<'a>) -> Self { + Self { + kind, + // `ty_name` might be an array, we just want the identifier + name: ty_name.split('[').next().unwrap(), + } + } +} + +impl ToSol for It<'_> { + fn to_sol(&self, out: &mut String) { + match self.kind { + ItKind::Enum => { + out.push_str("type "); + out.push_str(self.name); + out.push_str(" is uint8;"); + } + ItKind::Udvt(ty) => { + out.push_str("type "); + out.push_str(self.name); + out.push_str(" is "); + out.push_str(ty); + out.push(';'); + } + ItKind::Struct(components) => { + out.push_str("struct "); + out.push_str(self.name); + out.push_str(" {\n"); + for component in components { + out.push_str(INDENT); + out.push_str(INDENT); + component.to_sol(out); + out.push_str(";\n"); + } + out.push_str(INDENT); + out.push_str("}\n"); + } + } + } +} + +impl ToSol for Event { + fn to_sol(&self, out: &mut String) { + AbiFunction::<'_, EventParam> { + kw: "event", + name: Some(&self.name), + inputs: &self.inputs, + visibility: None, + state_mutability: None, + outputs: &[], + } + .to_sol(out); + } +} + +impl ToSol for Error { + fn to_sol(&self, out: &mut String) { + AbiFunction::<'_, Param> { + kw: "error", + name: Some(&self.name), + inputs: &self.inputs, + visibility: None, + state_mutability: None, + outputs: &[], + } + .to_sol(out); + } +} + +impl ToSol for Fallback { + fn to_sol(&self, out: &mut String) { + AbiFunction::<'_, Param> { + kw: "fallback", + name: None, + inputs: &[], + visibility: Some("external"), + state_mutability: Some(self.state_mutability), + outputs: &[], + } + .to_sol(out); + } +} + +impl ToSol for Receive { + fn to_sol(&self, out: &mut String) { + AbiFunction::<'_, Param> { + kw: "receive", + name: None, + inputs: &[], + visibility: Some("external"), + state_mutability: Some(self.state_mutability), + outputs: &[], + } + .to_sol(out); + } +} + +impl ToSol for Function { + fn to_sol(&self, out: &mut String) { + AbiFunction::<'_, Param> { + kw: "function", + name: Some(&self.name), + inputs: &self.inputs, + visibility: Some("external"), + state_mutability: Some(self.state_mutability), + outputs: &self.outputs, + } + .to_sol(out); + } +} + +struct AbiFunction<'a, IN> { + kw: &'static str, + name: Option<&'a str>, + inputs: &'a [IN], + visibility: Option<&'static str>, + state_mutability: Option, + outputs: &'a [Param], +} + +impl ToSol for AbiFunction<'_, IN> { + fn to_sol(&self, out: &mut String) { + out.push_str(self.kw); + if let Some(name) = self.name { + out.push(' '); + out.push_str(name); + } + + out.push('('); + for (i, input) in self.inputs.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + input.to_sol(out); + } + out.push(')'); + + if let Some(visibility) = self.visibility { + out.push(' '); + out.push_str(visibility); + } + + if let Some(state_mutability) = self.state_mutability { + if let Some(state_mutability) = state_mutability.as_str() { + out.push(' '); + out.push_str(state_mutability); + } + } + + if !self.outputs.is_empty() { + out.push_str(" returns ("); + for (i, output) in self.outputs.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + output.to_sol(out); + } + out.push(')'); + } + + out.push(';'); + } +} + +impl ToSol for Param { + fn to_sol(&self, out: &mut String) { + param( + &self.ty, + self.internal_type.as_ref(), + false, + &self.name, + out, + ); + } +} + +impl ToSol for EventParam { + fn to_sol(&self, out: &mut String) { + param( + &self.ty, + self.internal_type.as_ref(), + self.indexed, + &self.name, + out, + ); + } +} + +fn param<'a>( + mut type_name: &'a str, + internal_type: Option<&'a InternalType>, + indexed: bool, + name: &str, + out: &mut String, +) { + if let Some(it) = internal_type { + match it { + InternalType::Struct { ty, .. } + | InternalType::Enum { ty, .. } + | InternalType::Other { ty, .. } => type_name = ty, + _ => {} + } + }; + out.push_str(type_name); + if indexed { + out.push_str(" indexed"); + } + if !name.is_empty() { + out.push(' '); + out.push_str(name); + } +} diff --git a/crates/sol-macro/src/json.rs b/crates/sol-macro/src/json.rs index adcc96515d..51f29f2224 100644 --- a/crates/sol-macro/src/json.rs +++ b/crates/sol-macro/src/json.rs @@ -1,10 +1,6 @@ -use alloy_json_abi::{ - AbiItem, ContractObject, Error, Event, EventParam, InternalType, JsonAbi, Param, - StateMutability, -}; -use proc_macro2::{Delimiter, Group, Ident, Punct, Spacing, TokenStream}; +use alloy_json_abi::{ContractObject, JsonAbi}; +use proc_macro2::{Ident, TokenStream}; use quote::{quote, TokenStreamExt}; -use std::collections::{BTreeMap, BTreeSet}; use syn::Result; pub fn expand(name: Ident, json: ContractObject) -> Result { @@ -32,292 +28,38 @@ pub fn expand(name: Ident, json: ContractObject) -> Result { }; let ast = syn::parse2(tokens).map_err(|e| { - let msg = "\ - failed to parse generated tokens into a Solidity AST.\n\ - This is a bug, please report it at \ - https://github.com/alloy-rs/core/issues/new/choose"; - let mut e2 = syn::Error::new(name.span(), msg); - e2.combine(e); - e2 + let msg = format!( + "failed to parse ABI-generated tokens into a Solidity AST: {e}.\n\ + This is a bug. We would appreciate a bug report: \ + https://github.com/alloy-rs/core/issues/new/choose" + ); + syn::Error::new(name.span(), msg) })?; crate::expand::expand(ast) } /// Returns `sol!` tokens. fn expand_abi(name: &Ident, abi: JsonAbi) -> Result { - let mut structs = BTreeMap::new(); - let mut enums = BTreeSet::new(); - let mut udvts = BTreeMap::new(); - let mut add_items = |internal_type: Option<&_>, components: &Vec<_>, real_ty: &str| { - if let Some(internal_type) = internal_type { - match internal_type { - InternalType::AddressPayable(_) | InternalType::Contract(_) => {} - InternalType::Struct { contract: _, ty } => { - structs.insert(struct_ident(ty).to_owned(), components.clone()); - } - InternalType::Enum { contract: _, ty } => { - enums.insert(struct_ident(ty).to_owned()); - } - InternalType::Other { contract: _, ty } => { - // `Other` is a UDVT if it's not a basic Solidity type - if let Some(it) = internal_type.other_specifier() { - if it.try_basic_solidity().is_err() { - let _ = dbg!(it.try_basic_solidity()); - udvts.insert(struct_ident(ty).to_owned(), real_ty.to_owned()); - } - } - } - } - } - }; - for item in abi.items() { - recurse_item_params(item, &mut add_items); - } - - let enums = enums.iter().map(expand_enum); - let udvts = udvts.iter().map(expand_udvt); - - let structs = structs.iter().map(expand_struct); - let events = abi.events().map(expand_event); - let errors = abi.errors().map(expand_error); - - let constructor = abi - .constructor - .as_ref() - .map(|c| AbiFunction::Constructor(&c.inputs).expand(c.state_mutability)); - let fallback = abi - .fallback - .as_ref() - .map(|f| AbiFunction::Fallback.expand(f.state_mutability)); - let receive = abi - .receive - .as_ref() - .map(|r| AbiFunction::Receive.expand(r.state_mutability)); - let functions = abi - .functions() - .map(|f| AbiFunction::Function(&f.name, &f.inputs, &f.outputs).expand(f.state_mutability)); - - let tokens = quote! { - interface #name { - #(#enums)* - #(#udvts)* - - #(#structs)* - #(#events)* - #(#errors)* - - #constructor - #fallback - #receive - #(#functions)* - } + let mk_err = |s: &str| { + let msg = format!( + "`JsonAbi::to_sol` generated invalid Rust tokens: {s}\n\ + This is a bug. We would appreciate a bug report: \ + https://github.com/alloy-rs/core/issues/new/choose" + ); + syn::Error::new(name.span(), msg) }; + let s = abi.to_sol(&name.to_string()); + let brace_idx = s.find('{').ok_or_else(|| mk_err("missing `{`"))?; + let tts = syn::parse_str::(&s[brace_idx..]).map_err(|e| mk_err(&e.to_string()))?; + + let mut tokens = TokenStream::new(); + // append `name` manually for the span + tokens.append(id("interface")); + tokens.append(name.clone()); + tokens.extend(tts); Ok(tokens) } -fn recurse_item_params(item: AbiItem<'_>, f: &mut F) -where - F: FnMut(Option<&InternalType>, &Vec, &str), -{ - if let Some(params) = item.inputs() { - recurse_params(params, f) - } - if let Some(params) = item.outputs() { - recurse_params(params, f) - } - if let Some(params) = item.event_inputs() { - recurse_event_params(params, f) - } -} - -fn recurse_params(params: &[Param], f: &mut F) -where - F: FnMut(Option<&InternalType>, &Vec, &str), -{ - params.iter().for_each(|param| recurse_param(param, f)); -} - -fn recurse_event_params(params: &[EventParam], f: &mut F) -where - F: FnMut(Option<&InternalType>, &Vec, &str), -{ - for param in params { - f(param.internal_type.as_ref(), ¶m.components, ¶m.ty); - recurse_params(¶m.components, f); - } -} - -fn recurse_param(param: &Param, f: &mut F) -where - F: FnMut(Option<&InternalType>, &Vec, &str), -{ - f(param.internal_type.as_ref(), ¶m.components, ¶m.ty); - recurse_params(¶m.components, f); -} - -/// There is no way to get the variants of the enum from the ABI. -/// -/// `type #name is uint8;` -fn expand_enum(name: &String) -> TokenStream { - let name = id(name); - quote!(type #name is uint8;) -} - -/// `type #name is #ty;` -fn expand_udvt((name, ty): (&String, &String)) -> TokenStream { - let name = id(name); - let ty = syn::parse_str::(ty).unwrap(); - quote!(type #name is #ty;) -} - -/// `struct #name { #(#fields;)* }` -fn expand_struct((name, fields): (&String, &Vec)) -> TokenStream { - let name = id(name); - let fields = expand_params(fields); - quote!(struct #name { #(#fields;)* }) -} - -/// `event #name(#inputs) #anonymous;` -fn expand_event(event: &Event) -> TokenStream { - let name = id(&event.name); - let inputs = expand_event_params(&event.inputs); - let anonymous = event.anonymous.then(|| id("anonymous")); - quote!(event #name(#(#inputs),*) #anonymous;) -} - -/// `error #name(#inputs);` -fn expand_error(error: &Error) -> TokenStream { - let name = id(&error.name); - let inputs = expand_params(&error.inputs); - quote!(error #name(#(#inputs),*);) -} - -/// `#kind #(#name)? (#inputs) #state_mutability #(returns (#outputs))?;` -enum AbiFunction<'a> { - Constructor(&'a [Param]), - Fallback, - Receive, - Function(&'a str, &'a [Param], &'a [Param]), -} - -impl AbiFunction<'_> { - fn expand(self, state_mutability: StateMutability) -> TokenStream { - let (kw, name, inputs, visibility, outputs) = match self { - AbiFunction::Constructor(inputs) => ("constructor", None, Some(inputs), None, None), - AbiFunction::Fallback => ("fallback", None, None, Some("external"), None), - AbiFunction::Receive => ("receive", None, None, Some("external"), None), - AbiFunction::Function(name, inputs, outputs) => ( - "function", - Some(name), - Some(inputs), - Some("external"), - Some(outputs), - ), - }; - - let mut tokens = TokenStream::new(); - - tokens.append(id(kw)); - if let Some(name) = name { - tokens.append(id(name)); - } - - let inputs = match inputs.map(expand_params) { - Some(inputs) => quote!(#(#inputs),*), - None => quote!(), - }; - tokens.append(Group::new(Delimiter::Parenthesis, inputs)); - - if let Some(visibility) = visibility { - tokens.append(id(visibility)); - } - - if let Some(state_mutability) = state_mutability.as_str() { - tokens.append(id(state_mutability)); - } - - if let Some(outputs) = outputs { - if !outputs.is_empty() { - tokens.append(id("returns")); - let outputs = expand_params(outputs); - tokens.append(Group::new(Delimiter::Parenthesis, quote!(#(#outputs),*))); - } - } - - tokens.append(punct(';')); - - tokens - } -} - -// Param list -fn expand_params(params: &[Param]) -> impl Iterator + '_ { - expand_params_(params.iter().map(|p| { - ( - &p.name[..], - &p.ty[..], - p.internal_type.as_ref(), - &p.components[..], - false, - ) - })) -} - -fn expand_event_params(params: &[EventParam]) -> impl Iterator + '_ { - expand_params_(params.iter().map(|p| { - ( - &p.name[..], - &p.ty[..], - p.internal_type.as_ref(), - &p.components[..], - p.indexed, - ) - })) -} - -type Tuple<'a> = ( - &'a str, - &'a str, - Option<&'a InternalType>, - &'a [Param], - bool, -); - -fn expand_params_<'a, I>(params: I) -> impl Iterator + 'a -where - I: Iterator> + 'a, -{ - params.map(|(name, ty, internal_type, _components, indexed)| { - let mut tokens = TokenStream::new(); - let mut type_name = ty; - if let Some(it) = internal_type { - match it { - InternalType::Struct { ty, .. } - | InternalType::Enum { ty, .. } - | InternalType::Other { ty, .. } => { - type_name = ty; - } - _ => {} - } - } - - tokens.extend(syn::parse_str::(type_name).unwrap()); - if indexed { - tokens.append(id("indexed")) - } - if !name.is_empty() { - tokens.append(id(name)); - } - - tokens - }) -} - -#[inline] -fn struct_ident(s: &str) -> &str { - s.split('[').next().unwrap() -} - #[track_caller] #[inline] fn id(s: impl AsRef) -> Ident { @@ -325,12 +67,6 @@ fn id(s: impl AsRef) -> Ident { syn::parse_str(s.as_ref()).unwrap() } -#[track_caller] -#[inline] -fn punct(s: char) -> Punct { - Punct::new(s, Spacing::Alone) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/sol-type-parser/src/type_spec.rs b/crates/sol-type-parser/src/type_spec.rs index c9475e28b7..c832a9d0ab 100644 --- a/crates/sol-type-parser/src/type_spec.rs +++ b/crates/sol-type-parser/src/type_spec.rs @@ -135,6 +135,12 @@ impl<'a> TypeSpecifier<'a> { pub fn try_basic_solidity(&self) -> Result<()> { self.stem.try_basic_solidity() } + + /// Returns true if this type is an array. + #[inline] + pub fn is_array(&self) -> bool { + !self.sizes.is_empty() + } } #[cfg(test)]