#![deny(missing_docs, unsafe_code)]
#![deny(rustdoc::broken_intra_doc_links)]
#[cfg(test)]
#[allow(missing_docs)]
#[macro_use]
#[path = "test/macros.rs"]
mod test_macros;
pub mod contract;
pub use contract::structs::InternalStructs;
use contract::Context;
mod rustfmt;
mod source;
mod util;
pub mod filter;
pub use filter::{ContractFilter, ExcludeContracts, SelectContracts};
pub mod multi;
pub use multi::MultiAbigen;
pub use ethers_core::types::Address;
pub use source::Source;
pub use util::parse_address;
use crate::contract::ExpandedContract;
use eyre::Result;
use proc_macro2::TokenStream;
use std::{collections::HashMap, fs::File, io::Write, path::Path};
#[derive(Debug, Clone)]
pub struct Abigen {
abi_source: Source,
contract_name: String,
method_aliases: HashMap<String, String>,
event_derives: Vec<String>,
rustfmt: bool,
event_aliases: HashMap<String, String>,
error_aliases: HashMap<String, String>,
}
impl Abigen {
pub fn new<S: AsRef<str>>(contract_name: &str, abi_source: S) -> Result<Self> {
let abi_source = abi_source.as_ref().parse()?;
Ok(Self {
abi_source,
contract_name: contract_name.to_owned(),
method_aliases: HashMap::new(),
event_derives: Vec::new(),
event_aliases: HashMap::new(),
rustfmt: true,
error_aliases: Default::default(),
})
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let name = path
.as_ref()
.file_stem()
.ok_or_else(|| eyre::format_err!("Missing file stem in path"))?
.to_str()
.ok_or_else(|| eyre::format_err!("Unable to convert file stem to string"))?;
let name = name.split('.').next().expect("name not empty.");
Self::new(name, std::fs::read_to_string(path.as_ref())?)
}
#[must_use]
pub fn add_event_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
where
S1: Into<String>,
S2: Into<String>,
{
self.event_aliases.insert(signature.into(), alias.into());
self
}
#[must_use]
pub fn add_method_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
where
S1: Into<String>,
S2: Into<String>,
{
self.method_aliases.insert(signature.into(), alias.into());
self
}
#[must_use]
pub fn add_error_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
where
S1: Into<String>,
S2: Into<String>,
{
self.error_aliases.insert(signature.into(), alias.into());
self
}
#[must_use]
pub fn rustfmt(mut self, rustfmt: bool) -> Self {
self.rustfmt = rustfmt;
self
}
#[must_use]
pub fn add_event_derive<S>(mut self, derive: S) -> Self
where
S: Into<String>,
{
self.event_derives.push(derive.into());
self
}
pub fn generate(self) -> Result<ContractBindings> {
let rustfmt = self.rustfmt;
let name = self.contract_name.clone();
let (expanded, _) = self.expand()?;
Ok(ContractBindings { tokens: expanded.into_tokens(), rustfmt, name })
}
pub fn expand(self) -> Result<(ExpandedContract, Context)> {
let ctx = Context::from_abigen(self)?;
Ok((ctx.expand()?, ctx))
}
}
pub struct ContractBindings {
tokens: TokenStream,
rustfmt: bool,
name: String,
}
impl ContractBindings {
pub fn write<W>(&self, mut w: W) -> Result<()>
where
W: Write,
{
let source = {
let raw = self.tokens.to_string();
if self.rustfmt {
rustfmt::format(&raw).unwrap_or(raw)
} else {
raw
}
};
w.write_all(source.as_bytes())?;
Ok(())
}
pub fn to_vec(&self) -> Vec<u8> {
let mut bindings = vec![];
self.write(&mut bindings).expect("allocations don't fail");
bindings
}
pub fn write_to_file<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let file = File::create(path)?;
self.write(file)
}
pub fn write_module_in_dir<P>(&self, dir: P) -> Result<()>
where
P: AsRef<Path>,
{
let file = dir.as_ref().join(self.module_filename());
self.write_to_file(file)
}
pub fn into_tokens(self) -> TokenStream {
self.tokens
}
pub fn module_name(&self) -> String {
util::safe_module_name(&self.name)
}
pub fn module_filename(&self) -> String {
let mut name = self.module_name();
name.extend([".rs"]);
name
}
}
#[cfg(test)]
mod tests {
use super::*;
use ethers_solc::project_util::TempProject;
#[test]
fn can_generate_structs() {
let greeter = include_str!("../../tests/solidity-contracts/greeter_with_struct.json");
let abigen = Abigen::new("Greeter", greeter).unwrap();
let gen = abigen.generate().unwrap();
let out = gen.tokens.to_string();
assert!(out.contains("pub struct Stuff"));
}
#[test]
fn can_compile_and_generate() {
let tmp = TempProject::dapptools().unwrap();
tmp.add_source(
"Greeter",
r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
contract Greeter {
struct Inner {
bool a;
}
struct Stuff {
Inner inner;
}
function greet(Stuff calldata stuff) public view returns (Stuff memory) {
return stuff;
}
}
"#,
)
.unwrap();
let _ = tmp.compile().unwrap();
let abigen =
Abigen::from_file(tmp.artifacts_path().join("Greeter.sol/Greeter.json")).unwrap();
let gen = abigen.generate().unwrap();
let out = gen.tokens.to_string();
assert!(out.contains("pub struct Stuff"));
assert!(out.contains("pub struct Inner"));
}
#[test]
fn can_compile_and_generate_with_punctuation() {
let tmp = TempProject::dapptools().unwrap();
tmp.add_source(
"Greeter.t.sol",
r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
contract Greeter {
struct Inner {
bool a;
}
struct Stuff {
Inner inner;
}
function greet(Stuff calldata stuff) public view returns (Stuff memory) {
return stuff;
}
}
"#,
)
.unwrap();
let _ = tmp.compile().unwrap();
let abigen =
Abigen::from_file(tmp.artifacts_path().join("Greeter.t.sol/Greeter.json")).unwrap();
let gen = abigen.generate().unwrap();
let out = gen.tokens.to_string();
assert!(out.contains("pub struct Stuff"));
assert!(out.contains("pub struct Inner"));
}
}