use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Totals {
pub tokens: String,
pub orders: String,
pub traders: String,
pub settlements: String,
pub volume_usd: String,
pub volume_eth: String,
pub fees_usd: String,
pub fees_eth: String,
}
impl fmt::Display for Totals {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "totals(orders={}, traders={})", self.orders, self.traders)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DailyVolume {
pub timestamp: String,
pub volume_usd: String,
}
impl fmt::Display for DailyVolume {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "daily-vol(ts={}, ${})", self.timestamp, self.volume_usd)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HourlyVolume {
pub timestamp: String,
pub volume_usd: String,
}
impl fmt::Display for HourlyVolume {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hourly-vol(ts={}, ${})", self.timestamp, self.volume_usd)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DailyTotal {
pub timestamp: String,
pub orders: String,
pub traders: String,
pub tokens: String,
pub settlements: String,
pub volume_eth: String,
pub volume_usd: String,
pub fees_eth: String,
pub fees_usd: String,
}
impl fmt::Display for DailyTotal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "daily-total(ts={}, orders={})", self.timestamp, self.orders)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HourlyTotal {
pub timestamp: String,
pub orders: String,
pub traders: String,
pub tokens: String,
pub settlements: String,
pub volume_eth: String,
pub volume_usd: String,
pub fees_eth: String,
pub fees_usd: String,
}
impl fmt::Display for HourlyTotal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hourly-total(ts={}, orders={})", self.timestamp, self.orders)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Token {
pub id: String,
pub address: String,
pub first_trade_timestamp: String,
pub name: String,
pub symbol: String,
pub decimals: String,
pub total_volume: String,
pub price_eth: String,
pub price_usd: String,
pub number_of_trades: String,
}
impl Token {
#[must_use]
pub fn symbol_ref(&self) -> &str {
&self.symbol
}
#[must_use]
pub fn address_ref(&self) -> &str {
&self.address
}
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.symbol, self.address)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenDailyTotal {
pub id: String,
pub token: Token,
pub timestamp: String,
pub total_volume: String,
pub total_volume_usd: String,
pub total_trades: String,
pub open_price: String,
pub close_price: String,
pub higher_price: String,
pub lower_price: String,
pub average_price: String,
}
impl fmt::Display for TokenDailyTotal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "token-daily({}, ts={})", self.token, self.timestamp)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenHourlyTotal {
pub id: String,
pub token: Token,
pub timestamp: String,
pub total_volume: String,
pub total_volume_usd: String,
pub total_trades: String,
pub open_price: String,
pub close_price: String,
pub higher_price: String,
pub lower_price: String,
pub average_price: String,
}
impl fmt::Display for TokenHourlyTotal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "token-hourly({}, ts={})", self.token, self.timestamp)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenTradingEvent {
pub id: String,
pub token: Token,
pub price_usd: String,
pub timestamp: String,
}
impl fmt::Display for TokenTradingEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "trade-event({}, ts={}, price=${})", self.token, self.timestamp, self.price_usd)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: String,
pub address: String,
pub first_trade_timestamp: String,
pub number_of_trades: String,
pub solved_amount_eth: String,
pub solved_amount_usd: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.address)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Settlement {
pub id: String,
pub tx_hash: String,
pub first_trade_timestamp: String,
pub solver: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tx_cost: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tx_fee_in_eth: Option<String>,
}
impl Settlement {
#[must_use]
pub const fn has_gas_cost(&self) -> bool {
self.tx_cost.is_some()
}
#[must_use]
pub const fn has_tx_fee(&self) -> bool {
self.tx_fee_in_eth.is_some()
}
}
impl fmt::Display for Settlement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "settlement({})", self.tx_hash)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Trade {
pub id: String,
pub timestamp: String,
pub gas_price: String,
pub fee_amount: String,
pub tx_hash: String,
pub settlement: String,
pub buy_amount: String,
pub sell_amount: String,
pub sell_amount_before_fees: String,
pub buy_token: Token,
pub sell_token: Token,
pub owner: User,
pub order: String,
}
impl Trade {
#[must_use]
pub const fn has_tx_hash(&self) -> bool {
!self.tx_hash.is_empty()
}
}
impl fmt::Display for Trade {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "trade({} {} → {})", self.tx_hash, self.sell_token, self.buy_token)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub id: String,
pub owner: User,
pub sell_token: Token,
pub buy_token: Token,
#[serde(skip_serializing_if = "Option::is_none")]
pub receiver: Option<String>,
pub sell_amount: String,
pub buy_amount: String,
pub valid_to: String,
pub app_data: String,
pub fee_amount: String,
pub kind: String,
pub partially_fillable: bool,
pub status: String,
pub executed_sell_amount: String,
pub executed_sell_amount_before_fees: String,
pub executed_buy_amount: String,
pub executed_fee_amount: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub invalidate_timestamp: Option<String>,
pub timestamp: String,
pub tx_hash: String,
pub is_signer_safe: bool,
pub signing_scheme: String,
pub uid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub surplus: Option<String>,
}
impl Order {
#[must_use]
pub fn is_sell(&self) -> bool {
self.kind == "sell"
}
#[must_use]
pub fn is_buy(&self) -> bool {
self.kind == "buy"
}
#[must_use]
pub fn is_open(&self) -> bool {
self.status == "open"
}
#[must_use]
pub fn is_filled(&self) -> bool {
self.status == "filled"
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.status == "cancelled"
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.status == "expired"
}
#[must_use]
pub fn is_terminal(&self) -> bool {
self.is_filled() || self.is_cancelled() || self.is_expired()
}
#[must_use]
pub const fn has_receiver(&self) -> bool {
self.receiver.is_some()
}
#[must_use]
pub const fn has_invalidate_timestamp(&self) -> bool {
self.invalidate_timestamp.is_some()
}
#[must_use]
pub const fn has_surplus(&self) -> bool {
self.surplus.is_some()
}
#[must_use]
pub const fn is_partially_fillable(&self) -> bool {
self.partially_fillable
}
#[must_use]
pub const fn is_signer_safe(&self) -> bool {
self.is_signer_safe
}
}
impl fmt::Display for Order {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let short_uid = if self.uid.len() > 10 { &self.uid[..10] } else { &self.uid };
write!(f, "order({short_uid}… {} {})", self.kind, self.status)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Pair {
pub id: String,
pub token0: Token,
pub token1: Token,
pub volume_token0: String,
pub volume_token1: String,
pub number_of_trades: String,
}
impl fmt::Display for Pair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pair({}/{})", self.token0, self.token1)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PairDaily {
pub id: String,
pub token0: Token,
pub token1: Token,
pub timestamp: String,
pub volume_token0: String,
pub volume_token1: String,
pub number_of_trades: String,
}
impl fmt::Display for PairDaily {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pair-daily({}/{}, ts={})", self.token0, self.token1, self.timestamp)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PairHourly {
pub id: String,
pub token0: Token,
pub token1: Token,
pub timestamp: String,
pub volume_token0: String,
pub volume_token1: String,
pub number_of_trades: String,
}
impl fmt::Display for PairHourly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pair-hourly({}/{}, ts={})", self.token0, self.token1, self.timestamp)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bundle {
pub id: String,
pub eth_price_usd: String,
}
impl Bundle {
#[must_use]
pub fn eth_price_usd_ref(&self) -> &str {
&self.eth_price_usd
}
}
impl fmt::Display for Bundle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "eth-price=${}", self.eth_price_usd)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Total {
pub id: String,
pub orders: String,
pub settlements: String,
pub tokens: String,
pub traders: String,
pub number_of_trades: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_eth: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_usd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fees_eth: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fees_usd: Option<String>,
}
impl fmt::Display for Total {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "total(orders={}, traders={})", self.orders, self.traders)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UniswapToken {
pub id: String,
pub address: String,
pub name: String,
pub symbol: String,
pub decimals: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_eth: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_usd: Option<String>,
}
impl fmt::Display for UniswapToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.symbol, self.address)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UniswapPool {
pub id: String,
pub liquidity: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tick: Option<String>,
pub token0: UniswapToken,
pub token0_price: String,
pub token1: UniswapToken,
pub token1_price: String,
pub total_value_locked_token0: String,
pub total_value_locked_token1: String,
}
impl fmt::Display for UniswapPool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pool({}, {}/{})", self.id, self.token0, self.token1)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubgraphBlock {
#[serde(skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
pub number: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<i64>,
}
impl fmt::Display for SubgraphBlock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "block(#{})", self.number)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubgraphMeta {
pub block: SubgraphBlock,
pub deployment: String,
pub has_indexing_errors: bool,
}
impl fmt::Display for SubgraphMeta {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "meta(deploy={}, block={})", self.deployment, self.block)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_token() -> Token {
Token {
id: "0xabc".into(),
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into(),
first_trade_timestamp: "1700000000".into(),
name: "USD Coin".into(),
symbol: "USDC".into(),
decimals: "6".into(),
total_volume: "1000000".into(),
price_eth: "0.0003".into(),
price_usd: "1.0".into(),
number_of_trades: "42".into(),
}
}
fn sample_user() -> User {
User {
id: "0xuser".into(),
address: "0xUserAddress".into(),
first_trade_timestamp: "1700000000".into(),
number_of_trades: "10".into(),
solved_amount_eth: "5.0".into(),
solved_amount_usd: "10000".into(),
}
}
#[test]
fn totals_display() {
let t = Totals {
tokens: "100".into(),
orders: "500".into(),
traders: "50".into(),
settlements: "20".into(),
volume_usd: "1000000".into(),
volume_eth: "500".into(),
fees_usd: "1000".into(),
fees_eth: "0.5".into(),
};
assert_eq!(t.to_string(), "totals(orders=500, traders=50)");
}
#[test]
fn daily_volume_display() {
let d = DailyVolume { timestamp: "1700000000".into(), volume_usd: "500000".into() };
assert_eq!(d.to_string(), "daily-vol(ts=1700000000, $500000)");
}
#[test]
fn hourly_volume_display() {
let h = HourlyVolume { timestamp: "1700000000".into(), volume_usd: "10000".into() };
assert_eq!(h.to_string(), "hourly-vol(ts=1700000000, $10000)");
}
#[test]
fn daily_total_display() {
let d = DailyTotal {
timestamp: "1700000000".into(),
orders: "100".into(),
traders: "10".into(),
tokens: "5".into(),
settlements: "3".into(),
volume_eth: "50".into(),
volume_usd: "100000".into(),
fees_eth: "0.1".into(),
fees_usd: "200".into(),
};
assert_eq!(d.to_string(), "daily-total(ts=1700000000, orders=100)");
}
#[test]
fn hourly_total_display() {
let h = HourlyTotal {
timestamp: "1700000000".into(),
orders: "10".into(),
traders: "5".into(),
tokens: "3".into(),
settlements: "1".into(),
volume_eth: "10".into(),
volume_usd: "20000".into(),
fees_eth: "0.01".into(),
fees_usd: "20".into(),
};
assert_eq!(h.to_string(), "hourly-total(ts=1700000000, orders=10)");
}
#[test]
fn token_display() {
let t = sample_token();
assert_eq!(t.to_string(), "USDC (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)");
}
#[test]
fn token_accessors() {
let t = sample_token();
assert_eq!(t.symbol_ref(), "USDC");
assert_eq!(t.address_ref(), "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
}
#[test]
fn user_display() {
let u = sample_user();
assert_eq!(u.to_string(), "0xUserAddress");
}
#[test]
fn settlement_display_and_methods() {
let s = Settlement {
id: "0xtx".into(),
tx_hash: "0xdeadbeef".into(),
first_trade_timestamp: "1700000000".into(),
solver: "0xsolver".into(),
tx_cost: Some("1000".into()),
tx_fee_in_eth: None,
};
assert_eq!(s.to_string(), "settlement(0xdeadbeef)");
assert!(s.has_gas_cost());
assert!(!s.has_tx_fee());
let s2 = Settlement {
id: "0xtx".into(),
tx_hash: "0xdeadbeef".into(),
first_trade_timestamp: "1700000000".into(),
solver: "0xsolver".into(),
tx_cost: None,
tx_fee_in_eth: Some("0.01".into()),
};
assert!(!s2.has_gas_cost());
assert!(s2.has_tx_fee());
}
#[test]
fn trade_display_and_has_tx_hash() {
let t = Trade {
id: "0x-0".into(),
timestamp: "1700000000".into(),
gas_price: "20".into(),
fee_amount: "100".into(),
tx_hash: "0xabc".into(),
settlement: "0xsettle".into(),
buy_amount: "500".into(),
sell_amount: "1000".into(),
sell_amount_before_fees: "1100".into(),
buy_token: sample_token(),
sell_token: sample_token(),
owner: sample_user(),
order: "0xorder".into(),
};
assert!(t.has_tx_hash());
assert!(t.to_string().contains("0xabc"));
}
#[test]
fn order_methods() {
let o = Order {
id: "0xorderuid1234567890".into(),
owner: sample_user(),
sell_token: sample_token(),
buy_token: sample_token(),
receiver: Some("0xreceiver".into()),
sell_amount: "1000".into(),
buy_amount: "500".into(),
valid_to: "1700000000".into(),
app_data: "0xappdata".into(),
fee_amount: "10".into(),
kind: "sell".into(),
partially_fillable: true,
status: "open".into(),
executed_sell_amount: "0".into(),
executed_sell_amount_before_fees: "0".into(),
executed_buy_amount: "0".into(),
executed_fee_amount: "0".into(),
invalidate_timestamp: None,
timestamp: "1700000000".into(),
tx_hash: "0xtx".into(),
is_signer_safe: false,
signing_scheme: "eip712".into(),
uid: "0xorderuid1234567890abcdef".into(),
surplus: Some("50".into()),
};
assert!(o.is_sell());
assert!(!o.is_buy());
assert!(o.is_open());
assert!(!o.is_filled());
assert!(!o.is_cancelled());
assert!(!o.is_expired());
assert!(!o.is_terminal());
assert!(o.has_receiver());
assert!(!o.has_invalidate_timestamp());
assert!(o.has_surplus());
assert!(o.is_partially_fillable());
assert!(!o.is_signer_safe());
assert!(o.to_string().contains("sell"));
assert!(o.to_string().contains("open"));
}
#[test]
fn order_terminal_states() {
let make = |status: &str| Order {
id: "x".into(),
owner: sample_user(),
sell_token: sample_token(),
buy_token: sample_token(),
receiver: None,
sell_amount: "0".into(),
buy_amount: "0".into(),
valid_to: "0".into(),
app_data: "0x".into(),
fee_amount: "0".into(),
kind: "buy".into(),
partially_fillable: false,
status: status.into(),
executed_sell_amount: "0".into(),
executed_sell_amount_before_fees: "0".into(),
executed_buy_amount: "0".into(),
executed_fee_amount: "0".into(),
invalidate_timestamp: Some("100".into()),
timestamp: "0".into(),
tx_hash: String::new(),
is_signer_safe: true,
signing_scheme: "eip1271".into(),
uid: "short".into(),
surplus: None,
};
assert!(make("filled").is_filled());
assert!(make("filled").is_terminal());
assert!(make("cancelled").is_cancelled());
assert!(make("cancelled").is_terminal());
assert!(make("expired").is_expired());
assert!(make("expired").is_terminal());
let buy_order = make("open");
assert!(buy_order.is_buy());
assert!(buy_order.has_invalidate_timestamp());
assert!(!buy_order.has_surplus());
assert!(!buy_order.is_partially_fillable());
assert!(buy_order.is_signer_safe());
}
#[test]
fn bundle_display_and_accessor() {
let b = Bundle { id: "1".into(), eth_price_usd: "3500.00".into() };
assert_eq!(b.to_string(), "eth-price=$3500.00");
assert_eq!(b.eth_price_usd_ref(), "3500.00");
}
#[test]
fn total_display() {
let t = Total {
id: "1".into(),
orders: "100".into(),
settlements: "10".into(),
tokens: "20".into(),
traders: "30".into(),
number_of_trades: "200".into(),
volume_eth: None,
volume_usd: None,
fees_eth: None,
fees_usd: None,
};
assert_eq!(t.to_string(), "total(orders=100, traders=30)");
}
#[test]
fn subgraph_block_display() {
let b = SubgraphBlock {
hash: Some("0xabc".into()),
number: 12345,
parent_hash: None,
timestamp: Some(1700000000),
};
assert_eq!(b.to_string(), "block(#12345)");
}
#[test]
fn subgraph_meta_display() {
let m = SubgraphMeta {
block: SubgraphBlock { hash: None, number: 999, parent_hash: None, timestamp: None },
deployment: "deploy-123".into(),
has_indexing_errors: false,
};
assert_eq!(m.to_string(), "meta(deploy=deploy-123, block=block(#999))");
}
#[test]
fn uniswap_token_display() {
let t = UniswapToken {
id: "0xabc".into(),
address: "0xABC".into(),
name: "Token".into(),
symbol: "TKN".into(),
decimals: 18,
price_eth: None,
price_usd: None,
};
assert_eq!(t.to_string(), "TKN (0xABC)");
}
#[test]
fn uniswap_pool_display() {
let t0 = UniswapToken {
id: "0xa".into(),
address: "0xA".into(),
name: "A".into(),
symbol: "A".into(),
decimals: 18,
price_eth: None,
price_usd: None,
};
let t1 = UniswapToken {
id: "0xb".into(),
address: "0xB".into(),
name: "B".into(),
symbol: "B".into(),
decimals: 18,
price_eth: None,
price_usd: None,
};
let p = UniswapPool {
id: "0xpool".into(),
liquidity: "1000".into(),
tick: Some("100".into()),
token0: t0,
token0_price: "1.0".into(),
token1: t1,
token1_price: "1.0".into(),
total_value_locked_token0: "500".into(),
total_value_locked_token1: "500".into(),
};
assert_eq!(p.to_string(), "pool(0xpool, A (0xA)/B (0xB))");
}
#[test]
fn pair_display() {
let p = Pair {
id: "0xa-0xb".into(),
token0: sample_token(),
token1: sample_token(),
volume_token0: "100".into(),
volume_token1: "200".into(),
number_of_trades: "5".into(),
};
assert!(p.to_string().starts_with("pair("));
}
#[test]
fn pair_daily_display() {
let pd = PairDaily {
id: "0xa-0xb-123".into(),
token0: sample_token(),
token1: sample_token(),
timestamp: "1700000000".into(),
volume_token0: "100".into(),
volume_token1: "200".into(),
number_of_trades: "5".into(),
};
assert!(pd.to_string().contains("pair-daily("));
}
#[test]
fn pair_hourly_display() {
let ph = PairHourly {
id: "0xa-0xb-123".into(),
token0: sample_token(),
token1: sample_token(),
timestamp: "1700000000".into(),
volume_token0: "100".into(),
volume_token1: "200".into(),
number_of_trades: "5".into(),
};
assert!(ph.to_string().contains("pair-hourly("));
}
#[test]
fn token_daily_total_display() {
let tdt = TokenDailyTotal {
id: "0x-123".into(),
token: sample_token(),
timestamp: "1700000000".into(),
total_volume: "1000".into(),
total_volume_usd: "1000".into(),
total_trades: "10".into(),
open_price: "1.0".into(),
close_price: "1.01".into(),
higher_price: "1.02".into(),
lower_price: "0.99".into(),
average_price: "1.005".into(),
};
assert!(tdt.to_string().contains("token-daily("));
}
#[test]
fn token_hourly_total_display() {
let tht = TokenHourlyTotal {
id: "0x-123".into(),
token: sample_token(),
timestamp: "1700000000".into(),
total_volume: "500".into(),
total_volume_usd: "500".into(),
total_trades: "5".into(),
open_price: "1.0".into(),
close_price: "1.01".into(),
higher_price: "1.02".into(),
lower_price: "0.99".into(),
average_price: "1.005".into(),
};
assert!(tht.to_string().contains("token-hourly("));
}
#[test]
fn token_trading_event_display() {
let e = TokenTradingEvent {
id: "evt1".into(),
token: sample_token(),
price_usd: "1.01".into(),
timestamp: "1700000000".into(),
};
assert!(e.to_string().contains("trade-event("));
assert!(e.to_string().contains("price=$1.01"));
}
#[test]
fn totals_serde_roundtrip() {
let t = Totals {
tokens: "100".into(),
orders: "500".into(),
traders: "50".into(),
settlements: "20".into(),
volume_usd: "1000000".into(),
volume_eth: "500".into(),
fees_usd: "1000".into(),
fees_eth: "0.5".into(),
};
let json = serde_json::to_string(&t).unwrap();
let t2: Totals = serde_json::from_str(&json).unwrap();
assert_eq!(t2.orders, "500");
}
#[test]
fn settlement_serde_skips_none() {
let s = Settlement {
id: "x".into(),
tx_hash: "0x".into(),
first_trade_timestamp: "0".into(),
solver: "0x".into(),
tx_cost: None,
tx_fee_in_eth: None,
};
let json = serde_json::to_string(&s).unwrap();
assert!(!json.contains("txCost"));
assert!(!json.contains("txFeeInEth"));
}
#[test]
fn total_serde_roundtrip_with_optional_fields() {
let t = Total {
id: "1".into(),
orders: "10".into(),
settlements: "5".into(),
tokens: "3".into(),
traders: "2".into(),
number_of_trades: "20".into(),
volume_eth: Some("100".into()),
volume_usd: Some("200000".into()),
fees_eth: None,
fees_usd: None,
};
let json = serde_json::to_string(&t).unwrap();
assert!(json.contains("volumeEth"));
assert!(!json.contains("feesEth"));
let t2: Total = serde_json::from_str(&json).unwrap();
assert_eq!(t2.volume_eth, Some("100".into()));
assert_eq!(t2.fees_eth, None);
}
#[test]
fn bundle_serde_roundtrip() {
let b = Bundle { id: "1".into(), eth_price_usd: "3500".into() };
let json = serde_json::to_string(&b).unwrap();
assert!(json.contains("ethPriceUsd"));
let b2: Bundle = serde_json::from_str(&json).unwrap();
assert_eq!(b2.eth_price_usd, "3500");
}
}