feat(etherscan): source tree support (#990)
* Create a source tree of paths and their respective contents from the ContractMetadata. This is useful for file level analysis or writing the resulting files to disk. * Test writing to disk via cargo test * Test etherscan by uncommenting the ignore on can_fetch_contract_source_tree_for_multi_entry_contract and can_fetch_contract_source_tree_for_singleton_contract
This commit is contained in:
parent
46d7afd65b
commit
96dd34287e
|
@ -1275,6 +1275,7 @@ dependencies = [
|
||||||
"serde-aux",
|
"serde-aux",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
@ -23,6 +23,7 @@ serde-aux = { version = "3.0.1", default-features = false }
|
||||||
thiserror = "1.0.29"
|
thiserror = "1.0.29"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
tempfile = "3.3.0"
|
||||||
tokio = { version = "1.5", features = ["macros", "rt-multi-thread", "time"] }
|
tokio = { version = "1.5", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
serial_test = "0.6.0"
|
serial_test = "0.6.0"
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use ethers_core::abi::{Abi, Address};
|
use ethers_core::abi::{Abi, Address};
|
||||||
|
|
||||||
use crate::{Client, EtherscanError, Response, Result};
|
use crate::{
|
||||||
|
source_tree::{SourceTree, SourceTreeEntry},
|
||||||
|
Client, EtherscanError, Response, Result,
|
||||||
|
};
|
||||||
|
|
||||||
/// Arguments for verifying contracts
|
/// Arguments for verifying contracts
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
@ -148,6 +151,16 @@ impl IntoIterator for ContractMetadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EtherscanSourceEntry {
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EtherscanSourceJsonMetadata {
|
||||||
|
sources: HashMap<String, EtherscanSourceEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ContractMetadata {
|
impl ContractMetadata {
|
||||||
/// All ABI from all contracts in the source file
|
/// All ABI from all contracts in the source file
|
||||||
pub fn abis(&self) -> Result<Vec<Abi>> {
|
pub fn abis(&self) -> Result<Vec<Abi>> {
|
||||||
|
@ -162,6 +175,42 @@ impl ContractMetadata {
|
||||||
pub fn source_code(&self) -> String {
|
pub fn source_code(&self) -> String {
|
||||||
self.items.iter().map(|c| c.source_code.as_str()).collect::<Vec<_>>().join("\n")
|
self.items.iter().map(|c| c.source_code.as_str()).collect::<Vec<_>>().join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Etherscan can either return one raw string that includes all of the solidity for a verified
|
||||||
|
/// contract or a json struct surrounded in an extra set of {} that includes a directory
|
||||||
|
/// structure with paths and source code.
|
||||||
|
fn get_sources_from_etherscan_source_value(
|
||||||
|
contract_name: &str,
|
||||||
|
etherscan_source: &str,
|
||||||
|
) -> Result<Vec<(String, String)>> {
|
||||||
|
if etherscan_source.starts_with("{{") && etherscan_source.ends_with("}}") {
|
||||||
|
let json = ðerscan_source[1..etherscan_source.len() - 1];
|
||||||
|
let parsed: EtherscanSourceJsonMetadata = serde_json::from_str(json)?;
|
||||||
|
Ok(parsed
|
||||||
|
.sources
|
||||||
|
.into_iter()
|
||||||
|
.map(|(path, source_struct)| (path, source_struct.content))
|
||||||
|
.collect())
|
||||||
|
} else {
|
||||||
|
Ok(vec![(contract_name.to_string(), etherscan_source.to_string())])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source_tree(&self) -> Result<SourceTree> {
|
||||||
|
let mut entries = vec![];
|
||||||
|
for item in &self.items {
|
||||||
|
let contract_root = Path::new(&item.contract_name);
|
||||||
|
let source_paths = Self::get_sources_from_etherscan_source_value(
|
||||||
|
&item.contract_name,
|
||||||
|
&item.source_code,
|
||||||
|
)?;
|
||||||
|
for (path, contents) in source_paths {
|
||||||
|
let joined = contract_root.join(&path);
|
||||||
|
entries.push(SourceTreeEntry { path: joined, contents });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(SourceTree { entries })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Etherscan contract metadata
|
/// Etherscan contract metadata
|
||||||
|
@ -305,6 +354,45 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query a contract that has a single string source entry instead of underlying JSON metadata.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
#[ignore]
|
||||||
|
async fn can_fetch_contract_source_tree_for_singleton_contract() {
|
||||||
|
run_at_least_duration(Duration::from_millis(250), async {
|
||||||
|
let client = Client::new_from_env(Chain::Mainnet).unwrap();
|
||||||
|
|
||||||
|
let meta = client
|
||||||
|
.contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let source_tree = meta.source_tree().unwrap();
|
||||||
|
assert_eq!(source_tree.entries.len(), 1);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query a contract that has many source entries as JSON metadata and ensure they are
|
||||||
|
/// reflected.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
#[ignore]
|
||||||
|
async fn can_fetch_contract_source_tree_for_multi_entry_contract() {
|
||||||
|
run_at_least_duration(Duration::from_millis(250), async {
|
||||||
|
let client = Client::new_from_env(Chain::Mainnet).unwrap();
|
||||||
|
|
||||||
|
let meta = client
|
||||||
|
.contract_source_code("0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63".parse().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let source_tree = meta.source_tree().unwrap();
|
||||||
|
assert_eq!(source_tree.entries.len(), 15);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn can_flatten_and_verify_contract() {
|
async fn can_flatten_and_verify_contract() {
|
||||||
|
|
|
@ -23,4 +23,6 @@ pub enum EtherscanError {
|
||||||
Serde(#[from] serde_json::Error),
|
Serde(#[from] serde_json::Error),
|
||||||
#[error("Contract source code not verified: {0}")]
|
#[error("Contract source code not verified: {0}")]
|
||||||
ContractCodeNotVerified(Address),
|
ContractCodeNotVerified(Address),
|
||||||
|
#[error(transparent)]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub mod account;
|
||||||
pub mod contract;
|
pub mod contract;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod gas;
|
pub mod gas;
|
||||||
|
pub mod source_tree;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
|
||||||
pub(crate) type Result<T> = std::result::Result<T, EtherscanError>;
|
pub(crate) type Result<T> = std::result::Result<T, EtherscanError>;
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
use crate::Result;
|
||||||
|
use std::{
|
||||||
|
fs::create_dir_all,
|
||||||
|
path::{Component, Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SourceTreeEntry {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SourceTree {
|
||||||
|
pub entries: Vec<SourceTreeEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceTree {
|
||||||
|
/// Expand the source tree into the provided directory. This method sanitizes paths to ensure
|
||||||
|
/// that no directory traversal happens.
|
||||||
|
pub fn write_to(&self, dir: &Path) -> Result<()> {
|
||||||
|
create_dir_all(&dir)?;
|
||||||
|
for entry in &self.entries {
|
||||||
|
let sanitized_path = sanitize_path(&entry.path);
|
||||||
|
let joined = dir.join(sanitized_path);
|
||||||
|
if let Some(parent) = joined.parent() {
|
||||||
|
create_dir_all(parent)?;
|
||||||
|
std::fs::write(joined, &entry.contents)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove any components in a smart contract source path that could cause a directory traversal.
|
||||||
|
fn sanitize_path(path: &Path) -> PathBuf {
|
||||||
|
Path::new(path)
|
||||||
|
.components()
|
||||||
|
.filter(|x| x.as_os_str() != Component::ParentDir.as_os_str())
|
||||||
|
.collect::<PathBuf>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs::read_dir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_source_tree_write() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let st = SourceTree {
|
||||||
|
entries: vec![
|
||||||
|
SourceTreeEntry { path: PathBuf::from("a/a.sol"), contents: String::from("Test") },
|
||||||
|
SourceTreeEntry {
|
||||||
|
path: PathBuf::from("b/b.sol"),
|
||||||
|
contents: String::from("Test 2"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
st.write_to(&tempdir.path()).unwrap();
|
||||||
|
let written_paths = read_dir(tempdir.path()).unwrap();
|
||||||
|
let paths: Vec<PathBuf> =
|
||||||
|
written_paths.into_iter().filter_map(|x| x.ok()).map(|x| x.path()).collect();
|
||||||
|
assert_eq!(paths.len(), 2);
|
||||||
|
assert!(paths.contains(&tempdir.path().join("a")));
|
||||||
|
assert!(paths.contains(&tempdir.path().join("b")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that the .. are ignored when writing the source tree to disk because of
|
||||||
|
/// sanitization.
|
||||||
|
#[test]
|
||||||
|
fn test_malformed_source_tree_write() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let st = SourceTree {
|
||||||
|
entries: vec![
|
||||||
|
SourceTreeEntry {
|
||||||
|
path: PathBuf::from("../a/a.sol"),
|
||||||
|
contents: String::from("Test"),
|
||||||
|
},
|
||||||
|
SourceTreeEntry {
|
||||||
|
path: PathBuf::from("../b/../b.sol"),
|
||||||
|
contents: String::from("Test 2"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
st.write_to(&tempdir.path()).unwrap();
|
||||||
|
let written_paths = read_dir(tempdir.path()).unwrap();
|
||||||
|
let paths: Vec<PathBuf> =
|
||||||
|
written_paths.into_iter().filter_map(|x| x.ok()).map(|x| x.path()).collect();
|
||||||
|
assert_eq!(paths.len(), 2);
|
||||||
|
assert!(paths.contains(&tempdir.path().join("a")));
|
||||||
|
assert!(paths.contains(&tempdir.path().join("b")));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue