1402 lines
47 KiB
Rust
1402 lines
47 KiB
Rust
//! 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,
|
|
path::{Path, PathBuf},
|
|
str::FromStr,
|
|
};
|
|
|
|
use crate::{compile::*, error::SolcIoError, remappings::Remapping, utils};
|
|
use ethers_core::abi::Address;
|
|
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
|
|
|
/// An ordered list of files and their source
|
|
pub type Sources = BTreeMap<PathBuf, Source>;
|
|
|
|
pub type Contracts = BTreeMap<String, BTreeMap<String, Contract>>;
|
|
|
|
/// 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<Path>) -> Result<Self, SolcIoError> {
|
|
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
|
|
#[must_use]
|
|
pub fn evm_version(mut self, version: EvmVersion) -> Self {
|
|
self.settings.evm_version = Some(version);
|
|
self
|
|
}
|
|
|
|
/// Sets the optimizer runs (default = 200)
|
|
#[must_use]
|
|
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.
|
|
#[must_use]
|
|
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
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_remappings(mut self, remappings: Vec<Remapping>) -> Self {
|
|
self.settings.remappings = remappings;
|
|
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 {
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub remappings: Vec<Remapping>,
|
|
pub optimizer: Optimizer,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub metadata: Option<SettingsMetadata>,
|
|
/// 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<String, BTreeMap<String, Vec<String>>>,
|
|
#[serde(default, with = "display_from_str_opt", skip_serializing_if = "Option::is_none")]
|
|
pub evm_version: Option<EvmVersion>,
|
|
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
|
|
pub libraries: BTreeMap<String, BTreeMap<String, String>>,
|
|
}
|
|
|
|
impl Settings {
|
|
/// Default output selection for compiler output
|
|
pub fn default_output_selection() -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
|
|
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
|
|
#[must_use]
|
|
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::default()),
|
|
libraries: Default::default(),
|
|
remappings: 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<bool>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub runs: Option<usize>,
|
|
}
|
|
|
|
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,
|
|
SpuriousDragon,
|
|
Byzantium,
|
|
Constantinople,
|
|
Petersburg,
|
|
Istanbul,
|
|
Berlin,
|
|
London,
|
|
}
|
|
|
|
impl Default for EvmVersion {
|
|
fn default() -> Self {
|
|
Self::London
|
|
}
|
|
}
|
|
|
|
impl EvmVersion {
|
|
/// Checks against the given solidity `semver::Version`
|
|
pub fn normalize_version(self, version: &Version) -> Option<EvmVersion> {
|
|
// 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::SpuriousDragon => "spuriousDragon",
|
|
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<Self, Self::Err> {
|
|
match s {
|
|
"homestead" => Ok(EvmVersion::Homestead),
|
|
"tangerineWhistle" => Ok(EvmVersion::TangerineWhistle),
|
|
"spuriousDragon" => Ok(EvmVersion::SpuriousDragon),
|
|
"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 SettingsMetadata {
|
|
#[serde(default, rename = "useLiteralContent", skip_serializing_if = "Option::is_none")]
|
|
pub use_literal_content: Option<bool>,
|
|
#[serde(default, rename = "bytecodeHash", skip_serializing_if = "Option::is_none")]
|
|
pub bytecode_hash: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Metadata {
|
|
pub compiler: Compiler,
|
|
pub language: String,
|
|
pub output: Output,
|
|
pub settings: Settings,
|
|
pub sources: MetadataSources,
|
|
pub version: i64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct MetadataSources {
|
|
#[serde(flatten)]
|
|
pub inner: BTreeMap<String, serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Compiler {
|
|
pub version: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Output {
|
|
pub abi: Vec<SolcAbi>,
|
|
pub devdoc: Option<Doc>,
|
|
pub userdoc: Option<Doc>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, 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(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Item {
|
|
#[serde(rename = "internalType")]
|
|
pub internal_type: String,
|
|
pub name: String,
|
|
#[serde(rename = "type")]
|
|
pub put_type: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Doc {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub kind: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub methods: Option<Libraries>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub version: Option<u32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
pub struct Libraries {
|
|
#[serde(flatten)]
|
|
pub libs: BTreeMap<String, serde_json::Value>,
|
|
}
|
|
|
|
#[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<Path>) -> Result<Self, SolcIoError> {
|
|
let file = file.as_ref();
|
|
Ok(Self { content: fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))? })
|
|
}
|
|
|
|
/// Finds all source files under the given dir path and reads them all
|
|
pub fn read_all_from(dir: impl AsRef<Path>) -> Result<Sources, SolcIoError> {
|
|
Self::read_all(utils::source_files(dir))
|
|
}
|
|
|
|
/// Reads all files
|
|
pub fn read_all<T, I>(files: I) -> Result<Sources, SolcIoError>
|
|
where
|
|
I: IntoIterator<Item = T>,
|
|
T: Into<PathBuf>,
|
|
{
|
|
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<Path>) -> Result<Self, SolcIoError> {
|
|
let file = file.as_ref();
|
|
Ok(Self {
|
|
content: tokio::fs::read_to_string(file)
|
|
.await
|
|
.map_err(|err| SolcIoError::new(err, file))?,
|
|
})
|
|
}
|
|
|
|
/// Finds all source files under the given dir path and reads them all
|
|
pub async fn async_read_all_from(dir: impl AsRef<Path>) -> Result<Sources, SolcIoError> {
|
|
Self::async_read_all(utils::source_files(dir.as_ref())).await
|
|
}
|
|
|
|
/// async version of `Self::read_all`
|
|
pub async fn async_read_all<T, I>(files: I) -> Result<Sources, SolcIoError>
|
|
where
|
|
I: IntoIterator<Item = T>,
|
|
T: Into<PathBuf>,
|
|
{
|
|
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<str> 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<Error>,
|
|
#[serde(default)]
|
|
pub sources: BTreeMap<String, SourceFile>,
|
|
#[serde(default)]
|
|
pub contracts: Contracts,
|
|
}
|
|
|
|
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<str>) -> Option<CompactContractRef> {
|
|
let contract_name = contract.as_ref();
|
|
self.contracts_iter().find_map(|(name, contract)| {
|
|
(name == contract_name).then(|| CompactContractRef::from(contract))
|
|
})
|
|
}
|
|
|
|
/// Finds the first contract with the given name and removes it from the set
|
|
pub fn remove(&mut self, contract: impl AsRef<str>) -> Option<Contract> {
|
|
let contract_name = contract.as_ref();
|
|
self.contracts.values_mut().find_map(|c| c.remove(contract_name))
|
|
}
|
|
|
|
/// Iterate over all contracts and their names
|
|
pub fn contracts_iter(&self) -> impl Iterator<Item = (&String, &Contract)> {
|
|
self.contracts.values().flatten()
|
|
}
|
|
|
|
/// Iterate over all contracts and their names
|
|
pub fn contracts_into_iter(self) -> impl Iterator<Item = (String, Contract)> {
|
|
self.contracts.into_values().flatten()
|
|
}
|
|
|
|
/// 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<CompactContractRef> {
|
|
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> {
|
|
/// Returns true if there is at least one error of high severity
|
|
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 Metadata.
|
|
/// See https://docs.soliditylang.org/en/develop/metadata.html
|
|
pub abi: Option<Abi>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none", with = "json_string_opt")]
|
|
pub metadata: Option<Metadata>,
|
|
#[serde(default)]
|
|
pub userdoc: UserDoc,
|
|
#[serde(default)]
|
|
pub devdoc: DevDoc,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub ir: Option<String>,
|
|
#[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<Evm>,
|
|
/// Ewasm related outputs
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub ewasm: Option<Ewasm>,
|
|
}
|
|
|
|
/// Minimal representation of a contract's abi with bytecode
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
|
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<Abi>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub bin: Option<BytecodeObject>,
|
|
#[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")]
|
|
pub bin_runtime: Option<BytecodeObject>,
|
|
}
|
|
|
|
impl CompactContract {
|
|
/// Returns the contents of this type as a single tuple of abi, bytecode and deployed bytecode
|
|
pub fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
|
|
(
|
|
self.abi,
|
|
self.bin.and_then(|bin| bin.into_bytes()),
|
|
self.bin_runtime.and_then(|bin| bin.into_bytes()),
|
|
)
|
|
}
|
|
|
|
/// 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.and_then(|bin| bin.into_bytes()).unwrap_or_default(),
|
|
self.bin_runtime.and_then(|bin| bin.into_bytes()).unwrap_or_default(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Value> for CompactContract {
|
|
fn from(mut val: serde_json::Value) -> Self {
|
|
if let Some(map) = val.as_object_mut() {
|
|
let abi = map.remove("abi").and_then(|val| serde_json::from_value(val).ok());
|
|
let bin = map.remove("bin").and_then(|val| serde_json::from_value(val).ok());
|
|
let bin_runtime =
|
|
map.remove("bin-runtime").and_then(|val| serde_json::from_value(val).ok());
|
|
Self { abi, bin, bin_runtime }
|
|
} else {
|
|
CompactContract::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Contract> for CompactContract {
|
|
fn from(c: Contract) -> Self {
|
|
let (bin, bin_runtime) = if let Some(evm) = c.evm {
|
|
(
|
|
Some(evm.bytecode.object),
|
|
evm.deployed_bytecode.and_then(|deployed| deployed.bytecode.map(|evm| evm.object)),
|
|
)
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
Self { abi: c.abi, bin, bin_runtime }
|
|
}
|
|
}
|
|
|
|
impl<'a> From<CompactContractRef<'a>> 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 BytecodeObject>,
|
|
#[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")]
|
|
pub bin_runtime: Option<&'a BytecodeObject>,
|
|
}
|
|
|
|
impl<'a> CompactContractRef<'a> {
|
|
/// Clones the referenced values and returns as tuples
|
|
pub fn into_parts(self) -> (Option<Abi>, Option<Bytes>, Option<Bytes>) {
|
|
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()
|
|
}
|
|
|
|
pub fn bytecode(&self) -> Option<&Bytes> {
|
|
self.bin.as_ref().and_then(|bin| bin.as_bytes())
|
|
}
|
|
|
|
pub fn runtime_bytecode(&self) -> Option<&Bytes> {
|
|
self.bin_runtime.as_ref().and_then(|bin| bin.as_bytes())
|
|
}
|
|
}
|
|
|
|
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
|
|
.as_ref()
|
|
.and_then(|deployed| deployed.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<u32>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub kind: Option<String>,
|
|
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
|
|
pub methods: BTreeMap<String, String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub notice: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
|
|
pub struct DevDoc {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub version: Option<u32>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub kind: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub author: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub details: Option<String>,
|
|
#[serde(default, rename = "custom:experimental", skip_serializing_if = "Option::is_none")]
|
|
pub custom_experimental: Option<String>,
|
|
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
|
|
pub methods: BTreeMap<String, String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub title: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub legacy_assembly: Option<serde_json::Value>,
|
|
pub bytecode: Bytecode,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub deployed_bytecode: Option<DeployedBytecode>,
|
|
/// The list of function hashes
|
|
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
|
|
pub method_identifiers: BTreeMap<String, String>,
|
|
/// Function gas estimates
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub gas_estimates: Option<GasEstimates>,
|
|
}
|
|
|
|
#[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<String, FunctionDebugData>,
|
|
/// The bytecode as a hex string.
|
|
pub object: BytecodeObject,
|
|
/// Opcodes list (string)
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub opcodes: Option<String>,
|
|
/// The source mapping as a string. See the source mapping definition.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub source_map: Option<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<GeneratedSource>,
|
|
/// If given, this is an unlinked object.
|
|
#[serde(default)]
|
|
pub link_references: BTreeMap<String, BTreeMap<String, Vec<Offsets>>>,
|
|
}
|
|
|
|
impl Bytecode {
|
|
/// Same as `Bytecode::link` but with fully qualified name (`file.sol:Math`)
|
|
pub fn link_fully_qualified(&mut self, name: impl AsRef<str>, addr: Address) -> bool {
|
|
if let Some((file, lib)) = name.as_ref().split_once(':') {
|
|
self.link(file, lib, addr)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Tries to link the bytecode object with the `file` and `library` name.
|
|
/// Replaces all library placeholders with the given address.
|
|
///
|
|
/// Returns true if the bytecode object is fully linked, false otherwise
|
|
/// This is a noop if the bytecode object is already fully linked.
|
|
pub fn link(
|
|
&mut self,
|
|
file: impl AsRef<str>,
|
|
library: impl AsRef<str>,
|
|
address: Address,
|
|
) -> bool {
|
|
if !self.object.is_unlinked() {
|
|
return true
|
|
}
|
|
|
|
let file = file.as_ref();
|
|
let library = library.as_ref();
|
|
if let Some((key, mut contracts)) = self.link_references.remove_entry(file) {
|
|
if contracts.remove(library).is_some() {
|
|
self.object.link(file, library, address);
|
|
}
|
|
if !contracts.is_empty() {
|
|
self.link_references.insert(key, contracts);
|
|
}
|
|
if self.link_references.is_empty() {
|
|
return self.object.resolve().is_some()
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Links the bytecode object with all provided `(file, lib, addr)`
|
|
pub fn link_all<I, S, T>(&mut self, libs: I) -> bool
|
|
where
|
|
I: IntoIterator<Item = (S, T, Address)>,
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
{
|
|
for (file, lib, addr) in libs.into_iter() {
|
|
if self.link(file, lib, addr) {
|
|
return true
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Links the bytecode object with all provided `(fully_qualified, addr)`
|
|
pub fn link_all_fully_qualified<I, S>(&mut self, libs: I) -> bool
|
|
where
|
|
I: IntoIterator<Item = (S, Address)>,
|
|
S: AsRef<str>,
|
|
{
|
|
for (name, addr) in libs.into_iter() {
|
|
if self.link_fully_qualified(name, addr) {
|
|
return true
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Represents the bytecode of a contracts that might be not fully linked yet.
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
|
#[serde(untagged)]
|
|
pub enum BytecodeObject {
|
|
/// Fully linked bytecode object
|
|
#[serde(deserialize_with = "deserialize_bytes")]
|
|
Bytecode(Bytes),
|
|
/// Bytecode as hex string that's not fully linked yet and contains library placeholders
|
|
Unlinked(String),
|
|
}
|
|
|
|
impl BytecodeObject {
|
|
pub fn into_bytes(self) -> Option<Bytes> {
|
|
match self {
|
|
BytecodeObject::Bytecode(bytes) => Some(bytes),
|
|
BytecodeObject::Unlinked(_) => None,
|
|
}
|
|
}
|
|
|
|
pub fn as_bytes(&self) -> Option<&Bytes> {
|
|
match self {
|
|
BytecodeObject::Bytecode(bytes) => Some(bytes),
|
|
BytecodeObject::Unlinked(_) => None,
|
|
}
|
|
}
|
|
|
|
pub fn into_unlinked(self) -> Option<String> {
|
|
match self {
|
|
BytecodeObject::Bytecode(_) => None,
|
|
BytecodeObject::Unlinked(code) => Some(code),
|
|
}
|
|
}
|
|
|
|
/// Tries to resolve the unlinked string object a valid bytecode object in place
|
|
///
|
|
/// Returns the string if it is a valid
|
|
pub fn resolve(&mut self) -> Option<&Bytes> {
|
|
if let BytecodeObject::Unlinked(unlinked) = self {
|
|
if let Ok(linked) = hex::decode(unlinked) {
|
|
*self = BytecodeObject::Bytecode(linked.into());
|
|
}
|
|
}
|
|
self.as_bytes()
|
|
}
|
|
|
|
/// Link using the fully qualified name of a library
|
|
/// The fully qualified library name is the path of its source file and the library name
|
|
/// separated by `:` like `file.sol:Math`
|
|
///
|
|
/// This will replace all occurrences of the library placeholder with the given address.
|
|
///
|
|
/// See also: https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking
|
|
pub fn link_fully_qualified(&mut self, name: impl AsRef<str>, addr: Address) -> &mut Self {
|
|
if let BytecodeObject::Unlinked(ref mut unlinked) = self {
|
|
let name = name.as_ref();
|
|
let place_holder = utils::library_hash_placeholder(name);
|
|
// the address as hex without prefix
|
|
let hex_addr = hex::encode(addr);
|
|
|
|
// the library placeholder used to be the fully qualified name of the library instead of
|
|
// the hash. This is also still supported by `solc` so we handle this as well
|
|
let fully_qualified_placeholder = utils::library_fully_qualified_placeholder(name);
|
|
|
|
*unlinked = unlinked
|
|
.replace(&format!("__{}__", fully_qualified_placeholder), &hex_addr)
|
|
.replace(&format!("__{}__", place_holder), &hex_addr)
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Link using the `file` and `library` names as fully qualified name `<file>:<library>`
|
|
/// See `BytecodeObject::link_fully_qualified`
|
|
pub fn link(
|
|
&mut self,
|
|
file: impl AsRef<str>,
|
|
library: impl AsRef<str>,
|
|
addr: Address,
|
|
) -> &mut Self {
|
|
self.link_fully_qualified(format!("{}:{}", file.as_ref(), library.as_ref(),), addr)
|
|
}
|
|
|
|
/// Links the bytecode object with all provided `(file, lib, addr)`
|
|
pub fn link_all<I, S, T>(&mut self, libs: I) -> &mut Self
|
|
where
|
|
I: IntoIterator<Item = (S, T, Address)>,
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
{
|
|
for (file, lib, addr) in libs.into_iter() {
|
|
self.link(file, lib, addr);
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Whether this object is still unlinked
|
|
pub fn is_unlinked(&self) -> bool {
|
|
matches!(self, BytecodeObject::Unlinked(_))
|
|
}
|
|
|
|
/// Whether the bytecode contains a matching placeholder using the qualified name
|
|
pub fn contains_fully_qualified_placeholder(&self, name: impl AsRef<str>) -> bool {
|
|
if let BytecodeObject::Unlinked(unlinked) = self {
|
|
let name = name.as_ref();
|
|
unlinked.contains(&utils::library_hash_placeholder(name)) ||
|
|
unlinked.contains(&utils::library_fully_qualified_placeholder(name))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Whether the bytecode contains a matching placeholder
|
|
pub fn contains_placeholder(&self, file: impl AsRef<str>, library: impl AsRef<str>) -> bool {
|
|
self.contains_fully_qualified_placeholder(format!("{}:{}", file.as_ref(), library.as_ref()))
|
|
}
|
|
}
|
|
|
|
// Returns a not deployable bytecode by default as "0x"
|
|
impl Default for BytecodeObject {
|
|
fn default() -> Self {
|
|
BytecodeObject::Unlinked("0x".to_string())
|
|
}
|
|
}
|
|
|
|
impl AsRef<[u8]> for BytecodeObject {
|
|
fn as_ref(&self) -> &[u8] {
|
|
match self {
|
|
BytecodeObject::Bytecode(code) => code.as_ref(),
|
|
BytecodeObject::Unlinked(code) => code.as_bytes(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct FunctionDebugData {
|
|
pub entry_point: Option<u32>,
|
|
pub id: Option<u32>,
|
|
pub parameter_slots: Option<u32>,
|
|
pub return_slots: Option<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<Bytecode>,
|
|
#[serde(
|
|
default,
|
|
rename = "immutableReferences",
|
|
skip_serializing_if = "::std::collections::BTreeMap::is_empty"
|
|
)]
|
|
pub immutable_references: BTreeMap<String, Vec<Offsets>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
|
pub struct GasEstimates {
|
|
pub creation: Creation,
|
|
#[serde(default)]
|
|
pub external: BTreeMap<String, String>,
|
|
#[serde(default)]
|
|
pub internal: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[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<Storage>,
|
|
#[serde(default)]
|
|
pub types: BTreeMap<String, StorageType>,
|
|
}
|
|
|
|
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<SourceLocation>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub secondary_source_locations: Vec<SourceLocation>,
|
|
pub r#type: String,
|
|
pub component: String,
|
|
pub severity: Severity,
|
|
#[serde(default, with = "display_from_str_opt")]
|
|
pub error_code: Option<u64>,
|
|
pub message: String,
|
|
pub formatted_message: Option<String>,
|
|
}
|
|
|
|
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<Self, Self::Err> {
|
|
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
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<E>(self, value: &str) -> Result<Self::Value, E>
|
|
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<String>,
|
|
}
|
|
|
|
#[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<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
|
|
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<Option<T>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
T: FromStr,
|
|
T::Err: fmt::Display,
|
|
{
|
|
if let Some(s) = Option::<String>::deserialize(deserializer)? {
|
|
s.parse().map_err(de::Error::custom).map(Some)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
mod json_string_opt {
|
|
use serde::{
|
|
de::{self, DeserializeOwned},
|
|
ser, Deserialize, Deserializer, Serialize, Serializer,
|
|
};
|
|
|
|
pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
T: Serialize,
|
|
{
|
|
if let Some(value) = value {
|
|
let value = serde_json::to_string(value).map_err(ser::Error::custom)?;
|
|
serializer.serialize_str(&value)
|
|
} else {
|
|
serializer.serialize_none()
|
|
}
|
|
}
|
|
|
|
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
T: DeserializeOwned,
|
|
{
|
|
if let Some(s) = Option::<String>::deserialize(deserializer)? {
|
|
serde_json::from_str(&s).map_err(de::Error::custom).map(Some)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result<Bytes, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let value = String::deserialize(d)?;
|
|
if let Some(value) = value.strip_prefix("0x") {
|
|
hex::decode(value)
|
|
} else {
|
|
hex::decode(&value)
|
|
}
|
|
.map(Into::into)
|
|
.map_err(|e| serde::de::Error::custom(e.to_string()))
|
|
}
|
|
|
|
pub fn deserialize_opt_bytes<'de, D>(d: D) -> std::result::Result<Option<Bytes>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let value = Option::<String>::deserialize(d)?;
|
|
if let Some(value) = value {
|
|
Ok(Some(
|
|
if let Some(value) = value.strip_prefix("0x") {
|
|
hex::decode(value)
|
|
} else {
|
|
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_link_bytecode() {
|
|
// test cases taken from https://github.com/ethereum/solc-js/blob/master/test/linker.js
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct Mockject {
|
|
object: BytecodeObject,
|
|
}
|
|
fn parse_bytecode(bytecode: &str) -> BytecodeObject {
|
|
let object: Mockject =
|
|
serde_json::from_value(serde_json::json!({ "object": bytecode })).unwrap();
|
|
object.object
|
|
}
|
|
|
|
let bytecode = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__lib2.sol:L____________________________6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029";
|
|
|
|
let mut object = parse_bytecode(bytecode);
|
|
assert!(object.is_unlinked());
|
|
assert!(object.contains_placeholder("lib2.sol", "L"));
|
|
assert!(object.contains_fully_qualified_placeholder("lib2.sol:L"));
|
|
assert!(object.link("lib2.sol", "L", Address::random()).resolve().is_some());
|
|
assert!(!object.is_unlinked());
|
|
|
|
let mut code = Bytecode {
|
|
function_debug_data: Default::default(),
|
|
object: parse_bytecode(bytecode),
|
|
opcodes: None,
|
|
source_map: None,
|
|
generated_sources: vec![],
|
|
link_references: BTreeMap::from([(
|
|
"lib2.sol".to_string(),
|
|
BTreeMap::from([("L".to_string(), vec![])]),
|
|
)]),
|
|
};
|
|
|
|
assert!(!code.link("lib2.sol", "Y", Address::random()));
|
|
assert!(code.link("lib2.sol", "L", Address::random()));
|
|
assert!(code.link("lib2.sol", "L", Address::random()));
|
|
|
|
let hashed_placeholder = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__$cb901161e812ceb78cfe30ca65050c4337$__6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029";
|
|
let mut object = parse_bytecode(hashed_placeholder);
|
|
assert!(object.is_unlinked());
|
|
assert!(object.contains_placeholder("lib2.sol", "L"));
|
|
assert!(object.contains_fully_qualified_placeholder("lib2.sol:L"));
|
|
assert!(object.link("lib2.sol", "L", Address::default()).resolve().is_some());
|
|
assert!(!object.is_unlinked());
|
|
}
|
|
|
|
#[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::<CompilerOutput>(&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::<CompilerInput>(&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
|
|
)
|
|
}
|
|
}
|
|
}
|