1use anyhow::Result;
7use reqwest::Client;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize, Clone)]
12#[serde(rename_all = "camelCase")]
13pub struct DexScreenerResponseRaw {
14 #[serde(rename = "schemaVersion")]
16 pub schema_version: String,
17 pub pairs: Vec<PairInfoRaw>,
19}
20
21#[derive(Debug, Serialize, Deserialize, Clone)]
23#[serde(rename_all = "camelCase")]
24pub struct PairInfoRaw {
25 #[serde(rename = "chainId")]
27 pub chain_id: String,
28 #[serde(rename = "dexId")]
30 pub dex_id: String,
31 pub url: String,
33 #[serde(rename = "pairAddress")]
35 pub pair_address: String,
36 pub labels: Option<Vec<String>>,
38 pub base_token: TokenRaw,
40 pub quote_token: TokenRaw,
42 #[serde(rename = "priceNative")]
44 pub price_native: String,
45 #[serde(rename = "priceUsd")]
47 pub price_usd: Option<String>,
48 pub liquidity: Option<LiquidityRaw>,
50 pub volume: Option<VolumeRaw>,
52 pub price_change: Option<PriceChangeRaw>,
54 pub txns: Option<TransactionsRaw>,
56 #[serde(rename = "marketCap")]
58 pub market_cap: Option<f64>,
59 #[serde(rename = "fdv")]
61 pub fdv: Option<f64>,
62}
63
64#[derive(Debug, Serialize, Deserialize, Clone)]
66pub struct LiquidityRaw {
67 pub usd: Option<f64>,
69 pub base: Option<f64>,
71 pub quote: Option<f64>,
73}
74
75#[derive(Debug, Serialize, Deserialize, Clone, Default)]
77pub struct VolumeRaw {
78 #[serde(default)]
80 pub h24: Option<f64>,
81 #[serde(default)]
83 pub h6: Option<f64>,
84 #[serde(default)]
86 pub h1: Option<f64>,
87 #[serde(default)]
89 pub m5: Option<f64>,
90}
91
92#[derive(Debug, Serialize, Deserialize, Clone)]
94pub struct PriceChangeRaw {
95 #[serde(default)]
97 pub h24: Option<f64>,
98 #[serde(default)]
100 pub h6: Option<f64>,
101 #[serde(default)]
103 pub h1: Option<f64>,
104 #[serde(default)]
106 pub m5: Option<f64>,
107}
108
109#[derive(Debug, Serialize, Deserialize, Clone)]
111pub struct TransactionsRaw {
112 #[serde(default)]
114 pub h24: Option<TransactionStatsRaw>,
115 #[serde(default)]
117 pub h6: Option<TransactionStatsRaw>,
118 #[serde(default)]
120 pub h1: Option<TransactionStatsRaw>,
121 #[serde(default)]
123 pub m5: Option<TransactionStatsRaw>,
124}
125
126#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct TransactionStatsRaw {
129 pub buys: Option<u64>,
131 pub sells: Option<u64>,
133}
134
135#[derive(Debug, Serialize, Deserialize, Clone)]
137pub struct TokenRaw {
138 pub address: String,
140 pub name: String,
142 pub symbol: String,
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone)]
150#[serde(rename_all = "camelCase")]
151pub struct TokenProfile {
152 pub url: String,
154 #[serde(rename = "chainId")]
156 pub chain_id: String,
157 #[serde(rename = "tokenAddress")]
159 pub token_address: String,
160 pub icon: String,
162 pub header: Option<String>,
164 pub description: Option<String>,
166 pub links: Option<Vec<TokenLink>>,
168}
169
170#[derive(Debug, Serialize, Deserialize, Clone)]
172pub struct TokenLink {
173 #[serde(rename = "type")]
175 pub link_type: Option<String>,
176 pub label: Option<String>,
178 pub url: String,
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone)]
184#[serde(rename_all = "camelCase")]
185pub struct BoostsResponse {
186 pub url: String,
188 #[serde(rename = "chainId")]
190 pub chain_id: String,
191 #[serde(rename = "tokenAddress")]
193 pub token_address: String,
194 pub amount: f64,
196 #[serde(rename = "totalAmount")]
198 pub total_amount: f64,
199 pub icon: Option<String>,
201 pub header: Option<String>,
203 pub description: Option<String>,
205 pub links: Option<Vec<TokenLink>>,
207}
208
209#[derive(Debug, Serialize, Deserialize, Clone)]
211pub struct OrdersResponse {
212 pub orders: Vec<Order>,
214}
215
216#[derive(Debug, Serialize, Deserialize, Clone)]
218#[serde(rename_all = "camelCase")]
219pub struct Order {
220 #[serde(rename = "type")]
222 pub order_type: OrderType,
223 pub status: OrderStatus,
225 #[serde(rename = "paymentTimestamp")]
227 pub payment_timestamp: Option<f64>,
228}
229
230#[derive(Debug, Serialize, Deserialize, Clone)]
232#[serde(rename_all = "camelCase")]
233pub enum OrderType {
234 TokenProfile,
236 CommunityTakeover,
238 TokenAd,
240 TrendingBarAd,
242}
243
244#[derive(Debug, Serialize, Deserialize, Clone)]
246#[serde(rename_all = "kebab-case")]
247pub enum OrderStatus {
248 Processing,
250 Cancelled,
252 #[serde(rename = "on-hold")]
254 OnHold,
255 Approved,
257 Rejected,
259}
260
261pub async fn search_ticker(ticker: String) -> Result<DexScreenerResponseRaw> {
263 let client = Client::new();
264 let url = format!(
265 "https://api.dexscreener.com/latest/dex/search/?q={}&limit=8",
266 ticker
267 );
268
269 let response = client.get(&url).send().await?;
270
271 if response.status().is_client_error() {
272 let res = response.text().await?;
273 tracing::error!("DexScreener API error: {:?}", res);
274 return Err(anyhow::anyhow!("DexScreener API error: {:?}", res));
275 }
276
277 let data: serde_json::Value = response.json().await?;
278 let mut dex_response: DexScreenerResponseRaw = serde_json::from_value(data)?;
279
280 dex_response.pairs.truncate(8);
282
283 Ok(dex_response)
284}
285
286pub async fn get_pairs_by_token_v1(
288 chain_id: &str,
289 token_address: &str,
290) -> Result<DexScreenerResponseRaw> {
291 let client = Client::new();
292 let url = format!(
293 "https://api.dexscreener.com/tokens/v1/{}/{}",
294 chain_id, token_address
295 );
296
297 let response = client.get(&url).send().await?;
298
299 if !response.status().is_success() {
300 let res = response.text().await?;
301 return Err(anyhow::anyhow!("Failed to fetch token pairs: {}", res));
302 }
303
304 let data: serde_json::Value = response.json().await?;
305 let dex_response: DexScreenerResponseRaw = serde_json::from_value(data)?;
306
307 Ok(dex_response)
308}
309
310pub async fn get_pair_by_address_v1(chain_id: &str, pair_address: &str) -> Result<PairInfoRaw> {
312 let client = Client::new();
313 let url = format!(
314 "https://api.dexscreener.com/latest/dex/pairs/{}/{}",
315 chain_id, pair_address
316 );
317
318 let response = client.get(&url).send().await?;
319
320 if !response.status().is_success() {
321 let res = response.text().await?;
322 return Err(anyhow::anyhow!("Failed to fetch pair: {}", res));
323 }
324
325 let data: serde_json::Value = response.json().await?;
326 let pair_response: DexScreenerResponseRaw = serde_json::from_value(data)?;
327
328 pair_response
329 .pairs
330 .into_iter()
331 .next()
332 .ok_or_else(|| anyhow::anyhow!("No pair found for address: {}", pair_address))
333}
334
335pub async fn get_pairs_by_token(token_address: &str) -> Result<DexScreenerResponseRaw> {
338 get_pairs_by_token_v1("ethereum", token_address).await
340}
341
342pub async fn get_pair_by_address(pair_address: &str) -> Result<PairInfoRaw> {
345 let chain_id = if pair_address.starts_with("solana_") {
347 "solana"
348 } else if pair_address.starts_with("bsc_") {
349 "bsc"
350 } else {
351 "ethereum"
352 };
353 get_pair_by_address_v1(chain_id, pair_address).await
354}
355
356pub async fn get_token_pairs_v1(
358 chain_id: &str,
359 token_address: &str,
360) -> Result<DexScreenerResponseRaw> {
361 let client = Client::new();
362 let url = format!(
363 "https://api.dexscreener.com/token-pairs/v1/{}/{}",
364 chain_id, token_address
365 );
366
367 let response = client.get(&url).send().await?;
368
369 if !response.status().is_success() {
370 let res = response.text().await?;
371 return Err(anyhow::anyhow!("Failed to fetch token pairs: {}", res));
372 }
373
374 let data: serde_json::Value = response.json().await?;
375 let dex_response: DexScreenerResponseRaw = serde_json::from_value(data)?;
376
377 Ok(dex_response)
378}
379
380pub async fn get_latest_token_profiles() -> Result<Vec<TokenProfile>> {
384 let client = Client::new();
385 let url = "https://api.dexscreener.com/token-profiles/latest/v1";
386
387 let response = client.get(url).send().await?;
388
389 if !response.status().is_success() {
390 let res = response.text().await?;
391 return Err(anyhow::anyhow!("Failed to fetch token profiles: {}", res));
392 }
393
394 let profiles: Vec<TokenProfile> = response.json().await?;
395 Ok(profiles)
396}
397
398pub async fn get_latest_token_boosts() -> Result<Vec<BoostsResponse>> {
400 let client = Client::new();
401 let url = "https://api.dexscreener.com/token-boosts/latest/v1";
402
403 let response = client.get(url).send().await?;
404
405 if !response.status().is_success() {
406 let res = response.text().await?;
407 return Err(anyhow::anyhow!("Failed to fetch latest boosts: {}", res));
408 }
409
410 let boosts: Vec<BoostsResponse> = response.json().await?;
411 Ok(boosts)
412}
413
414pub async fn get_top_token_boosts() -> Result<Vec<BoostsResponse>> {
416 let client = Client::new();
417 let url = "https://api.dexscreener.com/token-boosts/top/v1";
418
419 let response = client.get(url).send().await?;
420
421 if !response.status().is_success() {
422 let res = response.text().await?;
423 return Err(anyhow::anyhow!("Failed to fetch top boosts: {}", res));
424 }
425
426 let boosts: Vec<BoostsResponse> = response.json().await?;
427 Ok(boosts)
428}
429
430pub async fn get_token_orders(chain_id: &str, token_address: &str) -> Result<Vec<Order>> {
432 let client = Client::new();
433 let url = format!(
434 "https://api.dexscreener.com/orders/v1/{}/{}",
435 chain_id, token_address
436 );
437
438 let response = client.get(&url).send().await?;
439
440 if !response.status().is_success() {
441 let res = response.text().await?;
442 return Err(anyhow::anyhow!("Failed to fetch token orders: {}", res));
443 }
444
445 let orders: Vec<Order> = response.json().await?;
446 Ok(orders)
447}
448
449pub fn find_best_liquidity_pair(mut pairs: Vec<PairInfoRaw>) -> Option<PairInfoRaw> {
451 if pairs.is_empty() {
452 return None;
453 }
454
455 pairs.sort_by(|a, b| {
457 let a_liq = a.liquidity.as_ref().and_then(|l| l.usd).unwrap_or(0.0);
458 let b_liq = b.liquidity.as_ref().and_then(|l| l.usd).unwrap_or(0.0);
459 b_liq
460 .partial_cmp(&a_liq)
461 .unwrap_or(std::cmp::Ordering::Equal)
462 });
463
464 pairs.into_iter().next()
465}
466
467pub fn get_token_price(pairs: &[PairInfoRaw], token_address: &str) -> Option<String> {
469 let mut matching_pairs: Vec<_> = pairs
471 .iter()
472 .filter(|p| p.base_token.address.eq_ignore_ascii_case(token_address))
473 .collect();
474
475 matching_pairs.sort_by(|a, b| {
477 let a_liq = a.liquidity.as_ref().and_then(|l| l.usd).unwrap_or(0.0);
478 let b_liq = b.liquidity.as_ref().and_then(|l| l.usd).unwrap_or(0.0);
479 b_liq
480 .partial_cmp(&a_liq)
481 .unwrap_or(std::cmp::Ordering::Equal)
482 });
483
484 matching_pairs.first().and_then(|p| p.price_usd.clone())
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct DexScreenerResponse {
492 pub schema_version: String,
494 pub pairs: Vec<PairInfo>,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct PairInfo {
501 pub chain_id: String,
503 pub dex_id: String,
505 pub url: String,
507 pub pair_address: String,
509 pub labels: Vec<String>,
511 pub base_token: Token,
513 pub quote_token: Token,
515 pub price_native: f64,
517 pub price_usd: Option<f64>,
519 pub liquidity: Option<Liquidity>,
521 pub volume: Option<Volume>,
523 pub price_change: Option<PriceChange>,
525 pub txns: Option<Transactions>,
527 pub market_cap: Option<f64>,
529 pub fdv: Option<f64>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct Token {
536 pub address: String,
538 pub name: String,
540 pub symbol: String,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct Liquidity {
547 pub usd: Option<f64>,
549 pub base: Option<f64>,
551 pub quote: Option<f64>,
553}
554
555#[derive(Debug, Clone, Default, Serialize, Deserialize)]
557pub struct Volume {
558 pub h24: Option<f64>,
560 pub h6: Option<f64>,
562 pub h1: Option<f64>,
564 pub m5: Option<f64>,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct PriceChange {
571 pub h24: Option<f64>,
573 pub h6: Option<f64>,
575 pub h1: Option<f64>,
577 pub m5: Option<f64>,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct Transactions {
584 pub h24: Option<TransactionStats>,
586 pub h6: Option<TransactionStats>,
588 pub h1: Option<TransactionStats>,
590 pub m5: Option<TransactionStats>,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct TransactionStats {
597 pub buys: Option<u64>,
599 pub sells: Option<u64>,
601}
602
603impl From<DexScreenerResponseRaw> for DexScreenerResponse {
606 fn from(raw: DexScreenerResponseRaw) -> Self {
607 Self {
608 schema_version: raw.schema_version,
609 pairs: raw.pairs.into_iter().map(Into::into).collect(),
610 }
611 }
612}
613
614impl From<PairInfoRaw> for PairInfo {
615 fn from(raw: PairInfoRaw) -> Self {
616 Self {
617 chain_id: raw.chain_id,
618 dex_id: raw.dex_id,
619 url: raw.url,
620 pair_address: raw.pair_address,
621 labels: raw.labels.unwrap_or_default(),
622 base_token: raw.base_token.into(),
623 quote_token: raw.quote_token.into(),
624 price_native: raw.price_native.parse().unwrap_or(0.0),
625 price_usd: raw.price_usd.and_then(|p| p.parse().ok()),
626 liquidity: raw.liquidity.map(Into::into),
627 volume: raw.volume.map(Into::into),
628 price_change: raw.price_change.map(Into::into),
629 txns: raw.txns.map(Into::into),
630 market_cap: raw.market_cap,
631 fdv: raw.fdv,
632 }
633 }
634}
635
636impl From<TokenRaw> for Token {
637 fn from(raw: TokenRaw) -> Self {
638 Self {
639 address: raw.address,
640 name: raw.name,
641 symbol: raw.symbol,
642 }
643 }
644}
645
646impl From<LiquidityRaw> for Liquidity {
647 fn from(raw: LiquidityRaw) -> Self {
648 Self {
649 usd: raw.usd,
650 base: raw.base,
651 quote: raw.quote,
652 }
653 }
654}
655
656impl From<VolumeRaw> for Volume {
657 fn from(raw: VolumeRaw) -> Self {
658 Self {
659 h24: raw.h24,
660 h6: raw.h6,
661 h1: raw.h1,
662 m5: raw.m5,
663 }
664 }
665}
666
667impl From<PriceChangeRaw> for PriceChange {
668 fn from(raw: PriceChangeRaw) -> Self {
669 Self {
670 h24: raw.h24,
671 h6: raw.h6,
672 h1: raw.h1,
673 m5: raw.m5,
674 }
675 }
676}
677
678impl From<TransactionsRaw> for Transactions {
679 fn from(raw: TransactionsRaw) -> Self {
680 Self {
681 h24: raw.h24.map(Into::into),
682 h6: raw.h6.map(Into::into),
683 h1: raw.h1.map(Into::into),
684 m5: raw.m5.map(Into::into),
685 }
686 }
687}
688
689impl From<TransactionStatsRaw> for TransactionStats {
690 fn from(raw: TransactionStatsRaw) -> Self {
691 Self {
692 buys: raw.buys,
693 sells: raw.sells,
694 }
695 }
696}
697
698pub fn aggregate_token_info(
700 pairs: Vec<PairInfo>,
701 token_address: &str,
702) -> Option<crate::dexscreener::TokenInfo> {
703 let token_pairs: Vec<PairInfo> = pairs
705 .into_iter()
706 .filter(|p| p.base_token.address.eq_ignore_ascii_case(token_address))
707 .collect();
708
709 if token_pairs.is_empty() {
710 return None;
711 }
712
713 let primary_pair = token_pairs.iter().max_by_key(|p| {
715 p.liquidity
716 .as_ref()
717 .and_then(|l| l.usd)
718 .map_or(0, |usd| (usd * 1000.0) as u64)
719 })?;
720
721 let total_volume_24h: f64 = token_pairs
723 .iter()
724 .filter_map(|p| p.volume.as_ref())
725 .filter_map(|v| v.h24)
726 .sum();
727
728 Some(crate::dexscreener::TokenInfo {
730 address: token_address.to_string(),
731 name: primary_pair.base_token.name.clone(),
732 symbol: primary_pair.base_token.symbol.clone(),
733 decimals: 18, price_usd: primary_pair.price_usd,
735 market_cap: primary_pair.market_cap,
736 volume_24h: Some(total_volume_24h),
737 price_change_24h: primary_pair.price_change.as_ref().and_then(|pc| pc.h24),
738 price_change_1h: primary_pair.price_change.as_ref().and_then(|pc| pc.h1),
739 price_change_5m: primary_pair.price_change.as_ref().and_then(|pc| pc.m5),
740 circulating_supply: None,
741 total_supply: None,
742 pair_count: token_pairs.len() as u32,
743 pairs: token_pairs
744 .iter()
745 .map(|p| convert_to_token_pair(p))
746 .collect(),
747 chain: crate::dexscreener::ChainInfo {
748 id: primary_pair.chain_id.clone(),
749 name: crate::dexscreener::format_chain_name(&primary_pair.chain_id),
750 logo: None,
751 native_token: crate::dexscreener::get_native_token(&primary_pair.chain_id),
752 },
753 security: crate::dexscreener::SecurityInfo {
754 is_verified: false,
755 liquidity_locked: None,
756 audit_status: None,
757 honeypot_status: None,
758 ownership_status: None,
759 risk_score: None,
760 },
761 socials: vec![],
762 updated_at: chrono::Utc::now(),
763 })
764}
765
766fn convert_to_token_pair(pair: &PairInfo) -> crate::dexscreener::TokenPair {
768 crate::dexscreener::TokenPair {
769 pair_id: pair.pair_address.clone(),
770 dex: crate::dexscreener::DexInfo {
771 id: pair.dex_id.clone(),
772 name: crate::dexscreener::format_dex_name(&pair.dex_id),
773 url: Some(pair.url.clone()),
774 logo: None,
775 },
776 base_token: crate::dexscreener::PairToken {
777 address: pair.base_token.address.clone(),
778 name: pair.base_token.name.clone(),
779 symbol: pair.base_token.symbol.clone(),
780 },
781 quote_token: crate::dexscreener::PairToken {
782 address: pair.quote_token.address.clone(),
783 name: pair.quote_token.name.clone(),
784 symbol: pair.quote_token.symbol.clone(),
785 },
786 price_usd: pair.price_usd,
787 price_native: Some(pair.price_native),
788 volume_24h: pair.volume.as_ref().and_then(|v| v.h24),
789 price_change_24h: pair.price_change.as_ref().and_then(|pc| pc.h24),
790 liquidity_usd: pair.liquidity.as_ref().and_then(|l| l.usd),
791 fdv: pair.fdv,
792 created_at: None,
793 last_trade_at: chrono::Utc::now(),
794 txns_24h: {
795 let buys = pair
796 .txns
797 .as_ref()
798 .and_then(|t| t.h24.as_ref())
799 .and_then(|h| h.buys.map(|b| b as u32));
800 let sells = pair
801 .txns
802 .as_ref()
803 .and_then(|t| t.h24.as_ref())
804 .and_then(|h| h.sells.map(|s| s as u32));
805 let total = match (buys, sells) {
806 (Some(b), Some(s)) => Some(b + s),
807 _ => None,
808 };
809
810 let (buy_volume_usd, sell_volume_usd) = if let (Some(volume), Some(b), Some(s)) =
812 (pair.volume.as_ref().and_then(|v| v.h24), buys, sells)
813 {
814 let total_txns = (b + s) as f64;
815 if total_txns > 0.0 {
816 let buy_ratio = b as f64 / total_txns;
817 let sell_ratio = s as f64 / total_txns;
818 (Some(volume * buy_ratio), Some(volume * sell_ratio))
819 } else {
820 (None, None)
821 }
822 } else {
823 (None, None)
824 };
825
826 crate::dexscreener::TransactionStats {
827 buys,
828 sells,
829 total,
830 buy_volume_usd,
831 sell_volume_usd,
832 }
833 },
834 url: pair.url.clone(),
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use serde_json::json;
842
843 fn create_test_token() -> TokenRaw {
845 TokenRaw {
846 address: "0x1234567890123456789012345678901234567890".to_string(),
847 name: "Test Token".to_string(),
848 symbol: "TEST".to_string(),
849 }
850 }
851
852 fn create_test_pair_info() -> PairInfoRaw {
854 PairInfoRaw {
855 chain_id: "ethereum".to_string(),
856 dex_id: "uniswap".to_string(),
857 url: "https://dexscreener.com/test".to_string(),
858 pair_address: "0xabcdef1234567890".to_string(),
859 labels: Some(vec!["test".to_string()]),
860 base_token: create_test_token(),
861 quote_token: TokenRaw {
862 address: "0x9876543210987654321098765432109876543210".to_string(),
863 name: "Quote Token".to_string(),
864 symbol: "QUOTE".to_string(),
865 },
866 price_native: "0.001".to_string(),
867 price_usd: Some("1.50".to_string()),
868 liquidity: Some(LiquidityRaw {
869 usd: Some(100000.0),
870 base: Some(50000.0),
871 quote: Some(50000.0),
872 }),
873 volume: Some(VolumeRaw {
874 h24: Some(10000.0),
875 h6: Some(2500.0),
876 h1: Some(416.0),
877 m5: Some(35.0),
878 }),
879 price_change: Some(PriceChangeRaw {
880 h24: Some(5.5),
881 h6: Some(2.1),
882 h1: Some(0.8),
883 m5: Some(0.1),
884 }),
885 txns: Some(TransactionsRaw {
886 h24: Some(TransactionStatsRaw {
887 buys: Some(100),
888 sells: Some(80),
889 }),
890 h6: Some(TransactionStatsRaw {
891 buys: Some(25),
892 sells: Some(20),
893 }),
894 h1: Some(TransactionStatsRaw {
895 buys: Some(4),
896 sells: Some(3),
897 }),
898 m5: Some(TransactionStatsRaw {
899 buys: Some(1),
900 sells: Some(0),
901 }),
902 }),
903 market_cap: Some(1000000.0),
904 fdv: Some(1500000.0),
905 }
906 }
907
908 #[test]
911 fn test_token_serialization() {
912 let token = create_test_token();
913 let serialized = serde_json::to_string(&token).unwrap();
914 let deserialized: TokenRaw = serde_json::from_str(&serialized).unwrap();
915
916 assert_eq!(token.address, deserialized.address);
917 assert_eq!(token.name, deserialized.name);
918 assert_eq!(token.symbol, deserialized.symbol);
919 }
920
921 #[test]
922 fn test_liquidity_serialization_with_all_fields() {
923 let liquidity = LiquidityRaw {
924 usd: Some(100000.0),
925 base: Some(50000.0),
926 quote: Some(50000.0),
927 };
928 let serialized = serde_json::to_string(&liquidity).unwrap();
929 let deserialized: LiquidityRaw = serde_json::from_str(&serialized).unwrap();
930
931 assert_eq!(liquidity.usd, deserialized.usd);
932 assert_eq!(liquidity.base, deserialized.base);
933 assert_eq!(liquidity.quote, deserialized.quote);
934 }
935
936 #[test]
937 fn test_liquidity_serialization_with_none_fields() {
938 let liquidity = LiquidityRaw {
939 usd: None,
940 base: None,
941 quote: None,
942 };
943 let serialized = serde_json::to_string(&liquidity).unwrap();
944 let deserialized: LiquidityRaw = serde_json::from_str(&serialized).unwrap();
945
946 assert_eq!(liquidity.usd, deserialized.usd);
947 assert_eq!(liquidity.base, deserialized.base);
948 assert_eq!(liquidity.quote, deserialized.quote);
949 }
950
951 #[test]
952 fn test_volume_default_serialization() {
953 let volume = VolumeRaw::default();
954 let serialized = serde_json::to_string(&volume).unwrap();
955 let deserialized: VolumeRaw = serde_json::from_str(&serialized).unwrap();
956
957 assert_eq!(volume.h24, deserialized.h24);
958 assert_eq!(volume.h6, deserialized.h6);
959 assert_eq!(volume.h1, deserialized.h1);
960 assert_eq!(volume.m5, deserialized.m5);
961 }
962
963 #[test]
964 fn test_volume_serialization_with_values() {
965 let volume = VolumeRaw {
966 h24: Some(10000.0),
967 h6: Some(2500.0),
968 h1: Some(416.0),
969 m5: Some(35.0),
970 };
971 let serialized = serde_json::to_string(&volume).unwrap();
972 let deserialized: VolumeRaw = serde_json::from_str(&serialized).unwrap();
973
974 assert_eq!(volume.h24, deserialized.h24);
975 assert_eq!(volume.h6, deserialized.h6);
976 assert_eq!(volume.h1, deserialized.h1);
977 assert_eq!(volume.m5, deserialized.m5);
978 }
979
980 #[test]
981 fn test_price_change_serialization() {
982 let price_change = PriceChangeRaw {
983 h24: Some(5.5),
984 h6: Some(2.1),
985 h1: Some(0.8),
986 m5: Some(0.1),
987 };
988 let serialized = serde_json::to_string(&price_change).unwrap();
989 let deserialized: PriceChangeRaw = serde_json::from_str(&serialized).unwrap();
990
991 assert_eq!(price_change.h24, deserialized.h24);
992 assert_eq!(price_change.h6, deserialized.h6);
993 assert_eq!(price_change.h1, deserialized.h1);
994 assert_eq!(price_change.m5, deserialized.m5);
995 }
996
997 #[test]
998 fn test_transaction_stats_serialization() {
999 let stats = TransactionStatsRaw {
1000 buys: Some(100),
1001 sells: Some(80),
1002 };
1003 let serialized = serde_json::to_string(&stats).unwrap();
1004 let deserialized: TransactionStatsRaw = serde_json::from_str(&serialized).unwrap();
1005
1006 assert_eq!(stats.buys, deserialized.buys);
1007 assert_eq!(stats.sells, deserialized.sells);
1008 }
1009
1010 #[test]
1011 fn test_transaction_stats_serialization_with_none() {
1012 let stats = TransactionStatsRaw {
1013 buys: None,
1014 sells: None,
1015 };
1016 let serialized = serde_json::to_string(&stats).unwrap();
1017 let deserialized: TransactionStatsRaw = serde_json::from_str(&serialized).unwrap();
1018
1019 assert_eq!(stats.buys, deserialized.buys);
1020 assert_eq!(stats.sells, deserialized.sells);
1021 }
1022
1023 #[test]
1024 fn test_transactions_serialization() {
1025 let transactions = TransactionsRaw {
1026 h24: Some(TransactionStatsRaw {
1027 buys: Some(100),
1028 sells: Some(80),
1029 }),
1030 h6: None,
1031 h1: Some(TransactionStatsRaw {
1032 buys: None,
1033 sells: Some(3),
1034 }),
1035 m5: None,
1036 };
1037 let serialized = serde_json::to_string(&transactions).unwrap();
1038 let deserialized: TransactionsRaw = serde_json::from_str(&serialized).unwrap();
1039
1040 assert!(deserialized.h24.is_some());
1041 assert!(deserialized.h6.is_none());
1042 assert!(deserialized.h1.is_some());
1043 assert!(deserialized.m5.is_none());
1044 }
1045
1046 #[test]
1047 fn test_pair_info_serialization_complete() {
1048 let pair = create_test_pair_info();
1049 let serialized = serde_json::to_string(&pair).unwrap();
1050 let deserialized: PairInfoRaw = serde_json::from_str(&serialized).unwrap();
1051
1052 assert_eq!(pair.chain_id, deserialized.chain_id);
1053 assert_eq!(pair.dex_id, deserialized.dex_id);
1054 assert_eq!(pair.url, deserialized.url);
1055 assert_eq!(pair.pair_address, deserialized.pair_address);
1056 assert_eq!(pair.labels, deserialized.labels);
1057 assert_eq!(pair.price_native, deserialized.price_native);
1058 assert_eq!(pair.price_usd, deserialized.price_usd);
1059 assert!(deserialized.liquidity.is_some());
1060 assert!(deserialized.volume.is_some());
1061 assert!(deserialized.price_change.is_some());
1062 assert!(deserialized.txns.is_some());
1063 assert_eq!(pair.market_cap, deserialized.market_cap);
1064 assert_eq!(pair.fdv, deserialized.fdv);
1065 }
1066
1067 #[test]
1068 fn test_pair_info_serialization_minimal() {
1069 let pair = PairInfoRaw {
1070 chain_id: "ethereum".to_string(),
1071 dex_id: "uniswap".to_string(),
1072 url: "https://dexscreener.com/test".to_string(),
1073 pair_address: "0xabcdef1234567890".to_string(),
1074 labels: None,
1075 base_token: create_test_token(),
1076 quote_token: create_test_token(),
1077 price_native: "0.001".to_string(),
1078 price_usd: None,
1079 liquidity: None,
1080 volume: None,
1081 price_change: None,
1082 txns: None,
1083 market_cap: None,
1084 fdv: None,
1085 };
1086 let serialized = serde_json::to_string(&pair).unwrap();
1087 let deserialized: PairInfoRaw = serde_json::from_str(&serialized).unwrap();
1088
1089 assert_eq!(pair.chain_id, deserialized.chain_id);
1090 assert_eq!(pair.dex_id, deserialized.dex_id);
1091 assert!(deserialized.labels.is_none());
1092 assert!(deserialized.price_usd.is_none());
1093 assert!(deserialized.liquidity.is_none());
1094 assert!(deserialized.volume.is_none());
1095 assert!(deserialized.price_change.is_none());
1096 assert!(deserialized.txns.is_none());
1097 assert!(deserialized.market_cap.is_none());
1098 assert!(deserialized.fdv.is_none());
1099 }
1100
1101 #[test]
1102 fn test_dexscreener_response_serialization() {
1103 let response = DexScreenerResponseRaw {
1104 schema_version: "1.0.0".to_string(),
1105 pairs: vec![create_test_pair_info()],
1106 };
1107 let serialized = serde_json::to_string(&response).unwrap();
1108 let deserialized: DexScreenerResponseRaw = serde_json::from_str(&serialized).unwrap();
1109
1110 assert_eq!(response.schema_version, deserialized.schema_version);
1111 assert_eq!(response.pairs.len(), deserialized.pairs.len());
1112 }
1113
1114 #[test]
1115 fn test_dexscreener_response_empty_pairs() {
1116 let response = DexScreenerResponseRaw {
1117 schema_version: "1.0.0".to_string(),
1118 pairs: vec![],
1119 };
1120 let serialized = serde_json::to_string(&response).unwrap();
1121 let deserialized: DexScreenerResponseRaw = serde_json::from_str(&serialized).unwrap();
1122
1123 assert_eq!(response.schema_version, deserialized.schema_version);
1124 assert!(deserialized.pairs.is_empty());
1125 }
1126
1127 #[test]
1130 fn test_find_best_liquidity_pair_when_empty_should_return_none() {
1131 let pairs = vec![];
1132 let result = find_best_liquidity_pair(pairs);
1133 assert!(result.is_none());
1134 }
1135
1136 #[test]
1137 fn test_find_best_liquidity_pair_when_no_liquidity_should_return_first() {
1138 let mut pair1 = create_test_pair_info();
1139 pair1.liquidity = None;
1140 let mut pair2 = create_test_pair_info();
1141 pair2.liquidity = None;
1142 let pairs = vec![pair1.clone(), pair2];
1143 let result = find_best_liquidity_pair(pairs);
1144
1145 assert!(result.is_some());
1146 assert_eq!(result.unwrap().pair_address, pair1.pair_address);
1147 }
1148
1149 #[test]
1150 fn test_find_best_liquidity_pair_when_liquidity_none_usd_should_return_first() {
1151 let mut pair1 = create_test_pair_info();
1152 pair1.liquidity = Some(LiquidityRaw {
1153 usd: None,
1154 base: Some(100.0),
1155 quote: Some(200.0),
1156 });
1157 let mut pair2 = create_test_pair_info();
1158 pair2.liquidity = Some(LiquidityRaw {
1159 usd: None,
1160 base: Some(300.0),
1161 quote: Some(400.0),
1162 });
1163 let pairs = vec![pair1.clone(), pair2];
1164 let result = find_best_liquidity_pair(pairs);
1165
1166 assert!(result.is_some());
1167 assert_eq!(result.unwrap().pair_address, pair1.pair_address);
1168 }
1169
1170 #[test]
1171 fn test_find_best_liquidity_pair_when_has_liquidity_should_return_highest() {
1172 let mut pair1 = create_test_pair_info();
1173 pair1.pair_address = "low_liquidity".to_string();
1174 pair1.liquidity = Some(LiquidityRaw {
1175 usd: Some(50000.0),
1176 base: Some(25000.0),
1177 quote: Some(25000.0),
1178 });
1179 let mut pair2 = create_test_pair_info();
1180 pair2.pair_address = "high_liquidity".to_string();
1181 pair2.liquidity = Some(LiquidityRaw {
1182 usd: Some(200000.0),
1183 base: Some(100000.0),
1184 quote: Some(100000.0),
1185 });
1186 let mut pair3 = create_test_pair_info();
1187 pair3.pair_address = "medium_liquidity".to_string();
1188 pair3.liquidity = Some(LiquidityRaw {
1189 usd: Some(100000.0),
1190 base: Some(50000.0),
1191 quote: Some(50000.0),
1192 });
1193 let pairs = vec![pair1, pair2.clone(), pair3];
1194 let result = find_best_liquidity_pair(pairs);
1195
1196 assert!(result.is_some());
1197 assert_eq!(result.unwrap().pair_address, "high_liquidity");
1198 }
1199
1200 #[test]
1201 fn test_find_best_liquidity_pair_when_mixed_liquidity_should_return_highest() {
1202 let mut pair1 = create_test_pair_info();
1203 pair1.pair_address = "no_liquidity".to_string();
1204 pair1.liquidity = None;
1205 let mut pair2 = create_test_pair_info();
1206 pair2.pair_address = "has_liquidity".to_string();
1207 pair2.liquidity = Some(LiquidityRaw {
1208 usd: Some(150000.0),
1209 base: Some(75000.0),
1210 quote: Some(75000.0),
1211 });
1212 let mut pair3 = create_test_pair_info();
1213 pair3.pair_address = "none_usd".to_string();
1214 pair3.liquidity = Some(LiquidityRaw {
1215 usd: None,
1216 base: Some(50000.0),
1217 quote: Some(50000.0),
1218 });
1219 let pairs = vec![pair1, pair2.clone(), pair3];
1220 let result = find_best_liquidity_pair(pairs);
1221
1222 assert!(result.is_some());
1223 assert_eq!(result.unwrap().pair_address, "has_liquidity");
1224 }
1225
1226 #[test]
1227 fn test_get_token_price_when_empty_pairs_should_return_none() {
1228 let pairs = vec![];
1229 let result = get_token_price(&pairs, "0x1234567890123456789012345678901234567890");
1230 assert!(result.is_none());
1231 }
1232
1233 #[test]
1234 fn test_get_token_price_when_no_matching_address_should_return_none() {
1235 let pairs = vec![create_test_pair_info()];
1236 let result = get_token_price(&pairs, "0xnonexistent");
1237 assert!(result.is_none());
1238 }
1239
1240 #[test]
1241 fn test_get_token_price_when_matching_address_case_insensitive_should_return_price() {
1242 let mut pair = create_test_pair_info();
1243 pair.base_token.address = "0xABCDEF1234567890".to_string();
1244 pair.price_usd = Some("2.50".to_string());
1245 let pairs = vec![pair];
1246
1247 let result = get_token_price(&pairs, "0xabcdef1234567890");
1248 assert!(result.is_some());
1249 assert_eq!(result.unwrap(), "2.50");
1250 }
1251
1252 #[test]
1253 fn test_get_token_price_when_matching_address_uppercase_should_return_price() {
1254 let mut pair = create_test_pair_info();
1255 pair.base_token.address = "0xabcdef1234567890".to_string();
1256 pair.price_usd = Some("3.75".to_string());
1257 let pairs = vec![pair];
1258
1259 let result = get_token_price(&pairs, "0XABCDEF1234567890");
1260 assert!(result.is_some());
1261 assert_eq!(result.unwrap(), "3.75");
1262 }
1263
1264 #[test]
1265 fn test_get_token_price_when_no_price_usd_should_return_none() {
1266 let mut pair = create_test_pair_info();
1267 pair.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1268 pair.price_usd = None;
1269 let pairs = vec![pair];
1270
1271 let result = get_token_price(&pairs, "0x1234567890123456789012345678901234567890");
1272 assert!(result.is_none());
1273 }
1274
1275 #[test]
1276 fn test_get_token_price_when_multiple_pairs_should_return_highest_liquidity() {
1277 let mut pair1 = create_test_pair_info();
1278 pair1.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1279 pair1.price_usd = Some("1.00".to_string());
1280 pair1.liquidity = Some(LiquidityRaw {
1281 usd: Some(50000.0),
1282 base: Some(25000.0),
1283 quote: Some(25000.0),
1284 });
1285
1286 let mut pair2 = create_test_pair_info();
1287 pair2.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1288 pair2.price_usd = Some("1.05".to_string());
1289 pair2.liquidity = Some(LiquidityRaw {
1290 usd: Some(150000.0),
1291 base: Some(75000.0),
1292 quote: Some(75000.0),
1293 });
1294
1295 let pairs = vec![pair1, pair2];
1296 let result = get_token_price(&pairs, "0x1234567890123456789012345678901234567890");
1297
1298 assert!(result.is_some());
1299 assert_eq!(result.unwrap(), "1.05");
1300 }
1301
1302 #[test]
1303 fn test_get_token_price_when_multiple_pairs_no_liquidity_should_return_first_match() {
1304 let mut pair1 = create_test_pair_info();
1305 pair1.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1306 pair1.price_usd = Some("1.00".to_string());
1307 pair1.liquidity = None;
1308
1309 let mut pair2 = create_test_pair_info();
1310 pair2.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1311 pair2.price_usd = Some("1.05".to_string());
1312 pair2.liquidity = None;
1313
1314 let pairs = vec![pair1, pair2];
1315 let result = get_token_price(&pairs, "0x1234567890123456789012345678901234567890");
1316
1317 assert!(result.is_some());
1318 assert_eq!(result.unwrap(), "1.00");
1319 }
1320
1321 #[test]
1322 fn test_get_token_price_when_mixed_liquidity_should_prefer_with_liquidity() {
1323 let mut pair1 = create_test_pair_info();
1324 pair1.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1325 pair1.price_usd = Some("1.00".to_string());
1326 pair1.liquidity = None;
1327
1328 let mut pair2 = create_test_pair_info();
1329 pair2.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1330 pair2.price_usd = Some("1.05".to_string());
1331 pair2.liquidity = Some(LiquidityRaw {
1332 usd: Some(100000.0),
1333 base: Some(50000.0),
1334 quote: Some(50000.0),
1335 });
1336
1337 let pairs = vec![pair1, pair2];
1338 let result = get_token_price(&pairs, "0x1234567890123456789012345678901234567890");
1339
1340 assert!(result.is_some());
1341 assert_eq!(result.unwrap(), "1.05");
1342 }
1343
1344 #[test]
1346 fn test_deserialize_pair_info_with_camel_case() {
1347 let json = json!({
1348 "chainId": "ethereum",
1349 "dexId": "uniswap",
1350 "url": "https://dexscreener.com/test",
1351 "pairAddress": "0xabcdef1234567890",
1352 "labels": ["test"],
1353 "baseToken": {
1354 "address": "0x1234567890123456789012345678901234567890",
1355 "name": "Test Token",
1356 "symbol": "TEST"
1357 },
1358 "quoteToken": {
1359 "address": "0x9876543210987654321098765432109876543210",
1360 "name": "Quote Token",
1361 "symbol": "QUOTE"
1362 },
1363 "priceNative": "0.001",
1364 "priceUsd": "1.50",
1365 "liquidity": {
1366 "usd": 100000.0,
1367 "base": 50000.0,
1368 "quote": 50000.0
1369 },
1370 "volume": {
1371 "h24": 10000.0,
1372 "h6": 2500.0,
1373 "h1": 416.0,
1374 "m5": 35.0
1375 },
1376 "priceChange": {
1377 "h24": 5.5,
1378 "h6": 2.1,
1379 "h1": 0.8,
1380 "m5": 0.1
1381 },
1382 "txns": {
1383 "h24": {
1384 "buys": 100,
1385 "sells": 80
1386 },
1387 "h6": {
1388 "buys": 25,
1389 "sells": 20
1390 },
1391 "h1": {
1392 "buys": 4,
1393 "sells": 3
1394 },
1395 "m5": {
1396 "buys": 1,
1397 "sells": 0
1398 }
1399 },
1400 "marketCap": 1000000.0,
1401 "fdv": 1500000.0
1402 });
1403
1404 let pair: PairInfoRaw = serde_json::from_value(json).unwrap();
1405 assert_eq!(pair.chain_id, "ethereum");
1406 assert_eq!(pair.dex_id, "uniswap");
1407 assert_eq!(pair.pair_address, "0xabcdef1234567890");
1408 assert_eq!(
1409 pair.base_token.address,
1410 "0x1234567890123456789012345678901234567890"
1411 );
1412 assert_eq!(pair.quote_token.symbol, "QUOTE");
1413 assert_eq!(pair.price_native, "0.001");
1414 assert_eq!(pair.price_usd, Some("1.50".to_string()));
1415 assert!(pair.liquidity.is_some());
1416 assert!(pair.volume.is_some());
1417 assert!(pair.price_change.is_some());
1418 assert!(pair.txns.is_some());
1419 assert_eq!(pair.market_cap, Some(1000000.0));
1420 assert_eq!(pair.fdv, Some(1500000.0));
1421 }
1422
1423 #[test]
1424 fn test_deserialize_dexscreener_response_with_camel_case() {
1425 let json = json!({
1426 "schemaVersion": "1.0.0",
1427 "pairs": []
1428 });
1429
1430 let response: DexScreenerResponseRaw = serde_json::from_value(json).unwrap();
1431 assert_eq!(response.schema_version, "1.0.0");
1432 assert!(response.pairs.is_empty());
1433 }
1434
1435 #[test]
1437 fn test_volume_default_values() {
1438 let volume = VolumeRaw::default();
1439 assert!(volume.h24.is_none());
1440 assert!(volume.h6.is_none());
1441 assert!(volume.h1.is_none());
1442 assert!(volume.m5.is_none());
1443 }
1444
1445 #[test]
1446 fn test_volume_deserialization_with_missing_fields() {
1447 let json = json!({});
1448 let volume: VolumeRaw = serde_json::from_value(json).unwrap();
1449 assert!(volume.h24.is_none());
1450 assert!(volume.h6.is_none());
1451 assert!(volume.h1.is_none());
1452 assert!(volume.m5.is_none());
1453 }
1454
1455 #[test]
1456 fn test_price_change_deserialization_with_missing_fields() {
1457 let json = json!({});
1458 let price_change: PriceChangeRaw = serde_json::from_value(json).unwrap();
1459 assert!(price_change.h24.is_none());
1460 assert!(price_change.h6.is_none());
1461 assert!(price_change.h1.is_none());
1462 assert!(price_change.m5.is_none());
1463 }
1464
1465 #[test]
1466 fn test_transactions_deserialization_with_missing_fields() {
1467 let json = json!({});
1468 let transactions: TransactionsRaw = serde_json::from_value(json).unwrap();
1469 assert!(transactions.h24.is_none());
1470 assert!(transactions.h6.is_none());
1471 assert!(transactions.h1.is_none());
1472 assert!(transactions.m5.is_none());
1473 }
1474
1475 #[test]
1478 fn test_find_best_liquidity_pair_with_single_pair() {
1479 let pair = create_test_pair_info();
1480 let pairs = vec![pair.clone()];
1481 let result = find_best_liquidity_pair(pairs);
1482
1483 assert!(result.is_some());
1484 assert_eq!(result.unwrap().pair_address, pair.pair_address);
1485 }
1486
1487 #[test]
1488 fn test_find_best_liquidity_pair_with_zero_liquidity() {
1489 let mut pair1 = create_test_pair_info();
1490 pair1.pair_address = "zero_liquidity_1".to_string();
1491 pair1.liquidity = Some(LiquidityRaw {
1492 usd: Some(0.0),
1493 base: Some(0.0),
1494 quote: Some(0.0),
1495 });
1496 let mut pair2 = create_test_pair_info();
1497 pair2.pair_address = "zero_liquidity_2".to_string();
1498 pair2.liquidity = Some(LiquidityRaw {
1499 usd: Some(0.0),
1500 base: Some(100.0),
1501 quote: Some(200.0),
1502 });
1503 let pairs = vec![pair1.clone(), pair2];
1504 let result = find_best_liquidity_pair(pairs);
1505
1506 assert!(result.is_some());
1507 assert_eq!(result.unwrap().pair_address, "zero_liquidity_1");
1508 }
1509
1510 #[test]
1511 fn test_find_best_liquidity_pair_with_negative_liquidity() {
1512 let mut pair = create_test_pair_info();
1513 pair.pair_address = "negative_liquidity".to_string();
1514 pair.liquidity = Some(LiquidityRaw {
1515 usd: Some(-1000.0),
1516 base: Some(-500.0),
1517 quote: Some(-500.0),
1518 });
1519 let pairs = vec![pair.clone()];
1520 let result = find_best_liquidity_pair(pairs);
1521
1522 assert!(result.is_some());
1523 assert_eq!(result.unwrap().pair_address, "negative_liquidity");
1524 }
1525
1526 #[test]
1527 fn test_get_token_price_with_empty_string_address() {
1528 let pairs = vec![create_test_pair_info()];
1529 let result = get_token_price(&pairs, "");
1530 assert!(result.is_none());
1531 }
1532
1533 #[test]
1534 fn test_get_token_price_with_partial_match() {
1535 let mut pair = create_test_pair_info();
1536 pair.base_token.address = "0x1234567890123456789012345678901234567890".to_string();
1537 let pairs = vec![pair];
1538
1539 let result = get_token_price(&pairs, "0x1234567890");
1541 assert!(result.is_none());
1542 }
1543
1544 #[test]
1545 fn test_get_token_price_with_special_characters() {
1546 let mut pair = create_test_pair_info();
1547 pair.base_token.address = "0x!@#$%^&*()_+".to_string();
1548 pair.price_usd = Some("1.00".to_string());
1549 let pairs = vec![pair];
1550
1551 let result = get_token_price(&pairs, "0x!@#$%^&*()_+");
1552 assert!(result.is_some());
1553 assert_eq!(result.unwrap(), "1.00");
1554 }
1555
1556 #[test]
1557 fn test_get_token_price_case_sensitivity_mixed() {
1558 let mut pair = create_test_pair_info();
1559 pair.base_token.address = "0xaBcDeF1234567890".to_string();
1560 pair.price_usd = Some("2.50".to_string());
1561 let pairs = vec![pair];
1562
1563 let result = get_token_price(&pairs, "0xAbCdEf1234567890");
1564 assert!(result.is_some());
1565 assert_eq!(result.unwrap(), "2.50");
1566 }
1567
1568 #[test]
1570 fn test_token_debug_format() {
1571 let token = create_test_token();
1572 let debug_str = format!("{:?}", token);
1573 assert!(debug_str.contains("Test Token"));
1574 assert!(debug_str.contains("TEST"));
1575 assert!(debug_str.contains("0x1234567890123456789012345678901234567890"));
1576 }
1577
1578 #[test]
1579 fn test_liquidity_debug_format() {
1580 let liquidity = LiquidityRaw {
1581 usd: Some(100000.0),
1582 base: Some(50000.0),
1583 quote: Some(50000.0),
1584 };
1585 let debug_str = format!("{:?}", liquidity);
1586 assert!(debug_str.contains("100000"));
1587 assert!(debug_str.contains("50000"));
1588 }
1589
1590 #[test]
1591 fn test_volume_debug_format() {
1592 let volume = VolumeRaw {
1593 h24: Some(10000.0),
1594 h6: Some(2500.0),
1595 h1: Some(416.0),
1596 m5: Some(35.0),
1597 };
1598 let debug_str = format!("{:?}", volume);
1599 assert!(debug_str.contains("10000"));
1600 assert!(debug_str.contains("2500"));
1601 assert!(debug_str.contains("416"));
1602 assert!(debug_str.contains("35"));
1603 }
1604
1605 #[test]
1606 fn test_price_change_debug_format() {
1607 let price_change = PriceChangeRaw {
1608 h24: Some(5.5),
1609 h6: Some(2.1),
1610 h1: Some(0.8),
1611 m5: Some(0.1),
1612 };
1613 let debug_str = format!("{:?}", price_change);
1614 assert!(debug_str.contains("5.5"));
1615 assert!(debug_str.contains("2.1"));
1616 assert!(debug_str.contains("0.8"));
1617 assert!(debug_str.contains("0.1"));
1618 }
1619
1620 #[test]
1621 fn test_transaction_stats_debug_format() {
1622 let stats = TransactionStatsRaw {
1623 buys: Some(100),
1624 sells: Some(80),
1625 };
1626 let debug_str = format!("{:?}", stats);
1627 assert!(debug_str.contains("100"));
1628 assert!(debug_str.contains("80"));
1629 }
1630
1631 #[test]
1632 fn test_transactions_debug_format() {
1633 let transactions = TransactionsRaw {
1634 h24: Some(TransactionStatsRaw {
1635 buys: Some(100),
1636 sells: Some(80),
1637 }),
1638 h6: None,
1639 h1: None,
1640 m5: None,
1641 };
1642 let debug_str = format!("{:?}", transactions);
1643 assert!(debug_str.contains("100"));
1644 assert!(debug_str.contains("80"));
1645 }
1646
1647 #[test]
1648 fn test_pair_info_debug_format() {
1649 let pair = create_test_pair_info();
1650 let debug_str = format!("{:?}", pair);
1651 assert!(debug_str.contains("ethereum"));
1652 assert!(debug_str.contains("uniswap"));
1653 assert!(debug_str.contains("0xabcdef1234567890"));
1654 }
1655
1656 #[test]
1657 fn test_dexscreener_response_debug_format() {
1658 let response = DexScreenerResponseRaw {
1659 schema_version: "1.0.0".to_string(),
1660 pairs: vec![create_test_pair_info()],
1661 };
1662 let debug_str = format!("{:?}", response);
1663 assert!(debug_str.contains("1.0.0"));
1664 assert!(debug_str.contains("ethereum"));
1665 }
1666
1667 #[test]
1669 fn test_token_clone() {
1670 let token = create_test_token();
1671 let cloned = token.clone();
1672 assert_eq!(token.address, cloned.address);
1673 assert_eq!(token.name, cloned.name);
1674 assert_eq!(token.symbol, cloned.symbol);
1675 }
1676
1677 #[test]
1678 fn test_liquidity_clone() {
1679 let liquidity = LiquidityRaw {
1680 usd: Some(100000.0),
1681 base: Some(50000.0),
1682 quote: Some(50000.0),
1683 };
1684 let cloned = liquidity.clone();
1685 assert_eq!(liquidity.usd, cloned.usd);
1686 assert_eq!(liquidity.base, cloned.base);
1687 assert_eq!(liquidity.quote, cloned.quote);
1688 }
1689
1690 #[test]
1691 fn test_volume_clone() {
1692 let volume = VolumeRaw {
1693 h24: Some(10000.0),
1694 h6: Some(2500.0),
1695 h1: Some(416.0),
1696 m5: Some(35.0),
1697 };
1698 let cloned = volume.clone();
1699 assert_eq!(volume.h24, cloned.h24);
1700 assert_eq!(volume.h6, cloned.h6);
1701 assert_eq!(volume.h1, cloned.h1);
1702 assert_eq!(volume.m5, cloned.m5);
1703 }
1704
1705 #[test]
1706 fn test_price_change_clone() {
1707 let price_change = PriceChangeRaw {
1708 h24: Some(5.5),
1709 h6: Some(2.1),
1710 h1: Some(0.8),
1711 m5: Some(0.1),
1712 };
1713 let cloned = price_change.clone();
1714 assert_eq!(price_change.h24, cloned.h24);
1715 assert_eq!(price_change.h6, cloned.h6);
1716 assert_eq!(price_change.h1, cloned.h1);
1717 assert_eq!(price_change.m5, cloned.m5);
1718 }
1719
1720 #[test]
1721 fn test_transaction_stats_clone() {
1722 let stats = TransactionStatsRaw {
1723 buys: Some(100),
1724 sells: Some(80),
1725 };
1726 let cloned = stats.clone();
1727 assert_eq!(stats.buys, cloned.buys);
1728 assert_eq!(stats.sells, cloned.sells);
1729 }
1730
1731 #[test]
1732 fn test_transactions_clone() {
1733 let transactions = TransactionsRaw {
1734 h24: Some(TransactionStatsRaw {
1735 buys: Some(100),
1736 sells: Some(80),
1737 }),
1738 h6: None,
1739 h1: None,
1740 m5: None,
1741 };
1742 let cloned = transactions.clone();
1743 assert!(cloned.h24.is_some());
1744 assert!(cloned.h6.is_none());
1745 assert!(cloned.h1.is_none());
1746 assert!(cloned.m5.is_none());
1747 }
1748
1749 #[test]
1750 fn test_pair_info_clone() {
1751 let pair = create_test_pair_info();
1752 let cloned = pair.clone();
1753 assert_eq!(pair.chain_id, cloned.chain_id);
1754 assert_eq!(pair.dex_id, cloned.dex_id);
1755 assert_eq!(pair.url, cloned.url);
1756 assert_eq!(pair.pair_address, cloned.pair_address);
1757 }
1758
1759 #[test]
1760 fn test_dexscreener_response_clone() {
1761 let response = DexScreenerResponseRaw {
1762 schema_version: "1.0.0".to_string(),
1763 pairs: vec![create_test_pair_info()],
1764 };
1765 let cloned = response.clone();
1766 assert_eq!(response.schema_version, cloned.schema_version);
1767 assert_eq!(response.pairs.len(), cloned.pairs.len());
1768 }
1769
1770 #[test]
1772 fn test_find_best_liquidity_pair_with_very_large_numbers() {
1773 let mut pair = create_test_pair_info();
1774 pair.pair_address = "large_liquidity".to_string();
1775 pair.liquidity = Some(LiquidityRaw {
1776 usd: Some(f64::MAX),
1777 base: Some(f64::MAX),
1778 quote: Some(f64::MAX),
1779 });
1780 let pairs = vec![pair.clone()];
1781 let result = find_best_liquidity_pair(pairs);
1782
1783 assert!(result.is_some());
1784 assert_eq!(result.unwrap().pair_address, "large_liquidity");
1785 }
1786
1787 #[test]
1788 fn test_find_best_liquidity_pair_with_infinity() {
1789 let mut pair = create_test_pair_info();
1790 pair.pair_address = "infinity_liquidity".to_string();
1791 pair.liquidity = Some(LiquidityRaw {
1792 usd: Some(f64::INFINITY),
1793 base: Some(100.0),
1794 quote: Some(200.0),
1795 });
1796 let pairs = vec![pair.clone()];
1797 let result = find_best_liquidity_pair(pairs);
1798
1799 assert!(result.is_some());
1800 assert_eq!(result.unwrap().pair_address, "infinity_liquidity");
1801 }
1802
1803 #[test]
1804 fn test_find_best_liquidity_pair_with_nan() {
1805 let mut pair = create_test_pair_info();
1806 pair.pair_address = "nan_liquidity".to_string();
1807 pair.liquidity = Some(LiquidityRaw {
1808 usd: Some(f64::NAN),
1809 base: Some(100.0),
1810 quote: Some(200.0),
1811 });
1812 let pairs = vec![pair.clone()];
1813 let result = find_best_liquidity_pair(pairs);
1814
1815 assert!(result.is_some());
1816 assert_eq!(result.unwrap().pair_address, "nan_liquidity");
1817 }
1818
1819 #[tokio::test]
1821 async fn test_search_ticker_truncates_to_8_results() {
1822 let response = search_ticker("ETH".to_string()).await.unwrap();
1825 assert!(response.pairs.len() <= 8);
1826 }
1827
1828 #[tokio::test]
1830 async fn test_search_ticker() {
1831 let response = search_ticker("BONK".to_string()).await.unwrap();
1832 assert_eq!(response.schema_version, "1.0.0");
1833 assert!(!response.pairs.is_empty());
1834 }
1835
1836 #[tokio::test]
1837 async fn test_search_by_mint() {
1838 let response = search_ticker(
1839 "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263".to_string(), )
1841 .await
1842 .unwrap();
1843 assert_eq!(response.schema_version, "1.0.0");
1844 assert!(!response.pairs.is_empty());
1845 }
1846
1847 #[tokio::test]
1848 async fn test_get_pairs_by_token() {
1849 let response = get_pairs_by_token("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") .await
1851 .unwrap();
1852 assert!(!response.pairs.is_empty());
1853 }
1854
1855 #[tokio::test]
1856 async fn test_get_pair_by_address_success() {
1857 let result =
1859 get_pair_by_address("ethereum_0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852").await;
1860 match result {
1861 Ok(pair) => {
1862 assert!(!pair.pair_address.is_empty());
1863 assert!(!pair.chain_id.is_empty());
1864 assert!(!pair.dex_id.is_empty());
1865 }
1866 Err(_) => {
1867 }
1870 }
1871 }
1872
1873 #[tokio::test]
1874 async fn test_get_pair_by_address_nonexistent() {
1875 let result = get_pair_by_address("invalid_pair_address_that_does_not_exist").await;
1877 assert!(result.is_err());
1878 }
1879}