1use crate::{
2 serde_helpers::{deserialize_stringified_bool_or_u64, deserialize_stringified_u64},
3 source_tree::{SourceTree, SourceTreeEntry},
4 utils::{deserialize_address_opt, deserialize_source_code},
5 Client, EtherscanError, Response, Result,
6};
7use alloy_json_abi::JsonAbi;
8use alloy_primitives::{Address, Bytes, B256};
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use std::{collections::HashMap, path::Path};
12
13#[cfg(feature = "foundry-compilers")]
14use foundry_compilers::{
15 artifacts::{EvmVersion, Settings},
16 compilers::solc::SolcCompiler,
17 solc::SolcSettings,
18 ProjectBuilder, SolcConfig,
19};
20
21#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
22pub enum SourceCodeLanguage {
23 #[default]
24 Solidity,
25 Vyper,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub struct SourceCodeEntry {
30 pub content: String,
31}
32
33impl<T: Into<String>> From<T> for SourceCodeEntry {
34 fn from(s: T) -> Self {
35 Self { content: s.into() }
36 }
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(untagged)]
42pub enum SourceCodeMetadata {
43 Sources(HashMap<String, SourceCodeEntry>),
46 Metadata {
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 language: Option<SourceCodeLanguage>,
51 #[serde(default)]
53 sources: HashMap<String, SourceCodeEntry>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 settings: Option<serde_json::Value>,
57 },
58 SourceCode(String),
60}
61
62impl SourceCodeMetadata {
63 pub fn source_code(&self) -> String {
64 match self {
65 Self::Metadata { sources, .. } | Self::Sources(sources) => {
66 sources.values().map(|s| s.content.clone()).collect::<Vec<_>>().join("\n")
67 }
68 Self::SourceCode(s) => s.clone(),
69 }
70 }
71
72 pub fn language(&self) -> Option<SourceCodeLanguage> {
73 match self {
74 Self::Metadata { language, .. } => *language,
75 Self::Sources(_) => None,
76 Self::SourceCode(_) => None,
77 }
78 }
79
80 pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
81 match self {
82 Self::Metadata { sources, .. } => sources.clone(),
83 Self::Sources(sources) => sources.clone(),
84 Self::SourceCode(s) => HashMap::from([("Contract".into(), s.into())]),
85 }
86 }
87
88 #[cfg(feature = "foundry-compilers")]
89 pub fn settings(&self) -> Result<Option<Settings>> {
90 match self {
91 Self::Metadata { settings, .. } => match settings {
92 Some(value) => {
93 if value.is_null() {
94 Ok(None)
95 } else {
96 let settings =
97 serde_json::from_value(value.to_owned()).map_err(|error| {
98 EtherscanError::Serde { error, content: value.to_string() }
99 })?;
100 Ok(Some(settings))
101 }
102 }
103 None => Ok(None),
104 },
105 Self::Sources(_) => Ok(None),
106 Self::SourceCode(_) => Ok(None),
107 }
108 }
109
110 #[cfg(not(feature = "foundry-compilers"))]
111 pub fn settings(&self) -> Option<&serde_json::Value> {
112 match self {
113 Self::Metadata { settings, .. } => settings.as_ref(),
114 Self::Sources(_) => None,
115 Self::SourceCode(_) => None,
116 }
117 }
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
122#[serde(rename_all = "PascalCase")]
123pub struct Metadata {
124 #[serde(deserialize_with = "deserialize_source_code")]
126 pub source_code: SourceCodeMetadata,
127
128 #[serde(rename = "ABI")]
130 pub abi: String,
131
132 pub contract_name: String,
134
135 pub compiler_version: String,
138
139 #[serde(deserialize_with = "deserialize_stringified_bool_or_u64")]
141 pub optimization_used: u64,
142
143 #[serde(deserialize_with = "deserialize_stringified_u64", alias = "OptimizationRuns", default)]
145 pub runs: u64,
146
147 #[serde(default)]
149 pub constructor_arguments: Bytes,
150
151 #[serde(rename = "EVMVersion")]
154 pub evm_version: String,
155
156 #[serde(default)]
158 pub library: String,
159
160 #[serde(default)]
162 pub license_type: String,
163
164 #[serde(deserialize_with = "deserialize_stringified_bool_or_u64", alias = "IsProxy")]
166 pub proxy: u64,
167
168 #[serde(
170 default,
171 skip_serializing_if = "Option::is_none",
172 deserialize_with = "deserialize_address_opt"
173 )]
174 pub implementation: Option<Address>,
175
176 #[serde(default)]
178 pub swarm_source: String,
179}
180
181impl Metadata {
182 pub fn source_code(&self) -> String {
184 self.source_code.source_code()
185 }
186
187 pub fn language(&self) -> SourceCodeLanguage {
189 self.source_code.language().unwrap_or_else(|| {
190 if self.is_vyper() {
191 SourceCodeLanguage::Vyper
192 } else {
193 SourceCodeLanguage::Solidity
194 }
195 })
196 }
197
198 pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
200 self.source_code.sources()
201 }
202
203 pub fn abi(&self) -> Result<JsonAbi> {
205 serde_json::from_str(&self.abi)
206 .map_err(|error| EtherscanError::Serde { error, content: self.abi.clone() })
207 }
208
209 pub fn compiler_version(&self) -> Result<Version> {
211 let v = &self.compiler_version;
212 let v = v.strip_prefix("vyper:").unwrap_or(v);
213 let v = v.strip_prefix('v').unwrap_or(v);
214 match v.parse() {
215 Err(e) => {
216 let v = v.replace('a', "-alpha.");
217 let v = v.replace('b', "-beta.");
218 v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}")))
219 }
220 Ok(v) => Ok(v),
221 }
222 }
223
224 pub fn is_vyper(&self) -> bool {
226 self.compiler_version.starts_with("vyper:")
227 }
228
229 pub fn source_entries(&self) -> Vec<SourceTreeEntry> {
231 let root = Path::new(&self.contract_name);
232 self.sources()
233 .into_iter()
234 .map(|(path, entry)| {
235 let sanitized_path = crate::source_tree::sanitize_path(path);
240 let path = root.join(sanitized_path);
241 SourceTreeEntry { path, contents: entry.content }
242 })
243 .collect()
244 }
245
246 pub fn source_tree(&self) -> SourceTree {
248 SourceTree { entries: self.source_entries() }
249 }
250
251 #[cfg(feature = "foundry-compilers")]
253 pub fn settings(&self) -> Result<Settings> {
254 let mut settings = self.source_code.settings()?.unwrap_or_default();
255
256 if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() {
257 settings.optimizer.enable();
258 settings.optimizer.runs(self.runs as usize);
259 }
260
261 settings.evm_version = self.evm_version()?;
262
263 Ok(settings)
264 }
265
266 #[cfg(feature = "foundry-compilers")]
268 pub fn project_builder(&self) -> Result<ProjectBuilder<SolcCompiler>> {
269 let solc_config = SolcConfig::builder().settings(self.settings()?).build();
270
271 Ok(ProjectBuilder::new(Default::default())
272 .settings(SolcSettings { settings: solc_config, ..Default::default() }))
273 }
274
275 #[cfg(feature = "foundry-compilers")]
277 pub fn evm_version(&self) -> Result<Option<EvmVersion>> {
278 match self.evm_version.to_lowercase().as_str() {
279 "" | "default" => Ok(EvmVersion::default_version_solc(&self.compiler_version()?)),
280 _ => {
281 let evm_version = self
282 .evm_version
283 .parse()
284 .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?;
285 Ok(Some(evm_version))
286 }
287 }
288 }
289}
290
291#[derive(Clone, Debug, Serialize, Deserialize)]
292#[serde(transparent)]
293pub struct ContractMetadata {
294 pub items: Vec<Metadata>,
295}
296
297impl IntoIterator for ContractMetadata {
298 type Item = Metadata;
299 type IntoIter = std::vec::IntoIter<Metadata>;
300
301 fn into_iter(self) -> Self::IntoIter {
302 self.items.into_iter()
303 }
304}
305
306impl ContractMetadata {
307 pub fn abis(&self) -> Result<Vec<JsonAbi>> {
309 self.items.iter().map(|c| c.abi()).collect()
310 }
311
312 pub fn source_code(&self) -> String {
314 self.items.iter().map(|c| c.source_code()).collect::<Vec<_>>().join("\n")
315 }
316
317 pub fn source_tree(&self) -> SourceTree {
319 SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() }
320 }
321}
322
323#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct ContractCreationData {
327 pub contract_address: Address,
329
330 pub contract_creator: Address,
334
335 #[serde(rename = "txHash")]
337 pub transaction_hash: B256,
338}
339
340impl Client {
341 pub async fn contract_abi(&self, address: Address) -> Result<JsonAbi> {
352 if let Some(ref cache) = self.cache {
354 if let Some(src) = cache.get_abi(address) {
356 return match src {
358 Some(src) => Ok(src),
359 None => Err(EtherscanError::ContractCodeNotVerified(address)),
360 };
361 }
362 }
363 trace!(target: "etherscan", ?address, "GET contract abi");
364
365 let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
366
367 let resp: Response<Option<String>> = self.get_json(&query).await.inspect_err(|err| {
368 error!(target: "etherscan", ?err, "Failed to deserialize ABI response");
369 })?;
370
371 let result = match resp.result {
372 Some(result) => result,
373 None => {
374 if resp.message.contains("Contract source code not verified") {
375 return Err(EtherscanError::ContractCodeNotVerified(address));
376 }
377 return Err(EtherscanError::EmptyResult {
378 message: resp.message,
379 status: resp.status,
380 });
381 }
382 };
383
384 if resp.status == "0" && result.to_lowercase().contains("invalid api key") {
385 return Err(EtherscanError::InvalidApiKey);
386 }
387
388 if result.starts_with("Max rate limit reached") {
389 return Err(EtherscanError::RateLimitExceeded);
390 }
391
392 if result.starts_with("Contract source code not verified")
393 || resp.message.starts_with("Contract source code not verified")
394 {
395 if let Some(ref cache) = self.cache {
396 cache.set_abi(address, None);
397 }
398 return Err(EtherscanError::ContractCodeNotVerified(address));
399 }
400 let abi = serde_json::from_str(&result)
401 .map_err(|error| EtherscanError::Serde { error, content: result })?;
402
403 if let Some(ref cache) = self.cache {
404 cache.set_abi(address, Some(&abi));
405 }
406
407 Ok(abi)
408 }
409
410 pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
422 if let Some(ref cache) = self.cache {
424 if let Some(src) = cache.get_source(address) {
426 return match src {
428 Some(src) => Ok(src),
429 None => Err(EtherscanError::ContractCodeNotVerified(address)),
430 };
431 }
432 }
433
434 let query =
435 self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
436 let response = self.get(&query).await?;
437
438 if response.contains("Contract source code not verified") {
440 if let Some(ref cache) = self.cache {
441 cache.set_source(address, None);
442 }
443 return Err(EtherscanError::ContractCodeNotVerified(address));
444 }
445
446 let response: Response<ContractMetadata> = self.sanitize_response(response)?;
447 let result = response.result;
448
449 if let Some(ref cache) = self.cache {
450 cache.set_source(address, Some(&result));
451 }
452
453 Ok(result)
454 }
455
456 pub async fn contract_creation_data(&self, address: Address) -> Result<ContractCreationData> {
469 let query = self.create_query(
470 "contract",
471 "getcontractcreation",
472 HashMap::from([("contractaddresses", address)]),
473 );
474
475 let response = self.get(&query).await?;
476
477 if response.contains("No data found") {
479 return Err(EtherscanError::ContractNotFound(address));
480 }
481
482 let response: Response<Vec<ContractCreationData>> = self.sanitize_response(response)?;
483
484 let data = response.result.first().ok_or(EtherscanError::EmptyResult {
486 message: response.message,
487 status: response.status,
488 })?;
489
490 Ok(*data)
491 }
492}