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));