use serde::de::{self, Deserializer, Visitor};
use serde::{Deserialize, Serialize};
fn string_or_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrF64Visitor;
impl<'de> Visitor<'de> for StringOrF64Visitor {
type Value = Option<f64>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a float, integer, or string containing a number")
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
Ok(Some(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(Some(v as f64))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
Ok(Some(v as f64))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
if v.is_empty() {
return Ok(None);
}
v.parse().map(Some).map_err(de::Error::custom)
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(StringOrF64Visitor)
}
fn int_or_nested_total<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
D: Deserializer<'de>,
{
struct IntOrNestedVisitor;
impl<'de> Visitor<'de> for IntOrNestedVisitor {
type Value = Option<i64>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an integer, a string containing an integer, or a map with a 'total' field")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(Some(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
Ok(Some(v as i64))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse().map(Some).map_err(de::Error::custom)
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut total: Option<i64> = None;
while let Some(key) = map.next_key::<String>()? {
if key == "total" {
total = Some(map.next_value::<i64>()?);
} else {
let _: serde_json::Value = map.next_value()?;
}
}
Ok(total)
}
}
deserializer.deserialize_any(IntOrNestedVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Nft {
pub token_address: Option<String>,
pub token_id: Option<String>,
pub owner_of: Option<String>,
pub token_hash: Option<String>,
pub block_number_minted: Option<String>,
pub block_number: Option<String>,
pub amount: Option<String>,
pub contract_type: Option<String>,
pub name: Option<String>,
pub symbol: Option<String>,
pub token_uri: Option<String>,
pub metadata: Option<String>,
pub last_token_uri_sync: Option<String>,
pub last_metadata_sync: Option<String>,
pub minter_address: Option<String>,
pub possible_spam: Option<bool>,
pub verified_collection: Option<bool>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price_usd: Option<f64>,
pub floor_price_currency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftCollection {
pub token_address: Option<String>,
pub contract_type: Option<String>,
pub name: Option<String>,
pub symbol: Option<String>,
pub possible_spam: Option<bool>,
pub verified_collection: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftTransfer {
pub transaction_hash: Option<String>,
pub token_address: Option<String>,
pub token_id: Option<String>,
pub from_address: Option<String>,
pub to_address: Option<String>,
pub value: Option<String>,
pub amount: Option<String>,
pub contract_type: Option<String>,
pub block_number: Option<String>,
pub block_timestamp: Option<String>,
pub block_hash: Option<String>,
pub log_index: Option<i32>,
pub operator: Option<String>,
pub possible_spam: Option<bool>,
pub verified_collection: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftOwner {
pub token_address: Option<String>,
pub token_id: Option<String>,
pub owner_of: Option<String>,
pub amount: Option<String>,
pub token_hash: Option<String>,
pub block_number: Option<String>,
pub block_number_minted: Option<String>,
pub contract_type: Option<String>,
pub token_uri: Option<String>,
pub metadata: Option<String>,
pub name: Option<String>,
pub symbol: Option<String>,
pub possible_spam: Option<bool>,
pub verified_collection: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftTrade {
pub transaction_hash: Option<String>,
pub transaction_index: Option<String>,
pub token_address: Option<String>,
pub token_ids: Option<Vec<String>>,
pub seller_address: Option<String>,
pub buyer_address: Option<String>,
pub marketplace_address: Option<String>,
pub price: Option<String>,
pub price_formatted: Option<String>,
pub usd_price: Option<f64>,
pub block_timestamp: Option<String>,
pub block_number: Option<String>,
pub block_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftFloorPrice {
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price_usd: Option<f64>,
pub floor_price_currency: Option<String>,
pub marketplace: Option<String>,
pub marketplace_address: Option<String>,
pub retrieved_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftCollectionStats {
pub total_tokens: Option<String>,
#[serde(default, deserialize_with = "int_or_nested_total")]
pub owners: Option<i64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price_usd: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub market_cap_usd: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub volume_24h: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub volume_24h_usd: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub average_price_24h: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub average_price_24h_usd: Option<f64>,
#[serde(default, deserialize_with = "int_or_nested_total")]
pub sales_24h: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftTrait {
pub trait_type: Option<String>,
pub value: Option<serde_json::Value>,
pub count: Option<i64>,
pub percentage: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftResponse<T> {
pub status: Option<String>,
pub page: Option<i32>,
pub page_size: Option<i32>,
pub cursor: Option<String>,
pub result: Vec<T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMultipleNftsRequest {
pub tokens: Vec<NftTokenInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub normalise_metadata: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_items: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftTokenInput {
pub token_address: String,
pub token_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftsByTraitsRequest {
pub traits: Vec<TraitFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraitFilter {
pub trait_type: String,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoricalFloorPrice {
pub timestamp: Option<String>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub floor_price_usd: Option<f64>,
pub floor_price_currency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftSyncStatus {
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMultipleCollectionsRequest {
pub addresses: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftSalePrice {
pub token_address: Option<String>,
pub token_id: Option<String>,
pub transaction_hash: Option<String>,
pub price: Option<String>,
pub price_formatted: Option<String>,
pub usd_price: Option<f64>,
pub payment_token: Option<String>,
pub block_timestamp: Option<String>,
pub block_number: Option<String>,
pub marketplace: Option<String>,
pub buyer_address: Option<String>,
pub seller_address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraitResyncStatus {
pub status: Option<String>,
pub message: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nft_floor_price_as_string() {
let json = r#"{
"floor_price": "0.0189",
"floor_price_usd": "6.36499",
"floor_price_currency": "ETH",
"marketplace": "opensea"
}"#;
let floor: NftFloorPrice = serde_json::from_str(json).unwrap();
assert!((floor.floor_price.unwrap() - 0.0189).abs() < 0.0001);
assert!((floor.floor_price_usd.unwrap() - 6.36499).abs() < 0.001);
}
#[test]
fn test_nft_floor_price_as_number() {
let json = r#"{
"floor_price": 0.0189,
"floor_price_usd": 6.36499
}"#;
let floor: NftFloorPrice = serde_json::from_str(json).unwrap();
assert!((floor.floor_price.unwrap() - 0.0189).abs() < 0.0001);
}
#[test]
fn test_nft_metadata_floor_price_string() {
let json = r#"{
"token_address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
"token_id": "1",
"name": "BoredApeYachtClub",
"floor_price": "6.36499",
"floor_price_usd": "19050.5"
}"#;
let nft: Nft = serde_json::from_str(json).unwrap();
assert!((nft.floor_price.unwrap() - 6.36499).abs() < 0.001);
assert!((nft.floor_price_usd.unwrap() - 19050.5).abs() < 0.1);
}
#[test]
fn test_collection_stats_nested_owners() {
let json = r#"{
"total_tokens": "10000",
"owners": {"total": 6500},
"floor_price": "6.36499",
"floor_price_usd": "19050.5",
"market_cap_usd": "190505000",
"volume_24h": "100.5",
"sales_24h": {"total": 42}
}"#;
let stats: NftCollectionStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.owners, Some(6500));
assert_eq!(stats.sales_24h, Some(42));
assert!((stats.floor_price.unwrap() - 6.36499).abs() < 0.001);
}
#[test]
fn test_collection_stats_flat_integers() {
let json = r#"{
"total_tokens": "10000",
"owners": 6500,
"floor_price": 6.36499,
"sales_24h": 42
}"#;
let stats: NftCollectionStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.owners, Some(6500));
assert_eq!(stats.sales_24h, Some(42));
}
#[test]
fn test_collection_stats_string_market_cap() {
let json = r#"{
"total_tokens": "10000",
"market_cap_usd": "190505000.50"
}"#;
let stats: NftCollectionStats = serde_json::from_str(json).unwrap();
assert!((stats.market_cap_usd.unwrap() - 190_505_000.50).abs() < 0.1);
}
}