use super::SolClient;
use crate::config::{DEXSCREENER_BASE, DEXSCREENER_CHAIN};
use anyhow::Result;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TokenRef {
pub address: String,
#[serde(default)]
pub symbol: String,
#[serde(default)]
pub name: String,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Liquidity {
pub usd: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Socials {
#[serde(default)]
pub websites: Vec<serde_json::Value>,
#[serde(default)]
pub socials: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TxnBucket {
#[serde(default)]
pub buys: u64,
#[serde(default)]
pub sells: u64,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Txns {
#[serde(default)]
pub m5: TxnBucket,
#[serde(default)]
pub h1: TxnBucket,
#[serde(default)]
pub h6: TxnBucket,
#[serde(default)]
pub h24: TxnBucket,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct PriceChange {
pub m5: Option<f64>,
pub h1: Option<f64>,
pub h6: Option<f64>,
pub h24: Option<f64>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Volume {
pub m5: Option<f64>,
pub h1: Option<f64>,
pub h6: Option<f64>,
pub h24: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DexPair {
#[serde(rename = "chainId")]
pub chain_id: String,
#[serde(rename = "dexId", default)]
pub dex_id: String,
#[serde(rename = "pairAddress")]
pub pair_address: String,
#[serde(default)]
pub labels: Vec<String>,
#[serde(rename = "baseToken")]
pub base_token: TokenRef,
#[serde(rename = "quoteToken", default)]
pub quote_token: TokenRef,
#[serde(rename = "priceUsd")]
pub price_usd: Option<String>,
pub fdv: Option<f64>,
#[serde(rename = "marketCap")]
pub market_cap: Option<f64>,
pub liquidity: Option<Liquidity>,
#[serde(default)]
pub txns: Txns,
#[serde(rename = "priceChange", default)]
pub price_change: PriceChange,
#[serde(default)]
pub volume: Volume,
#[serde(rename = "pairCreatedAt")]
pub pair_created_at: Option<i64>,
pub info: Option<Socials>,
}
#[derive(Debug, Clone, Deserialize, Default)]
struct PairsResp {
pairs: Option<Vec<DexPair>>,
}
impl SolClient {
async fn ds(&self, path: &str) -> Result<Vec<DexPair>> {
let url = format!("{DEXSCREENER_BASE}{path}");
let mut last = anyhow::anyhow!("dexscreener {path} failed");
for attempt in 0..3u32 {
if attempt > 0 {
tokio::time::sleep(std::time::Duration::from_millis(400 * attempt as u64)).await;
}
match self.http.get(&url).send().await {
Ok(resp) => {
let status = resp.status();
if status.as_u16() == 429 || status.is_server_error() {
last = anyhow::anyhow!("dexscreener HTTP {status}");
continue;
}
if !status.is_success() {
return Err(anyhow::anyhow!("dexscreener HTTP {status}"));
}
match resp.json::<PairsResp>().await {
Ok(r) => return Ok(r.pairs.unwrap_or_default()),
Err(e) => {
last = anyhow::anyhow!("dexscreener decode: {e}");
continue;
}
}
}
Err(e) => last = anyhow::anyhow!("dexscreener: {e}"),
}
}
Err(last)
}
pub async fn pairs_for_token(&self, mint: &str) -> Result<Vec<DexPair>> {
let mut pairs: Vec<DexPair> = self
.ds(&format!("/latest/dex/tokens/{mint}"))
.await?
.into_iter()
.filter(|p| p.chain_id == DEXSCREENER_CHAIN && p.base_token.address == mint)
.collect();
pairs.sort_by(|a, b| {
liq(b)
.partial_cmp(&liq(a))
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(pairs)
}
pub async fn find_copycats(&self, symbol: &str, real: &str) -> Result<Vec<DexPair>> {
let hits = self.ds(&format!("/latest/dex/search?q={symbol}")).await?;
let mut seen = std::collections::HashSet::new();
let mut out = vec![];
for p in hits {
if p.chain_id != DEXSCREENER_CHAIN {
continue;
}
if !p.base_token.symbol.eq_ignore_ascii_case(symbol) {
continue;
}
if p.base_token.address == real {
continue;
}
if seen.insert(p.base_token.address.clone()) {
out.push(p);
}
}
out.sort_by(|a, b| {
liq(b)
.partial_cmp(&liq(a))
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(out)
}
}
pub fn liq(p: &DexPair) -> f64 {
p.liquidity.as_ref().and_then(|l| l.usd).unwrap_or(0.0)
}