use serde::de::{self, Deserializer, Visitor};
use serde::{Deserialize, Serialize};
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 NativeBalance {
pub balance: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenBalance {
pub token_address: String,
pub name: Option<String>,
pub symbol: Option<String>,
pub logo: Option<String>,
pub thumbnail: Option<String>,
pub decimals: Option<u8>,
pub balance: String,
pub usd_price: Option<f64>,
pub usd_value: Option<f64>,
pub possible_spam: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalletTransaction {
pub hash: String,
pub nonce: Option<String>,
pub transaction_index: Option<String>,
pub from_address: String,
pub to_address: Option<String>,
pub value: String,
pub gas: Option<String>,
pub gas_price: Option<String>,
pub input: Option<String>,
pub receipt_status: Option<String>,
pub block_timestamp: Option<String>,
pub block_number: Option<String>,
pub block_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
pub page: Option<i32>,
pub page_size: Option<i32>,
pub cursor: Option<String>,
pub result: Vec<T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetWorth {
pub total_networth_usd: String,
pub chains: Vec<ChainNetWorth>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainNetWorth {
pub chain: String,
pub native_balance_usd: String,
pub token_balance_usd: String,
pub networth_usd: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveChains {
pub address: String,
pub active_chains: Vec<ActiveChain>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveChain {
pub chain: String,
pub chain_id: String,
pub first_transaction: Option<TransactionInfo>,
pub last_transaction: Option<TransactionInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionInfo {
pub block_timestamp: Option<String>,
pub block_number: Option<String>,
pub transaction_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenApproval {
#[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>,
#[serde(alias = "spender_address")]
pub spender_address: Option<String>,
#[serde(alias = "spender_name")]
pub spender_name: Option<String>,
pub allowance: Option<String>,
#[serde(alias = "allowance_formatted")]
pub allowance_formatted: Option<String>,
#[serde(alias = "usd_at_risk")]
pub usd_at_risk: Option<f64>,
#[serde(alias = "is_unlimited")]
pub is_unlimited: Option<bool>,
#[serde(alias = "block_timestamp")]
pub block_timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletHistoryEntry {
#[serde(alias = "hash")]
pub hash: Option<String>,
#[serde(alias = "from_address")]
pub from_address: Option<String>,
#[serde(alias = "to_address")]
pub to_address: Option<String>,
pub value: Option<String>,
#[serde(alias = "block_number")]
pub block_number: Option<String>,
#[serde(alias = "block_timestamp")]
pub block_timestamp: Option<String>,
pub category: Option<String>,
pub summary: Option<String>,
#[serde(alias = "possible_spam")]
pub possible_spam: Option<bool>,
#[serde(default, alias = "nft_transfers")]
pub nft_transfers: Option<Vec<serde_json::Value>>,
#[serde(default, alias = "erc20_transfers")]
pub erc20_transfers: Option<Vec<serde_json::Value>>,
#[serde(default, alias = "native_transfers")]
pub native_transfers: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalletStats {
pub address: Option<String>,
#[serde(default, deserialize_with = "int_or_nested_total", alias = "nfts")]
pub nfts_owned: Option<i64>,
#[serde(
default,
deserialize_with = "int_or_nested_total",
alias = "collections"
)]
pub collections_owned: Option<i64>,
#[serde(
default,
deserialize_with = "int_or_nested_total",
alias = "nftTransfers"
)]
pub nft_transfers: Option<i64>,
#[serde(
default,
deserialize_with = "int_or_nested_total",
alias = "tokenTransfers"
)]
pub token_transfers: Option<i64>,
#[serde(
default,
deserialize_with = "int_or_nested_total",
alias = "transactions"
)]
pub transactions_count: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletProfitability {
#[serde(alias = "total_realized_profit_usd")]
pub total_realized_profit_usd: Option<f64>,
#[serde(alias = "total_realized_loss_usd")]
pub total_realized_loss_usd: Option<f64>,
#[serde(alias = "total_count_of_profitable_trades")]
pub total_count_of_profitable_trades: Option<i64>,
#[serde(alias = "total_count_of_losing_trades")]
pub total_count_of_losing_trades: Option<i64>,
#[serde(alias = "total_count_of_trades")]
pub total_count_of_trades: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenProfitability {
#[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 = "realized_profit_usd")]
pub realized_profit_usd: Option<f64>,
#[serde(alias = "avg_buy_price_usd")]
pub avg_buy_price_usd: Option<f64>,
#[serde(alias = "avg_sell_price_usd")]
pub avg_sell_price_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(alias = "count_of_trades")]
pub count_of_trades: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetWalletBalancesRequest {
pub wallet_addresses: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalletBalances {
pub address: Option<String>,
pub native_balance: Option<String>,
pub native_balance_formatted: Option<String>,
pub native_balance_usd: Option<f64>,
pub token_balances: Option<Vec<TokenBalance>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wallet_stats_nested_objects() {
let json = r#"{
"address": "0xvitalik",
"nfts_owned": {"total": 42},
"collections_owned": {"total": 10},
"nft_transfers": {"total": 100},
"token_transfers": {"total": 500},
"transactions_count": {"total": 1000}
}"#;
let stats: WalletStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.address, Some("0xvitalik".to_string()));
assert_eq!(stats.nfts_owned, Some(42));
assert_eq!(stats.collections_owned, Some(10));
assert_eq!(stats.nft_transfers, Some(100));
assert_eq!(stats.token_transfers, Some(500));
assert_eq!(stats.transactions_count, Some(1000));
}
#[test]
fn test_wallet_stats_flat_integers() {
let json = r#"{
"address": "0xvitalik",
"nfts_owned": 42,
"collections_owned": 10,
"nft_transfers": 100,
"token_transfers": 500,
"transactions_count": 1000
}"#;
let stats: WalletStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.nfts_owned, Some(42));
assert_eq!(stats.transactions_count, Some(1000));
}
#[test]
fn test_wallet_stats_with_aliases() {
let json = r#"{
"address": "0xvitalik",
"nfts": {"total": 42},
"collections": {"total": 10},
"nftTransfers": {"total": 100},
"tokenTransfers": {"total": 500},
"transactions": {"total": 1000}
}"#;
let stats: WalletStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.nfts_owned, Some(42));
assert_eq!(stats.collections_owned, Some(10));
assert_eq!(stats.transactions_count, Some(1000));
}
#[test]
fn test_token_approval_camel_case() {
let json = r#"{
"tokenAddress": "0xtoken",
"tokenName": "USD Coin",
"tokenSymbol": "USDC",
"tokenLogo": "https://logo.png",
"tokenDecimals": 6,
"spenderAddress": "0xspender",
"spenderName": "Uniswap",
"allowance": "1000000",
"allowanceFormatted": "1.0",
"usdAtRisk": 1.0,
"isUnlimited": false,
"blockTimestamp": "2024-01-01"
}"#;
let approval: TokenApproval = serde_json::from_str(json).unwrap();
assert_eq!(approval.token_address, Some("0xtoken".to_string()));
assert_eq!(approval.token_name, Some("USD Coin".to_string()));
assert_eq!(approval.token_symbol, Some("USDC".to_string()));
assert_eq!(approval.spender_address, Some("0xspender".to_string()));
assert_eq!(approval.spender_name, Some("Uniswap".to_string()));
assert_eq!(approval.usd_at_risk, Some(1.0));
}
#[test]
fn test_wallet_profitability_camel_case() {
let json = r#"{
"totalRealizedProfitUsd": 1000.0,
"totalRealizedLossUsd": 500.0,
"totalCountOfProfitableTrades": 10,
"totalCountOfLosingTrades": 5,
"totalCountOfTrades": 15
}"#;
let profit: WalletProfitability = serde_json::from_str(json).unwrap();
assert_eq!(profit.total_realized_profit_usd, Some(1000.0));
assert_eq!(profit.total_count_of_trades, Some(15));
}
#[test]
fn test_token_profitability_camel_case() {
let json = r#"{
"tokenAddress": "0xtoken",
"tokenName": "Test",
"tokenSymbol": "TST",
"realizedProfitUsd": 500.0,
"avgBuyPriceUsd": 1.0,
"avgSellPriceUsd": 1.5,
"countOfTrades": 10
}"#;
let profit: TokenProfitability = serde_json::from_str(json).unwrap();
assert_eq!(profit.token_address, Some("0xtoken".to_string()));
assert_eq!(profit.realized_profit_usd, Some(500.0));
assert_eq!(profit.count_of_trades, Some(10));
}
#[test]
fn test_wallet_history_camel_case() {
let json = r#"{
"hash": "0xabc",
"fromAddress": "0xfrom",
"toAddress": "0xto",
"value": "1000",
"blockNumber": "18000000",
"blockTimestamp": "2024-01-01",
"category": "send",
"possibleSpam": false,
"nftTransfers": [],
"erc20Transfers": [],
"nativeTransfers": []
}"#;
let entry: WalletHistoryEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.from_address, Some("0xfrom".to_string()));
assert_eq!(entry.to_address, Some("0xto".to_string()));
assert_eq!(entry.category, Some("send".to_string()));
}
}