//! Solc artifact types 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}; /// Input type `solc` expects #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CompilerInput { pub language: String, pub sources: BTreeMap, 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: BTreeMap) -> 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 } } impl Default for CompilerInput { fn default() -> Self { Self::with_sources(Default::default()) } } #[derive(Clone, Debug, 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. #[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) -> &mut 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(), } } } #[derive(Clone, Debug, 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, London, Byzantium, } 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, 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() } } #[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() } } /// Output type `solc` produces #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct CompilerOutput { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub errors: Vec, #[serde(default)] pub sources: BTreeMap, #[serde(default)] pub contracts: BTreeMap>, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] pub struct Contract { /// 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: Vec, #[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, Eq, 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: Vec, #[serde(default, 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 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, } } } /// Helper type to serialize while borrowing from `Contract` #[derive(Clone, Debug, Serialize)] pub struct CompactContractRef<'a> { pub abi: &'a [serde_json::Value], #[serde(default, skip_serializing_if = "Option::is_none")] pub bin: Option<&'a str>, #[serde( default, rename = "bin-runtime", skip_serializing_if = "Option::is_none" )] pub bin_runtime: Option<&'a str>, } 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.as_str()), evm.deployed_bytecode .bytecode .as_ref() .map(|evm| evm.object.as_str()), ) } else { (None, None) }; Self { abi: &c.abi, 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. pub object: String, /// 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: u32, pub id: u32, pub parameter_slots: u32, pub return_slots: u32, } #[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, pub external: BTreeMap, 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, pub error_code: Option, pub message: String, pub formatted_message: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Severity { Error, Warning, 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) } } } #[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 ) }); } } }