use std::{collections::HashMap, fmt, io::BufRead, path::PathBuf, process::Command, str::FromStr};
use glob::glob;
use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::{abi::Abi, types::Bytes};
use once_cell::sync::Lazy;
use semver::Version;
const SOLC: &str = "solc";
static CONSTANTINOPLE_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.4.21").unwrap());
static PETERSBURG_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.5.5").unwrap());
static ISTANBUL_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.5.14").unwrap());
static BERLIN_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.8.5").unwrap());
static LONDON_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.8.7").unwrap());
type Result<T> = std::result::Result<T, SolcError>;
#[derive(Debug, Error)]
pub enum SolcError {
#[error("Solc Error: {0}")]
SolcError(String),
#[error(transparent)]
SemverError(#[from] semver::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompiledContract {
pub abi: Abi,
pub bytecode: Bytes,
pub runtime_bytecode: Bytes,
}
pub struct Solc {
pub solc_path: Option<PathBuf>,
pub paths: Vec<String>,
pub optimizer: Option<usize>,
pub evm_version: EvmVersion,
pub allowed_paths: Vec<PathBuf>,
pub combined_json: Option<String>,
pub args: Vec<String>,
}
impl Solc {
pub fn new(path: &str) -> Self {
let paths = glob(path)
.expect("could not get glob")
.map(|path| path.expect("path not found").to_string_lossy().to_string())
.collect::<Vec<String>>();
Self::new_with_paths(paths)
}
pub fn new_with_paths(paths: Vec<String>) -> Self {
Self {
paths,
solc_path: None,
optimizer: Some(200), evm_version: EvmVersion::Istanbul,
allowed_paths: Vec::new(),
combined_json: Some("abi,bin,bin-runtime".to_string()),
args: Vec::new(),
}
}
pub fn exec(self) -> Result<serde_json::Value> {
let path = self.solc_path.unwrap_or_else(|| PathBuf::from(SOLC));
let mut command = Command::new(&path);
let version = Solc::version(Some(path));
if let Some(combined_json) = self.combined_json {
command.arg("--combined-json").arg(combined_json);
}
if let Some(evm_version) = normalize_evm_version(&version, self.evm_version) {
command.arg("--evm-version").arg(evm_version.to_string());
}
if let Some(runs) = self.optimizer {
command
.arg("--optimize")
.arg("--optimize-runs")
.arg(runs.to_string());
}
command.args(self.args);
for path in self.paths {
command.arg(path);
}
let command = command.output().expect("could not run `solc`");
if !command.status.success() {
return Err(SolcError::SolcError(
String::from_utf8_lossy(&command.stderr).to_string(),
));
}
Ok(serde_json::from_slice(&command.stdout)?)
}
pub fn build_raw(self) -> Result<HashMap<String, CompiledContractStr>> {
let mut output = self.exec()?;
let contract_values = output["contracts"].as_object_mut().ok_or_else(|| {
SolcError::SolcError("no contracts found in `solc` output".to_string())
})?;
let mut contracts = HashMap::with_capacity(contract_values.len());
for (name, contract) in contract_values {
if let serde_json::Value::String(bin) = contract["bin"].take() {
let name = name
.rsplit(':')
.next()
.expect("could not strip fname")
.to_owned();
let abi = match contract["abi"].take() {
serde_json::Value::String(abi) => abi,
val @ serde_json::Value::Array(_) => val.to_string(),
val => {
return Err(SolcError::SolcError(format!(
"Expected abi in solc output, found {:?}",
val
)))
}
};
let runtime_bin =
if let serde_json::Value::String(bin) = contract["bin-runtime"].take() {
bin
} else {
panic!("no runtime bytecode found")
};
contracts.insert(
name,
CompiledContractStr {
abi,
bin,
runtime_bin,
},
);
} else {
return Err(SolcError::SolcError(
"could not find `bin` in solc output".to_string(),
));
}
}
Ok(contracts)
}
pub fn build(self) -> Result<HashMap<String, CompiledContract>> {
let contracts = self
.build_raw()?
.into_iter()
.map(|(name, contract)| {
let abi = serde_json::from_str(&contract.abi)
.expect("could not parse `solc` abi, this should never happen");
let bytecode = hex::decode(contract.bin)
.expect("solc did not produce valid bytecode")
.into();
let runtime_bytecode = hex::decode(contract.runtime_bin)
.expect("solc did not produce valid runtime-bytecode")
.into();
(
name,
CompiledContract {
abi,
bytecode,
runtime_bytecode,
},
)
})
.collect::<HashMap<String, CompiledContract>>();
Ok(contracts)
}
pub fn version(solc_path: Option<PathBuf>) -> Version {
let solc_path = solc_path.unwrap_or_else(|| PathBuf::from(SOLC));
let command_output = Command::new(&solc_path)
.arg("--version")
.output()
.unwrap_or_else(|_| panic!("`{:?}` not found", solc_path));
let version = command_output
.stdout
.lines()
.last()
.expect("expected version in solc output")
.expect("could not get solc version");
let version = version.replace("Version: ", "");
Version::from_str(&version[0..5]).expect("not a version")
}
pub fn evm_version(mut self, version: EvmVersion) -> Self {
self.evm_version = version;
self
}
pub fn solc_path(mut self, path: PathBuf) -> Self {
self.solc_path = Some(std::fs::canonicalize(path).unwrap());
self
}
pub fn combined_json(mut self, combined_json: impl Into<String>) -> Self {
self.combined_json = Some(combined_json.into());
self
}
pub fn optimizer(mut self, runs: Option<usize>) -> Self {
self.optimizer = runs;
self
}
pub fn allowed_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.allowed_paths = paths;
self
}
pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
self.args.push(arg.into());
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for arg in args {
self = self.arg(arg);
}
self
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EvmVersion {
Homestead,
TangerineWhistle,
SpuriusDragon,
Constantinople,
Petersburg,
Istanbul,
Berlin,
London,
}
impl fmt::Display for EvmVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let string = match self {
EvmVersion::Homestead => "homestead",
EvmVersion::TangerineWhistle => "tangerineWhistle",
EvmVersion::SpuriusDragon => "spuriusDragon",
EvmVersion::Constantinople => "constantinople",
EvmVersion::Petersburg => "petersburg",
EvmVersion::Istanbul => "istanbul",
EvmVersion::Berlin => "berlin",
EvmVersion::London => "london",
};
write!(f, "{}", string)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct SolcOutput {
contracts: HashMap<String, CompiledContractStr>,
version: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompiledContractStr {
pub abi: String,
pub bin: String,
pub runtime_bin: String,
}
fn normalize_evm_version(version: &Version, evm_version: EvmVersion) -> Option<EvmVersion> {
if version >= &CONSTANTINOPLE_SOLC {
Some(if version >= &LONDON_SOLC {
evm_version
} else if version >= &BERLIN_SOLC && evm_version >= EvmVersion::Berlin {
EvmVersion::Berlin
} else if version >= &ISTANBUL_SOLC && evm_version >= EvmVersion::Istanbul {
EvmVersion::Istanbul
} else if version >= &PETERSBURG_SOLC && evm_version >= EvmVersion::Petersburg {
EvmVersion::Petersburg
} else if evm_version >= EvmVersion::Constantinople {
EvmVersion::Constantinople
} else {
evm_version
})
} else {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
pub abi: Abi,
pub evm: Evm,
#[serde(
deserialize_with = "de_from_json_opt",
serialize_with = "ser_to_inner_json",
skip_serializing_if = "Option::is_none"
)]
pub metadata: Option<Metadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Evm {
pub bytecode: Bytecode,
pub deployed_bytecode: Bytecode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bytecode {
#[serde(deserialize_with = "deserialize_bytes")]
pub object: Bytes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub compiler: Compiler,
pub language: String,
pub output: Output,
pub settings: Settings,
pub sources: Sources,
pub version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Compiler {
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output {
pub abi: Vec<SolcAbi>,
pub devdoc: Option<Doc>,
pub userdoc: Option<Doc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolcAbi {
pub inputs: Vec<Item>,
#[serde(rename = "stateMutability")]
pub state_mutability: Option<String>,
#[serde(rename = "type")]
pub abi_type: String,
pub name: Option<String>,
pub outputs: Option<Vec<Item>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
#[serde(rename = "internalType")]
pub internal_type: String,
pub name: String,
#[serde(rename = "type")]
pub put_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Doc {
pub kind: String,
pub methods: Libraries,
pub version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Libraries {
#[serde(flatten)]
pub libs: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(rename = "compilationTarget")]
pub compilation_target: CompilationTarget,
#[serde(rename = "evmVersion")]
pub evm_version: String,
pub libraries: Libraries,
pub metadata: MetadataClass,
pub optimizer: Optimizer,
pub remappings: Vec<Option<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilationTarget {
#[serde(flatten)]
pub inner: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetadataClass {
#[serde(rename = "bytecodeHash")]
pub bytecode_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Optimizer {
pub enabled: bool,
pub runs: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sources {
#[serde(flatten)]
pub inner: HashMap<String, serde_json::Value>,
}
pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result<Bytes, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(d)?;
Ok(hex::decode(&value)
.map_err(|e| serde::de::Error::custom(e.to_string()))?
.into())
}
fn de_from_json_opt<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
if let Some(val) = <Option<String>>::deserialize(deserializer)? {
serde_json::from_str(&val).map_err(serde::de::Error::custom)
} else {
Ok(None)
}
}
fn ser_to_inner_json<S, T>(val: &T, s: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
T: Serialize,
{
let val = serde_json::to_string(val).map_err(serde::ser::Error::custom)?;
s.serialize_str(&val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_solc_version() {
Solc::version(None);
}
#[test]
fn test_evm_version_normalization() {
for (solc_version, evm_version, expected) in &[
("0.4.20", EvmVersion::Homestead, None),
("0.4.21", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
(
"0.4.21",
EvmVersion::Constantinople,
Some(EvmVersion::Constantinople),
),
(
"0.4.21",
EvmVersion::London,
Some(EvmVersion::Constantinople),
),
("0.5.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
(
"0.5.5",
EvmVersion::Petersburg,
Some(EvmVersion::Petersburg),
),
("0.5.5", EvmVersion::London, Some(EvmVersion::Petersburg)),
("0.5.14", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.5.14", EvmVersion::Istanbul, Some(EvmVersion::Istanbul)),
("0.5.14", EvmVersion::London, Some(EvmVersion::Istanbul)),
("0.8.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.8.5", EvmVersion::Berlin, Some(EvmVersion::Berlin)),
("0.8.5", EvmVersion::London, Some(EvmVersion::Berlin)),
("0.8.7", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.8.7", EvmVersion::London, Some(EvmVersion::London)),
("0.8.7", EvmVersion::London, Some(EvmVersion::London)),
] {
assert_eq!(
&normalize_evm_version(&Version::from_str(solc_version).unwrap(), *evm_version),
expected
)
}
}
}