From 184cffaca3c1d59b2b142f1764d6ad4d26fc4010 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 23 Feb 2022 11:46:52 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 2 + Cargo.lock | 1 + .../ethers-contract-abigen/Cargo.toml | 1 + .../ethers-contract-abigen/src/contract.rs | 16 +++--- .../ethers-contract-abigen/src/lib.rs | 39 +++++++++++++++ .../ethers-contract-abigen/src/multi.rs | 2 +- .../ethers-contract-abigen/src/rawabi.rs | 3 +- .../src/artifact_output/configurable.rs | 9 ++-- ethers-solc/src/artifacts/mod.rs | 50 +++++++++++++++++-- ethers-solc/src/hh.rs | 9 ++-- 10 files changed, 110 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31e0e9cd..8d5e4185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ ### Unreleased +- Wrap `ethabi::Contract` into new type `LosslessAbi` and `abi: Option` with `abi: Option` in `ConfigurableContractArtifact` + [#952](https://github.com/gakonst/ethers-rs/pull/952) - Let `Project` take ownership of `ArtifactOutput` and change trait interface [#907](https://github.com/gakonst/ethers-rs/pull/907) - Total revamp of the `Project::compile` pipeline diff --git a/Cargo.lock b/Cargo.lock index 511305ca..7b397336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,6 +1187,7 @@ dependencies = [ "cfg-if 1.0.0", "dunce", "ethers-core", + "ethers-solc", "eyre", "getrandom 0.2.5", "hex", diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 4c25310c..8a69ead6 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -42,3 +42,4 @@ rustls = ["reqwest/rustls-tls"] [dev-dependencies] tempfile = "3.2.0" +ethers-solc = { path = "../../ethers-solc", default-features = false, features = ["project-util"] } \ No newline at end of file diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 813bf575..4a667e06 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -173,12 +173,16 @@ impl Context { internal_structs } else if abi_str.starts_with('{') { - abi_str = serde_json::to_string(&abi).context("fail to serialize abi to json")?; - - serde_json::from_str::(&abi_str) - .ok() - .map(InternalStructs::new) - .unwrap_or_default() + if let Ok(abi) = serde_json::from_str::(&abi_str) { + if let Ok(s) = serde_json::to_string(&abi) { + // need to update the `abi_str` here because we only want the `"abi": [...]` + // part of the json object in the contract binding + abi_str = s; + } + InternalStructs::new(abi) + } else { + InternalStructs::default() + } } else { serde_json::from_str::(&abi_str) .ok() diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index acdee628..11e439e7 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -238,6 +238,7 @@ impl ContractBindings { #[cfg(test)] mod tests { use super::*; + use ethers_solc::project_util::TempProject; #[test] fn can_generate_structs() { @@ -247,4 +248,42 @@ mod tests { let out = gen.tokens.to_string(); 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")); + } } diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs index 6b3c3662..ad72c151 100644 --- a/ethers-contract/ethers-contract-abigen/src/multi.rs +++ b/ethers-contract/ethers-contract-abigen/src/multi.rs @@ -718,7 +718,7 @@ mod tests { } #[test] - fn can_detect_incosistent_single_file_crate() { + fn can_detect_inconsistent_single_file_crate() { run_test(|context| { let Context { multi_gen, mod_root } = context; diff --git a/ethers-contract/ethers-contract-abigen/src/rawabi.rs b/ethers-contract/ethers-contract-abigen/src/rawabi.rs index 6c66b6f0..3d1bd713 100644 --- a/ethers-contract/ethers-contract-abigen/src/rawabi.rs +++ b/ethers-contract/ethers-contract-abigen/src/rawabi.rs @@ -8,7 +8,8 @@ use serde::{ }; /// 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); impl IntoIterator for RawAbi { diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs index ae6b4162..dbd56b89 100644 --- a/ethers-solc/src/artifact_output/configurable.rs +++ b/ethers-solc/src/artifact_output/configurable.rs @@ -4,12 +4,11 @@ use crate::{ artifacts::{ output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection}, CompactBytecode, CompactContract, CompactContractBytecode, CompactDeployedBytecode, - CompactEvm, DevDoc, Ewasm, GasEstimates, Metadata, Offsets, Settings, StorageLayout, - UserDoc, + CompactEvm, DevDoc, Ewasm, GasEstimates, LosslessAbi, Metadata, Offsets, Settings, + StorageLayout, UserDoc, }, ArtifactOutput, Contract, SolcConfig, SolcError, }; -use ethers_core::abi::Abi; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fs, path::Path}; @@ -21,7 +20,7 @@ use std::{collections::BTreeMap, fs, path::Path}; pub struct ConfigurableContractArtifact { /// The Ethereum Contract ABI. If empty, it is represented as an empty /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html - pub abi: Option, + pub abi: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub bytecode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -74,7 +73,7 @@ impl ConfigurableContractArtifact { impl From for CompactContractBytecode { fn from(artifact: ConfigurableContractArtifact) -> Self { CompactContractBytecode { - abi: artifact.abi, + abi: artifact.abi.map(Into::into), bytecode: artifact.bytecode, deployed_bytecode: artifact.deployed_bytecode, } diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs index d8f55081..0d5038a4 100644 --- a/ethers-solc/src/artifacts/mod.rs +++ b/ethers-solc/src/artifacts/mod.rs @@ -843,7 +843,7 @@ impl OutputContracts { pub struct Contract { /// The Ethereum Contract Metadata. /// See https://docs.soliditylang.org/en/develop/metadata.html - pub abi: Option, + pub abi: Option, #[serde( default, skip_serializing_if = "Option::is_none", @@ -868,6 +868,48 @@ pub struct Contract { pub ir_optimized: Option, } +/// 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 for Abi { + fn from(abi: LosslessAbi) -> Self { + abi.abi + } +} + +impl Serialize for LosslessAbi { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.abi_value.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for LosslessAbi { + fn deserialize(deserializer: D) -> Result + 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. /// /// Unlike `CompactContractSome` which contains the `BytecodeObject`, this holds the whole @@ -933,7 +975,7 @@ impl From for ContractBytecode { (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 for CompactContractBytecode { (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) }; - Self { abi: c.abi.as_ref(), bin, bin_runtime } + Self { abi: c.abi.as_ref().map(|abi| &abi.abi), bin, bin_runtime } } } diff --git a/ethers-solc/src/hh.rs b/ethers-solc/src/hh.rs index 722316dd..8657aac6 100644 --- a/ethers-solc/src/hh.rs +++ b/ethers-solc/src/hh.rs @@ -3,11 +3,10 @@ use crate::{ artifacts::{ Bytecode, BytecodeObject, CompactContract, CompactContractBytecode, Contract, - ContractBytecode, DeployedBytecode, Offsets, + ContractBytecode, DeployedBytecode, LosslessAbi, Offsets, }, ArtifactOutput, }; -use ethers_core::abi::Abi; use serde::{Deserialize, Serialize}; 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` pub source_name: String, /// 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 /// deployable, this has the string "0x" pub bytecode: Option, @@ -44,7 +43,7 @@ pub struct HardhatArtifact { impl From for CompactContract { fn from(artifact: HardhatArtifact) -> Self { CompactContract { - abi: Some(artifact.abi), + abi: Some(artifact.abi.abi), bin: artifact.bytecode, bin_runtime: artifact.deployed_bytecode, } @@ -65,7 +64,7 @@ impl From for ContractBytecode { bcode.into() }); - ContractBytecode { abi: Some(artifact.abi), bytecode, deployed_bytecode } + ContractBytecode { abi: Some(artifact.abi.abi), bytecode, deployed_bytecode } } }