use std::path::PathBuf;
use alloy_primitives::Address;
use edb_common::{Cache, EdbCache};
use eyre::Result;
use foundry_block_explorers::{contract::Metadata, errors::EtherscanError, Client};
use foundry_compilers::{
artifacts::{output_selection::OutputSelection, Libraries, SolcInput, Source, Sources},
solc::{Solc, SolcLanguage},
};
use itertools::Itertools;
use tracing::{debug, trace};
use crate::{etherscan_rate_limit_guard, Artifact};
#[derive(Debug, Clone)]
pub struct OnchainCompiler {
pub cache: Option<EdbCache<Option<Artifact>>>,
}
impl OnchainCompiler {
pub fn new(cache_root: Option<PathBuf>) -> Result<Self> {
Ok(Self {
cache: EdbCache::new(cache_root, None)?,
})
}
pub async fn compile(&self, etherscan: &Client, addr: Address) -> Result<Option<Artifact>> {
if let Some(output) = self.cache.load_cache(addr.to_string()) {
Ok(output)
} else {
let mut meta =
match etherscan_rate_limit_guard!(etherscan.contract_source_code(addr).await) {
Ok(meta) => meta,
Err(EtherscanError::ContractCodeNotVerified(_)) => {
return Ok(None);
}
Err(e) => return Err(e.into()),
};
eyre::ensure!(meta.items.len() == 1, "contract not found or ill-formed");
let meta = meta.items.remove(0);
if meta.is_vyper() {
return Ok(None);
}
let input = get_compilation_input_from_metadata(&meta, addr)?;
let version = meta.compiler_version()?;
let compiler = Solc::find_or_install(&version)?;
trace!(addr=?addr, compiler=?compiler, "using compiler");
let output = match compiler.compile_exact(&input) {
Ok(output) => Some(Artifact { meta, input, output }),
Err(_) if version.major == 0 && version.minor == 4 => None,
Err(e) => {
return Err(eyre::eyre!("failed to compile contract: {}", e));
}
};
self.cache.save_cache(addr.to_string(), &output)?;
Ok(output)
}
}
}
pub fn get_compilation_input_from_metadata(meta: &Metadata, addr: Address) -> Result<SolcInput> {
let mut settings = meta.settings()?;
settings.output_selection = OutputSelection::complete_output_selection();
trace!(addr=?addr, settings=?settings, "using settings");
let sources: Sources =
meta.sources().into_iter().map(|(k, v)| (k.into(), Source::new(v.content))).collect();
if !meta.library.is_empty() {
let prefix = if sources.keys().unique().count() == 1 {
sources.keys().next().unwrap().to_string_lossy().to_string()
} else {
String::new()
};
let libs = meta
.library
.split(';')
.filter_map(|lib| {
debug!(lib=?lib, addr=?addr, "parsing library");
let mut parts = lib.split(':');
let file =
if parts.clone().count() == 2 { prefix.as_str() } else { parts.next()? };
let name = parts.next()?;
let addr = parts.next()?;
if addr.starts_with("0x") {
Some(format!("{file}:{name}:{addr}"))
} else {
Some(format!("{file}:{name}:0x{addr}"))
}
})
.collect::<Vec<_>>();
settings.libraries = Libraries::parse(&libs)?;
}
let input = SolcInput::new(SolcLanguage::Solidity, sources, settings);
Ok(input)
}
#[cfg(test)]
mod tests {
use std::{str::FromStr, time::Duration};
use alloy_chains::Chain;
use serial_test::serial;
use crate::utils::next_etherscan_api_key;
use super::*;
async fn run_compile(chain_id: Chain, addr: &str) -> eyre::Result<Option<Artifact>> {
let etherscan_cache_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../testdata/cache/etherscan")
.join(chain_id.to_string());
let etherscan = Client::builder()
.with_api_key(next_etherscan_api_key())
.with_cache(Some(etherscan_cache_root), Duration::from_secs(24 * 60 * 60)) .chain(chain_id)?
.build()?;
let compiler = OnchainCompiler::new(None)?;
compiler.compile(ðerscan, Address::from_str(addr)?).await
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn test_tailing_slash() {
run_compile(Chain::mainnet(), "0x22F9dCF4647084d6C31b2765F6910cd85C178C18").await.unwrap();
}
}