use std::fmt::Display;
pub use reqwest::{self, Client as RClient, IntoUrl, Url};
use reqwest::{header, RequestBuilder};
mod pair;
pub use pair::{Liquidity, Pair, Timed, Token, Transactions};
mod response;
pub use response::{ClientError, PairResponse, Result};
pub const BASE_URL: &str = "https://api.dexscreener.com/latest/";
#[derive(Clone, Debug)]
pub struct Client {
pub client: RClient,
pub url: Url,
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
pub fn new() -> Self {
Self::with_url(BASE_URL).unwrap()
}
pub fn with_url(url: impl IntoUrl) -> Result<Self> {
Self::with_url_and_client(url, RClient::new())
}
pub fn with_url_and_client(url: impl IntoUrl, client: RClient) -> Result<Self> {
Ok(Self { client, url: url.into_url()? })
}
async fn get_pair(&self, path: &str) -> Result<PairResponse> {
Ok(self._get(path)?.send().await?.error_for_status()?.json().await?)
}
fn _get(&self, path: &str) -> Result<RequestBuilder> {
let url = self.url.join(path)?;
Ok(self.client.get(url).header(header::ACCEPT, "application/json"))
}
}
impl Client {
pub async fn pairs(
&self,
chain_id: impl Display,
pair_addresses: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<PairResponse> {
let addresses = format_addresses(pair_addresses)?;
let path = format!("dex/pairs/{chain_id}/{addresses}");
self.get_pair(&path).await
}
pub async fn tokens(
&self,
token_addresses: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<PairResponse> {
let addresses = format_addresses(token_addresses)?;
let path = format!("dex/tokens/{addresses}");
self.get_pair(&path).await
}
pub async fn search(&self, query: impl AsRef<str>) -> Result<PairResponse> {
Ok(self
._get("dex/search")?
.query(&[("q", query.as_ref())])
.send()
.await?
.error_for_status()?
.json()
.await?)
}
}
pub fn format_addresses(
pair_addresses: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<String> {
let mut iter = pair_addresses.into_iter();
let first = match iter.next() {
Some(first) => first,
None => return Ok(String::new()),
};
let cap = iter.size_hint().1.unwrap_or(5);
let mut out = String::with_capacity(cap * 45);
format_address(first.as_ref(), &mut out)?;
for address in iter {
out.push(',');
format_address(address.as_ref(), &mut out)?;
}
Ok(out)
}
fn format_address(address: &str, out: &mut String) -> Result<()> {
match address.len() {
40 if address.chars().all(|c| c.is_ascii_hexdigit()) => {
out.push('0');
out.push('x');
out.push_str(address);
Ok(())
}
42 if address.starts_with("0x")
&& address.chars().skip(2).all(|c| c.is_ascii_hexdigit()) =>
{
out.push_str(address);
Ok(())
}
44 if address.chars().all(|c| c.is_ascii_alphanumeric()) => {
out.push_str(address);
Ok(())
}
_ => Err(ClientError::InvalidAddress(address.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_pairs() {
let client = Client::new();
let pair_addresses = [
"0x7213a321F1855CF1779f42c0CD85d3D95291D34C".to_string(),
"0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae".to_string(),
];
let result = client.pairs("bsc", pair_addresses.clone()).await.unwrap().pairs.unwrap();
assert_eq!(result.len(), 2);
}
#[tokio::test]
async fn test_tokens() {
let client = Client::new();
let token_addresses = [
"0x2170Ed0880ac9A755fd29B2688956BD959F933F8".to_string(),
"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c".to_string(),
];
let result = client.tokens(token_addresses.clone()).await.unwrap().pairs.unwrap();
assert!(result.len() > 20);
}
#[tokio::test]
async fn test_search() {
let client = Client::new();
let result = client.search("WBNB USDC").await.unwrap().pairs.unwrap();
assert!(result.len() > 20);
}
}