ethers_contract_abigen/
multi.rs

1//! Generate bindings for multiple `Abigen`
2use crate::{util, Abigen, Context, ContractBindings, ContractFilter, ExpandedContract};
3use eyre::Result;
4use inflector::Inflector;
5use proc_macro2::TokenStream;
6use quote::quote;
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    fs,
10    io::Write,
11    path::{Path, PathBuf},
12};
13use toml::Value;
14
15/// The default ethers dependency to generate.
16const DEFAULT_ETHERS_DEP: &str =
17    "ethers = { version = \"2\", default-features = false, features = [\"abigen\"] }";
18
19/// Collects Abigen structs for a series of contracts, pending generation of
20/// the contract bindings.
21#[derive(Debug, Clone)]
22pub struct MultiAbigen {
23    /// Abigen objects to be written
24    abigens: Vec<Abigen>,
25}
26
27impl std::ops::Deref for MultiAbigen {
28    type Target = Vec<Abigen>;
29
30    fn deref(&self) -> &Self::Target {
31        &self.abigens
32    }
33}
34
35impl std::ops::DerefMut for MultiAbigen {
36    fn deref_mut(&mut self) -> &mut Self::Target {
37        &mut self.abigens
38    }
39}
40
41impl From<Vec<Abigen>> for MultiAbigen {
42    fn from(abigens: Vec<Abigen>) -> Self {
43        Self { abigens }
44    }
45}
46
47impl std::iter::FromIterator<Abigen> for MultiAbigen {
48    fn from_iter<I: IntoIterator<Item = Abigen>>(iter: I) -> Self {
49        iter.into_iter().collect::<Vec<_>>().into()
50    }
51}
52
53impl MultiAbigen {
54    /// Create a new instance from a series (`contract name`, `abi_source`)
55    ///
56    /// See `Abigen::new`
57    pub fn new<I, Name, Source>(abis: I) -> Result<Self>
58    where
59        I: IntoIterator<Item = (Name, Source)>,
60        Name: AsRef<str>,
61        Source: AsRef<str>,
62    {
63        let abis = abis
64            .into_iter()
65            .map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source))
66            .collect::<Result<Vec<_>>>()?;
67
68        Ok(Self::from_abigens(abis))
69    }
70
71    /// Create a new instance from a series of already resolved `Abigen`
72    pub fn from_abigens(abis: impl IntoIterator<Item = Abigen>) -> Self {
73        abis.into_iter().collect()
74    }
75
76    /// Reads all json files contained in the given `dir` and use the file name for the name of the
77    /// `ContractBindings`.
78    /// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename.
79    ///
80    /// # Example
81    ///
82    /// ```text
83    /// abi
84    /// ├── ERC20.json
85    /// ├── Contract1.json
86    /// ├── Contract2.json
87    /// ...
88    /// ```
89    ///
90    /// ```
91    /// # fn t() {
92    /// # use ethers_contract_abigen::MultiAbigen;
93    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
94    /// # }
95    /// ```
96    pub fn from_json_files(root: impl AsRef<Path>) -> Result<Self> {
97        let root = root.as_ref();
98        let files = util::json_files(root);
99        eyre::ensure!(!files.is_empty(), "No json files found in directory: {}", root.display());
100        files.into_iter().map(Abigen::from_file).collect()
101    }
102
103    /// See `apply_filter`
104    ///
105    /// # Example
106    ///
107    /// Only Select specific contracts
108    ///
109    /// ```
110    /// use ethers_contract_abigen::{MultiAbigen, SelectContracts};
111    /// # fn t() {
112    ///    let gen = MultiAbigen::from_json_files("./abi").unwrap().with_filter(
113    ///        SelectContracts::default().add_name("MyContract").add_name("MyOtherContract"),
114    ///    );
115    /// # }
116    /// ```
117    ///
118    /// Exclude all contracts that end with test
119    ///
120    /// ```
121    /// use ethers_contract_abigen::{ExcludeContracts, MultiAbigen};
122    /// # fn t() {
123    ///    let gen = MultiAbigen::from_json_files("./abi").unwrap().with_filter(
124    ///        ExcludeContracts::default().add_pattern(".*Test"),
125    ///    );
126    /// # }
127    /// ```
128    #[must_use]
129    pub fn with_filter(mut self, filter: impl Into<ContractFilter>) -> Self {
130        self.apply_filter(&filter.into());
131        self
132    }
133
134    /// Removes all `Abigen` items that should not be included based on the given filter
135    pub fn apply_filter(&mut self, filter: &ContractFilter) {
136        self.abigens.retain(|abi| filter.is_match(abi.contract_name.to_string()))
137    }
138
139    /// Add another Abigen to the module or lib
140    pub fn push(&mut self, abigen: Abigen) {
141        self.abigens.push(abigen)
142    }
143
144    /// Build the contract bindings and prepare for writing
145    pub fn build(self) -> Result<MultiBindings> {
146        let format = self.abigens.iter().any(|gen| gen.format);
147        Ok(MultiBindings {
148            expansion: MultiExpansion::from_abigen(self.abigens)?.expand(),
149            format,
150            dependencies: vec![],
151        })
152    }
153}
154
155/// Represents a collection of [`Abigen::expand()`]
156pub struct MultiExpansion {
157    // all expanded contracts collection from [`Abigen::expand()`]
158    contracts: Vec<(ExpandedContract, Context)>,
159}
160
161impl MultiExpansion {
162    /// Create a new instance that wraps the given `contracts`
163    pub fn new(contracts: Vec<(ExpandedContract, Context)>) -> Self {
164        Self { contracts }
165    }
166
167    /// Create a new instance by expanding all `Abigen` elements the given iterator yields
168    pub fn from_abigen(abigens: impl IntoIterator<Item = Abigen>) -> Result<Self> {
169        let contracts = abigens.into_iter().map(|abigen| abigen.expand()).collect::<Result<_>>()?;
170        Ok(Self::new(contracts))
171    }
172
173    /// Expands all contracts into a single `TokenStream`
174    ///
175    /// This will deduplicate types into a separate `mod __shared_types` module, if any.
176    pub fn expand_inplace(self) -> TokenStream {
177        self.expand().expand_inplace()
178    }
179
180    /// Expands all contracts into separated [`TokenStream`]s
181    ///
182    /// If there was type deduplication, this returns a list of [`TokenStream`] containing the type
183    /// definitions of all shared types.
184    pub fn expand(self) -> MultiExpansionResult {
185        let mut expansions = self.contracts;
186        let mut shared_types = Vec::new();
187        // this keeps track of those contracts that need to be updated after a struct was
188        // extracted from the contract's module and moved to the shared module
189        let mut dirty_contracts = BTreeSet::new();
190
191        // merge all types if more than 1 contract
192        if expansions.len() > 1 {
193            // check for type conflicts across all contracts
194            let mut conflicts: BTreeMap<String, Vec<usize>> = BTreeMap::new();
195            for (idx, (_, ctx)) in expansions.iter().enumerate() {
196                for type_identifier in ctx.internal_structs().rust_type_names().keys() {
197                    conflicts
198                        .entry(type_identifier.clone())
199                        .or_insert_with(|| Vec::with_capacity(1))
200                        .push(idx);
201                }
202            }
203
204            // resolve type conflicts
205            for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) {
206                // extract the shared type once
207                shared_types.push(
208                    expansions[contracts[0]]
209                        .1
210                        .struct_definition(id)
211                        .expect("struct def succeeded previously"),
212                );
213
214                // remove the shared type from the contract's bindings
215                for contract in contracts.iter().copied() {
216                    expansions[contract].1.remove_struct(id);
217                    dirty_contracts.insert(contract);
218                }
219            }
220
221            // regenerate all struct definitions that were hit
222            for contract in dirty_contracts.iter().copied() {
223                let (expanded, ctx) = &mut expansions[contract];
224                expanded.abi_structs = ctx.abi_structs().expect("struct def succeeded previously");
225            }
226        }
227
228        MultiExpansionResult { root: None, contracts: expansions, dirty_contracts, shared_types }
229    }
230}
231
232/// Represents an intermediary result of [`MultiExpansion::expand()`]
233pub struct MultiExpansionResult {
234    /// The root dir at which this should be executed.
235    ///
236    /// This is used to check if there's an existing `Cargo.toml`, from which we can derive the
237    /// proper `ethers` dependencies.
238    root: Option<PathBuf>,
239    contracts: Vec<(ExpandedContract, Context)>,
240    /// contains the indices of contracts with structs that need to be updated
241    dirty_contracts: BTreeSet<usize>,
242    /// all type definitions of types that are shared by multiple contracts
243    shared_types: Vec<TokenStream>,
244}
245
246impl MultiExpansionResult {
247    /// Expands all contracts into a single [`TokenStream`]
248    pub fn expand_inplace(mut self) -> TokenStream {
249        let mut tokens = TokenStream::new();
250
251        let shared_types_module = quote! {__shared_types};
252        // the import path to the shared types
253        let shared_path = quote!(
254            pub use super::#shared_types_module::*;
255        );
256        self.add_shared_import_path(shared_path);
257
258        let Self { contracts, shared_types, .. } = self;
259
260        if !shared_types.is_empty() {
261            tokens.extend(quote! {
262                pub mod #shared_types_module {
263                    #( #shared_types )*
264                }
265            });
266        }
267
268        tokens.extend(contracts.into_iter().map(|(exp, _)| exp.into_tokens()));
269
270        tokens
271    }
272
273    /// Sets the directory from which this type should expand from.
274    ///
275    /// This is used to try to find the proper `ethers` dependency if the `root` is an existing
276    /// workspace. By default, the cwd is assumed to be the `root`.
277    pub fn set_root(&mut self, root: impl Into<PathBuf>) {
278        self.root = Some(root.into());
279    }
280
281    /// Sets the path to the shared types module according to the value of `single_file`
282    ///
283    /// If `single_file` then it's expected that types will be written to `shared_types.rs`
284    fn set_shared_import_path(&mut self, single_file: bool) {
285        let shared_path = if single_file {
286            quote!(
287                pub use super::__shared_types::*;
288            )
289        } else {
290            quote!(
291                pub use super::super::shared_types::*;
292            )
293        };
294        self.add_shared_import_path(shared_path);
295    }
296
297    /// adds the `shared` import path to every `dirty` contract
298    fn add_shared_import_path(&mut self, shared: TokenStream) {
299        for contract in self.dirty_contracts.iter().copied() {
300            let (expanded, ..) = &mut self.contracts[contract];
301            expanded.imports.extend(shared.clone());
302        }
303    }
304
305    /// Converts this result into [`MultiBindingsInner`]
306    fn into_bindings(
307        mut self,
308        single_file: bool,
309        format: bool,
310        dependencies: Vec<String>,
311    ) -> MultiBindingsInner {
312        self.set_shared_import_path(single_file);
313        let Self { contracts, shared_types, root, .. } = self;
314        let bindings = contracts
315            .into_iter()
316            .map(|(expanded, ctx)| ContractBindings {
317                tokens: expanded.into_tokens(),
318                format,
319                name: ctx.contract_name().to_string(),
320            })
321            .map(|v| (v.name.clone(), v))
322            .collect();
323
324        let shared_types = if !shared_types.is_empty() {
325            let shared_types = if single_file {
326                quote! {
327                    pub mod __shared_types {
328                        #( #shared_types )*
329                    }
330                }
331            } else {
332                quote! {
333                    #( #shared_types )*
334                }
335            };
336            Some(ContractBindings {
337                tokens: shared_types,
338                format,
339                name: "shared_types".to_string(),
340            })
341        } else {
342            None
343        };
344
345        MultiBindingsInner { root, bindings, shared_types, dependencies }
346    }
347}
348
349/// Output of the [`MultiAbigen`] build process. `MultiBindings` wraps a group
350/// of built contract bindings that have yet to be written to disk.
351///
352/// `MultiBindings` enables the user to
353/// 1. Write a collection of bindings to a rust module
354/// 2. Write a collection of bindings to a rust lib
355/// 3. Ensure that a collection of bindings matches an on-disk module or lib.
356///
357/// Generally we recommend writing the bindings to a module folder within your
358/// rust project. Users seeking to create "official" bindings for some project
359/// may instead write an entire library to publish via crates.io.
360///
361/// Rather than using `MultiAbigen` in a build script, we recommend committing
362/// the generated files, and replacing the build script with an integration
363/// test. To enable this, we have provided
364/// `MultiBindings::ensure_consistent_bindings` and
365/// `MultiBindings::ensure_consistent_crate`. These functions generate the
366/// expected module or library in memory, and check that the on-disk files
367/// match the expected files. We recommend running these inside CI.
368///
369/// This has several advantages:
370///   * No need for downstream users to compile the build script
371///   * No need for downstream users to run the whole `abigen!` generation steps
372///   * The generated code is more usable in an IDE
373///   * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
374///     changed)
375pub struct MultiBindings {
376    expansion: MultiExpansionResult,
377    format: bool,
378    dependencies: Vec<String>,
379}
380
381impl MultiBindings {
382    /// Returns the number of contracts to generate bindings for.
383    pub fn len(&self) -> usize {
384        self.expansion.contracts.len()
385    }
386
387    /// Returns whether there are any bindings to be generated
388    pub fn is_empty(&self) -> bool {
389        self.expansion.contracts.is_empty()
390    }
391
392    #[must_use]
393    #[deprecated = "Use format instead"]
394    #[doc(hidden)]
395    pub fn rustfmt(mut self, rustfmt: bool) -> Self {
396        self.format = rustfmt;
397        self
398    }
399
400    /// Specify whether to format the code or not. True by default.
401    ///
402    /// This will use [`prettyplease`], so the resulting formatted code **will not** be affected by
403    /// the local `rustfmt` version or config.
404    pub fn format(mut self, format: bool) -> Self {
405        self.format = format;
406        self
407    }
408
409    /// Specify a set of dependencies to use for the generated crate.
410    ///
411    /// By default, this is empty and only the `ethers` dependency is added.
412    pub fn dependencies(
413        mut self,
414        dependencies: impl IntoIterator<Item = impl Into<String>>,
415    ) -> Self {
416        self.dependencies = dependencies.into_iter().map(|dep| dep.into()).collect();
417        self
418    }
419
420    fn into_inner(self, single_file: bool) -> MultiBindingsInner {
421        self.expansion.into_bindings(single_file, self.format, self.dependencies)
422    }
423
424    /// Generates all the bindings and writes them to the given module
425    ///
426    /// # Example
427    ///
428    /// Read all json abi files from the `./abi` directory
429    /// ```text
430    /// abi
431    /// ├── ERC20.json
432    /// ├── Contract1.json
433    /// ├── Contract2.json
434    /// ...
435    /// ```
436    ///
437    /// and write them to the `./src/contracts` location as
438    ///
439    /// ```text
440    /// src/contracts
441    /// ├── mod.rs
442    /// ├── er20.rs
443    /// ├── contract1.rs
444    /// ├── contract2.rs
445    /// ...
446    /// ```
447    ///
448    /// ```no_run
449    /// # use ethers_contract_abigen::MultiAbigen;
450    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
451    /// let bindings = gen.build().unwrap();
452    /// bindings.write_to_module("./src/contracts", false).unwrap();
453    /// ```
454    pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
455        self.into_inner(single_file).write_to_module(module, single_file)
456    }
457
458    /// Generates all the bindings and writes a library crate containing them
459    /// to the provided path
460    ///
461    /// # Example
462    ///
463    /// Read all json abi files from the `./abi` directory
464    /// ```text
465    /// abi
466    /// ├── ERC20.json
467    /// ├── Contract1.json
468    /// ├── Contract2.json
469    /// ├── Contract3/
470    ///     ├── Contract3.json
471    /// ...
472    /// ```
473    ///
474    /// and write them to the `./bindings` location as
475    ///
476    /// ```text
477    /// bindings
478    /// ├── Cargo.toml
479    /// ├── src/
480    ///     ├── lib.rs
481    ///     ├── er20.rs
482    ///     ├── contract1.rs
483    ///     ├── contract2.rs
484    /// ...
485    /// ```
486    ///
487    /// ```no_run
488    /// # use ethers_contract_abigen::MultiAbigen;
489    /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
490    /// let bindings = gen.build().unwrap();
491    /// bindings.write_to_crate(
492    ///     "my-crate", "0.0.5", "./bindings", false
493    /// ).unwrap();
494    /// ```
495    pub fn write_to_crate(
496        self,
497        name: impl AsRef<str>,
498        version: impl AsRef<str>,
499        lib: impl AsRef<Path>,
500        single_file: bool,
501    ) -> Result<()> {
502        self.into_inner(single_file).write_to_crate(name, version, lib, single_file)
503    }
504
505    /// This ensures that the already generated bindings crate matches the
506    /// output of a fresh new run. Run this in a rust test, to get notified in
507    /// CI if the newly generated bindings deviate from the already generated
508    /// ones, and it's time to generate them again. This could happen if the
509    /// ABI of a contract or the output that `ethers` generates changed.
510    ///
511    /// If this functions is run within a test during CI and fails, then it's
512    /// time to update all bindings.
513    ///
514    /// # Returns
515    ///
516    /// `Ok(())` if the freshly generated bindings match with the
517    /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
518    /// with more information.
519    ///
520    /// # Example
521    ///
522    /// Check that the generated files are up to date
523    ///
524    /// ```no_run
525    /// # use ethers_contract_abigen::MultiAbigen;
526    /// #[test]
527    /// fn generated_bindings_are_fresh() {
528    ///  let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
529    ///  let abi_dir = project_root.join("abi");
530    ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
531    ///  let bindings = gen.build().unwrap();
532    ///  bindings.ensure_consistent_crate(
533    ///     "my-crate", "0.0.1", project_root.join("src/contracts"), false, true
534    ///  ).expect("inconsistent bindings");
535    /// }
536    /// ```
537    pub fn ensure_consistent_crate(
538        self,
539        name: impl AsRef<str>,
540        version: impl AsRef<str>,
541        crate_path: impl AsRef<Path>,
542        single_file: bool,
543        check_cargo_toml: bool,
544    ) -> Result<()> {
545        self.into_inner(single_file).ensure_consistent_crate(
546            name,
547            version,
548            crate_path,
549            single_file,
550            check_cargo_toml,
551        )
552    }
553
554    /// This ensures that the already generated bindings module matches the
555    /// output of a fresh new run. Run this in a rust test, to get notified in
556    /// CI if the newly generated bindings deviate from the already generated
557    /// ones, and it's time to generate them again. This could happen if the
558    /// ABI of a contract or the output that `ethers` generates changed.
559    ///
560    /// If this functions is run within a test during CI and fails, then it's
561    /// time to update all bindings.
562    ///
563    /// # Returns
564    ///
565    /// `Ok(())` if the freshly generated bindings match with the
566    /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report`
567    /// with more information.
568    ///
569    /// # Example
570    ///
571    /// Check that the generated files are up to date
572    ///
573    /// ```no_run
574    /// # use ethers_contract_abigen::MultiAbigen;
575    /// #[test]
576    /// fn generated_bindings_are_fresh() {
577    ///  let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
578    ///  let abi_dir = project_root.join("abi");
579    ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
580    ///  let bindings = gen.build().unwrap();
581    ///  bindings.ensure_consistent_module(
582    ///     project_root.join("src/contracts"), false
583    ///  ).expect("inconsistent bindings");
584    /// }
585    /// ```
586    pub fn ensure_consistent_module(
587        self,
588        module: impl AsRef<Path>,
589        single_file: bool,
590    ) -> Result<()> {
591        self.into_inner(single_file).ensure_consistent_module(module, single_file)
592    }
593}
594
595struct MultiBindingsInner {
596    /// The root dir at which this should be executed.
597    ///
598    /// This is used to check if there's an existing `Cargo.toml`, from which we can derive the
599    /// proper `ethers` dependencies.
600    root: Option<PathBuf>,
601    /// Abigen objects to be written
602    bindings: BTreeMap<String, ContractBindings>,
603    /// contains the content of the shared types if any
604    shared_types: Option<ContractBindings>,
605    /// Dependencies other than `ethers-rs` to add to the `Cargo.toml` for bindings generated as a
606    /// crate.
607    dependencies: Vec<String>,
608}
609
610// deref allows for inspection without modification
611impl std::ops::Deref for MultiBindingsInner {
612    type Target = BTreeMap<String, ContractBindings>;
613
614    fn deref(&self) -> &Self::Target {
615        &self.bindings
616    }
617}
618
619impl MultiBindingsInner {
620    /// Generate the contents of the `Cargo.toml` file for a lib
621    fn generate_cargo_toml(
622        &self,
623        name: impl AsRef<str>,
624        version: impl AsRef<str>,
625        crate_version: String,
626    ) -> Result<Vec<u8>> {
627        let mut toml = vec![];
628
629        writeln!(toml, "[package]")?;
630        writeln!(toml, r#"name = "{}""#, name.as_ref())?;
631        writeln!(toml, r#"version = "{}""#, version.as_ref())?;
632        writeln!(toml, r#"edition = "2021""#)?;
633        writeln!(toml)?;
634        writeln!(toml, "# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html")?;
635        writeln!(toml)?;
636        writeln!(toml, "[dependencies]")?;
637        writeln!(toml, r#"{crate_version}"#)?;
638        for dependency in self.dependencies.clone() {
639            writeln!(toml, "{}", dependency)?;
640        }
641        Ok(toml)
642    }
643
644    /// Returns the ethers crate version to use.
645    ///
646    /// If we fail to detect a matching `ethers` dependency, this returns the [`DEFAULT_ETHERS_DEP`]
647    /// version.
648    fn crate_version(&self) -> String {
649        self.try_find_crate_version().unwrap_or_else(|_| DEFAULT_ETHERS_DEP.to_string())
650    }
651
652    /// parses the active Cargo.toml to get what version of ethers we are using.
653    ///
654    /// Fails if the existing `Cargo.toml` does not contain a valid ethers dependency
655    fn try_find_crate_version(&self) -> Result<String> {
656        let cargo_toml =
657            if let Some(root) = self.root.clone() { root } else { std::env::current_dir()? }
658                .join("Cargo.toml");
659
660        if !cargo_toml.exists() {
661            return Ok(DEFAULT_ETHERS_DEP.to_string())
662        }
663
664        let data = fs::read_to_string(cargo_toml)?;
665        let toml = data.parse::<Value>()?;
666
667        let ethers = toml
668            .get("dependencies")
669            .and_then(|v| v.get("ethers").or_else(|| v.get("ethers-contract")))
670            .ok_or_else(|| eyre::eyre!("couldn't find ethers or ethers-contract dependency"))?;
671
672        if let Some(rev) = ethers.get("rev") {
673            Ok(format!("ethers = {{ git = \"https://github.com/gakonst/ethers-rs\", rev = {rev}, default-features = false, features = [\"abigen\"] }}"))
674        } else if let Some(version) = ethers.get("version") {
675            Ok(format!(
676                "ethers = {{ version = {version}, default-features = false, features = [\"abigen\"] }}"
677            ))
678        } else {
679            Ok(DEFAULT_ETHERS_DEP.to_string())
680        }
681    }
682
683    /// Write the contents of `Cargo.toml` to disk
684    fn write_cargo_toml(
685        &self,
686        lib: &Path,
687        name: impl AsRef<str>,
688        version: impl AsRef<str>,
689    ) -> Result<()> {
690        let crate_version = self.crate_version();
691        let contents = self.generate_cargo_toml(name, version, crate_version)?;
692
693        let mut file = fs::OpenOptions::new()
694            .read(true)
695            .write(true)
696            .create_new(true)
697            .open(lib.join("Cargo.toml"))?;
698        file.write_all(&contents)?;
699
700        Ok(())
701    }
702
703    /// Append module declarations to the `lib.rs` or `mod.rs`
704    fn append_module_names(&self, mut buf: impl Write) -> Result<()> {
705        let mut mod_names: BTreeSet<_> =
706            self.bindings.keys().map(|name| util::safe_module_name(name)).collect();
707        if let Some(ref shared) = self.shared_types {
708            mod_names.insert(shared.name.to_snake_case());
709        }
710
711        for module in mod_names.into_iter().map(|name| format!("pub mod {name};")) {
712            writeln!(buf, "{module}")?;
713        }
714
715        Ok(())
716    }
717
718    /// Generate the contents of `lib.rs` or `mod.rs`
719    fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result<Vec<u8>> {
720        let mut contents = vec![];
721        generate_prefix(&mut contents, is_crate, single_file)?;
722
723        if single_file {
724            if let Some(ref shared) = self.shared_types {
725                shared.write(&mut contents)?;
726            }
727            for binding in self.bindings.values() {
728                binding.write(&mut contents)?;
729            }
730        } else {
731            self.append_module_names(&mut contents)?;
732        }
733
734        Ok(contents)
735    }
736
737    /// Write the `lib.rs` or `mod.rs` to disk
738    fn write_super_file(&self, path: &Path, is_crate: bool, single_file: bool) -> Result<()> {
739        let filename = if is_crate { "lib.rs" } else { "mod.rs" };
740        let contents = self.generate_super_contents(is_crate, single_file)?;
741        fs::write(path.join(filename), contents)?;
742        Ok(())
743    }
744
745    /// Write all contract bindings to their respective files
746    fn write_bindings(&self, path: &Path) -> Result<()> {
747        if let Some(ref shared) = self.shared_types {
748            shared.write_module_in_dir(path)?;
749        }
750        for binding in self.bindings.values() {
751            binding.write_module_in_dir(path)?;
752        }
753        Ok(())
754    }
755
756    fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
757        let module = module.as_ref();
758        fs::create_dir_all(module)?;
759
760        self.write_super_file(module, false, single_file)?;
761
762        if !single_file {
763            self.write_bindings(module)?;
764        }
765        Ok(())
766    }
767
768    fn write_to_crate(
769        self,
770        name: impl AsRef<str>,
771        version: impl AsRef<str>,
772        lib: impl AsRef<Path>,
773        single_file: bool,
774    ) -> Result<()> {
775        let lib = lib.as_ref();
776        let src = lib.join("src");
777        fs::create_dir_all(&src)?;
778
779        self.write_cargo_toml(lib, name, version)?;
780        self.write_super_file(&src, true, single_file)?;
781
782        if !single_file {
783            self.write_bindings(&src)?;
784        }
785
786        Ok(())
787    }
788
789    /// Ensures the contents of the bindings directory are correct
790    ///
791    /// Does this by first generating the `lib.rs` or `mod.rs`, then the
792    /// contents of each binding file in turn.
793    fn ensure_consistent_bindings(
794        self,
795        dir: impl AsRef<Path>,
796        is_crate: bool,
797        single_file: bool,
798    ) -> Result<()> {
799        let dir = dir.as_ref();
800        let super_name = if is_crate { "lib.rs" } else { "mod.rs" };
801
802        let super_contents = self.generate_super_contents(is_crate, single_file)?;
803        check_file_in_dir(dir, super_name, &super_contents)?;
804
805        // If it is single file, we skip checking anything but the super
806        // contents
807        if !single_file {
808            for binding in self.bindings.values() {
809                check_binding_in_dir(dir, binding)?;
810            }
811        }
812
813        Ok(())
814    }
815
816    fn ensure_consistent_crate(
817        self,
818        name: impl AsRef<str>,
819        version: impl AsRef<str>,
820        crate_path: impl AsRef<Path>,
821        single_file: bool,
822        check_cargo_toml: bool,
823    ) -> Result<()> {
824        let crate_path = crate_path.as_ref();
825
826        if check_cargo_toml {
827            // additionally check the contents of the cargo
828            let crate_version = self.crate_version();
829            let cargo_contents = self.generate_cargo_toml(name, version, crate_version)?;
830            check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?;
831        }
832
833        self.ensure_consistent_bindings(crate_path.join("src"), true, single_file)?;
834        Ok(())
835    }
836
837    fn ensure_consistent_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
838        self.ensure_consistent_bindings(module, false, single_file)?;
839        Ok(())
840    }
841}
842
843/// Generate the shared prefix of the `lib.rs` or `mod.rs`
844fn generate_prefix(mut buf: impl Write, is_crate: bool, single_file: bool) -> Result<()> {
845    writeln!(buf, "#![allow(clippy::all)]")?;
846    writeln!(
847        buf,
848        "//! This {} contains abigen! generated bindings for solidity contracts.",
849        if is_crate { "lib" } else { "module" }
850    )?;
851    writeln!(buf, "//! This is autogenerated code.")?;
852    writeln!(buf, "//! Do not manually edit these files.")?;
853    writeln!(
854        buf,
855        "//! {} may be overwritten by the codegen system at any time.",
856        if single_file && !is_crate { "This file" } else { "These files" }
857    )?;
858    Ok(())
859}
860
861fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> {
862    eyre::ensure!(dir.is_dir(), "Not a directory: {}", dir.display());
863
864    let file_path = dir.join(file_name);
865    eyre::ensure!(file_path.is_file(), "Not a file: {}", file_path.display());
866
867    let contents = fs::read(&file_path).expect("Unable to read file");
868    eyre::ensure!(contents == expected_contents, format!("The contents of `{}` do not match the expected output of the newest `ethers::Abigen` version.\
869This indicates that the existing bindings are outdated and need to be generated again.", file_path.display()));
870    Ok(())
871}
872
873fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> {
874    let name = binding.module_filename();
875    let contents = binding.to_vec();
876
877    check_file_in_dir(dir, &name, &contents)?;
878    Ok(())
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use crate::{ExcludeContracts, SelectContracts};
885    use std::env;
886
887    struct Context {
888        multi_gen: MultiAbigen,
889        mod_root: PathBuf,
890    }
891
892    fn run_test<T>(test: T)
893    where
894        T: FnOnce(Context),
895    {
896        let crate_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
897        let console = Abigen::new(
898            "Console",
899            crate_root.join("../tests/solidity-contracts/console.json").display().to_string(),
900        )
901        .unwrap();
902
903        let simple_storage = Abigen::new(
904            "SimpleStorage",
905            crate_root.join("../tests/solidity-contracts/SimpleStorage.json").display().to_string(),
906        )
907        .unwrap();
908
909        let human_readable = Abigen::new(
910            "HrContract",
911            r"[
912        struct Foo { uint256 x; }
913        function foo(Foo memory x)
914        function bar(uint256 x, uint256 y, address addr)
915        yeet(uint256,uint256,address)
916    ]",
917        )
918        .unwrap();
919
920        let multi_gen = MultiAbigen::from_abigens([console, simple_storage, human_readable]);
921
922        let tmp = tempfile::tempdir().unwrap();
923        let mod_root = tmp.path().join("contracts");
924        // fs::create_dir(&mod_root).unwrap();
925        let context = Context { multi_gen, mod_root };
926        test(context)
927    }
928
929    #[test]
930    fn can_generate_multi_file_module() {
931        run_test(|context| {
932            let Context { multi_gen, mod_root } = context;
933
934            let single_file = false;
935
936            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
937            multi_gen
938                .build()
939                .unwrap()
940                .ensure_consistent_module(mod_root, single_file)
941                .expect("Inconsistent bindings");
942        })
943    }
944
945    #[test]
946    fn can_find_ethers_dep() {
947        run_test(|context| {
948            let Context { multi_gen, mod_root } = context;
949
950            let single_file = true;
951            let mut inner = multi_gen.build().unwrap().into_inner(single_file);
952            inner.root = Some(PathBuf::from("this does not exist"));
953            inner.write_to_module(mod_root, single_file).unwrap();
954        })
955    }
956
957    #[test]
958    fn can_generate_single_file_module() {
959        run_test(|context| {
960            let Context { multi_gen, mod_root } = context;
961
962            let single_file = true;
963
964            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
965            multi_gen
966                .build()
967                .unwrap()
968                .ensure_consistent_module(mod_root, single_file)
969                .expect("Inconsistent bindings");
970        })
971    }
972
973    #[test]
974    fn can_generate_multi_file_crate() {
975        run_test(|context| {
976            let Context { multi_gen, mod_root } = context;
977
978            let single_file = false;
979            let name = "a-name";
980            let version = "290.3782.3";
981
982            multi_gen
983                .clone()
984                .build()
985                .unwrap()
986                .write_to_crate(name, version, &mod_root, single_file)
987                .unwrap();
988            multi_gen
989                .build()
990                .unwrap()
991                .ensure_consistent_crate(name, version, mod_root, single_file, true)
992                .expect("Inconsistent bindings");
993        })
994    }
995
996    #[test]
997    fn can_generate_single_file_crate() {
998        run_test(|context| {
999            let Context { multi_gen, mod_root } = context;
1000
1001            let single_file = true;
1002            let name = "a-name";
1003            let version = "290.3782.3";
1004
1005            multi_gen
1006                .clone()
1007                .build()
1008                .unwrap()
1009                .write_to_crate(name, version, &mod_root, single_file)
1010                .unwrap();
1011            multi_gen
1012                .build()
1013                .unwrap()
1014                .ensure_consistent_crate(name, version, mod_root, single_file, true)
1015                .expect("Inconsistent bindings");
1016        })
1017    }
1018
1019    #[test]
1020    fn can_detect_incosistent_multi_file_module() {
1021        run_test(|context| {
1022            let Context { mut multi_gen, mod_root } = context;
1023
1024            let single_file = false;
1025
1026            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
1027
1028            multi_gen.push(
1029                Abigen::new(
1030                    "AdditionalContract",
1031                    r"[
1032                        getValue() (uint256)
1033                    ]",
1034                )
1035                .unwrap(),
1036            );
1037
1038            let result =
1039                multi_gen.build().unwrap().ensure_consistent_module(mod_root, single_file).is_err();
1040
1041            // ensure inconsistent bindings are detected
1042            assert!(result, "Inconsistent bindings wrongly approved");
1043        })
1044    }
1045
1046    #[test]
1047    fn can_detect_incosistent_single_file_module() {
1048        run_test(|context| {
1049            let Context { mut multi_gen, mod_root } = context;
1050
1051            let single_file = true;
1052
1053            multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
1054
1055            multi_gen.push(
1056                Abigen::new(
1057                    "AdditionalContract",
1058                    r"[
1059                        getValue() (uint256)
1060                    ]",
1061                )
1062                .unwrap(),
1063            );
1064
1065            let result =
1066                multi_gen.build().unwrap().ensure_consistent_module(mod_root, single_file).is_err();
1067
1068            // ensure inconsistent bindings are detected
1069            assert!(result, "Inconsistent bindings wrongly approved");
1070        })
1071    }
1072
1073    #[test]
1074    fn can_detect_incosistent_multi_file_crate() {
1075        run_test(|context| {
1076            let Context { mut multi_gen, mod_root } = context;
1077
1078            let single_file = false;
1079            let name = "a-name";
1080            let version = "290.3782.3";
1081
1082            multi_gen
1083                .clone()
1084                .build()
1085                .unwrap()
1086                .write_to_crate(name, version, &mod_root, single_file)
1087                .unwrap();
1088
1089            multi_gen.push(
1090                Abigen::new(
1091                    "AdditionalContract",
1092                    r"[
1093                            getValue() (uint256)
1094                        ]",
1095                )
1096                .unwrap(),
1097            );
1098
1099            let result = multi_gen
1100                .build()
1101                .unwrap()
1102                .ensure_consistent_crate(name, version, mod_root, single_file, true)
1103                .is_err();
1104
1105            // ensure inconsistent bindings are detected
1106            assert!(result, "Inconsistent bindings wrongly approved");
1107        })
1108    }
1109
1110    #[test]
1111    fn can_detect_inconsistent_single_file_crate() {
1112        run_test(|context| {
1113            let Context { mut multi_gen, mod_root } = context;
1114
1115            let single_file = true;
1116            let name = "a-name";
1117            let version = "290.3782.3";
1118
1119            multi_gen
1120                .clone()
1121                .build()
1122                .unwrap()
1123                .write_to_crate(name, version, &mod_root, single_file)
1124                .unwrap();
1125
1126            multi_gen.push(
1127                Abigen::new(
1128                    "AdditionalContract",
1129                    r"[
1130                            getValue() (uint256)
1131                        ]",
1132                )
1133                .unwrap(),
1134            );
1135
1136            let result = multi_gen
1137                .build()
1138                .unwrap()
1139                .ensure_consistent_crate(name, version, mod_root, single_file, true)
1140                .is_err();
1141
1142            // ensure inconsistent bindings are detected
1143            assert!(result, "Inconsistent bindings wrongly approved");
1144        })
1145    }
1146
1147    #[test]
1148    fn does_not_generate_shared_types_if_empty() {
1149        let gen = Abigen::new(
1150            "Greeter",
1151            r"[
1152                        struct Inner {bool a;}
1153                        greet1() (uint256)
1154                        greet2(Inner inner) (string)
1155                    ]",
1156        )
1157        .unwrap();
1158
1159        let tokens = MultiExpansion::new(vec![gen.expand().unwrap()]).expand_inplace().to_string();
1160        assert!(!tokens.contains("mod __shared_types"));
1161    }
1162
1163    #[test]
1164    fn can_filter_abigen() {
1165        let abi = Abigen::new(
1166            "MyGreeter",
1167            r"[
1168                        greet() (string)
1169                    ]",
1170        )
1171        .unwrap();
1172        let mut gen = MultiAbigen::from_abigens(vec![abi]).with_filter(ContractFilter::All);
1173        assert_eq!(gen.abigens.len(), 1);
1174        gen.apply_filter(&SelectContracts::default().add_name("MyGreeter").into());
1175        assert_eq!(gen.abigens.len(), 1);
1176
1177        gen.apply_filter(&ExcludeContracts::default().add_name("MyGreeter2").into());
1178        assert_eq!(gen.abigens.len(), 1);
1179
1180        let filtered = gen.clone().with_filter(SelectContracts::default().add_name("MyGreeter2"));
1181        assert!(filtered.abigens.is_empty());
1182
1183        let filtered = gen.clone().with_filter(ExcludeContracts::default().add_name("MyGreeter"));
1184        assert!(filtered.abigens.is_empty());
1185
1186        let filtered =
1187            gen.clone().with_filter(SelectContracts::default().add_pattern("MyGreeter2"));
1188        assert!(filtered.abigens.is_empty());
1189
1190        let filtered =
1191            gen.clone().with_filter(ExcludeContracts::default().add_pattern("MyGreeter"));
1192        assert!(filtered.abigens.is_empty());
1193
1194        gen.push(
1195            Abigen::new(
1196                "MyGreeterTest",
1197                r"[
1198                        greet() (string)
1199                    ]",
1200            )
1201            .unwrap(),
1202        );
1203        let filtered = gen.clone().with_filter(SelectContracts::default().add_pattern(".*Test"));
1204        assert_eq!(filtered.abigens.len(), 1);
1205        assert_eq!(filtered.abigens[0].contract_name, "MyGreeterTest");
1206
1207        let filtered = gen.clone().with_filter(ExcludeContracts::default().add_pattern(".*Test"));
1208        assert_eq!(filtered.abigens.len(), 1);
1209        assert_eq!(filtered.abigens[0].contract_name, "MyGreeter");
1210    }
1211
1212    #[test]
1213    fn can_deduplicate_types() {
1214        let root = tempfile::tempdir().unwrap();
1215        let json_files = "../tests/solidity-contracts/greeter";
1216
1217        let gen = MultiAbigen::from_json_files(json_files).unwrap();
1218        let bindings = gen.clone().build().unwrap();
1219        let single_file_dir = root.path().join("single_bindings");
1220        bindings.write_to_module(&single_file_dir, true).unwrap();
1221
1222        let single_file_mod = single_file_dir.join("mod.rs");
1223        assert!(single_file_mod.exists());
1224        let content = fs::read_to_string(&single_file_mod).unwrap();
1225        assert!(content.contains("mod __shared_types"));
1226        assert!(content.contains("pub struct Inner"));
1227        assert!(content.contains("pub struct Stuff"));
1228
1229        // multiple files
1230        let bindings = gen.build().unwrap();
1231        let multi_file_dir = root.path().join("multi_bindings");
1232        bindings.write_to_module(&multi_file_dir, false).unwrap();
1233        let multi_file_mod = multi_file_dir.join("mod.rs");
1234        assert!(multi_file_mod.exists());
1235        let content = fs::read_to_string(&multi_file_mod).unwrap();
1236        assert!(content.contains("pub mod shared_types"));
1237
1238        let greeter1 = multi_file_dir.join("greeter_1.rs");
1239        assert!(greeter1.exists());
1240        let content = fs::read_to_string(&greeter1).unwrap();
1241        assert!(!content.contains("pub struct Inner"));
1242        assert!(!content.contains("pub struct Stuff"));
1243
1244        let greeter2 = multi_file_dir.join("greeter_2.rs");
1245        assert!(greeter2.exists());
1246        let content = fs::read_to_string(&greeter2).unwrap();
1247        assert!(!content.contains("pub struct Inner"));
1248        assert!(!content.contains("pub struct Stuff"));
1249
1250        let shared_types = multi_file_dir.join("shared_types.rs");
1251        assert!(shared_types.exists());
1252        let content = fs::read_to_string(&shared_types).unwrap();
1253        assert!(content.contains("pub struct Inner"));
1254        assert!(content.contains("pub struct Stuff"));
1255    }
1256
1257    #[test]
1258    fn can_sanitize_reserved_words() {
1259        let root = tempfile::tempdir().unwrap();
1260        let json_files = "../tests/solidity-contracts/ReservedWords";
1261
1262        let gen = MultiAbigen::from_json_files(json_files).unwrap();
1263        let bindings = gen.clone().build().unwrap();
1264        let single_file_dir = root.path().join("single_bindings");
1265        bindings.write_to_module(&single_file_dir, true).unwrap();
1266
1267        let single_file_mod = single_file_dir.join("mod.rs");
1268        assert!(single_file_mod.exists());
1269        let content = fs::read_to_string(&single_file_mod).unwrap();
1270        assert!(content.contains("pub mod mod_ {"));
1271        assert!(content.contains("pub mod enum_ {"));
1272
1273        // multiple files
1274        let bindings = gen.build().unwrap();
1275        let multi_file_dir = root.path().join("multi_bindings");
1276        bindings.write_to_module(&multi_file_dir, false).unwrap();
1277        let multi_file_mod = multi_file_dir.join("mod.rs");
1278        assert!(multi_file_mod.exists());
1279        let content = fs::read_to_string(&multi_file_mod).unwrap();
1280        assert!(content.contains("pub mod enum_;"));
1281        assert!(content.contains("pub mod mod_;"));
1282
1283        let enum_ = multi_file_dir.join("enum_.rs");
1284        assert!(enum_.exists());
1285        let content = fs::read_to_string(&enum_).unwrap();
1286        assert!(content.contains("pub mod enum_ {"));
1287
1288        let mod_ = multi_file_dir.join("mod_.rs");
1289        assert!(mod_.exists());
1290        let content = fs::read_to_string(&mod_).unwrap();
1291        assert!(content.contains("pub mod mod_ {"));
1292    }
1293
1294    #[test]
1295    fn parse_ethers_crate() {
1296        // gotta bunch these all together as we are overwriting env vars
1297        run_test(|context| {
1298            let Context { multi_gen, mod_root } = context;
1299            let manifest = r#"
1300[package]
1301    name = "ethers-contract"
1302    version = "1.0.0"
1303    edition = "2021"
1304    rust-version = "1.64"
1305    authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
1306    license = "MIT OR Apache-2.0"
1307    description = "Smart contract bindings for the ethers-rs crate"
1308    homepage = "https://docs.rs/ethers"
1309    repository = "https://github.com/gakonst/ethers-rs"
1310    keywords = ["ethereum", "web3", "celo", "ethers"]
1311
1312    [dependencies]
1313    ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-features = false }
1314"#;
1315
1316            let root = mod_root.parent().unwrap();
1317            fs::write(root.join("../Cargo.toml"), manifest).unwrap();
1318            env::set_var("CARGO_MANIFEST_DIR", root);
1319            let single_file = false;
1320            let name = "a-name";
1321            let version = "290.3782.3";
1322
1323            multi_gen
1324                .clone()
1325                .build()
1326                .unwrap()
1327                .write_to_crate(name, version, &mod_root, single_file)
1328                .unwrap();
1329
1330            multi_gen
1331                .build()
1332                .unwrap()
1333                .ensure_consistent_crate(name, version, &mod_root, single_file, true)
1334                .expect("Inconsistent bindings");
1335        });
1336
1337        run_test(|context| {
1338            let Context { multi_gen, mod_root } = context;
1339
1340            let manifest = r#"
1341 [package]
1342    name = "ethers-contract"
1343    version = "1.0.0"
1344    edition = "2021"
1345    rust-version = "1.64"
1346    authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
1347    license = "MIT OR Apache-2.0"
1348    description = "Smart contract bindings for the ethers-rs crate"
1349    homepage = "https://docs.rs/ethers"
1350    repository = "https://github.com/gakonst/ethers-rs"
1351    keywords = ["ethereum", "web3", "celo", "ethers"]
1352
1353    [dependencies]
1354    ethers-contracts = "0.4.0"
1355"#;
1356
1357            let root = mod_root.parent().unwrap();
1358            fs::write(root.join("../Cargo.toml"), manifest).unwrap();
1359            env::set_var("CARGO_MANIFEST_DIR", root);
1360
1361            let single_file = false;
1362            let name = "a-name";
1363            let version = "290.3782.3";
1364
1365            multi_gen
1366                .clone()
1367                .build()
1368                .unwrap()
1369                .write_to_crate(name, version, &mod_root, single_file)
1370                .unwrap();
1371
1372            multi_gen
1373                .build()
1374                .unwrap()
1375                .ensure_consistent_crate(name, version, mod_root, single_file, true)
1376                .expect("Inconsistent bindings");
1377        });
1378
1379        run_test(|context| {
1380            let Context { multi_gen, mod_root } = context;
1381
1382            let manifest = r#"
1383[package]
1384    name = "ethers-contract"
1385    version = "1.0.0"
1386    edition = "2021"
1387    rust-version = "1.64"
1388    authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
1389    license = "MIT OR Apache-2.0"
1390    description = "Smart contract bindings for the ethers-rs crate"
1391    homepage = "https://docs.rs/ethers"
1392    repository = "https://github.com/gakonst/ethers-rs"
1393    keywords = ["ethereum", "web3", "celo", "ethers"]
1394
1395    [dependencies]
1396    ethers = {git="https://github.com/gakonst/ethers-rs", rev = "fd8ebf5",features = ["ws", "rustls", "ipc"] }
1397"#;
1398
1399            let root = mod_root.parent().unwrap();
1400            fs::write(root.join("../Cargo.toml"), manifest).unwrap();
1401            env::set_var("CARGO_MANIFEST_DIR", root);
1402
1403            let single_file = false;
1404            let name = "a-name";
1405            let version = "290.3782.3";
1406
1407            multi_gen
1408                .clone()
1409                .build()
1410                .unwrap()
1411                .write_to_crate(name, version, &mod_root, single_file)
1412                .unwrap();
1413
1414            multi_gen
1415                .build()
1416                .unwrap()
1417                .ensure_consistent_crate(name, version, mod_root, single_file, true)
1418                .expect("Inconsistent bindings");
1419        });
1420
1421        run_test(|context| {
1422            let Context { multi_gen, mod_root } = context;
1423
1424            let manifest = r#"
1425[package]
1426    name = "ethers-contract"
1427    version = "1.0.0"
1428    edition = "2021"
1429    rust-version = "1.64"
1430    authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
1431    license = "MIT OR Apache-2.0"
1432    description = "Smart contract bindings for the ethers-rs crate"
1433    homepage = "https://docs.rs/ethers"
1434    repository = "https://github.com/gakonst/ethers-rs"
1435    keywords = ["ethereum", "web3", "celo", "ethers"]
1436
1437    [dependencies]
1438    ethers = {git = "https://github.com/gakonst/ethers-rs", features = ["ws", "rustls", "ipc"] }
1439"#;
1440
1441            let root = mod_root.parent().unwrap();
1442            fs::write(root.join("../Cargo.toml"), manifest).unwrap();
1443            env::set_var("CARGO_MANIFEST_DIR", root);
1444
1445            let single_file = false;
1446            let name = "a-name";
1447            let version = "290.3782.3";
1448
1449            multi_gen
1450                .clone()
1451                .build()
1452                .unwrap()
1453                .write_to_crate(name, version, &mod_root, single_file)
1454                .unwrap();
1455
1456            multi_gen
1457                .build()
1458                .unwrap()
1459                .ensure_consistent_crate(name, version, mod_root, single_file, true)
1460                .expect("Inconsistent bindings");
1461        });
1462    }
1463}