1use crate::{
2 Client, EtherscanError, Response, Result,
3 serde_helpers::{deserialize_stringified_bool_or_u64, deserialize_stringified_u64},
4 source_tree::{SourceTree, SourceTreeEntry},
5 utils::{deserialize_address_opt, deserialize_source_code},
6};
7use alloy_json_abi::JsonAbi;
8use alloy_primitives::{Address, B256, Bytes};
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use std::{collections::HashMap, path::Path};
12
13#[cfg(feature = "foundry-compilers")]
14use foundry_compilers::{
15 ProjectBuilder, SolcConfig,
16 artifacts::{EvmVersion, Settings},
17 compilers::solc::SolcCompiler,
18 solc::SolcSettings,
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() { SourceCodeLanguage::Vyper } else { SourceCodeLanguage::Solidity }
191 })
192 }
193
194 pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
196 self.source_code.sources()
197 }
198
199 pub fn abi(&self) -> Result<JsonAbi> {
201 serde_json::from_str(&self.abi)
202 .map_err(|error| EtherscanError::Serde { error, content: self.abi.clone() })
203 }
204
205 pub fn compiler_version(&self) -> Result<Version> {
207 let v = &self.compiler_version;
208 let v = v.strip_prefix("vyper:").unwrap_or(v);
209 let v = v.strip_prefix('v').unwrap_or(v);
210 match v.parse() {
211 Err(e) => {
212 let v = v.replace('a', "-alpha.");
213 let v = v.replace('b', "-beta.");
214 v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}")))
215 }
216 Ok(v) => Ok(v),
217 }
218 }
219
220 pub fn is_vyper(&self) -> bool {
222 self.compiler_version.starts_with("vyper:")
223 }
224
225 pub fn source_entries(&self) -> Vec<SourceTreeEntry> {
227 let root = Path::new(&self.contract_name);
228 self.sources()
229 .into_iter()
230 .map(|(path, entry)| {
231 let sanitized_path = crate::source_tree::sanitize_path(path);
236 let path = root.join(sanitized_path);
237 SourceTreeEntry { path, contents: entry.content }
238 })
239 .collect()
240 }
241
242 pub fn source_tree(&self) -> SourceTree {
244 SourceTree { entries: self.source_entries() }
245 }
246
247 #[cfg(feature = "foundry-compilers")]
249 pub fn settings(&self) -> Result<Settings> {
250 let mut settings = self.source_code.settings()?.unwrap_or_default();
251
252 if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() {
253 settings.optimizer.enable();
254 settings.optimizer.runs(self.runs as usize);
255 }
256
257 settings.evm_version = self.evm_version()?;
258
259 Ok(settings)
260 }
261
262 #[cfg(feature = "foundry-compilers")]
264 pub fn project_builder(&self) -> Result<ProjectBuilder<SolcCompiler>> {
265 let solc_config = SolcConfig::builder().settings(self.settings()?).build();
266
267 Ok(ProjectBuilder::new(Default::default())
268 .settings(SolcSettings { settings: solc_config, ..Default::default() }))
269 }
270
271 #[cfg(feature = "foundry-compilers")]
273 pub fn evm_version(&self) -> Result<Option<EvmVersion>> {
274 match self.evm_version.to_lowercase().as_str() {
275 "" | "default" => Ok(EvmVersion::default_version_solc(&self.compiler_version()?)),
276 _ => {
277 let evm_version = self
278 .evm_version
279 .parse()
280 .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?;
281 Ok(Some(evm_version))
282 }
283 }
284 }
285}
286
287#[derive(Clone, Debug, Serialize, Deserialize)]
288#[serde(transparent)]
289pub struct ContractMetadata {
290 pub items: Vec<Metadata>,
291}
292
293impl IntoIterator for ContractMetadata {
294 type Item = Metadata;
295 type IntoIter = std::vec::IntoIter<Metadata>;
296
297 fn into_iter(self) -> Self::IntoIter {
298 self.items.into_iter()
299 }
300}
301
302impl ContractMetadata {
303 pub fn abis(&self) -> Result<Vec<JsonAbi>> {
305 self.items.iter().map(|c| c.abi()).collect()
306 }
307
308 pub fn source_code(&self) -> String {
310 self.items.iter().map(|c| c.source_code()).collect::<Vec<_>>().join("\n")
311 }
312
313 pub fn source_tree(&self) -> SourceTree {
315 SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() }
316 }
317}
318
319#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct ContractCreationData {
323 pub contract_address: Address,
325
326 pub contract_creator: Address,
330
331 #[serde(rename = "txHash")]
333 pub transaction_hash: B256,
334}
335
336impl Client {
337 pub async fn contract_abi(&self, address: Address) -> Result<JsonAbi> {
348 if let Some(ref cache) = self.cache {
350 if let Some(src) = cache.get_abi(address) {
352 return match src {
354 Some(src) => Ok(src),
355 None => Err(EtherscanError::ContractCodeNotVerified(address)),
356 };
357 }
358 }
359 trace!(target: "etherscan", ?address, "GET contract abi");
360
361 let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
362
363 let resp: Response<Option<String>> = self.get_json(&query).await.inspect_err(|err| {
364 error!(target: "etherscan", ?err, "Failed to deserialize ABI response");
365 })?;
366
367 let result = match resp.result {
368 Some(result) => result,
369 None => {
370 if resp.message.contains("Contract source code not verified") {
371 return Err(EtherscanError::ContractCodeNotVerified(address));
372 }
373 return Err(EtherscanError::EmptyResult {
374 message: resp.message,
375 status: resp.status,
376 });
377 }
378 };
379
380 if resp.status == "0" && result.to_lowercase().contains("invalid api key") {
381 return Err(EtherscanError::InvalidApiKey);
382 }
383
384 if result.starts_with("Max rate limit reached") {
385 return Err(EtherscanError::RateLimitExceeded);
386 }
387
388 if result.starts_with("Contract source code not verified")
389 || resp.message.starts_with("Contract source code not verified")
390 {
391 if let Some(ref cache) = self.cache {
392 cache.set_abi(address, None);
393 }
394 return Err(EtherscanError::ContractCodeNotVerified(address));
395 }
396 let abi = serde_json::from_str(&result)
397 .map_err(|error| EtherscanError::Serde { error, content: result })?;
398
399 if let Some(ref cache) = self.cache {
400 cache.set_abi(address, Some(&abi));
401 }
402
403 Ok(abi)
404 }
405
406 pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
418 if let Some(ref cache) = self.cache {
420 if let Some(src) = cache.get_source(address) {
422 return match src {
424 Some(src) => Ok(src),
425 None => Err(EtherscanError::ContractCodeNotVerified(address)),
426 };
427 }
428 }
429
430 let query =
431 self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
432 let response = self.get(&query).await?;
433
434 if response.contains("Contract source code not verified") {
436 if let Some(ref cache) = self.cache {
437 cache.set_source(address, None);
438 }
439 return Err(EtherscanError::ContractCodeNotVerified(address));
440 }
441 if is_address_only_source_code_response(&response) {
445 if let Some(ref cache) = self.cache {
446 cache.set_source(address, None);
447 }
448 return Err(EtherscanError::ContractCodeNotVerified(address));
449 }
450
451 let response: Response<ContractMetadata> = self.sanitize_response(response)?;
452 let result = response.result;
453
454 if let Some(ref cache) = self.cache {
455 cache.set_source(address, Some(&result));
456 }
457
458 Ok(result)
459 }
460
461 pub async fn contract_creation_data(&self, address: Address) -> Result<ContractCreationData> {
474 let query = self.create_query(
475 "contract",
476 "getcontractcreation",
477 HashMap::from([("contractaddresses", address)]),
478 );
479
480 let response = self.get(&query).await?;
481
482 if response.contains("No data found") {
484 return Err(EtherscanError::ContractNotFound(address));
485 }
486
487 let response: Response<Vec<ContractCreationData>> = self.sanitize_response(response)?;
488
489 let data = response.result.first().ok_or(EtherscanError::EmptyResult {
491 message: response.message,
492 status: response.status,
493 })?;
494
495 Ok(*data)
496 }
497}
498
499fn is_address_only_source_code_response(response: &str) -> bool {
500 let Ok(value) = serde_json::from_str::<serde_json::Value>(response) else {
501 return false;
502 };
503 let Some(result) = value.get("result").and_then(|v| v.as_array()) else {
504 return false;
505 };
506 !result.is_empty()
507 && result.iter().all(|entry| {
508 let Some(obj) = entry.as_object() else {
509 return false;
510 };
511 obj.len() == 1 && obj.contains_key("Address")
512 })
513}
514
515#[cfg(test)]
516mod tests {
517 use super::is_address_only_source_code_response;
518
519 #[test]
520 fn detects_address_only_result_shape() {
521 let response = r#"{"message":"OK","result":[{"Address":"0x0000000000000000000000000000000000000000"}],"status":"1"}"#;
522 assert!(is_address_only_source_code_response(response));
523 }
524
525 #[test]
526 fn ignores_valid_contract_metadata_shape() {
527 let response = r#"{"message":"OK","result":[{"ABI":"[]","ContractName":"C","CompilerVersion":"v0.8.26+commit.8a97fa7a","OptimizationUsed":"0","OptimizationRuns":"0","SourceCode":"","EVMVersion":"Default","IsProxy":"0","ConstructorArguments":""}],"status":"1"}"#;
528 assert!(!is_address_only_source_code_response(response));
529 }
530}