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 string_or_int<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrIntVisitor;
impl<'de> Visitor<'de> for StringOrIntVisitor {
type Value = Option<String>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a string or an integer")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E> {
Ok(Some(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
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(StringOrIntVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenMetadata {
pub address: String,
pub name: Option<String>,
pub symbol: Option<String>,
#[serde(default, deserialize_with = "string_or_int")]
pub decimals: Option<String>,
pub logo: Option<String>,
pub thumbnail: Option<String>,
#[serde(default, deserialize_with = "string_or_int")]
pub block_number: Option<String>,
pub validated: Option<i32>,
pub created_at: Option<String>,
pub possible_spam: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenPrice {
pub token_address: Option<String>,
pub usd_price: Option<f64>,
pub usd_price_formatted: Option<String>,
#[serde(rename = "24hrPercentChange")]
pub percent_change_24h: Option<String>,
pub exchange_name: Option<String>,
pub exchange_address: Option<String>,
pub native_price: Option<NativePrice>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NativePrice {
pub value: Option<String>,
pub decimals: Option<u8>,
pub name: Option<String>,
pub symbol: Option<String>,
pub address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenTransfer {
pub transaction_hash: String,
pub address: String,
pub block_timestamp: Option<String>,
#[serde(default, deserialize_with = "string_or_int")]
pub block_number: Option<String>,
pub block_hash: Option<String>,
pub from_address: String,
pub to_address: String,
pub value: String,
pub log_index: Option<i32>,
pub possible_spam: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenTransferResponse {
pub cursor: Option<String>,
pub page: Option<i32>,
pub page_size: Option<i32>,
#[serde(default)]
pub result: Vec<TokenTransfer>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenPair {
#[serde(alias = "pair_address")]
pub pair_address: Option<String>,
#[serde(alias = "pair_label")]
pub pair_label: Option<String>,
#[serde(alias = "exchange_name")]
pub exchange_name: Option<String>,
#[serde(alias = "exchange_logo")]
pub exchange_logo: Option<String>,
#[serde(default, deserialize_with = "string_or_f64", alias = "usd_price")]
pub usd_price: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
rename = "usdPrice24hrPercentChange",
alias = "usd_price_24hr_percent_change"
)]
pub usd_price_24hr_percent_change: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64", alias = "liquidity_usd")]
pub liquidity_usd: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenPairsResponse {
pub pairs: Vec<TokenPair>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenHolder {
pub owner_address: String,
pub owner_address_label: Option<String>,
pub entity: Option<String>,
pub entity_logo: Option<String>,
pub balance: String,
pub balance_formatted: Option<String>,
pub is_contract: Option<bool>,
#[serde(default, deserialize_with = "string_or_f64")]
pub usd_value: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub percentage_relative_to_total_supply: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenHoldersResponse {
pub cursor: Option<String>,
pub page_size: Option<i32>,
pub result: Vec<TokenHolder>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenSwap {
#[serde(alias = "transaction_hash")]
pub transaction_hash: Option<String>,
#[serde(alias = "block_timestamp")]
pub block_timestamp: Option<String>,
#[serde(default, deserialize_with = "string_or_int", alias = "block_number")]
pub block_number: Option<String>,
#[serde(alias = "pair_address")]
pub pair_address: Option<String>,
#[serde(alias = "pair_label")]
pub pair_label: Option<String>,
#[serde(alias = "exchange_name")]
pub exchange_name: Option<String>,
pub token0_address: Option<String>,
pub token1_address: Option<String>,
pub amount0_in: Option<String>,
pub amount1_in: Option<String>,
pub amount0_out: Option<String>,
pub amount1_out: Option<String>,
#[serde(alias = "total_value_usd")]
pub total_value_usd: Option<f64>,
#[serde(alias = "wallet_address")]
pub wallet_address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenStats {
#[serde(alias = "token_address")]
pub token_address: Option<String>,
#[serde(alias = "total_supply")]
pub total_supply: Option<String>,
#[serde(alias = "total_supply_formatted")]
pub total_supply_formatted: Option<String>,
#[serde(alias = "circulating_supply")]
pub circulating_supply: Option<String>,
#[serde(default, deserialize_with = "string_or_f64", alias = "market_cap_usd")]
pub market_cap_usd: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "fully_diluted_valuation"
)]
pub fully_diluted_valuation: Option<f64>,
#[serde(alias = "holders_count")]
pub holders_count: Option<i64>,
#[serde(alias = "transfer_count")]
pub transfer_count: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenSearchResult {
#[serde(alias = "token_address")]
pub token_address: Option<String>,
#[serde(alias = "token_name")]
pub token_name: Option<String>,
#[serde(alias = "token_symbol")]
pub token_symbol: Option<String>,
#[serde(alias = "token_logo")]
pub token_logo: Option<String>,
#[serde(alias = "token_decimals")]
pub token_decimals: Option<u8>,
pub chain: Option<String>,
#[serde(default, deserialize_with = "string_or_f64", alias = "usd_price")]
pub usd_price: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64", alias = "market_cap_usd")]
pub market_cap_usd: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64", alias = "liquidity_usd")]
pub liquidity_usd: Option<f64>,
#[serde(alias = "possible_spam")]
pub possible_spam: Option<bool>,
pub verified: Option<bool>,
#[serde(alias = "security_score")]
pub security_score: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrendingToken {
#[serde(alias = "token_address", alias = "address")]
pub token_address: Option<String>,
#[serde(alias = "token_name", alias = "name")]
pub token_name: Option<String>,
#[serde(alias = "token_symbol", alias = "symbol")]
pub token_symbol: Option<String>,
#[serde(alias = "token_logo", alias = "logo")]
pub token_logo: Option<String>,
pub chain: Option<String>,
#[serde(default, deserialize_with = "string_or_f64", alias = "usd_price")]
pub usd_price: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "price_change_24h"
)]
pub price_change_24h: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64", alias = "volume_24h_usd")]
pub volume_24h_usd: Option<f64>,
pub rank: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PairOhlcv {
#[serde(alias = "timestamp")]
pub timestamp: Option<String>,
#[serde(default, deserialize_with = "string_or_f64")]
pub open: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub high: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub low: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub close: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64")]
pub volume: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PairStats {
#[serde(alias = "pair_address")]
pub pair_address: Option<String>,
#[serde(alias = "pair_label")]
pub pair_label: Option<String>,
pub token0_address: Option<String>,
pub token1_address: Option<String>,
pub reserve0: Option<String>,
pub reserve1: Option<String>,
#[serde(default, deserialize_with = "string_or_f64", alias = "liquidity_usd")]
pub liquidity_usd: Option<f64>,
#[serde(default, deserialize_with = "string_or_f64", alias = "volume_24h_usd")]
pub volume_24h_usd: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "price_change_24h"
)]
pub price_change_24h: Option<f64>,
pub buys_24h: Option<i64>,
pub sells_24h: Option<i64>,
pub buyers_24h: Option<i64>,
pub sellers_24h: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenCategory {
pub id: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NewToken {
pub token_address: Option<String>,
pub token_name: Option<String>,
pub token_symbol: Option<String>,
pub token_logo: Option<String>,
pub chain: Option<String>,
pub created_at: Option<String>,
pub pair_address: Option<String>,
pub exchange_name: Option<String>,
pub usd_price: Option<f64>,
pub liquidity_usd: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenResponse<T> {
pub cursor: Option<String>,
pub page: Option<i32>,
pub page_size: Option<i32>,
pub result: Vec<T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMultiplePricesRequest {
pub tokens: Vec<TokenAddressInput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenAddressInput {
pub token_address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exchange: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTokensBySymbolsRequest {
pub symbols: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenHoldersSummary {
pub total_holders: Option<i64>,
pub holders_change_24h: Option<i64>,
pub holders_change_percent_24h: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HistoricalHolders {
pub timestamp: Option<String>,
pub total_holders: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AggregatedPairStats {
#[serde(alias = "total_pairs")]
pub total_pairs: Option<i32>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "total_liquidity_usd"
)]
pub total_liquidity_usd: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "total_volume_24h_usd"
)]
pub total_volume_24h_usd: Option<f64>,
#[serde(default, alias = "top_pairs")]
pub top_pairs: Option<Vec<PairStats>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TopTrader {
#[serde(alias = "wallet_address")]
pub wallet_address: Option<String>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "realized_profit_usd"
)]
pub realized_profit_usd: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "unrealized_profit_usd"
)]
pub unrealized_profit_usd: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "total_profit_usd"
)]
pub total_profit_usd: Option<f64>,
#[serde(alias = "total_tokens_bought")]
pub total_tokens_bought: Option<String>,
#[serde(alias = "total_tokens_sold")]
pub total_tokens_sold: Option<String>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "avg_buy_price_usd"
)]
pub avg_buy_price_usd: Option<f64>,
#[serde(
default,
deserialize_with = "string_or_f64",
alias = "avg_sell_price_usd"
)]
pub avg_sell_price_usd: Option<f64>,
#[serde(alias = "trade_count")]
pub trade_count: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PairSniper {
pub wallet_address: Option<String>,
pub block_number: Option<String>,
pub transaction_hash: Option<String>,
pub amount_bought: Option<String>,
pub usd_value: Option<f64>,
pub profit_usd: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenBondingStatus {
pub token_address: Option<String>,
pub is_bonding: Option<bool>,
pub graduated: Option<bool>,
pub bonding_progress: Option<f64>,
pub bonding_curve_address: Option<String>,
pub market_cap_usd: Option<f64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_swap_block_number_as_integer() {
let json = r#"{
"transactionHash": "0xabc",
"blockTimestamp": "2024-01-01",
"blockNumber": 24448562,
"pairAddress": "0xdef",
"exchangeName": "uniswap"
}"#;
let swap: TokenSwap = serde_json::from_str(json).unwrap();
assert_eq!(swap.block_number, Some("24448562".to_string()));
assert_eq!(swap.transaction_hash, Some("0xabc".to_string()));
}
#[test]
fn test_token_swap_block_number_as_string() {
let json = r#"{
"transactionHash": "0xabc",
"blockNumber": "24448562"
}"#;
let swap: TokenSwap = serde_json::from_str(json).unwrap();
assert_eq!(swap.block_number, Some("24448562".to_string()));
}
#[test]
fn test_token_transfer_response_wrapper() {
let json = r#"{
"page": 0,
"page_size": 100,
"cursor": null,
"result": [
{
"transaction_hash": "0xabc",
"address": "0xtoken",
"from_address": "0xfrom",
"to_address": "0xto",
"value": "1000000"
}
]
}"#;
let response: TokenTransferResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.result.len(), 1);
assert_eq!(response.result[0].transaction_hash, "0xabc");
}
#[test]
fn test_token_pair_camel_case() {
let json = r#"{
"pairAddress": "0xpair",
"pairLabel": "WETH/USDC",
"exchangeName": "uniswap",
"usdPrice": 3500.0,
"liquidityUsd": 1000000.0
}"#;
let pair: TokenPair = serde_json::from_str(json).unwrap();
assert_eq!(pair.pair_address, Some("0xpair".to_string()));
assert_eq!(pair.pair_label, Some("WETH/USDC".to_string()));
assert_eq!(pair.usd_price, Some(3500.0));
}
#[test]
fn test_token_pair_string_numbers() {
let json = r#"{
"pairAddress": "0xpair",
"usdPrice": "3500.50",
"liquidityUsd": "1000000"
}"#;
let pair: TokenPair = serde_json::from_str(json).unwrap();
assert_eq!(pair.usd_price, Some(3500.50));
assert_eq!(pair.liquidity_usd, Some(1000000.0));
}
#[test]
fn test_token_stats_camel_case() {
let json = r#"{
"tokenAddress": "0xtoken",
"totalSupply": "1000000",
"marketCapUsd": 5000000.0,
"holdersCount": 1500
}"#;
let stats: TokenStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.token_address, Some("0xtoken".to_string()));
assert_eq!(stats.market_cap_usd, Some(5000000.0));
assert_eq!(stats.holders_count, Some(1500));
}
#[test]
fn test_trending_token_with_name_alias() {
let json = r#"{
"tokenAddress": "0xtoken",
"name": "Test Token",
"symbol": "TEST",
"logo": "https://logo.png",
"usdPrice": 1.5,
"rank": 1
}"#;
let token: TrendingToken = serde_json::from_str(json).unwrap();
assert_eq!(token.token_name, Some("Test Token".to_string()));
assert_eq!(token.token_symbol, Some("TEST".to_string()));
assert_eq!(token.token_logo, Some("https://logo.png".to_string()));
}
#[test]
fn test_trending_token_with_token_prefix() {
let json = r#"{
"tokenAddress": "0xtoken",
"tokenName": "Test Token",
"tokenSymbol": "TEST",
"tokenLogo": "https://logo.png",
"usdPrice": 1.5
}"#;
let token: TrendingToken = serde_json::from_str(json).unwrap();
assert_eq!(token.token_name, Some("Test Token".to_string()));
assert_eq!(token.token_symbol, Some("TEST".to_string()));
}
#[test]
fn test_pair_stats_camel_case() {
let json = r#"{
"pairAddress": "0xpair",
"pairLabel": "WETH/USDC",
"liquidityUsd": 1000000.0,
"volume24hUsd": 500000.0,
"buys24h": 100,
"sells24h": 50
}"#;
let stats: PairStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.pair_address, Some("0xpair".to_string()));
assert_eq!(stats.liquidity_usd, Some(1000000.0));
assert_eq!(stats.volume_24h_usd, Some(500000.0));
}
#[test]
fn test_pair_ohlcv_string_numbers() {
let json = r#"{
"timestamp": "2024-01-01",
"open": "3500.5",
"high": "3600.0",
"low": "3400.0",
"close": "3550.0",
"volume": "1000000"
}"#;
let ohlcv: PairOhlcv = serde_json::from_str(json).unwrap();
assert_eq!(ohlcv.open, Some(3500.5));
assert_eq!(ohlcv.volume, Some(1000000.0));
}
#[test]
fn test_token_metadata_block_number_as_int() {
let json = r#"{
"address": "0xtoken",
"name": "Test",
"symbol": "TST",
"block_number": 18000000
}"#;
let meta: TokenMetadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.block_number, Some("18000000".to_string()));
}
#[test]
fn test_string_or_f64_with_empty_string() {
let json = r#"{"pairAddress": "0x", "usdPrice": ""}"#;
let pair: TokenPair = serde_json::from_str(json).unwrap();
assert_eq!(pair.usd_price, None);
}
}