use crate::{contract::SourceCodeMetadata, units::Units, EtherscanError, Result};
use alloy_primitives::{Address, ParseSignedError, I256, U256};
use semver::Version;
use serde::{Deserialize, Deserializer};
use std::{fmt, str::FromStr};
use thiserror::Error;
static SOLC_BIN_LIST_URL: &str = "https://binaries.soliditylang.org/bin/list.txt";
pub async fn lookup_compiler_version(version: &Version) -> Result<Version> {
let response = reqwest::get(SOLC_BIN_LIST_URL).await?.text().await?;
let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
let v = response
.lines()
.find(|l| !l.contains("nightly") && l.contains(&version))
.map(|l| l.trim_start_matches("soljson-v").trim_end_matches(".js"))
.ok_or_else(|| EtherscanError::MissingSolcVersion(version))?;
Ok(v.parse().expect("failed to parse semver"))
}
pub fn deserialize_address_opt<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<Address>, D::Error> {
match Option::<String>::deserialize(deserializer)? {
None => Ok(None),
Some(s) => match s.is_empty() {
true => Ok(None),
_ => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)),
},
}
}
pub fn deserialize_source_code<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<SourceCodeMetadata, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum SourceCode {
String(String), Obj(SourceCodeMetadata),
}
let s = SourceCode::deserialize(deserializer)?;
match s {
SourceCode::String(s) => {
if s.starts_with('{') && s.ends_with('}') {
let mut s = s.as_str();
if s.starts_with("{{") && s.ends_with("}}") {
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),
}
}
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub enum ParseUnits {
U256(U256),
I256(I256),
}
impl From<ParseUnits> for U256 {
fn from(n: ParseUnits) -> Self {
match n {
ParseUnits::U256(n) => n,
ParseUnits::I256(n) => n.into_raw(),
}
}
}
impl From<ParseUnits> for I256 {
fn from(n: ParseUnits) -> Self {
match n {
ParseUnits::I256(n) => n,
ParseUnits::U256(n) => I256::from_raw(n),
}
}
}
impl From<alloy_primitives::Signed<256, 4>> for ParseUnits {
fn from(n: alloy_primitives::Signed<256, 4>) -> Self {
Self::I256(n)
}
}
impl fmt::Display for ParseUnits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseUnits::U256(val) => val.fmt(f),
ParseUnits::I256(val) => val.fmt(f),
}
}
}
macro_rules! construct_format_units_from {
($( $t:ty[$convert:ident] ),*) => {
$(
impl From<$t> for ParseUnits {
fn from(num: $t) -> Self {
Self::$convert(U256::from(num))
}
}
)*
}
}
macro_rules! construct_signed_format_units_from {
($( $t:ty[$convert:ident] ),*) => {
$(
impl From<$t> for ParseUnits {
fn from(num: $t) -> Self {
Self::$convert(I256::from_raw(U256::from(num)))
}
}
)*
}
}
construct_format_units_from! {
u8[U256], u16[U256], u32[U256], u64[U256], u128[U256], U256[U256], usize[U256]
}
construct_signed_format_units_from! {
i8[I256], i16[I256], i32[I256], i64[I256], i128[I256], isize[I256]
}
#[derive(Error, Debug)]
pub enum ConversionError {
#[error("Unknown units: {0}")]
UnrecognizedUnits(String),
#[error("bytes32 strings must not exceed 32 bytes in length")]
TextTooLong,
#[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
#[error(transparent)]
InvalidFloat(#[from] std::num::ParseFloatError),
#[error("Invalid decimal string: {0}")]
FromDecStrError(String),
#[error("Overflow parsing string")]
ParseOverflow,
#[error("Parse Signed Error")]
ParseI256Error(#[from] ParseSignedError),
#[error("Invalid address checksum")]
InvalidAddressChecksum,
#[error(transparent)]
FromHexError(<Address as std::str::FromStr>::Err),
}
pub fn parse_units<K, S>(amount: S, units: K) -> Result<ParseUnits, ConversionError>
where
S: ToString,
K: TryInto<Units, Error = ConversionError> + Copy,
{
let exponent: u32 = units.try_into()?.as_num();
let mut amount_str = amount.to_string().replace('_', "");
let negative = amount_str.chars().next().unwrap_or_default() == '-';
let dec_len = if let Some(di) = amount_str.find('.') {
amount_str.remove(di);
amount_str[di..].len() as u32
} else {
0
};
if dec_len > exponent {
let amount_str = &amount_str[..(amount_str.len() - (dec_len - exponent) as usize)];
if negative {
if amount_str == "-" {
Ok(ParseUnits::I256(I256::ZERO))
} else {
Ok(ParseUnits::I256(
I256::from_dec_str(amount_str)
.map_err(|e| ConversionError::FromDecStrError(e.to_string()))?,
))
}
} else {
Ok(ParseUnits::U256(
U256::from_str(amount_str)
.map_err(|e| ConversionError::FromDecStrError(e.to_string()))?,
))
}
} else if negative {
if amount_str == "-" {
Ok(ParseUnits::I256(I256::ZERO))
} else {
let _fi = U256::from(10_i64);
let mut n = I256::from_str(&amount_str)?;
n *= I256::from_raw(U256::from(10))
.checked_pow(U256::from(exponent) - U256::from(dec_len))
.ok_or(ConversionError::ParseOverflow)?;
Ok(ParseUnits::I256(n))
}
} else {
let mut a_uint = U256::from_str(&amount_str)
.map_err(|e| ConversionError::FromDecStrError(e.to_string()))?;
a_uint *= U256::from(10)
.checked_pow(U256::from(exponent - dec_len))
.ok_or(ConversionError::ParseOverflow)?;
Ok(ParseUnits::U256(a_uint))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contract::SourceCodeLanguage;
#[test]
fn can_deserialize_address_opt() {
#[derive(serde::Serialize, Deserialize)]
struct Test {
#[serde(deserialize_with = "deserialize_address_opt")]
address: Option<Address>,
}
let json = r#"{"address":""}"#;
let de: Test = serde_json::from_str(json).unwrap();
assert_eq!(de.address, None);
let json = serde_json::to_string(&de).unwrap();
let de: Test = serde_json::from_str(&json).unwrap();
assert_eq!(de.address, None);
let json = r#"{"address":"0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01"}"#;
let de: Test = serde_json::from_str(json).unwrap();
let expected = "0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01".parse().unwrap();
assert_eq!(de.address, Some(expected));
}
#[test]
fn can_deserialize_source_code() {
#[derive(Deserialize)]
struct Test {
#[serde(deserialize_with = "deserialize_source_code")]
source_code: SourceCodeMetadata,
}
let src = "source code text";
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));
assert_eq!(de.source_code.sources().len(), 1);
assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
#[cfg(feature = "foundry-compilers")]
assert!(de.source_code.settings().unwrap().is_none());
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));
assert_eq!(de.source_code.sources().len(), 1);
assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
#[cfg(feature = "foundry-compilers")]
assert!(de.source_code.settings().unwrap().is_none());
let json = r#"{"source_code": "source code text"}"#;
let de: Test = serde_json::from_str(json).unwrap();
assert_eq!(de.source_code.source_code(), src);
}
}