diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs index ae19abf6..2b5adb23 100644 --- a/ethers-etherscan/src/contract.rs +++ b/ethers-etherscan/src/contract.rs @@ -1,6 +1,6 @@ use crate::{ source_tree::{SourceTree, SourceTreeEntry}, - utils::{deserialize_address_opt, deserialize_stringified_source_code}, + utils::{deserialize_address_opt, deserialize_source_code}, Client, EtherscanError, Response, Result, }; use ethers_core::{ @@ -107,7 +107,7 @@ impl SourceCodeMetadata { #[serde(rename_all = "PascalCase")] pub struct Metadata { /// Includes metadata for compiler settings and language. - #[serde(deserialize_with = "deserialize_stringified_source_code")] + #[serde(deserialize_with = "deserialize_source_code")] pub source_code: SourceCodeMetadata, /// The ABI of the contract. @@ -148,7 +148,11 @@ pub struct Metadata { pub proxy: u64, /// If this contract is a proxy, the address of its implementation. - #[serde(deserialize_with = "deserialize_address_opt")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_address_opt" + )] pub implementation: Option
, /// The swarm source of the contract. diff --git a/ethers-etherscan/src/utils.rs b/ethers-etherscan/src/utils.rs index b76745eb..3107492a 100644 --- a/ethers-etherscan/src/utils.rs +++ b/ethers-etherscan/src/utils.rs @@ -36,20 +36,35 @@ pub fn deserialize_address_opt<'de, D: Deserializer<'de>>( /// Deserializes as JSON: /// -/// `{ "SourceCode": "{{ .. }}", ..}` +/// Object: `{ "SourceCode": { language: "Solidity", .. }, ..}` /// /// or /// -/// `{ "SourceCode": "..", .. }` -pub fn deserialize_stringified_source_code<'de, D: Deserializer<'de>>( +/// Stringified JSON: `{ "SourceCode": "{{\r\n \"language\": \"Solidity\", ..}}", ..}` +/// +/// or +/// +/// Normal source code: `{ "SourceCode": "// SPDX-License-Identifier: ...", .. }` +pub fn deserialize_source_code<'de, D: Deserializer<'de>>( deserializer: D, ) -> std::result::Result { - let s = String::deserialize(deserializer)?; - if s.starts_with("{{") && s.ends_with("}}") { - let s = &s[1..s.len() - 1]; - serde_json::from_str(s).map_err(serde::de::Error::custom) - } else { - Ok(SourceCodeMetadata::SourceCode(s)) + #[derive(Deserialize)] + #[serde(untagged)] + enum SourceCode { + String(String), // this must come first + Obj(SourceCodeMetadata), + } + let s = SourceCode::deserialize(deserializer)?; + match s { + SourceCode::String(s) => { + if s.starts_with("{{") && s.ends_with("}}") { + let s = &s[1..s.len() - 1]; + serde_json::from_str(s).map_err(serde::de::Error::custom) + } else { + Ok(SourceCodeMetadata::SourceCode(s)) + } + } + SourceCode::Obj(obj) => Ok(obj), } } @@ -108,17 +123,29 @@ mod tests { } #[test] - fn can_deserialize_stringified_source_code() { + fn can_deserialize_source_code() { #[derive(Deserialize)] struct Test { - #[serde(deserialize_with = "deserialize_stringified_source_code")] + #[serde(deserialize_with = "deserialize_source_code")] source_code: SourceCodeMetadata, } let src = "source code text"; + // Normal JSON let json = r#"{ - "source_code": "{{ \"language\": \"Solidity\", \"sources\": {\"Contract\": { \"content\": \"source code text\" } } }}" + "source_code": { "language": "Solidity", "sources": { "Contract": { "content": "source code text" } } } + }"#; + let de: Test = serde_json::from_str(json).unwrap(); + assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity)); + assert_eq!(de.source_code.sources().len(), 1); + assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src); + #[cfg(feature = "ethers-solc")] + assert!(matches!(de.source_code.settings().unwrap(), None)); + + // Stringified JSON + let json = r#"{ + "source_code": "{{ \"language\": \"Solidity\", \"sources\": { \"Contract\": { \"content\": \"source code text\" } } }}" }"#; let de: Test = serde_json::from_str(json).unwrap(); assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));