fix(etherscan): support stringified numbers in response (#1524)
* fix(etherscan): support stringified numbers in response * feat: add helper function * improve string parsing * chore(clippy): make clippy happy
This commit is contained in:
parent
2a14e5510d
commit
81a2a5ed68
|
@ -1,9 +1,11 @@
|
|||
//! Some convenient serde helpers
|
||||
|
||||
use crate::types::{BlockNumber, U256};
|
||||
use ethabi::ethereum_types::FromDecStrErr;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::convert::TryFrom;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Helper type to parse both `u64` and `U256`
|
||||
#[derive(Copy, Clone, Deserialize)]
|
||||
|
@ -22,7 +24,21 @@ impl From<Numeric> for U256 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Helper type to parse both `u64` and `U256`
|
||||
impl FromStr for Numeric {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(val) = s.parse::<u128>() {
|
||||
Ok(Numeric::U256(val.into()))
|
||||
} else if s.starts_with("0x") {
|
||||
U256::from_str(s).map(Numeric::U256).map_err(|err| err.to_string())
|
||||
} else {
|
||||
U256::from_dec_str(s).map(Numeric::U256).map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type to parse numeric strings, `u64` and `U256`
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum StringifiedNumeric {
|
||||
|
@ -32,7 +48,7 @@ pub enum StringifiedNumeric {
|
|||
}
|
||||
|
||||
impl TryFrom<StringifiedNumeric> for U256 {
|
||||
type Error = FromDecStrErr;
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
|
@ -41,14 +57,71 @@ impl TryFrom<StringifiedNumeric> for U256 {
|
|||
StringifiedNumeric::String(s) => {
|
||||
if let Ok(val) = s.parse::<u128>() {
|
||||
Ok(val.into())
|
||||
} else if s.starts_with("0x") {
|
||||
U256::from_str(&s).map_err(|err| err.to_string())
|
||||
} else {
|
||||
U256::from_dec_str(&s)
|
||||
U256::from_dec_str(&s).map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports parsing numbers as strings
|
||||
///
|
||||
/// See <https://github.com/gakonst/ethers-rs/issues/1507>
|
||||
pub fn deserialize_stringified_numeric<'de, D>(deserializer: D) -> Result<U256, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let num = StringifiedNumeric::deserialize(deserializer)?;
|
||||
num.try_into().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Supports parsing numbers as strings
|
||||
///
|
||||
/// See <https://github.com/gakonst/ethers-rs/issues/1507>
|
||||
pub fn deserialize_stringified_numeric_opt<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<U256>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
if let Some(num) = Option::<StringifiedNumeric>::deserialize(deserializer)? {
|
||||
num.try_into().map(Some).map_err(serde::de::Error::custom)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports parsing u64
|
||||
///
|
||||
/// See <https://github.com/gakonst/ethers-rs/issues/1507>
|
||||
pub fn deserialize_stringified_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let num = StringifiedNumeric::deserialize(deserializer)?;
|
||||
let num: U256 = num.try_into().map_err(serde::de::Error::custom)?;
|
||||
num.try_into().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Supports parsing u64
|
||||
///
|
||||
/// See <https://github.com/gakonst/ethers-rs/issues/1507>
|
||||
pub fn deserialize_stringified_u64_opt<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
if let Some(num) = Option::<StringifiedNumeric>::deserialize(deserializer)? {
|
||||
let num: U256 = num.try_into().map_err(serde::de::Error::custom)?;
|
||||
let num: u64 = num.try_into().map_err(serde::de::Error::custom)?;
|
||||
Ok(Some(num))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type to deserialize sequence of numbers
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
|
@ -94,6 +167,42 @@ where
|
|||
Ok(num)
|
||||
}
|
||||
|
||||
/// Helper type to parse numeric strings, `u64` and `U256`
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum StringifiedBlockNumber {
|
||||
Numeric(StringifiedNumeric),
|
||||
BlockNumber(BlockNumber),
|
||||
}
|
||||
|
||||
impl TryFrom<StringifiedBlockNumber> for BlockNumber {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: StringifiedBlockNumber) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
StringifiedBlockNumber::Numeric(num) => {
|
||||
let num = U256::try_from(num)
|
||||
.and_then(|num| u64::try_from(num).map_err(str::to_string))?;
|
||||
Ok(BlockNumber::Number(num.into()))
|
||||
}
|
||||
StringifiedBlockNumber::BlockNumber(b) => Ok(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports parsing block number as strings
|
||||
///
|
||||
/// See <https://github.com/gakonst/ethers-rs/issues/1507>
|
||||
pub fn deserialize_stringified_block_number<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<BlockNumber, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let num = StringifiedBlockNumber::deserialize(deserializer)?;
|
||||
num.try_into().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Various block number representations, See [`lenient_block_number()`]
|
||||
#[derive(Clone, Copy, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
use crate::{Client, EtherscanError, Query, Response, Result};
|
||||
use ethers_core::{
|
||||
abi::Address,
|
||||
types::{serde_helpers::*, BlockNumber, Bytes, H256, U256},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
fmt::{Display, Error, Formatter},
|
||||
};
|
||||
|
||||
use ethers_core::{
|
||||
abi::Address,
|
||||
types::{BlockNumber, Bytes, H256, U256, U64},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Client, EtherscanError, Query, Response, Result};
|
||||
|
||||
/// The raw response from the balance-related API endpoints
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AccountBalance {
|
||||
|
@ -100,6 +98,7 @@ impl<T> GenesisOption<T> {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NormalTransaction {
|
||||
pub is_error: String,
|
||||
#[serde(deserialize_with = "deserialize_stringified_block_number")]
|
||||
pub block_number: BlockNumber,
|
||||
pub time_stamp: String,
|
||||
#[serde(with = "jsonstring")]
|
||||
|
@ -108,12 +107,16 @@ pub struct NormalTransaction {
|
|||
pub nonce: GenesisOption<U256>,
|
||||
#[serde(with = "jsonstring")]
|
||||
pub block_hash: GenesisOption<U256>,
|
||||
pub transaction_index: Option<U64>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64_opt")]
|
||||
pub transaction_index: Option<u64>,
|
||||
#[serde(with = "jsonstring")]
|
||||
pub from: GenesisOption<Address>,
|
||||
pub to: Option<Address>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub value: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
|
||||
pub gas_price: Option<U256>,
|
||||
#[serde(rename = "txreceipt_status")]
|
||||
pub tx_receipt_status: String,
|
||||
|
@ -121,21 +124,26 @@ pub struct NormalTransaction {
|
|||
pub input: GenesisOption<Bytes>,
|
||||
#[serde(with = "jsonstring")]
|
||||
pub contract_address: GenesisOption<Address>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas_used: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub cumulative_gas_used: U256,
|
||||
pub confirmations: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub confirmations: u64,
|
||||
}
|
||||
|
||||
/// The raw response from the internal transaction list API endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InternalTransaction {
|
||||
#[serde(deserialize_with = "deserialize_stringified_block_number")]
|
||||
pub block_number: BlockNumber,
|
||||
pub time_stamp: String,
|
||||
pub hash: H256,
|
||||
pub from: Address,
|
||||
#[serde(with = "jsonstring")]
|
||||
pub to: GenesisOption<Address>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub value: U256,
|
||||
#[serde(with = "jsonstring")]
|
||||
pub contract_address: GenesisOption<Address>,
|
||||
|
@ -143,7 +151,9 @@ pub struct InternalTransaction {
|
|||
pub input: GenesisOption<Bytes>,
|
||||
#[serde(rename = "type")]
|
||||
pub result_type: String,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas_used: U256,
|
||||
pub trace_id: String,
|
||||
pub is_error: String,
|
||||
|
@ -154,35 +164,46 @@ pub struct InternalTransaction {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ERC20TokenTransferEvent {
|
||||
#[serde(deserialize_with = "deserialize_stringified_block_number")]
|
||||
pub block_number: BlockNumber,
|
||||
pub time_stamp: String,
|
||||
pub hash: H256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub nonce: U256,
|
||||
pub block_hash: H256,
|
||||
pub from: Address,
|
||||
pub contract_address: Address,
|
||||
pub to: Option<Address>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub value: U256,
|
||||
pub token_name: String,
|
||||
pub token_symbol: String,
|
||||
pub token_decimal: String,
|
||||
pub transaction_index: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub transaction_index: u64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
|
||||
pub gas_price: Option<U256>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas_used: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub cumulative_gas_used: U256,
|
||||
/// deprecated
|
||||
pub input: String,
|
||||
pub confirmations: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub confirmations: u64,
|
||||
}
|
||||
|
||||
/// The raw response from the ERC721 transfer list API endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ERC721TokenTransferEvent {
|
||||
#[serde(deserialize_with = "deserialize_stringified_block_number")]
|
||||
pub block_number: BlockNumber,
|
||||
pub time_stamp: String,
|
||||
pub hash: H256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub nonce: U256,
|
||||
pub block_hash: H256,
|
||||
pub from: Address,
|
||||
|
@ -193,23 +214,31 @@ pub struct ERC721TokenTransferEvent {
|
|||
pub token_name: String,
|
||||
pub token_symbol: String,
|
||||
pub token_decimal: String,
|
||||
pub transaction_index: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub transaction_index: u64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
|
||||
pub gas_price: Option<U256>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas_used: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub cumulative_gas_used: U256,
|
||||
/// deprecated
|
||||
pub input: String,
|
||||
pub confirmations: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub confirmations: u64,
|
||||
}
|
||||
|
||||
/// The raw response from the ERC1155 transfer list API endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ERC1155TokenTransferEvent {
|
||||
#[serde(deserialize_with = "deserialize_stringified_block_number")]
|
||||
pub block_number: BlockNumber,
|
||||
pub time_stamp: String,
|
||||
pub hash: H256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub nonce: U256,
|
||||
pub block_hash: H256,
|
||||
pub from: Address,
|
||||
|
@ -220,20 +249,27 @@ pub struct ERC1155TokenTransferEvent {
|
|||
pub token_value: String,
|
||||
pub token_name: String,
|
||||
pub token_symbol: String,
|
||||
pub transaction_index: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub transaction_index: u64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
|
||||
pub gas_price: Option<U256>,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub gas_used: U256,
|
||||
#[serde(deserialize_with = "deserialize_stringified_numeric")]
|
||||
pub cumulative_gas_used: U256,
|
||||
/// deprecated
|
||||
pub input: String,
|
||||
pub confirmations: U64,
|
||||
#[serde(deserialize_with = "deserialize_stringified_u64")]
|
||||
pub confirmations: u64,
|
||||
}
|
||||
|
||||
/// The raw response from the mined blocks API endpoint
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MinedBlock {
|
||||
#[serde(deserialize_with = "deserialize_stringified_block_number")]
|
||||
pub block_number: BlockNumber,
|
||||
pub time_stamp: String,
|
||||
pub block_reward: String,
|
||||
|
@ -739,8 +775,12 @@ mod tests {
|
|||
),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(txs.is_ok());
|
||||
.await
|
||||
.unwrap();
|
||||
let tx = txs.get(0).unwrap();
|
||||
assert_eq!(tx.gas_used, 93657u64.into());
|
||||
assert_eq!(tx.nonce, 10u64.into());
|
||||
assert_eq!(tx.block_number, 2228258u64.into());
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -70,15 +70,11 @@ impl Client {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use serial_test::serial;
|
||||
|
||||
use ethers_core::types::Chain;
|
||||
|
||||
use crate::tests::run_at_least_duration;
|
||||
|
||||
use super::*;
|
||||
use crate::tests::run_at_least_duration;
|
||||
use ethers_core::types::Chain;
|
||||
use serial_test::serial;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
|
|
|
@ -52,13 +52,10 @@ impl Client {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use serial_test::serial;
|
||||
|
||||
use crate::{tests::run_at_least_duration, Chain};
|
||||
|
||||
use super::*;
|
||||
use crate::{tests::run_at_least_duration, Chain};
|
||||
use serial_test::serial;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
|
|
Loading…
Reference in New Issue