feat(abigen): add MultiAbigen to generate multiple contract bindings (#724)
* feat(abigen): add MultiAbigen to generate multiple contract bindings * docs: more docs * chore: update changelog * rustmft * chore: add json extension check
This commit is contained in:
parent
bb0cd7acdd
commit
c7cf5bedbd
|
@ -23,6 +23,8 @@
|
|||
|
||||
## ethers-contract-abigen
|
||||
|
||||
- Add `MultiAbigen` to generate a series of contract bindings that can be kept in the repo
|
||||
[#724](https://github.com/gakonst/ethers-rs/pull/724).
|
||||
- Add provided `event_derives` to call and event enums as well
|
||||
[#721](https://github.com/gakonst/ethers-rs/pull/721).
|
||||
- Implement snowtrace and polygonscan on par with the etherscan integration
|
||||
|
|
|
@ -1109,6 +1109,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
|
|
@ -38,3 +38,6 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||
default = ["reqwest", "rustls"]
|
||||
openssl = ["reqwest/native-tls"]
|
||||
rustls = ["reqwest/rustls-tls"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.2.0"
|
||||
|
|
|
@ -154,27 +154,8 @@ impl Context {
|
|||
// get the actual ABI string
|
||||
let abi_str =
|
||||
args.abi_source.get().map_err(|e| anyhow!("failed to get ABI JSON: {}", e))?;
|
||||
let mut abi_parser = AbiParser::default();
|
||||
|
||||
let (abi, human_readable): (Abi, _) = if let Ok(abi) = abi_parser.parse_str(&abi_str) {
|
||||
(abi, true)
|
||||
} else {
|
||||
// a best-effort coercion of an ABI or an artifact JSON into an artifact JSON.
|
||||
let json_abi_str = if abi_str.trim().starts_with('[') {
|
||||
format!(r#"{{"abi":{}}}"#, abi_str.trim())
|
||||
} else {
|
||||
abi_str.clone()
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Contract {
|
||||
abi: Abi,
|
||||
}
|
||||
|
||||
let contract = serde_json::from_str::<Contract>(&json_abi_str)?;
|
||||
|
||||
(contract.abi, false)
|
||||
};
|
||||
let (abi, human_readable, abi_parser) = parse_abi(&abi_str)?;
|
||||
|
||||
// try to extract all the solidity structs from the normal JSON ABI
|
||||
// we need to parse the json abi again because we need the internalType fields which are
|
||||
|
@ -251,3 +232,25 @@ impl Context {
|
|||
&mut self.internal_structs
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the abi via `Source::parse` and return if the abi defined as human readable
|
||||
fn parse_abi(abi_str: &str) -> Result<(Abi, bool, AbiParser)> {
|
||||
let mut abi_parser = AbiParser::default();
|
||||
let res = if let Ok(abi) = abi_parser.parse_str(abi_str) {
|
||||
(abi, true, abi_parser)
|
||||
} else {
|
||||
#[derive(Deserialize)]
|
||||
struct Contract {
|
||||
abi: Abi,
|
||||
}
|
||||
// a best-effort coercion of an ABI or an artifact JSON into an artifact JSON.
|
||||
let contract: Contract = if abi_str.trim_start().starts_with('[') {
|
||||
serde_json::from_str(&format!(r#"{{"abi":{}}}"#, abi_str.trim()))?
|
||||
} else {
|
||||
serde_json::from_str::<Contract>(abi_str)?
|
||||
};
|
||||
|
||||
(contract.abi, false, abi_parser)
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
@ -24,8 +24,14 @@ pub use source::Source;
|
|||
pub use util::parse_address;
|
||||
|
||||
use anyhow::Result;
|
||||
use inflector::Inflector;
|
||||
use proc_macro2::TokenStream;
|
||||
use std::{collections::HashMap, fs::File, io::Write, path::Path};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Builder struct for generating type-safe bindings from a contract's ABI
|
||||
///
|
||||
|
@ -44,6 +50,7 @@ use std::{collections::HashMap, fs::File, io::Write, path::Path};
|
|||
/// Abigen::new("ERC20Token", "./abi.json")?.generate()?.write_to_file("token.rs")?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Abigen {
|
||||
/// The source of the ABI JSON for the contract whose bindings
|
||||
/// are being generated.
|
||||
|
@ -180,3 +187,286 @@ impl ContractBindings {
|
|||
self.tokens
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates bindings for a series of contracts
|
||||
///
|
||||
/// This type can be used to generate multiple `ContractBindings` and put them all in a single rust
|
||||
/// module, (eg. a `contracts` directory).
|
||||
///
|
||||
/// 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 fs::read_dir(dir)?.into_iter().filter_map(std::io::Result::ok).filter(|p| {
|
||||
p.path().is_file() && p.path().extension().and_then(|ext| ext.to_str()) == Some("json")
|
||||
}) {
|
||||
let file: fs::DirEntry = file;
|
||||
if let Some(file_name) = file.path().file_stem().and_then(|s| s.to_str()) {
|
||||
let content = fs::read_to_string(file.path())?;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue