1use crate::{
2 source_tree::{SourceTree, SourceTreeEntry},
3 utils::{deserialize_address_opt, deserialize_source_code},
4 Client, EtherscanError, Response, Result,
5};
6use ethers_core::{
7 abi::{Abi, Address, RawAbi},
8 types::{serde_helpers::deserialize_stringified_u64, Bytes},
9};
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use std::{collections::HashMap, path::Path};
13
14#[cfg(feature = "ethers-solc")]
15use ethers_solc::{artifacts::Settings, EvmVersion, Project, ProjectBuilder, SolcConfig};
16
17#[derive(Clone, Debug, Default, Serialize, Deserialize)]
18pub enum SourceCodeLanguage {
19 #[default]
20 Solidity,
21 Vyper,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct SourceCodeEntry {
26 pub content: String,
27}
28
29impl<T: Into<String>> From<T> for SourceCodeEntry {
30 fn from(s: T) -> Self {
31 Self { content: s.into() }
32 }
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
37#[serde(untagged)]
38pub enum SourceCodeMetadata {
39 Sources(HashMap<String, SourceCodeEntry>),
42 Metadata {
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 language: Option<SourceCodeLanguage>,
47 #[serde(default)]
49 sources: HashMap<String, SourceCodeEntry>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 settings: Option<serde_json::Value>,
53 },
54 SourceCode(String),
56}
57
58impl SourceCodeMetadata {
59 pub fn source_code(&self) -> String {
60 match self {
61 Self::Metadata { sources, .. } => {
62 sources.values().map(|s| s.content.clone()).collect::<Vec<_>>().join("\n")
63 }
64 Self::Sources(sources) => {
65 sources.values().map(|s| s.content.clone()).collect::<Vec<_>>().join("\n")
66 }
67 Self::SourceCode(s) => s.clone(),
68 }
69 }
70
71 pub fn language(&self) -> Option<SourceCodeLanguage> {
72 match self {
73 Self::Metadata { language, .. } => language.clone(),
74 Self::Sources(_) => None,
75 Self::SourceCode(_) => None,
76 }
77 }
78
79 pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
80 match self {
81 Self::Metadata { sources, .. } => sources.clone(),
82 Self::Sources(sources) => sources.clone(),
83 Self::SourceCode(s) => HashMap::from([("Contract".into(), s.into())]),
84 }
85 }
86
87 #[cfg(feature = "ethers-solc")]
88 pub fn settings(&self) -> Result<Option<Settings>> {
89 match self {
90 Self::Metadata { settings, .. } => match settings {
91 Some(value) => {
92 if value.is_null() {
93 Ok(None)
94 } else {
95 Ok(Some(serde_json::from_value(value.to_owned())?))
96 }
97 }
98 None => Ok(None),
99 },
100 Self::Sources(_) => Ok(None),
101 Self::SourceCode(_) => Ok(None),
102 }
103 }
104
105 #[cfg(not(feature = "ethers-solc"))]
106 pub fn settings(&self) -> Option<&serde_json::Value> {
107 match self {
108 Self::Metadata { settings, .. } => settings.as_ref(),
109 Self::Sources(_) => None,
110 Self::SourceCode(_) => None,
111 }
112 }
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize)]
117#[serde(rename_all = "PascalCase")]
118pub struct Metadata {
119 #[serde(deserialize_with = "deserialize_source_code")]
121 pub source_code: SourceCodeMetadata,
122
123 #[serde(rename = "ABI")]
125 pub abi: String,
126
127 pub contract_name: String,
129
130 pub compiler_version: String,
133
134 #[serde(deserialize_with = "deserialize_stringified_u64")]
136 pub optimization_used: u64,
137
138 #[serde(deserialize_with = "deserialize_stringified_u64")]
140 pub runs: u64,
141
142 pub constructor_arguments: Bytes,
144
145 #[serde(rename = "EVMVersion")]
148 pub evm_version: String,
149
150 pub library: String,
152
153 pub license_type: String,
155
156 #[serde(deserialize_with = "deserialize_stringified_u64")]
158 pub proxy: u64,
159
160 #[serde(
162 default,
163 skip_serializing_if = "Option::is_none",
164 deserialize_with = "deserialize_address_opt"
165 )]
166 pub implementation: Option<Address>,
167
168 pub swarm_source: String,
170}
171
172impl Metadata {
173 pub fn source_code(&self) -> String {
175 self.source_code.source_code()
176 }
177
178 pub fn language(&self) -> SourceCodeLanguage {
180 self.source_code.language().unwrap_or_else(|| {
181 if self.is_vyper() {
182 SourceCodeLanguage::Vyper
183 } else {
184 SourceCodeLanguage::Solidity
185 }
186 })
187 }
188
189 pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
191 self.source_code.sources()
192 }
193
194 pub fn raw_abi(&self) -> Result<RawAbi> {
196 Ok(serde_json::from_str(&self.abi)?)
197 }
198
199 pub fn abi(&self) -> Result<Abi> {
201 Ok(serde_json::from_str(&self.abi)?)
202 }
203
204 pub fn compiler_version(&self) -> Result<Version> {
206 let v = &self.compiler_version;
207 let v = v.strip_prefix("vyper:").unwrap_or(v);
208 let v = v.strip_prefix('v').unwrap_or(v);
209 match v.parse() {
210 Err(e) => {
211 let v = v.replace('a', "-alpha.");
212 let v = v.replace('b', "-beta.");
213 v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}")))
214 }
215 Ok(v) => Ok(v),
216 }
217 }
218
219 pub fn is_vyper(&self) -> bool {
221 self.compiler_version.starts_with("vyper:")
222 }
223
224 pub fn source_entries(&self) -> Vec<SourceTreeEntry> {
226 let root = Path::new(&self.contract_name);
227 self.sources()
228 .into_iter()
229 .map(|(path, entry)| {
230 let path = root.join(path);
231 SourceTreeEntry { path, contents: entry.content }
232 })
233 .collect()
234 }
235
236 pub fn source_tree(&self) -> SourceTree {
238 SourceTree { entries: self.source_entries() }
239 }
240
241 #[cfg(feature = "ethers-solc")]
243 pub fn settings(&self) -> Result<Settings> {
244 let mut settings = self.source_code.settings()?.unwrap_or_default();
245
246 if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() {
247 settings.optimizer.enable();
248 settings.optimizer.runs(self.runs as usize);
249 }
250
251 settings.evm_version = self.evm_version()?;
252
253 Ok(settings)
254 }
255
256 #[cfg(feature = "ethers-solc")]
258 pub fn project_builder(&self) -> Result<ProjectBuilder> {
259 let solc_config = SolcConfig::builder().settings(self.settings()?).build();
260
261 Ok(Project::builder().solc_config(solc_config))
262 }
263
264 #[cfg(feature = "ethers-solc")]
266 pub fn evm_version(&self) -> Result<Option<EvmVersion>> {
267 match self.evm_version.as_str() {
268 "" | "Default" => {
269 Ok(EvmVersion::default().normalize_version(&self.compiler_version()?))
270 }
271 _ => {
272 let evm_version = self
273 .evm_version
274 .parse()
275 .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?;
276 Ok(Some(evm_version))
277 }
278 }
279 }
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize)]
283#[serde(transparent)]
284pub struct ContractMetadata {
285 pub items: Vec<Metadata>,
286}
287
288impl IntoIterator for ContractMetadata {
289 type Item = Metadata;
290 type IntoIter = std::vec::IntoIter<Metadata>;
291
292 fn into_iter(self) -> Self::IntoIter {
293 self.items.into_iter()
294 }
295}
296
297impl ContractMetadata {
298 pub fn abis(&self) -> Result<Vec<Abi>> {
300 self.items.iter().map(|c| c.abi()).collect()
301 }
302
303 pub fn raw_abis(&self) -> Result<Vec<RawAbi>> {
305 self.items.iter().map(|c| c.raw_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
319impl Client {
320 pub async fn contract_abi(&self, address: Address) -> Result<Abi> {
331 if let Some(ref cache) = self.cache {
333 if let Some(src) = cache.get_abi(address) {
335 return match src {
337 Some(src) => Ok(src),
338 None => Err(EtherscanError::ContractCodeNotVerified(address)),
339 }
340 }
341 }
342
343 let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
344 let resp: Response<Option<String>> = self.get_json(&query).await?;
345
346 let result = match resp.result {
347 Some(result) => result,
348 None => {
349 if resp.message.contains("Contract source code not verified") {
350 return Err(EtherscanError::ContractCodeNotVerified(address))
351 }
352 return Err(EtherscanError::EmptyResult {
353 message: resp.message,
354 status: resp.status,
355 })
356 }
357 };
358
359 if result.starts_with("Max rate limit reached") {
360 return Err(EtherscanError::RateLimitExceeded)
361 }
362
363 if result.starts_with("Contract source code not verified") ||
364 resp.message.starts_with("Contract source code not verified")
365 {
366 if let Some(ref cache) = self.cache {
367 cache.set_abi(address, None);
368 }
369 return Err(EtherscanError::ContractCodeNotVerified(address))
370 }
371 let abi = serde_json::from_str(&result)?;
372
373 if let Some(ref cache) = self.cache {
374 cache.set_abi(address, Some(&abi));
375 }
376
377 Ok(abi)
378 }
379
380 pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
392 if let Some(ref cache) = self.cache {
394 if let Some(src) = cache.get_source(address) {
396 return match src {
398 Some(src) => Ok(src),
399 None => Err(EtherscanError::ContractCodeNotVerified(address)),
400 }
401 }
402 }
403
404 let query =
405 self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
406 let response = self.get(&query).await?;
407
408 if response.contains("Contract source code not verified") {
410 if let Some(ref cache) = self.cache {
411 cache.set_source(address, None);
412 }
413 return Err(EtherscanError::ContractCodeNotVerified(address))
414 }
415
416 let response: Response<ContractMetadata> = self.sanitize_response(response)?;
417 let result = response.result;
418
419 if let Some(ref cache) = self.cache {
420 cache.set_source(address, Some(&result));
421 }
422
423 Ok(result)
424 }
425}