fix(abigen/solc): make abigen work with ethers-solc and abiencoderv2 (#952)

* feat(solc): add lossless abi

* fix(abigen): make abigen work with ethers-solc and abiencoderv2

* chore: update CHANGELOG
This commit is contained in:
Matthias Seitz 2022-02-23 11:46:52 +01:00 committed by GitHub
parent 9876b49e6c
commit 184cffaca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 110 additions and 22 deletions

View File

@ -58,6 +58,8 @@
### Unreleased ### Unreleased
- Wrap `ethabi::Contract` into new type `LosslessAbi` and `abi: Option<Abi>` with `abi: Option<LosslessAbi>` in `ConfigurableContractArtifact`
[#952](https://github.com/gakonst/ethers-rs/pull/952)
- Let `Project` take ownership of `ArtifactOutput` and change trait interface - Let `Project` take ownership of `ArtifactOutput` and change trait interface
[#907](https://github.com/gakonst/ethers-rs/pull/907) [#907](https://github.com/gakonst/ethers-rs/pull/907)
- Total revamp of the `Project::compile` pipeline - Total revamp of the `Project::compile` pipeline

1
Cargo.lock generated
View File

@ -1187,6 +1187,7 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"dunce", "dunce",
"ethers-core", "ethers-core",
"ethers-solc",
"eyre", "eyre",
"getrandom 0.2.5", "getrandom 0.2.5",
"hex", "hex",

View File

@ -42,3 +42,4 @@ rustls = ["reqwest/rustls-tls"]
[dev-dependencies] [dev-dependencies]
tempfile = "3.2.0" tempfile = "3.2.0"
ethers-solc = { path = "../../ethers-solc", default-features = false, features = ["project-util"] }

View File

@ -173,12 +173,16 @@ impl Context {
internal_structs internal_structs
} else if abi_str.starts_with('{') { } else if abi_str.starts_with('{') {
abi_str = serde_json::to_string(&abi).context("fail to serialize abi to json")?; if let Ok(abi) = serde_json::from_str::<RawAbi>(&abi_str) {
if let Ok(s) = serde_json::to_string(&abi) {
serde_json::from_str::<RawAbi>(&abi_str) // need to update the `abi_str` here because we only want the `"abi": [...]`
.ok() // part of the json object in the contract binding
.map(InternalStructs::new) abi_str = s;
.unwrap_or_default() }
InternalStructs::new(abi)
} else {
InternalStructs::default()
}
} else { } else {
serde_json::from_str::<RawAbi>(&abi_str) serde_json::from_str::<RawAbi>(&abi_str)
.ok() .ok()

View File

@ -238,6 +238,7 @@ impl ContractBindings {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ethers_solc::project_util::TempProject;
#[test] #[test]
fn can_generate_structs() { fn can_generate_structs() {
@ -247,4 +248,42 @@ mod tests {
let out = gen.tokens.to_string(); let out = gen.tokens.to_string();
assert!(out.contains("pub struct Stuff")); assert!(out.contains("pub struct Stuff"));
} }
#[test]
fn can_compile_and_generate() {
let tmp = TempProject::dapptools().unwrap();
tmp.add_source(
"Greeter",
r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
contract Greeter {
struct Inner {
bool a;
}
struct Stuff {
Inner inner;
}
function greet(Stuff calldata stuff) public view returns (Stuff memory) {
return stuff;
}
}
"#,
)
.unwrap();
let _ = tmp.compile().unwrap();
let abigen =
Abigen::from_file(tmp.artifacts_path().join("Greeter.sol/Greeter.json")).unwrap();
let gen = abigen.generate().unwrap();
let out = gen.tokens.to_string();
assert!(out.contains("pub struct Stuff"));
assert!(out.contains("pub struct Inner"));
}
} }

View File

@ -718,7 +718,7 @@ mod tests {
} }
#[test] #[test]
fn can_detect_incosistent_single_file_crate() { fn can_detect_inconsistent_single_file_crate() {
run_test(|context| { run_test(|context| {
let Context { multi_gen, mod_root } = context; let Context { multi_gen, mod_root } = context;

View File

@ -8,7 +8,8 @@ use serde::{
}; };
/// Contract ABI as a list of items where each item can be a function, constructor or event /// Contract ABI as a list of items where each item can be a function, constructor or event
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
#[serde(transparent)]
pub struct RawAbi(Vec<Item>); pub struct RawAbi(Vec<Item>);
impl IntoIterator for RawAbi { impl IntoIterator for RawAbi {

View File

@ -4,12 +4,11 @@ use crate::{
artifacts::{ artifacts::{
output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection}, output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection},
CompactBytecode, CompactContract, CompactContractBytecode, CompactDeployedBytecode, CompactBytecode, CompactContract, CompactContractBytecode, CompactDeployedBytecode,
CompactEvm, DevDoc, Ewasm, GasEstimates, Metadata, Offsets, Settings, StorageLayout, CompactEvm, DevDoc, Ewasm, GasEstimates, LosslessAbi, Metadata, Offsets, Settings,
UserDoc, StorageLayout, UserDoc,
}, },
ArtifactOutput, Contract, SolcConfig, SolcError, ArtifactOutput, Contract, SolcConfig, SolcError,
}; };
use ethers_core::abi::Abi;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs, path::Path}; use std::{collections::BTreeMap, fs, path::Path};
@ -21,7 +20,7 @@ use std::{collections::BTreeMap, fs, path::Path};
pub struct ConfigurableContractArtifact { pub struct ConfigurableContractArtifact {
/// The Ethereum Contract ABI. If empty, it is represented as an empty /// The Ethereum Contract ABI. If empty, it is represented as an empty
/// array. See https://docs.soliditylang.org/en/develop/abi-spec.html /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html
pub abi: Option<Abi>, pub abi: Option<LosslessAbi>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub bytecode: Option<CompactBytecode>, pub bytecode: Option<CompactBytecode>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@ -74,7 +73,7 @@ impl ConfigurableContractArtifact {
impl From<ConfigurableContractArtifact> for CompactContractBytecode { impl From<ConfigurableContractArtifact> for CompactContractBytecode {
fn from(artifact: ConfigurableContractArtifact) -> Self { fn from(artifact: ConfigurableContractArtifact) -> Self {
CompactContractBytecode { CompactContractBytecode {
abi: artifact.abi, abi: artifact.abi.map(Into::into),
bytecode: artifact.bytecode, bytecode: artifact.bytecode,
deployed_bytecode: artifact.deployed_bytecode, deployed_bytecode: artifact.deployed_bytecode,
} }

View File

@ -843,7 +843,7 @@ impl OutputContracts {
pub struct Contract { pub struct Contract {
/// The Ethereum Contract Metadata. /// The Ethereum Contract Metadata.
/// See https://docs.soliditylang.org/en/develop/metadata.html /// See https://docs.soliditylang.org/en/develop/metadata.html
pub abi: Option<Abi>, pub abi: Option<LosslessAbi>,
#[serde( #[serde(
default, default,
skip_serializing_if = "Option::is_none", skip_serializing_if = "Option::is_none",
@ -868,6 +868,48 @@ pub struct Contract {
pub ir_optimized: Option<String>, pub ir_optimized: Option<String>,
} }
/// A helper type that ensures lossless (de)serialisation unlike [`ethabi::Contract`] which omits
/// some information of (nested) components in a serde roundtrip. This is a problem for
/// abienconderv2 structs because `ethabi::Contract`'s representation of those are [`ethabi::Param`]
/// and the `kind` field of type [`ethabi::ParamType`] does not support deeply nested components as
/// it's the case for structs. This is not easily fixable in ethabi as it would require a redesign
/// of the overall `Param` and `ParamType` types. Instead, this type keeps a copy of the
/// [`serde_json::Value`] when deserialized from the `solc` json compiler output and uses it to
/// serialize the `abi` without loss.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct LosslessAbi {
/// The complete abi as json value
pub abi_value: serde_json::Value,
/// The deserialised version of `abi_value`
pub abi: Abi,
}
impl From<LosslessAbi> for Abi {
fn from(abi: LosslessAbi) -> Self {
abi.abi
}
}
impl Serialize for LosslessAbi {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.abi_value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for LosslessAbi {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let abi_value = serde_json::Value::deserialize(deserializer)?;
let abi = serde_json::from_value(abi_value.clone()).map_err(serde::de::Error::custom)?;
Ok(Self { abi_value, abi })
}
}
/// Minimal representation of a contract with a present abi and bytecode. /// Minimal representation of a contract with a present abi and bytecode.
/// ///
/// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole /// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole
@ -933,7 +975,7 @@ impl From<Contract> for ContractBytecode {
(None, None) (None, None)
}; };
Self { abi: c.abi, bytecode, deployed_bytecode } Self { abi: c.abi.map(Into::into), bytecode, deployed_bytecode }
} }
} }
@ -979,7 +1021,7 @@ impl From<Contract> for CompactContractBytecode {
(None, None) (None, None)
}; };
Self { abi: c.abi, bytecode, deployed_bytecode } Self { abi: c.abi.map(Into::into), bytecode, deployed_bytecode }
} }
} }
@ -1300,7 +1342,7 @@ impl<'a> From<&'a Contract> for CompactContractRef<'a> {
(None, None) (None, None)
}; };
Self { abi: c.abi.as_ref(), bin, bin_runtime } Self { abi: c.abi.as_ref().map(|abi| &abi.abi), bin, bin_runtime }
} }
} }

View File

@ -3,11 +3,10 @@
use crate::{ use crate::{
artifacts::{ artifacts::{
Bytecode, BytecodeObject, CompactContract, CompactContractBytecode, Contract, Bytecode, BytecodeObject, CompactContract, CompactContractBytecode, Contract,
ContractBytecode, DeployedBytecode, Offsets, ContractBytecode, DeployedBytecode, LosslessAbi, Offsets,
}, },
ArtifactOutput, ArtifactOutput,
}; };
use ethers_core::abi::Abi;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::btree_map::BTreeMap; use std::collections::btree_map::BTreeMap;
@ -24,7 +23,7 @@ pub struct HardhatArtifact {
/// The source name of this contract in the workspace like `contracts/Greeter.sol` /// The source name of this contract in the workspace like `contracts/Greeter.sol`
pub source_name: String, pub source_name: String,
/// The contract's ABI /// The contract's ABI
pub abi: Abi, pub abi: LosslessAbi,
/// A "0x"-prefixed hex string of the unlinked deployment bytecode. If the contract is not /// A "0x"-prefixed hex string of the unlinked deployment bytecode. If the contract is not
/// deployable, this has the string "0x" /// deployable, this has the string "0x"
pub bytecode: Option<BytecodeObject>, pub bytecode: Option<BytecodeObject>,
@ -44,7 +43,7 @@ pub struct HardhatArtifact {
impl From<HardhatArtifact> for CompactContract { impl From<HardhatArtifact> for CompactContract {
fn from(artifact: HardhatArtifact) -> Self { fn from(artifact: HardhatArtifact) -> Self {
CompactContract { CompactContract {
abi: Some(artifact.abi), abi: Some(artifact.abi.abi),
bin: artifact.bytecode, bin: artifact.bytecode,
bin_runtime: artifact.deployed_bytecode, bin_runtime: artifact.deployed_bytecode,
} }
@ -65,7 +64,7 @@ impl From<HardhatArtifact> for ContractBytecode {
bcode.into() bcode.into()
}); });
ContractBytecode { abi: Some(artifact.abi), bytecode, deployed_bytecode } ContractBytecode { abi: Some(artifact.abi.abi), bytecode, deployed_bytecode }
} }
} }