Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(etherscan): add caching #1108

Merged
merged 14 commits into from
Apr 6, 2022
30 changes: 28 additions & 2 deletions ethers-etherscan/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,25 @@ impl Client {
/// # }
/// ```
pub async fn contract_abi(&self, address: Address) -> Result<Abi> {
// apply caching
if let Some(ref cache) = self.cache {
if let Some(abi) = cache.get_abi(address)? {
return Ok(abi)
}
}

let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
let resp: Response<String> = self.get_json(&query).await?;
if resp.result.starts_with("Contract source code not verified") {
return Err(EtherscanError::ContractCodeNotVerified(address))
}
Ok(serde_json::from_str(&resp.result)?)
let abi = serde_json::from_str(&resp.result)?;

if let Some(ref cache) = self.cache {
cache.set_abi(address, &abi)?;
}

Ok(abi)
}

/// Get Contract Source Code for Verified Contract Source Codes
Expand All @@ -307,13 +320,26 @@ impl Client {
/// # }
/// ```
pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
// apply caching
if let Some(ref cache) = self.cache {
if let Some(src) = cache.get_source(address)? {
return Ok(src)
}
}

let query =
self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
let response: Response<Vec<Metadata>> = self.get_json(&query).await?;
if response.result.iter().any(|item| item.abi == "Contract source code not verified") {
return Err(EtherscanError::ContractCodeNotVerified(address))
}
Ok(ContractMetadata { items: response.result })
let res = ContractMetadata { items: response.result };

if let Some(ref cache) = self.cache {
cache.set_source(address, &res)?;
}

Ok(res)
}
}

Expand Down
62 changes: 60 additions & 2 deletions ethers-etherscan/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
//! Bindings for [etherscan.io web api](https://docs.etherscan.io/)

use std::borrow::Cow;
use std::{borrow::Cow, io::Write, path::PathBuf};

use contract::ContractMetadata;
use reqwest::{header, Url};
use serde::{de::DeserializeOwned, Deserialize, Serialize};

use errors::EtherscanError;
use ethers_core::{abi::Address, types::Chain};
use ethers_core::{
abi::{Abi, Address},
types::Chain,
};

pub mod account;
pub mod contract;
Expand All @@ -28,9 +32,62 @@ pub struct Client {
etherscan_api_url: Url,
/// Etherscan base endpoint like <https://etherscan.io>
etherscan_url: Url,
/// Path to where ABI files should be cached
cache: Option<Cache>,
}

#[derive(Clone, Debug)]
// Simple cache for etherscan requests
struct Cache(PathBuf);

impl Cache {
fn get_abi(&self, address: Address) -> Result<Option<ethers_core::abi::Abi>> {
self.get("abi", address)
}

fn set_abi(&self, address: Address, abi: &Abi) -> Result<()> {
self.set("abi", address, abi)
}

fn get_source(&self, address: Address) -> Result<Option<ContractMetadata>> {
self.get("sources", address)
}

fn set_source(&self, address: Address, source: &ContractMetadata) -> Result<()> {
self.set("sources", address, source)
}

fn set<T: Serialize>(&self, prefix: &str, address: Address, item: T) -> Result<()> {
let path = self.0.join(prefix).join(format!("{:?}.json", address));
let mut writer = std::io::BufWriter::new(std::fs::File::create(path)?);
serde_json::to_writer(&mut writer, &item)?;
// TODO: Trace
// TODO: Should we cache if the contract is *not* verified?
onbjerg marked this conversation as resolved.
Show resolved Hide resolved
let _ = writer.flush();
Ok(())
}

fn get<T: DeserializeOwned>(&self, prefix: &str, address: Address) -> Result<Option<T>> {
let path = self.0.join(prefix).join(format!("{:?}.json", address));
let reader = std::io::BufReader::new(std::fs::File::create(path)?);
if let Ok(inner) = serde_json::from_reader(reader) {
return Ok(Some(inner))
}
Ok(None)
}
}

impl Client {
pub fn new_cached(
chain: Chain,
api_key: impl Into<String>,
cache: Option<PathBuf>,
) -> Result<Self> {
let mut this = Self::new(chain, api_key)?;
this.cache = cache.map(Cache);
Ok(this)
}

/// Create a new client with the correct endpoints based on the chain and provided API key
pub fn new(chain: Chain, api_key: impl Into<String>) -> Result<Self> {
let (etherscan_api_url, etherscan_url) = match chain {
Expand Down Expand Up @@ -101,6 +158,7 @@ impl Client {
api_key: api_key.into(),
etherscan_api_url: etherscan_api_url.expect("is valid http"),
etherscan_url: etherscan_url.expect("is valid http"),
cache: None,
})
}

Expand Down