ethers_contract_abigen/
lib.rs

1//! # Abigen
2//!
3//! Programmatically generate type-safe Rust bindings for Ethereum smart contracts.
4//!
5//! This crate is intended to be used either indirectly with the [`abigen` procedural macro][abigen]
6//! or directly from a build script / CLI.
7//!
8//! [abigen]: https://docs.rs/ethers/latest/ethers/contract/macro.abigen.html
9
10#![deny(rustdoc::broken_intra_doc_links, missing_docs, unsafe_code)]
11#![warn(unreachable_pub)]
12#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
13
14#[cfg(test)]
15#[allow(missing_docs)]
16#[macro_use]
17#[path = "test/macros.rs"]
18mod test_macros;
19
20pub mod contract;
21pub use contract::structs::InternalStructs;
22
23pub mod filter;
24pub use filter::{ContractFilter, ExcludeContracts, SelectContracts};
25
26pub mod multi;
27pub use multi::MultiAbigen;
28
29mod source;
30#[cfg(all(feature = "online", not(target_arch = "wasm32")))]
31pub use source::Explorer;
32pub use source::Source;
33
34mod util;
35mod verbatim;
36
37pub use ethers_core::types::Address;
38
39use contract::{Context, ExpandedContract};
40use eyre::{Result, WrapErr};
41use proc_macro2::{Ident, TokenStream};
42use quote::ToTokens;
43use std::{collections::HashMap, fmt, fs, io, path::Path};
44
45/// Programmatically generate type-safe Rust bindings for an Ethereum smart contract from its ABI.
46///
47/// For all the supported ABI sources, see [Source].
48///
49/// To generate bindings for *multiple* contracts at once, see [`MultiAbigen`].
50///
51/// To generate bindings at compile time, see [the abigen! macro][abigen], or use in a `build.rs`
52/// file.
53///
54/// [abigen]: https://docs.rs/ethers/latest/ethers/contract/macro.abigen.html
55///
56/// # Example
57///
58/// Running the code below will generate a file called `token.rs` containing the bindings inside,
59/// which exports an `ERC20Token` struct, along with all its events.
60///
61/// ```no_run
62/// use ethers_contract_abigen::Abigen;
63///
64/// Abigen::new("ERC20Token", "./abi.json")?.generate()?.write_to_file("token.rs")?;
65/// # Ok::<_, Box<dyn std::error::Error>>(())
66#[derive(Clone, Debug)]
67#[must_use = "Abigen does nothing unless you generate or expand it."]
68pub struct Abigen {
69    /// The source of the ABI JSON for the contract whose bindings are being generated.
70    abi_source: Source,
71
72    /// The contract's name to use for the generated type.
73    contract_name: Ident,
74
75    /// Whether to format the generated bindings using [`prettyplease`].
76    format: bool,
77
78    /// Whether to emit [cargo build script directives][ref].
79    ///
80    /// [ref]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
81    emit_cargo_directives: bool,
82
83    /// Manually specified contract method aliases.
84    method_aliases: HashMap<String, String>,
85
86    /// Manually specified event name aliases.
87    event_aliases: HashMap<String, String>,
88
89    /// Manually specified error name aliases.
90    error_aliases: HashMap<String, String>,
91
92    /// Manually specified `derive` macros added to all structs and enums.
93    derives: Vec<syn::Path>,
94}
95
96impl Default for Abigen {
97    fn default() -> Self {
98        Self {
99            abi_source: Source::default(),
100            contract_name: Ident::new("DefaultContract", proc_macro2::Span::call_site()),
101            format: true,
102            emit_cargo_directives: false,
103            method_aliases: HashMap::new(),
104            derives: Vec::new(),
105            event_aliases: HashMap::new(),
106            error_aliases: HashMap::new(),
107        }
108    }
109}
110
111impl Abigen {
112    /// Creates a new builder with the given contract name and ABI source strings.
113    ///
114    /// # Errors
115    ///
116    /// If `contract_name` could not be parsed as a valid [Ident], or if `abi_source` could not be
117    /// parsed as a valid [Source].
118    pub fn new<T: AsRef<str>, S: AsRef<str>>(contract_name: T, abi_source: S) -> Result<Self> {
119        let abi_source: Source = abi_source.as_ref().parse()?;
120        Ok(Self {
121            emit_cargo_directives: abi_source.is_local() && in_build_script(),
122            abi_source,
123            contract_name: syn::parse_str(contract_name.as_ref())?,
124            ..Default::default()
125        })
126    }
127
128    /// Creates a new builder with the given contract name [Ident] and [ABI source][Source].
129    pub fn new_raw(contract_name: Ident, abi_source: Source) -> Self {
130        Self {
131            emit_cargo_directives: abi_source.is_local() && in_build_script(),
132            abi_source,
133            contract_name,
134            ..Default::default()
135        }
136    }
137
138    /// Attempts to load a new builder from an ABI JSON file at the specific path.
139    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
140        let path = path.as_ref();
141        let path = path
142            .to_str()
143            .ok_or_else(|| eyre::eyre!("path is not valid UTF-8: {}", path.display()))?;
144        let source = Source::local(path)?;
145        // cannot panic because of errors above
146        let name = source.as_local().unwrap().file_name().unwrap().to_str().unwrap();
147        // name is an absolute path and not empty
148        let name = name.split('.').next().unwrap();
149
150        // best effort ident cleanup
151        let name = name.replace('-', "_").replace(|c: char| c.is_whitespace(), "");
152
153        Ok(Self::new_raw(
154            syn::parse_str(&util::safe_identifier_name(name)).wrap_err_with(|| {
155                format!("failed convert file name to contract identifier {}", path)
156            })?,
157            source,
158        ))
159    }
160
161    /// Manually adds a solidity event alias to specify what the event struct and function name will
162    /// be in Rust.
163    ///
164    /// For events without an alias, the `PascalCase` event name will be used.
165    pub fn add_event_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
166    where
167        S1: Into<String>,
168        S2: Into<String>,
169    {
170        self.event_aliases.insert(signature.into(), alias.into());
171        self
172    }
173
174    /// Add a Solidity method error alias to specify the generated method name.
175    ///
176    /// For methods without an alias, the `snake_case` method name will be used.
177    pub fn add_method_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
178    where
179        S1: Into<String>,
180        S2: Into<String>,
181    {
182        self.method_aliases.insert(signature.into(), alias.into());
183        self
184    }
185
186    /// Add a Solidity custom error alias to specify the generated struct's name.
187    ///
188    /// For errors without an alias, the `PascalCase` error name will be used.
189    pub fn add_error_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
190    where
191        S1: Into<String>,
192        S2: Into<String>,
193    {
194        self.error_aliases.insert(signature.into(), alias.into());
195        self
196    }
197
198    #[deprecated = "Use add_derive instead"]
199    #[doc(hidden)]
200    pub fn add_event_derive<S: AsRef<str>>(self, derive: S) -> Result<Self> {
201        self.add_derive(derive)
202    }
203
204    /// Add a custom derive to the derives for all structs and enums.
205    ///
206    /// For example, this makes it possible to derive serde::Serialize and serde::Deserialize.
207    pub fn add_derive<S: AsRef<str>>(mut self, derive: S) -> Result<Self> {
208        self.derives.push(syn::parse_str(derive.as_ref())?);
209        Ok(self)
210    }
211
212    #[deprecated = "Use format instead"]
213    #[doc(hidden)]
214    pub fn rustfmt(mut self, rustfmt: bool) -> Self {
215        self.format = rustfmt;
216        self
217    }
218
219    /// Specify whether to format the code or not. True by default.
220    ///
221    /// This will use [`prettyplease`], so the resulting formatted code **will not** be affected by
222    /// the local `rustfmt` version or config.
223    pub fn format(mut self, format: bool) -> Self {
224        self.format = format;
225        self
226    }
227
228    /// Specify whether to print [cargo build script directives][ref] if the source is a path. By
229    /// default, this is true only when executing inside of a build script.
230    ///
231    /// [ref]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
232    pub fn emit_cargo_directives(mut self, emit_cargo_directives: bool) -> Self {
233        self.emit_cargo_directives = emit_cargo_directives;
234        self
235    }
236
237    /// Generates the contract bindings.
238    pub fn generate(self) -> Result<ContractBindings> {
239        let format = self.format;
240        let emit = self.emit_cargo_directives;
241        let path = self.abi_source.as_local().cloned();
242        let name = self.contract_name.to_string();
243
244        let (expanded, _) = self.expand()?;
245
246        // Don't generate `include` tokens if we're printing cargo directives.
247        let path = if let (true, Some(path)) = (emit, &path) {
248            println!("cargo:rerun-if-changed={}", path.display());
249            None
250        } else {
251            path.as_deref()
252        };
253
254        Ok(ContractBindings { tokens: expanded.into_tokens_with_path(path), format, name })
255    }
256
257    /// Expands the `Abigen` and returns the [`ExpandedContract`] that holds all tokens and the
258    /// [`Context`] that holds the state used during expansion.
259    pub fn expand(self) -> Result<(ExpandedContract, Context)> {
260        let ctx = Context::from_abigen(self)?;
261        Ok((ctx.expand()?, ctx))
262    }
263}
264
265impl Abigen {
266    /// Returns a reference to the contract's ABI source.
267    pub fn source(&self) -> &Source {
268        &self.abi_source
269    }
270
271    /// Returns a mutable reference to the contract's ABI source.
272    pub fn source_mut(&mut self) -> &mut Source {
273        &mut self.abi_source
274    }
275
276    /// Returns a reference to the contract's name.
277    pub fn name(&self) -> &Ident {
278        &self.contract_name
279    }
280
281    /// Returns a mutable reference to the contract's name.
282    pub fn name_mut(&mut self) -> &mut Ident {
283        &mut self.contract_name
284    }
285
286    /// Returns a reference to the contract's method aliases.
287    pub fn method_aliases(&self) -> &HashMap<String, String> {
288        &self.method_aliases
289    }
290
291    /// Returns a mutable reference to the contract's method aliases.
292    pub fn method_aliases_mut(&mut self) -> &mut HashMap<String, String> {
293        &mut self.method_aliases
294    }
295
296    /// Returns a reference to the contract's event aliases.
297    pub fn event_aliases(&self) -> &HashMap<String, String> {
298        &self.event_aliases
299    }
300
301    /// Returns a mutable reference to the contract's event aliases.
302    pub fn error_aliases_mut(&mut self) -> &mut HashMap<String, String> {
303        &mut self.error_aliases
304    }
305
306    /// Returns a reference to the contract's derives.
307    pub fn derives(&self) -> &Vec<syn::Path> {
308        &self.derives
309    }
310
311    /// Returns a mutable reference to the contract's derives.
312    pub fn derives_mut(&mut self) -> &mut Vec<syn::Path> {
313        &mut self.derives
314    }
315}
316
317/// Type-safe contract bindings generated by [Abigen].
318///
319/// This type can be either written to file or converted to a token stream for a procedural macro.
320#[derive(Clone)]
321pub struct ContractBindings {
322    /// The contract's name.
323    pub name: String,
324
325    /// The generated bindings as a `TokenStream`.
326    pub tokens: TokenStream,
327
328    /// Whether to format the generated bindings using [`prettyplease`].
329    pub format: bool,
330}
331
332impl ToTokens for ContractBindings {
333    fn into_token_stream(self) -> TokenStream {
334        self.tokens
335    }
336
337    fn to_tokens(&self, tokens: &mut TokenStream) {
338        tokens.extend(std::iter::once(self.tokens.clone()))
339    }
340
341    fn to_token_stream(&self) -> TokenStream {
342        self.tokens.clone()
343    }
344}
345
346impl fmt::Display for ContractBindings {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        if self.format {
349            let syntax_tree = syn::parse2::<syn::File>(self.tokens.clone()).unwrap();
350            let s = prettyplease::unparse(&syntax_tree);
351            f.write_str(&s)
352        } else {
353            fmt::Display::fmt(&self.tokens, f)
354        }
355    }
356}
357
358impl fmt::Debug for ContractBindings {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("ContractBindings")
361            .field("name", &self.name)
362            .field("format", &self.format)
363            .finish()
364    }
365}
366
367impl ContractBindings {
368    /// Writes the bindings to a new Vec.
369    pub fn to_vec(&self) -> Vec<u8> {
370        self.to_string().into_bytes()
371    }
372
373    /// Writes the bindings to a given `io::Write`.
374    pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> {
375        let tokens = self.to_string();
376        w.write_all(tokens.as_bytes())
377    }
378
379    /// Writes the bindings to a given `fmt::Write`.
380    pub fn write_fmt(&self, w: &mut impl fmt::Write) -> fmt::Result {
381        let tokens = self.to_string();
382        w.write_str(&tokens)
383    }
384
385    /// Writes the bindings to the specified file.
386    pub fn write_to_file(&self, file: impl AsRef<Path>) -> io::Result<()> {
387        fs::write(file.as_ref(), self.to_string())
388    }
389
390    /// Writes the bindings to a `contract_name.rs` file in the specified directory.
391    pub fn write_module_in_dir(&self, dir: impl AsRef<Path>) -> io::Result<()> {
392        let file = dir.as_ref().join(self.module_filename());
393        self.write_to_file(file)
394    }
395
396    #[deprecated = "Use ::quote::ToTokens::into_token_stream instead"]
397    #[doc(hidden)]
398    pub fn into_tokens(self) -> TokenStream {
399        self.tokens
400    }
401
402    /// Generate the default module name (snake case of the contract name).
403    pub fn module_name(&self) -> String {
404        util::safe_module_name(&self.name)
405    }
406
407    /// Generate the default file name of the module.
408    pub fn module_filename(&self) -> String {
409        let mut name = self.module_name();
410        name.push_str(".rs");
411        name
412    }
413}
414
415/// Returns whether the current executable is a cargo build script.
416fn in_build_script() -> bool {
417    std::env::var("TARGET").is_ok()
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn can_generate_structs() {
426        let greeter = include_str!("../../tests/solidity-contracts/greeter_with_struct.json");
427        let abigen = Abigen::new("Greeter", greeter).unwrap();
428        let gen = abigen.generate().unwrap();
429        let out = gen.tokens.to_string();
430        assert!(out.contains("pub struct Stuff"));
431    }
432
433    #[test]
434    fn can_generate_constructor_params() {
435        let contract = include_str!("../../tests/solidity-contracts/StructConstructor.json");
436        let abigen = Abigen::new("MyContract", contract).unwrap();
437        let gen = abigen.generate().unwrap();
438        let out = gen.tokens.to_string();
439        assert!(out.contains("pub struct ConstructorParams"));
440    }
441
442    // <https://github.com/foundry-rs/foundry/issues/6010>
443    #[test]
444    fn parse_empty_abigen() {
445        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
446            .join("../tests/solidity-contracts/draft-IERCPermit.json");
447        let abigen = Abigen::from_file(path).unwrap();
448        assert_eq!(abigen.name().to_string(), "draft_IERCPermit");
449    }
450}