refactor: MultiAbigen rework (#852)
* refactor: MultiAbigen rework * docs: update ensure family docs * feature: FromIterator for MultiAbigen * refactor: cleaner instantiation of Abigen from files * docs: update examples to use build step * chore: add 854 to changelog * lint: clippy * Update ethers-contract/ethers-contract-abigen/src/lib.rs Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
8c07861b09
commit
5f68bf52d2
|
@ -25,6 +25,9 @@
|
||||||
- Move `fill_transaction` implementation to the provider, to allow middleware
|
- Move `fill_transaction` implementation to the provider, to allow middleware
|
||||||
to properly override its behavior.
|
to properly override its behavior.
|
||||||
- Add informational messages to solc installation and compilation.
|
- Add informational messages to solc installation and compilation.
|
||||||
|
- Significantly refactor `MultiAbigen` module generation. Now allows for lib
|
||||||
|
generation, and does not make unnecessary disk writes.
|
||||||
|
[#854](https://github.com/gakonst/ethers-rs/pull/852)
|
||||||
|
|
||||||
## ethers-contract-abigen
|
## ethers-contract-abigen
|
||||||
|
|
||||||
|
@ -105,6 +108,7 @@
|
||||||
[640](https://github.com/gakonst/ethers-rs/pull/640)
|
[640](https://github.com/gakonst/ethers-rs/pull/640)
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
- Add support for basic and bearer authentication in http and non-wasm websockets.
|
- Add support for basic and bearer authentication in http and non-wasm websockets.
|
||||||
[829](https://github.com/gakonst/ethers-rs/pull/829)
|
[829](https://github.com/gakonst/ethers-rs/pull/829)
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ mod rustfmt;
|
||||||
mod source;
|
mod source;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
pub mod multi;
|
||||||
|
pub use multi::MultiAbigen;
|
||||||
|
|
||||||
pub use ethers_core::types::Address;
|
pub use ethers_core::types::Address;
|
||||||
pub use source::Source;
|
pub use source::Source;
|
||||||
pub use util::parse_address;
|
pub use util::parse_address;
|
||||||
|
@ -26,12 +29,7 @@ pub use util::parse_address;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use inflector::Inflector;
|
use inflector::Inflector;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use std::{
|
use std::{collections::HashMap, fs::File, io::Write, path::Path};
|
||||||
collections::HashMap,
|
|
||||||
fs::{self, File},
|
|
||||||
io::Write,
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Builder struct for generating type-safe bindings from a contract's ABI
|
/// Builder struct for generating type-safe bindings from a contract's ABI
|
||||||
///
|
///
|
||||||
|
@ -86,6 +84,19 @@ impl Abigen {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attemtps to load a new builder from an ABI JSON file at the specific
|
||||||
|
/// path.
|
||||||
|
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
|
let name = path
|
||||||
|
.as_ref()
|
||||||
|
.file_stem()
|
||||||
|
.ok_or_else(|| anyhow::format_err!("Missing file stem in path"))?
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::format_err!("Unable to convert file stem to string"))?;
|
||||||
|
|
||||||
|
Self::new(name, std::fs::read_to_string(path.as_ref())?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Manually adds a solidity event alias to specify what the event struct
|
/// Manually adds a solidity event alias to specify what the event struct
|
||||||
/// and function name will be in Rust.
|
/// and function name will be in Rust.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
@ -138,8 +149,9 @@ impl Abigen {
|
||||||
/// Generates the contract bindings.
|
/// Generates the contract bindings.
|
||||||
pub fn generate(self) -> Result<ContractBindings> {
|
pub fn generate(self) -> Result<ContractBindings> {
|
||||||
let rustfmt = self.rustfmt;
|
let rustfmt = self.rustfmt;
|
||||||
|
let name = self.contract_name.clone();
|
||||||
let tokens = Context::from_abigen(self)?.expand()?.into_tokens();
|
let tokens = Context::from_abigen(self)?.expand()?.into_tokens();
|
||||||
Ok(ContractBindings { tokens, rustfmt })
|
Ok(ContractBindings { tokens, rustfmt, name })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +162,8 @@ pub struct ContractBindings {
|
||||||
tokens: TokenStream,
|
tokens: TokenStream,
|
||||||
/// The output options used for serialization.
|
/// The output options used for serialization.
|
||||||
rustfmt: bool,
|
rustfmt: bool,
|
||||||
|
/// The contract name
|
||||||
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContractBindings {
|
impl ContractBindings {
|
||||||
|
@ -172,6 +186,13 @@ impl ContractBindings {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the bindings to a new Vec. Panics if unable to allocate
|
||||||
|
pub fn to_vec(&self) -> Vec<u8> {
|
||||||
|
let mut bindings = vec![];
|
||||||
|
self.write(&mut bindings).expect("allocations don't fail");
|
||||||
|
bindings
|
||||||
|
}
|
||||||
|
|
||||||
/// Writes the bindings to the specified file.
|
/// Writes the bindings to the specified file.
|
||||||
pub fn write_to_file<P>(&self, path: P) -> Result<()>
|
pub fn write_to_file<P>(&self, path: P) -> Result<()>
|
||||||
where
|
where
|
||||||
|
@ -181,289 +202,32 @@ impl ContractBindings {
|
||||||
self.write(file)
|
self.write(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the bindings to a `contract_name.rs` file in the specified
|
||||||
|
/// directory. The filename is the snake_case transformation of the contract
|
||||||
|
/// name.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts the bindings into its underlying token stream. This allows it
|
/// Converts the bindings into its underlying token stream. This allows it
|
||||||
/// to be used within a procedural macro.
|
/// to be used within a procedural macro.
|
||||||
pub fn into_tokens(self) -> TokenStream {
|
pub fn into_tokens(self) -> TokenStream {
|
||||||
self.tokens
|
self.tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate the default module name (snake case of the contract name)
|
||||||
|
pub fn module_name(&self) -> String {
|
||||||
|
self.name.to_snake_case()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates bindings for a series of contracts
|
/// Generate the default filename of the module
|
||||||
///
|
pub fn module_filename(&self) -> String {
|
||||||
/// This type can be used to generate multiple `ContractBindings` and put them all in a single rust
|
let mut name = self.module_name();
|
||||||
/// module, (eg. a `contracts` directory).
|
name.extend([".rs"]);
|
||||||
///
|
name
|
||||||
/// This can be used to
|
|
||||||
/// 1) write all bindings directly into a new directory in the project's source directory, so that
|
|
||||||
/// it is included in the repository. 2) write all bindings to the value of cargo's `OUT_DIR` in a
|
|
||||||
/// build script and import the bindings as `include!(concat!(env!("OUT_DIR"), "/mod.rs"));`.
|
|
||||||
///
|
|
||||||
/// However, the main purpose of this generator is to create bindings for option `1)` and write all
|
|
||||||
/// contracts to some `contracts` module in `src`, like `src/contracts/mod.rs` __once__ via a build
|
|
||||||
/// script or a test. After that it's recommend to remove the build script and replace it with an
|
|
||||||
/// integration test (See `MultiAbigen::ensure_consistent_bindings`) that fails if the generated
|
|
||||||
/// code is out of date. This has several advantages:
|
|
||||||
///
|
|
||||||
/// * No need for downstream users to compile the build script
|
|
||||||
/// * No need for downstream users to run the whole `abigen!` generation steps
|
|
||||||
/// * The generated code is more usable in an IDE
|
|
||||||
/// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
|
|
||||||
/// changed)
|
|
||||||
///
|
|
||||||
/// See `MultiAbigen::ensure_consistent_bindings` for the recommended way to set this up to generate
|
|
||||||
/// the bindings once via a test and then use the test to ensure consistency.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MultiAbigen {
|
|
||||||
/// whether to write all contracts in a single file instead of separated modules
|
|
||||||
single_file: bool,
|
|
||||||
|
|
||||||
abigens: Vec<Abigen>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MultiAbigen {
|
|
||||||
/// Create a new instance from a series of already resolved `Abigen`
|
|
||||||
pub fn from_abigen(abis: impl IntoIterator<Item = Abigen>) -> Self {
|
|
||||||
Self {
|
|
||||||
single_file: false,
|
|
||||||
abigens: abis.into_iter().map(|abi| abi.rustfmt(true)).collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new instance from a series (`contract name`, `abi_source`)
|
|
||||||
///
|
|
||||||
/// See `Abigen::new`
|
|
||||||
pub fn new<I, Name, Source>(abis: I) -> Result<Self>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = (Name, Source)>,
|
|
||||||
Name: AsRef<str>,
|
|
||||||
Source: AsRef<str>,
|
|
||||||
{
|
|
||||||
let abis = abis
|
|
||||||
.into_iter()
|
|
||||||
.map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source))
|
|
||||||
.collect::<Result<Vec<_>>>()?;
|
|
||||||
|
|
||||||
Ok(Self::from_abigen(abis))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads all json files contained in the given `dir` and use the file name for the name of the
|
|
||||||
/// `ContractBindings`.
|
|
||||||
/// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// abi
|
|
||||||
/// ├── ERC20.json
|
|
||||||
/// ├── Contract1.json
|
|
||||||
/// ├── Contract2.json
|
|
||||||
/// ...
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// # use ethers_contract_abigen::MultiAbigen;
|
|
||||||
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
|
|
||||||
/// ```
|
|
||||||
pub fn from_json_files(dir: impl AsRef<Path>) -> Result<Self> {
|
|
||||||
let mut abis = Vec::new();
|
|
||||||
for file in util::json_files(dir) {
|
|
||||||
if let Some(file_name) = file.file_stem().and_then(|s| s.to_str()) {
|
|
||||||
let content = fs::read_to_string(&file)?;
|
|
||||||
abis.push((file_name.to_string(), content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::new(abis)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write all bindings into a single rust file instead of separate modules
|
|
||||||
#[must_use]
|
|
||||||
pub fn single_file(mut self) -> Self {
|
|
||||||
self.single_file = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates all the bindings and writes them to the given module
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Read all json abi files from the `./abi` directory
|
|
||||||
/// ```text
|
|
||||||
/// abi
|
|
||||||
/// ├── ERC20.json
|
|
||||||
/// ├── Contract1.json
|
|
||||||
/// ├── Contract2.json
|
|
||||||
/// ...
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// and write them to the `./src/contracts` location as
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// src/contracts
|
|
||||||
/// ├── mod.rs
|
|
||||||
/// ├── er20.rs
|
|
||||||
/// ├── contract1.rs
|
|
||||||
/// ├── contract2.rs
|
|
||||||
/// ...
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// # use ethers_contract_abigen::MultiAbigen;
|
|
||||||
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
|
|
||||||
/// gen.write_to_module("./src/contracts").unwrap();
|
|
||||||
/// ```
|
|
||||||
pub fn write_to_module(self, module: impl AsRef<Path>) -> Result<()> {
|
|
||||||
let module = module.as_ref();
|
|
||||||
fs::create_dir_all(module)?;
|
|
||||||
|
|
||||||
let mut contracts_mod =
|
|
||||||
b"/// This module contains all the autogenerated abigen! contract bindings\n".to_vec();
|
|
||||||
|
|
||||||
let mut modules = Vec::new();
|
|
||||||
for abi in self.abigens {
|
|
||||||
let name = abi.contract_name.to_snake_case();
|
|
||||||
let bindings = abi.generate()?;
|
|
||||||
if self.single_file {
|
|
||||||
// append to the mod file
|
|
||||||
bindings.write(&mut contracts_mod)?;
|
|
||||||
} else {
|
|
||||||
// create a contract rust file
|
|
||||||
let output = module.join(format!("{}.rs", name));
|
|
||||||
bindings.write_to_file(output)?;
|
|
||||||
modules.push(format!("pub mod {};", name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !modules.is_empty() {
|
|
||||||
modules.sort();
|
|
||||||
write!(contracts_mod, "{}", modules.join("\n"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the mod file
|
|
||||||
fs::write(module.join("mod.rs"), contracts_mod)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This ensures that the already generated contract bindings match the output of a fresh new
|
|
||||||
/// run. Run this in a rust test, to get notified in CI if the newly generated bindings
|
|
||||||
/// deviate from the already generated ones, and it's time to generate them again. This could
|
|
||||||
/// happen if the ABI of a contract or the output that `ethers` generates changed.
|
|
||||||
///
|
|
||||||
/// So if this functions is run within a test during CI and fails, then it's time to update all
|
|
||||||
/// bindings.
|
|
||||||
///
|
|
||||||
/// Returns `true` if the freshly generated bindings match with the existing bindings, `false`
|
|
||||||
/// otherwise
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Check that the generated files are up to date
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// # use ethers_contract_abigen::MultiAbigen;
|
|
||||||
/// #[test]
|
|
||||||
/// fn generated_bindings_are_fresh() {
|
|
||||||
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
|
|
||||||
/// let abi_dir = project_root.join("abi");
|
|
||||||
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
|
|
||||||
/// assert!(gen.ensure_consistent_bindings(project_root.join("src/contracts")));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// gen.write_to_module("./src/contracts").unwrap();
|
|
||||||
/// ```
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn ensure_consistent_bindings(self, module: impl AsRef<Path>) -> bool {
|
|
||||||
let module = module.as_ref();
|
|
||||||
let dir = tempfile::tempdir().expect("Failed to create temp dir");
|
|
||||||
let temp_module = dir.path().join("contracts");
|
|
||||||
self.write_to_module(&temp_module).expect("Failed to generate bindings");
|
|
||||||
|
|
||||||
for file in fs::read_dir(&temp_module).unwrap() {
|
|
||||||
let fresh_file = file.unwrap();
|
|
||||||
let fresh_file_path = fresh_file.path();
|
|
||||||
let file_name = fresh_file_path.file_name().and_then(|p| p.to_str()).unwrap();
|
|
||||||
assert!(file_name.ends_with(".rs"), "Expected rust file");
|
|
||||||
|
|
||||||
let existing_bindings_file = module.join(file_name);
|
|
||||||
|
|
||||||
if !existing_bindings_file.is_file() {
|
|
||||||
// file does not already exist
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the existing file
|
|
||||||
let existing_contract_bindings = fs::read_to_string(existing_bindings_file).unwrap();
|
|
||||||
|
|
||||||
let fresh_bindings = fs::read_to_string(fresh_file.path()).unwrap();
|
|
||||||
|
|
||||||
if existing_contract_bindings != fresh_bindings {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_generate_multi_abi() {
|
|
||||||
let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
|
|
||||||
|
|
||||||
let tempdir = tempfile::tempdir().unwrap();
|
|
||||||
let mod_root = tempdir.path().join("contracts");
|
|
||||||
|
|
||||||
let console = Abigen::new(
|
|
||||||
"Console",
|
|
||||||
crate_root.join("../tests/solidity-contracts/console.json").display().to_string(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let simple_storage = Abigen::new(
|
|
||||||
"SimpleStorage",
|
|
||||||
crate_root
|
|
||||||
.join("../tests/solidity-contracts/simplestorage_abi.json")
|
|
||||||
.display()
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let human_readable = Abigen::new(
|
|
||||||
"HrContract",
|
|
||||||
r#"[
|
|
||||||
struct Foo { uint256 x; }
|
|
||||||
function foo(Foo memory x)
|
|
||||||
function bar(uint256 x, uint256 y, address addr)
|
|
||||||
yeet(uint256,uint256,address)
|
|
||||||
]"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut multi_gen = MultiAbigen::from_abigen([console, simple_storage, human_readable]);
|
|
||||||
|
|
||||||
multi_gen.clone().write_to_module(&mod_root).unwrap();
|
|
||||||
assert!(multi_gen.clone().ensure_consistent_bindings(&mod_root));
|
|
||||||
|
|
||||||
// add another contract
|
|
||||||
multi_gen.abigens.push(
|
|
||||||
Abigen::new(
|
|
||||||
"AdditionalContract",
|
|
||||||
r#"[
|
|
||||||
getValue() (uint256)
|
|
||||||
getValue(uint256 otherValue) (uint256)
|
|
||||||
getValue(uint256 otherValue, address addr) (uint256)
|
|
||||||
]"#,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ensure inconsistent bindings are detected
|
|
||||||
assert!(!multi_gen.clone().ensure_consistent_bindings(&mod_root));
|
|
||||||
|
|
||||||
// update with new contract
|
|
||||||
multi_gen.clone().write_to_module(&mod_root).unwrap();
|
|
||||||
assert!(multi_gen.clone().ensure_consistent_bindings(&mod_root));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,754 @@
|
||||||
|
//! TODO
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use inflector::Inflector;
|
||||||
|
use std::{collections::BTreeMap, fs, io::Write, path::Path};
|
||||||
|
|
||||||
|
use crate::{util, Abigen, ContractBindings};
|
||||||
|
|
||||||
|
/// Collects Abigen structs for a series of contracts, pending generation of
|
||||||
|
/// the contract bindings.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MultiAbigen {
|
||||||
|
/// Abigen objects to be written
|
||||||
|
abigens: Vec<Abigen>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for MultiAbigen {
|
||||||
|
type Target = Vec<Abigen>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.abigens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Abigen>> for MultiAbigen {
|
||||||
|
fn from(abigens: Vec<Abigen>) -> Self {
|
||||||
|
Self { abigens }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::iter::FromIterator<Abigen> for MultiAbigen {
|
||||||
|
fn from_iter<I: IntoIterator<Item = Abigen>>(iter: I) -> Self {
|
||||||
|
iter.into_iter().collect::<Vec<_>>().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiAbigen {
|
||||||
|
/// Create a new instance from a series (`contract name`, `abi_source`)
|
||||||
|
///
|
||||||
|
/// See `Abigen::new`
|
||||||
|
pub fn new<I, Name, Source>(abis: I) -> Result<Self>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (Name, Source)>,
|
||||||
|
Name: AsRef<str>,
|
||||||
|
Source: AsRef<str>,
|
||||||
|
{
|
||||||
|
let abis = abis
|
||||||
|
.into_iter()
|
||||||
|
.map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source))
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
Ok(Self::from_abigens(abis))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance from a series of already resolved `Abigen`
|
||||||
|
pub fn from_abigens(abis: impl IntoIterator<Item = Abigen>) -> Self {
|
||||||
|
abis.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads all json files contained in the given `dir` and use the file name for the name of the
|
||||||
|
/// `ContractBindings`.
|
||||||
|
/// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// abi
|
||||||
|
/// ├── ERC20.json
|
||||||
|
/// ├── Contract1.json
|
||||||
|
/// ├── Contract2.json
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_contract_abigen::MultiAbigen;
|
||||||
|
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn from_json_files(root: impl AsRef<Path>) -> Result<Self> {
|
||||||
|
util::json_files(root.as_ref()).into_iter().map(Abigen::from_file).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add another Abigen to the module or lib
|
||||||
|
pub fn push(&mut self, abigen: Abigen) {
|
||||||
|
self.abigens.push(abigen)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the contract bindings and prepare for writing
|
||||||
|
pub fn build(self) -> Result<MultiBindings> {
|
||||||
|
let bindings = self
|
||||||
|
.abigens
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.generate())
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| (v.name.clone(), v))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(MultiBindings { bindings })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output of the [`MultiAbigen`] build process. `MultiBindings` wraps a group
|
||||||
|
/// of built contract bindings that have yet to be written to disk.
|
||||||
|
///
|
||||||
|
/// `MultiBindings` enables the user to
|
||||||
|
/// 1. Write a collection of bindings to a rust module
|
||||||
|
/// 2. Write a collection of bindings to a rust lib
|
||||||
|
/// 3. Ensure that a collection of bindings matches an on-disk module or lib.
|
||||||
|
///
|
||||||
|
/// Generally we recommend writing the bindings to a module folder within your
|
||||||
|
/// rust project. Users seeking to create "official" bindings for some project
|
||||||
|
/// may instead write an entire library to publish via crates.io.
|
||||||
|
///
|
||||||
|
/// Rather than using `MultiAbigen` in a build script, we recommend committing
|
||||||
|
/// the generated files, and replacing the build script with an integration
|
||||||
|
/// test. To enable this, we have provided
|
||||||
|
/// `MultiBindings::ensure_consistent_bindings` and
|
||||||
|
/// `MultiBindings::ensure_consistent_crate`. These functions generate the
|
||||||
|
/// expected module or library in memory, and check that the on-disk files
|
||||||
|
/// match the expected files. We recommend running these inside CI.
|
||||||
|
///
|
||||||
|
/// This has several advantages:
|
||||||
|
/// * No need for downstream users to compile the build script
|
||||||
|
/// * No need for downstream users to run the whole `abigen!` generation steps
|
||||||
|
/// * The generated code is more usable in an IDE
|
||||||
|
/// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
|
||||||
|
/// changed)
|
||||||
|
pub struct MultiBindings {
|
||||||
|
/// Abigen objects to be written
|
||||||
|
bindings: BTreeMap<String, ContractBindings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// deref allows for inspection without modification
|
||||||
|
impl std::ops::Deref for MultiBindings {
|
||||||
|
type Target = BTreeMap<String, ContractBindings>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.bindings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiBindings {
|
||||||
|
/// Generat the contents of the `Cargo.toml` file for a lib
|
||||||
|
fn generate_cargo_toml(
|
||||||
|
&self,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
version: impl AsRef<str>,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let mut toml = vec![];
|
||||||
|
|
||||||
|
writeln!(toml, "[package]")?;
|
||||||
|
writeln!(toml, r#"name = "{}""#, name.as_ref())?;
|
||||||
|
writeln!(toml, r#"version = "{}""#, version.as_ref())?;
|
||||||
|
writeln!(toml, r#"edition = "2021""#)?;
|
||||||
|
writeln!(toml)?;
|
||||||
|
writeln!(toml, "# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html")?;
|
||||||
|
writeln!(toml)?;
|
||||||
|
writeln!(toml, "[dependencies]")?;
|
||||||
|
writeln!(
|
||||||
|
toml,
|
||||||
|
r#"ethers = {{ git = "https://github.com/gakonst/ethers-rs", default-features = false }}"#
|
||||||
|
)?;
|
||||||
|
Ok(toml)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the contents of `Cargo.toml` to disk
|
||||||
|
fn write_cargo_toml(
|
||||||
|
&self,
|
||||||
|
lib: &Path,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
version: impl AsRef<str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let contents = self.generate_cargo_toml(name, version)?;
|
||||||
|
|
||||||
|
let mut file = fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(lib.join("Cargo.toml"))?;
|
||||||
|
file.write_all(&contents)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the shared prefix of the `lib.rs` or `mod.rs`
|
||||||
|
fn generate_prefix(
|
||||||
|
&self,
|
||||||
|
mut buf: impl Write,
|
||||||
|
is_crate: bool,
|
||||||
|
single_file: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
writeln!(buf, "#![allow(clippy::all)]")?;
|
||||||
|
writeln!(
|
||||||
|
buf,
|
||||||
|
"//! This {} contains abigen! generated bindings for solidity contracts.",
|
||||||
|
if is_crate { "lib" } else { "module" }
|
||||||
|
)?;
|
||||||
|
writeln!(buf, "//! This is autogenerated code.")?;
|
||||||
|
writeln!(buf, "//! Do not manually edit these files.")?;
|
||||||
|
writeln!(
|
||||||
|
buf,
|
||||||
|
"//! {} may be overwritten by the codegen system at any time.",
|
||||||
|
if single_file && !is_crate { "This file" } else { "These files" }
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append module declarations to the `lib.rs` or `mod.rs`
|
||||||
|
fn append_module_names(&self, mut buf: impl Write) -> Result<()> {
|
||||||
|
// sorting here not necessary, as btreemap keys are ordered
|
||||||
|
for module in self.bindings.keys().map(|name| format!("pub mod {};", name.to_snake_case()))
|
||||||
|
{
|
||||||
|
writeln!(buf, "{}", module)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the contents of `lib.rs` or `mod.rs`
|
||||||
|
fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result<Vec<u8>> {
|
||||||
|
let mut contents = vec![];
|
||||||
|
self.generate_prefix(&mut contents, is_crate, single_file)?;
|
||||||
|
|
||||||
|
if !single_file {
|
||||||
|
self.append_module_names(&mut contents)?;
|
||||||
|
} else {
|
||||||
|
for binding in self.bindings.values() {
|
||||||
|
binding.write(&mut contents)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the `lib.rs` or `mod.rs` to disk
|
||||||
|
fn write_super_file(&self, path: &Path, is_crate: bool, single_file: bool) -> Result<()> {
|
||||||
|
let filename = if is_crate { "lib.rs" } else { "mod.rs" };
|
||||||
|
let contents = self.generate_super_contents(is_crate, single_file)?;
|
||||||
|
fs::write(path.join(filename), contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write all contract bindings to their respective files
|
||||||
|
fn write_bindings(&self, path: &Path) -> Result<()> {
|
||||||
|
for binding in self.bindings.values() {
|
||||||
|
binding.write_module_in_dir(path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates all the bindings and writes them to the given module
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Read all json abi files from the `./abi` directory
|
||||||
|
/// ```text
|
||||||
|
/// abi
|
||||||
|
/// ├── ERC20.json
|
||||||
|
/// ├── Contract1.json
|
||||||
|
/// ├── Contract2.json
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// and write them to the `./src/contracts` location as
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// src/contracts
|
||||||
|
/// ├── mod.rs
|
||||||
|
/// ├── er20.rs
|
||||||
|
/// ├── contract1.rs
|
||||||
|
/// ├── contract2.rs
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_contract_abigen::MultiAbigen;
|
||||||
|
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
|
||||||
|
/// let bindings = gen.build().unwrap();
|
||||||
|
/// bindings.write_to_module("./src/contracts", false).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
|
||||||
|
let module = module.as_ref();
|
||||||
|
fs::create_dir_all(module)?;
|
||||||
|
|
||||||
|
self.write_super_file(module, false, single_file)?;
|
||||||
|
|
||||||
|
if !single_file {
|
||||||
|
self.write_bindings(module)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates all the bindings and writes a library crate containing them
|
||||||
|
/// to the provided path
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Read all json abi files from the `./abi` directory
|
||||||
|
/// ```text
|
||||||
|
/// abi
|
||||||
|
/// ├── ERC20.json
|
||||||
|
/// ├── Contract1.json
|
||||||
|
/// ├── Contract2.json
|
||||||
|
/// ├── Contract3/
|
||||||
|
/// ├── Contract3.json
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// and write them to the `./bindings` location as
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// bindings
|
||||||
|
/// ├── Cargo.toml
|
||||||
|
/// ├── src/
|
||||||
|
/// ├── lib.rs
|
||||||
|
/// ├── er20.rs
|
||||||
|
/// ├── contract1.rs
|
||||||
|
/// ├── contract2.rs
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_contract_abigen::MultiAbigen;
|
||||||
|
/// let gen = MultiAbigen::from_json_files("./abi").unwrap();
|
||||||
|
/// let bindings = gen.build().unwrap();
|
||||||
|
/// bindings.write_to_crate(
|
||||||
|
/// "my-crate", "0.0.5", "./bindings", false
|
||||||
|
/// ).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn write_to_crate(
|
||||||
|
self,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
version: impl AsRef<str>,
|
||||||
|
lib: impl AsRef<Path>,
|
||||||
|
single_file: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let lib = lib.as_ref();
|
||||||
|
let src = lib.join("src");
|
||||||
|
fs::create_dir_all(&src)?;
|
||||||
|
|
||||||
|
self.write_cargo_toml(lib, name, version)?;
|
||||||
|
self.write_super_file(&src, true, single_file)?;
|
||||||
|
|
||||||
|
if !single_file {
|
||||||
|
self.write_bindings(&src)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures the contents of the bindings directory are correct
|
||||||
|
///
|
||||||
|
/// Does this by first generating the `lib.rs` or `mod.rs`, then the
|
||||||
|
/// contents of each binding file in turn.
|
||||||
|
fn ensure_consistent_bindings(
|
||||||
|
self,
|
||||||
|
dir: impl AsRef<Path>,
|
||||||
|
is_crate: bool,
|
||||||
|
single_file: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dir = dir.as_ref();
|
||||||
|
let super_name = if is_crate { "lib.rs" } else { "mod.rs" };
|
||||||
|
|
||||||
|
let super_contents = self.generate_super_contents(is_crate, single_file)?;
|
||||||
|
check_file_in_dir(dir, super_name, &super_contents)?;
|
||||||
|
|
||||||
|
// If it is single file, we skip checking anything but the super
|
||||||
|
// contents
|
||||||
|
if !single_file {
|
||||||
|
for binding in self.bindings.values() {
|
||||||
|
check_binding_in_dir(dir, binding)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This ensures that the already generated bindings crate matches the
|
||||||
|
/// output of a fresh new run. Run this in a rust test, to get notified in
|
||||||
|
/// CI if the newly generated bindings deviate from the already generated
|
||||||
|
/// ones, and it's time to generate them again. This could happen if the
|
||||||
|
/// ABI of a contract or the output that `ethers` generates changed.
|
||||||
|
///
|
||||||
|
/// If this functions is run within a test during CI and fails, then it's
|
||||||
|
/// time to update all bindings.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `Ok(())` if the freshly generated bindings match with the
|
||||||
|
/// existing bindings. Otherwise an `Err(_)` containing an `anyhow::Report`
|
||||||
|
/// with more information.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Check that the generated files are up to date
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_contract_abigen::MultiAbigen;
|
||||||
|
/// #[test]
|
||||||
|
/// fn generated_bindings_are_fresh() {
|
||||||
|
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
|
||||||
|
/// let abi_dir = project_root.join("abi");
|
||||||
|
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
|
||||||
|
/// let bindings = gen.build().unwrap();
|
||||||
|
/// bindings.ensure_consistent_crate(
|
||||||
|
/// "my-crate", "0.0.1", project_root.join("src/contracts"), false
|
||||||
|
/// ).expect("inconsistent bindings");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn ensure_consistent_crate(
|
||||||
|
self,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
version: impl AsRef<str>,
|
||||||
|
crate_path: impl AsRef<Path>,
|
||||||
|
single_file: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let crate_path = crate_path.as_ref();
|
||||||
|
|
||||||
|
// additionally check the contents of the cargo
|
||||||
|
let cargo_contents = self.generate_cargo_toml(name, version)?;
|
||||||
|
check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?;
|
||||||
|
|
||||||
|
self.ensure_consistent_bindings(crate_path.join("src"), true, single_file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This ensures that the already generated bindings module matches the
|
||||||
|
/// output of a fresh new run. Run this in a rust test, to get notified in
|
||||||
|
/// CI if the newly generated bindings deviate from the already generated
|
||||||
|
/// ones, and it's time to generate them again. This could happen if the
|
||||||
|
/// ABI of a contract or the output that `ethers` generates changed.
|
||||||
|
///
|
||||||
|
/// If this functions is run within a test during CI and fails, then it's
|
||||||
|
/// time to update all bindings.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `Ok(())` if the freshly generated bindings match with the
|
||||||
|
/// existing bindings. Otherwise an `Err(_)` containing an `anyhow::Report`
|
||||||
|
/// with more information.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Check that the generated files are up to date
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_contract_abigen::MultiAbigen;
|
||||||
|
/// #[test]
|
||||||
|
/// fn generated_bindings_are_fresh() {
|
||||||
|
/// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
|
||||||
|
/// let abi_dir = project_root.join("abi");
|
||||||
|
/// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
|
||||||
|
/// let bindings = gen.build().unwrap();
|
||||||
|
/// bindings.ensure_consistent_module(
|
||||||
|
/// project_root.join("src/contracts"), false
|
||||||
|
/// ).expect("inconsistent bindings");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn ensure_consistent_module(
|
||||||
|
self,
|
||||||
|
module: impl AsRef<Path>,
|
||||||
|
single_file: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.ensure_consistent_bindings(module, false, single_file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> {
|
||||||
|
anyhow::ensure!(dir.is_dir(), "Not a directory: {}", dir.display());
|
||||||
|
|
||||||
|
let file_path = dir.join(file_name);
|
||||||
|
anyhow::ensure!(file_path.is_file(), "Not a file: {}", file_path.display());
|
||||||
|
|
||||||
|
let contents = fs::read(file_path).expect("Unable to read file");
|
||||||
|
anyhow::ensure!(contents == expected_contents, "file contents do not match");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> {
|
||||||
|
let name = binding.module_filename();
|
||||||
|
let contents = binding.to_vec();
|
||||||
|
|
||||||
|
check_file_in_dir(dir, &name, &contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::{panic, path::PathBuf};
|
||||||
|
|
||||||
|
struct Context {
|
||||||
|
multi_gen: MultiAbigen,
|
||||||
|
mod_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test<T>(test: T)
|
||||||
|
where
|
||||||
|
T: FnOnce(&Context) -> () + panic::UnwindSafe,
|
||||||
|
{
|
||||||
|
let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")).to_owned();
|
||||||
|
let console = Abigen::new(
|
||||||
|
"Console",
|
||||||
|
crate_root.join("../tests/solidity-contracts/console.json").display().to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let simple_storage = Abigen::new(
|
||||||
|
"SimpleStorage",
|
||||||
|
crate_root
|
||||||
|
.join("../tests/solidity-contracts/simplestorage_abi.json")
|
||||||
|
.display()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let human_readable = Abigen::new(
|
||||||
|
"HrContract",
|
||||||
|
r#"[
|
||||||
|
struct Foo { uint256 x; }
|
||||||
|
function foo(Foo memory x)
|
||||||
|
function bar(uint256 x, uint256 y, address addr)
|
||||||
|
yeet(uint256,uint256,address)
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let multi_gen = MultiAbigen::from_abigens([console, simple_storage, human_readable]);
|
||||||
|
|
||||||
|
let mod_root = tempfile::tempdir().unwrap().path().join("contracts");
|
||||||
|
let context = Context { multi_gen, mod_root };
|
||||||
|
|
||||||
|
let result = panic::catch_unwind(|| test(&context));
|
||||||
|
|
||||||
|
assert!(result.is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_generate_multi_file_module() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = false;
|
||||||
|
|
||||||
|
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.ensure_consistent_module(&mod_root, single_file)
|
||||||
|
.expect("Inconsistent bindings");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_generate_single_file_module() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = true;
|
||||||
|
|
||||||
|
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.ensure_consistent_module(&mod_root, single_file)
|
||||||
|
.expect("Inconsistent bindings");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_generate_multi_file_crate() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = false;
|
||||||
|
let name = "a-name";
|
||||||
|
let version = "290.3782.3";
|
||||||
|
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_crate(name, version, &mod_root, single_file)
|
||||||
|
.unwrap();
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.ensure_consistent_crate(name, version, &mod_root, single_file)
|
||||||
|
.expect("Inconsistent bindings");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_generate_single_file_crate() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = true;
|
||||||
|
let name = "a-name";
|
||||||
|
let version = "290.3782.3";
|
||||||
|
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_crate(name, version, &mod_root, single_file)
|
||||||
|
.unwrap();
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.ensure_consistent_crate(name, version, &mod_root, single_file)
|
||||||
|
.expect("Inconsistent bindings");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_detect_incosistent_multi_file_module() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = false;
|
||||||
|
|
||||||
|
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||||
|
|
||||||
|
let mut cloned = multi_gen.clone();
|
||||||
|
cloned.push(
|
||||||
|
Abigen::new(
|
||||||
|
"AdditionalContract",
|
||||||
|
r#"[
|
||||||
|
getValue() (uint256)
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err();
|
||||||
|
|
||||||
|
// ensure inconsistent bindings are detected
|
||||||
|
assert!(result, "Inconsistent bindings wrongly approved");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_detect_incosistent_single_file_module() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = true;
|
||||||
|
|
||||||
|
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||||
|
|
||||||
|
let mut cloned = multi_gen.clone();
|
||||||
|
cloned.push(
|
||||||
|
Abigen::new(
|
||||||
|
"AdditionalContract",
|
||||||
|
r#"[
|
||||||
|
getValue() (uint256)
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err();
|
||||||
|
|
||||||
|
// ensure inconsistent bindings are detected
|
||||||
|
assert!(result, "Inconsistent bindings wrongly approved");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_detect_incosistent_multi_file_crate() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = false;
|
||||||
|
let name = "a-name";
|
||||||
|
let version = "290.3782.3";
|
||||||
|
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_crate(name, version, &mod_root, single_file)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut cloned = multi_gen.clone();
|
||||||
|
cloned.push(
|
||||||
|
Abigen::new(
|
||||||
|
"AdditionalContract",
|
||||||
|
r#"[
|
||||||
|
getValue() (uint256)
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = cloned
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.ensure_consistent_crate(name, version, &mod_root, single_file)
|
||||||
|
.is_err();
|
||||||
|
|
||||||
|
// ensure inconsistent bindings are detected
|
||||||
|
assert!(result, "Inconsistent bindings wrongly approved");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_detect_incosistent_single_file_crate() {
|
||||||
|
run_test(|context| {
|
||||||
|
let Context { multi_gen, mod_root } = context;
|
||||||
|
|
||||||
|
let single_file = true;
|
||||||
|
let name = "a-name";
|
||||||
|
let version = "290.3782.3";
|
||||||
|
|
||||||
|
multi_gen
|
||||||
|
.clone()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.write_to_crate(name, version, &mod_root, single_file)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut cloned = multi_gen.clone();
|
||||||
|
cloned.push(
|
||||||
|
Abigen::new(
|
||||||
|
"AdditionalContract",
|
||||||
|
r#"[
|
||||||
|
getValue() (uint256)
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = cloned
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.ensure_consistent_crate(name, version, &mod_root, single_file)
|
||||||
|
.is_err();
|
||||||
|
|
||||||
|
// ensure inconsistent bindings are detected
|
||||||
|
assert!(result, "Inconsistent bindings wrongly approved");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue