//! Solc artifact types use ethers_core::{abi::Abi, types::Bytes}; use colored::Colorize; use md5::Digest; use semver::Version; use std::{ collections::BTreeMap, fmt, fs, io, path::{Path, PathBuf}, str::FromStr, }; use crate::{compile::*, utils}; use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; /// An ordered list of files and their source pub type Sources = BTreeMap; /// Input type `solc` expects #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CompilerInput { pub language: String, pub sources: Sources, pub settings: Settings, } impl CompilerInput { /// Reads all contracts found under the path pub fn new(path: impl AsRef) -> io::Result { Source::read_all_from(path.as_ref()).map(Self::with_sources) } /// Creates a new Compiler input with default settings and the given sources pub fn with_sources(sources: Sources) -> Self { Self { language: "Solidity".to_string(), sources, settings: Default::default() } } /// Sets the EVM version for compilation pub fn evm_version(mut self, version: EvmVersion) -> Self { self.settings.evm_version = Some(version); self } /// Sets the optimizer runs (default = 200) pub fn optimizer(mut self, runs: usize) -> Self { self.settings.optimizer.runs(runs); self } /// Normalizes the EVM version used in the settings to be up to the latest one /// supported by the provided compiler version. pub fn normalize_evm_version(mut self, version: &Version) -> Self { if let Some(ref mut evm_version) = self.settings.evm_version { self.settings.evm_version = evm_version.normalize_version(version); } self } } impl Default for CompilerInput { fn default() -> Self { Self::with_sources(Default::default()) } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Settings { pub optimizer: Optimizer, #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, /// This field can be used to select desired outputs based /// on file and contract names. /// If this field is omitted, then the compiler loads and does type /// checking, but will not generate any outputs apart from errors. /// The first level key is the file name and the second level key is the /// contract name. An empty contract name is used for outputs that are /// not tied to a contract but to the whole source file like the AST. /// A star as contract name refers to all contracts in the file. /// Similarly, a star as a file name matches all files. /// To select all outputs the compiler can possibly generate, use /// "outputSelection: { "*": { "*": [ "*" ], "": [ "*" ] } }" /// but note that this might slow down the compilation process needlessly. /// /// The available output types are as follows: /// /// File level (needs empty string as contract name): /// ast - AST of all source files /// /// Contract level (needs the contract name or "*"): /// abi - ABI /// devdoc - Developer documentation (natspec) /// userdoc - User documentation (natspec) /// metadata - Metadata /// ir - Yul intermediate representation of the code before optimization /// irOptimized - Intermediate representation after optimization /// storageLayout - Slots, offsets and types of the contract's state /// variables. /// evm.assembly - New assembly format /// evm.legacyAssembly - Old-style assembly format in JSON /// evm.bytecode.functionDebugData - Debugging information at function level /// evm.bytecode.object - Bytecode object /// evm.bytecode.opcodes - Opcodes list /// evm.bytecode.sourceMap - Source mapping (useful for debugging) /// evm.bytecode.linkReferences - Link references (if unlinked object) /// evm.bytecode.generatedSources - Sources generated by the compiler /// evm.deployedBytecode* - Deployed bytecode (has all the options that /// evm.bytecode has) /// evm.deployedBytecode.immutableReferences - Map from AST ids to /// bytecode ranges that reference immutables /// evm.methodIdentifiers - The list of function hashes /// evm.gasEstimates - Function gas estimates /// ewasm.wast - Ewasm in WebAssembly S-expressions format /// ewasm.wasm - Ewasm in WebAssembly binary format /// /// Note that using a using `evm`, `evm.bytecode`, `ewasm`, etc. will select /// every target part of that output. Additionally, `*` can be used as a /// wildcard to request everything. /// /// The default output selection is /// /// ```json /// { /// "*": { /// "*": [ /// "abi", /// "evm.bytecode", /// "evm.deployedBytecode", /// "evm.methodIdentifiers" /// ], /// "": [ /// "ast" /// ] /// } /// } /// ``` #[serde(default)] pub output_selection: BTreeMap>>, #[serde(default, with = "display_from_str_opt", skip_serializing_if = "Option::is_none")] pub evm_version: Option, #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub libraries: BTreeMap>, } impl Settings { /// Default output selection for compiler output pub fn default_output_selection() -> BTreeMap>> { let mut output_selection = BTreeMap::default(); let mut output = BTreeMap::default(); output.insert( "*".to_string(), vec![ "abi".to_string(), "evm.bytecode".to_string(), "evm.deployedBytecode".to_string(), "evm.methodIdentifiers".to_string(), ], ); output_selection.insert("*".to_string(), output); output_selection } /// Adds `ast` to output pub fn with_ast(mut self) -> Self { let output = self.output_selection.entry("*".to_string()).or_insert_with(BTreeMap::default); output.insert("".to_string(), vec!["ast".to_string()]); self } } impl Default for Settings { fn default() -> Self { Self { optimizer: Default::default(), metadata: None, output_selection: Self::default_output_selection(), evm_version: Some(EvmVersion::Istanbul), libraries: Default::default(), } .with_ast() } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Optimizer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub runs: Option, } impl Optimizer { pub fn runs(&mut self, runs: usize) { self.runs = Some(runs); } pub fn disable(&mut self) { self.enabled.take(); } pub fn enable(&mut self) { self.enabled = Some(true) } } impl Default for Optimizer { fn default() -> Self { Self { enabled: Some(false), runs: Some(200) } } } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum EvmVersion { Homestead, TangerineWhistle, SpuriusDragon, Constantinople, Petersburg, Istanbul, Berlin, Byzantium, London, } impl EvmVersion { /// Checks against the given solidity `semver::Version` pub fn normalize_version(self, version: &Version) -> Option { // the EVM version flag was only added at 0.4.21 // we work our way backwards if version >= &CONSTANTINOPLE_SOLC { // If the Solc is at least at london, it supports all EVM versions Some(if version >= &LONDON_SOLC { self // For all other cases, cap at the at-the-time highest possible // fork } else if version >= &BERLIN_SOLC && self >= EvmVersion::Berlin { EvmVersion::Berlin } else if version >= &ISTANBUL_SOLC && self >= EvmVersion::Istanbul { EvmVersion::Istanbul } else if version >= &PETERSBURG_SOLC && self >= EvmVersion::Petersburg { EvmVersion::Petersburg } else if self >= EvmVersion::Constantinople { EvmVersion::Constantinople } else { self }) } else { None } } } 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", EvmVersion::Byzantium => "byzantium", }; write!(f, "{}", string) } } impl FromStr for EvmVersion { type Err = String; fn from_str(s: &str) -> Result { match s { "homestead" => Ok(EvmVersion::Homestead), "tangerineWhistle" => Ok(EvmVersion::TangerineWhistle), "spuriusDragon" => Ok(EvmVersion::SpuriusDragon), "constantinople" => Ok(EvmVersion::Constantinople), "petersburg" => Ok(EvmVersion::Petersburg), "istanbul" => Ok(EvmVersion::Istanbul), "berlin" => Ok(EvmVersion::Berlin), "london" => Ok(EvmVersion::London), "byzantium" => Ok(EvmVersion::Byzantium), s => Err(format!("Unknown evm version: {}", s)), } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Metadata { #[serde(rename = "useLiteralContent")] pub use_literal_content: bool, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Source { pub content: String, } impl Source { /// Reads the file content pub fn read(file: impl AsRef) -> io::Result { Ok(Self { content: fs::read_to_string(file.as_ref())? }) } /// Finds all source files under the given dir path and reads them all pub fn read_all_from(dir: impl AsRef) -> io::Result { Self::read_all(utils::source_files(dir)?) } /// Reads all files pub fn read_all(files: I) -> io::Result where I: IntoIterator, T: Into, { files .into_iter() .map(Into::into) .map(|file| Self::read(&file).map(|source| (file, source))) .collect() } /// Generate a non-cryptographically secure checksum of the file's content pub fn content_hash(&self) -> String { let mut hasher = md5::Md5::new(); hasher.update(&self.content); let result = hasher.finalize(); hex::encode(result) } /// Returns all import statements of the file pub fn parse_imports(&self) -> Vec<&str> { utils::find_import_paths(self.as_ref()) } } #[cfg(feature = "async")] impl Source { /// async version of `Self::read` pub async fn async_read(file: impl AsRef) -> io::Result { Ok(Self { content: tokio::fs::read_to_string(file.as_ref()).await? }) } /// Finds all source files under the given dir path and reads them all pub async fn async_read_all_from(dir: impl AsRef) -> io::Result { Self::async_read_all(utils::source_files(dir.as_ref())?).await } /// async version of `Self::read_all` pub async fn async_read_all(files: I) -> io::Result where I: IntoIterator, T: Into, { futures_util::future::join_all( files .into_iter() .map(Into::into) .map(|file| async { Self::async_read(&file).await.map(|source| (file, source)) }), ) .await .into_iter() .collect() } } impl AsRef for Source { fn as_ref(&self) -> &str { &self.content } } /// Output type `solc` produces #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] pub struct CompilerOutput { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub errors: Vec, #[serde(default)] pub sources: BTreeMap, #[serde(default)] pub contracts: BTreeMap>, } impl CompilerOutput { /// Whether the output contains an compiler error pub fn has_error(&self) -> bool { self.errors.iter().any(|err| err.severity.is_error()) } pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64]) -> OutputDiagnostics { OutputDiagnostics { errors: &self.errors, ignored_error_codes } } /// Finds the first contract with the given name pub fn find(&self, contract: impl AsRef) -> Option { let contract = contract.as_ref(); self.contracts .values() .find_map(|contracts| contracts.get(contract)) .map(CompactContractRef::from) } /// Given the contract file's path and the contract's name, tries to return the contract's /// bytecode, runtime bytecode, and abi pub fn get(&self, path: &str, contract: &str) -> Option { self.contracts .get(path) .and_then(|contracts| contracts.get(contract)) .map(CompactContractRef::from) } } /// Helper type to implement display for solc errors #[derive(Clone, Debug)] pub struct OutputDiagnostics<'a> { errors: &'a [Error], ignored_error_codes: &'a [u64], } impl<'a> OutputDiagnostics<'a> { pub fn has_error(&self) -> bool { self.errors.iter().any(|err| err.severity.is_error()) } } impl<'a> fmt::Display for OutputDiagnostics<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.has_error() { f.write_str("Compiler run successful")?; } for err in self.errors { // Do not log any ignored error codes if let Some(error_code) = err.error_code { if !self.ignored_error_codes.contains(&error_code) { writeln!(f, "\n{}", err)?; } } else { writeln!(f, "\n{}", err)?; } } Ok(()) } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Contract { /// The Ethereum Contract ABI. /// See https://docs.soliditylang.org/en/develop/abi-spec.html pub abi: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, #[serde(default)] pub userdoc: UserDoc, #[serde(default)] pub devdoc: DevDoc, #[serde(default, skip_serializing_if = "Option::is_none")] pub ir: Option, #[serde(default, rename = "storageLayout", skip_serializing_if = "StorageLayout::is_empty")] pub storage_layout: StorageLayout, /// EVM-related outputs #[serde(default, skip_serializing_if = "Option::is_none")] pub evm: Option, /// Ewasm related outputs #[serde(default, skip_serializing_if = "Option::is_none")] pub ewasm: Option, } /// Minimal representation of a contract's abi with bytecode #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CompactContract { /// 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, #[serde( default, deserialize_with = "deserialize_opt_bytes", skip_serializing_if = "Option::is_none" )] pub bin: Option, #[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")] pub bin_runtime: Option, } impl CompactContract { /// Returns the contents of this type as a single pub fn into_parts(self) -> (Option, Option, Option) { (self.abi, self.bin, self.bin_runtime) } /// Returns the individual parts of this contract. /// /// If the values are `None`, then `Default` is returned. pub fn into_parts_or_default(self) -> (Abi, Bytes, Bytes) { ( self.abi.unwrap_or_default(), self.bin.unwrap_or_default(), self.bin_runtime.unwrap_or_default(), ) } } impl From for CompactContract { fn from(c: Contract) -> Self { let (bin, bin_runtime) = if let Some(evm) = c.evm { (Some(evm.bytecode.object), evm.deployed_bytecode.bytecode.map(|evm| evm.object)) } else { (None, None) }; Self { abi: c.abi, bin, bin_runtime } } } impl<'a> From> for CompactContract { fn from(c: CompactContractRef<'a>) -> Self { Self { abi: c.abi.cloned(), bin: c.bin.cloned(), bin_runtime: c.bin_runtime.cloned() } } } /// Helper type to serialize while borrowing from `Contract` #[derive(Copy, Clone, Debug, Serialize)] pub struct CompactContractRef<'a> { pub abi: Option<&'a Abi>, #[serde(default, skip_serializing_if = "Option::is_none")] pub bin: Option<&'a Bytes>, #[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")] pub bin_runtime: Option<&'a Bytes>, } impl<'a> CompactContractRef<'a> { /// Clones the referenced values and returns as tuples pub fn into_parts(self) -> (Option, Option, Option) { CompactContract::from(self).into_parts() } /// Returns the individual parts of this contract. /// /// If the values are `None`, then `Default` is returned. pub fn into_parts_or_default(self) -> (Abi, Bytes, Bytes) { CompactContract::from(self).into_parts_or_default() } } impl<'a> From<&'a Contract> for CompactContractRef<'a> { fn from(c: &'a Contract) -> Self { let (bin, bin_runtime) = if let Some(ref evm) = c.evm { ( Some(&evm.bytecode.object), evm.deployed_bytecode.bytecode.as_ref().map(|evm| &evm.object), ) } else { (None, None) }; Self { abi: c.abi.as_ref(), bin, bin_runtime } } } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct UserDoc { #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub methods: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub notice: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct DevDoc { #[serde(default, skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub details: Option, #[serde(default, rename = "custom:experimental", skip_serializing_if = "Option::is_none")] pub custom_experimental: Option, #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub methods: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Evm { #[serde(default, skip_serializing_if = "Option::is_none")] pub assembly: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub legacy_assembly: Option, pub bytecode: Bytecode, pub deployed_bytecode: DeployedBytecode, /// The list of function hashes #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub method_identifiers: BTreeMap, /// Function gas estimates #[serde(default, skip_serializing_if = "Option::is_none")] pub gas_estimates: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Bytecode { /// Debugging information at function level #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub function_debug_data: BTreeMap, /// The bytecode as a hex string. #[serde(deserialize_with = "deserialize_bytes")] pub object: Bytes, /// Opcodes list (string) pub opcodes: String, /// The source mapping as a string. See the source mapping definition. pub source_map: String, /// Array of sources generated by the compiler. Currently only contains a /// single Yul file. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub generated_sources: Vec, /// If given, this is an unlinked object. #[serde(default)] pub link_references: BTreeMap>>, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionDebugData { pub entry_point: Option, pub id: Option, pub parameter_slots: Option, pub return_slots: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct GeneratedSource { pub ast: serde_json::Value, pub contents: String, pub id: u32, pub language: String, pub name: String, } /// Byte offsets into the bytecode. /// Linking replaces the 20 bytes located there. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Offsets { pub start: u32, pub length: u32, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct DeployedBytecode { #[serde(flatten)] pub bytecode: Option, #[serde( default, rename = "immutableReferences", skip_serializing_if = "::std::collections::BTreeMap::is_empty" )] pub immutable_references: BTreeMap>, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct GasEstimates { pub creation: Creation, #[serde(default)] pub external: BTreeMap, #[serde(default)] pub internal: BTreeMap, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Creation { pub code_deposit_cost: String, pub execution_cost: String, pub total_cost: String, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Ewasm { pub wast: String, pub wasm: String, } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct StorageLayout { pub storage: Vec, pub types: BTreeMap, } impl StorageLayout { fn is_empty(&self) -> bool { self.storage.is_empty() && self.types.is_empty() } } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Storage { #[serde(rename = "astId")] pub ast_id: u64, pub contract: String, pub label: String, pub offset: i64, pub slot: String, #[serde(rename = "type")] pub storage_type: String, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct StorageType { pub encoding: String, pub label: String, #[serde(rename = "numberOfBytes")] pub number_of_bytes: String, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Error { #[serde(default, skip_serializing_if = "Option::is_none")] pub source_location: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub secondary_source_locations: Vec, pub r#type: String, pub component: String, pub severity: Severity, #[serde(default, with = "display_from_str_opt")] pub error_code: Option, pub message: String, pub formatted_message: Option, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(msg) = &self.formatted_message { match self.severity { Severity::Error => msg.as_str().red().fmt(f), Severity::Warning | Severity::Info => msg.as_str().yellow().fmt(f), } } else { self.severity.fmt(f)?; writeln!(f, ": {}", self.message) } } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Severity { Error, Warning, Info, } impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Severity::Error => f.write_str(&"Error".red()), Severity::Warning => f.write_str(&"Warning".yellow()), Severity::Info => f.write_str("Info"), } } } impl Severity { pub fn is_error(&self) -> bool { matches!(self, Severity::Error) } pub fn is_warning(&self) -> bool { matches!(self, Severity::Warning) } pub fn is_info(&self) -> bool { matches!(self, Severity::Info) } } impl FromStr for Severity { type Err = String; fn from_str(s: &str) -> Result { match s { "error" => Ok(Severity::Error), "warning" => Ok(Severity::Warning), "info" => Ok(Severity::Info), s => Err(format!("Invalid severity: {}", s)), } } } impl Serialize for Severity { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Severity::Error => serializer.serialize_str("error"), Severity::Warning => serializer.serialize_str("warning"), Severity::Info => serializer.serialize_str("info"), } } } impl<'de> Deserialize<'de> for Severity { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct SeverityVisitor; impl<'de> Visitor<'de> for SeverityVisitor { type Value = Severity; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "severity string") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { value.parse().map_err(serde::de::Error::custom) } } deserializer.deserialize_str(SeverityVisitor) } } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct SourceLocation { pub file: String, pub start: i32, pub end: i32, pub message: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct SourceFile { pub id: u32, pub ast: serde_json::Value, } mod display_from_str_opt { use serde::{de, Deserialize, Deserializer, Serializer}; use std::{fmt, str::FromStr}; pub fn serialize(value: &Option, serializer: S) -> Result where T: fmt::Display, S: Serializer, { if let Some(value) = value { serializer.collect_str(value) } else { serializer.serialize_none() } } pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: FromStr, T::Err: fmt::Display, { if let Some(s) = Option::::deserialize(deserializer)? { s.parse().map_err(de::Error::custom).map(Some) } else { Ok(None) } } } pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result where D: Deserializer<'de>, { let value = String::deserialize(d)?; Ok(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into()) } pub fn deserialize_opt_bytes<'de, D>(d: D) -> std::result::Result, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(d)?; if let Some(value) = value { Ok(Some(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into())) } else { Ok(None) } } #[cfg(test)] mod tests { use super::*; use std::{fs, path::PathBuf}; #[test] fn can_parse_compiler_output() { let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); dir.push("test-data/out"); for path in fs::read_dir(dir).unwrap() { let path = path.unwrap().path(); let compiler_output = fs::read_to_string(&path).unwrap(); serde_json::from_str::(&compiler_output).unwrap_or_else(|err| { panic!("Failed to read compiler output of {} {}", path.display(), err) }); } } #[test] fn can_parse_compiler_input() { let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); dir.push("test-data/in"); for path in fs::read_dir(dir).unwrap() { let path = path.unwrap().path(); let compiler_output = fs::read_to_string(&path).unwrap(); serde_json::from_str::(&compiler_output).unwrap_or_else(|err| { panic!("Failed to read compiler output of {} {}", path.display(), err) }); } } #[test] fn test_evm_version_normalization() { for (solc_version, evm_version, expected) in &[ // Ensure 0.4.21 it always returns None ("0.4.20", EvmVersion::Homestead, None), // Constantinople clipping ("0.4.21", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.4.21", EvmVersion::Constantinople, Some(EvmVersion::Constantinople)), ("0.4.21", EvmVersion::London, Some(EvmVersion::Constantinople)), // Petersburg ("0.5.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.5.5", EvmVersion::Petersburg, Some(EvmVersion::Petersburg)), ("0.5.5", EvmVersion::London, Some(EvmVersion::Petersburg)), // Istanbul ("0.5.14", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.5.14", EvmVersion::Istanbul, Some(EvmVersion::Istanbul)), ("0.5.14", EvmVersion::London, Some(EvmVersion::Istanbul)), // Berlin ("0.8.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)), ("0.8.5", EvmVersion::Berlin, Some(EvmVersion::Berlin)), ("0.8.5", EvmVersion::London, Some(EvmVersion::Berlin)), // London ("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!( &evm_version.normalize_version(&Version::from_str(solc_version).unwrap()), expected ) } } }