#![deny(clippy::all)]
use reqwest::{Client as HttpClient, Method, Url, cookie::Jar};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
use thiserror::Error;
pub mod models;
use models::*;
#[derive(Clone)]
pub struct RugplayClient {
base_url: String,
token: String,
http: HttpClient,
}
#[derive(Debug, Error)]
pub enum RugplayError {
#[error("HTTP request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("Failed to deserialize JSON: {source}\nResponse text: {text}")]
Deserialize {
source: serde_json::Error,
text: String,
},
#[error("Invalid response: {0}")]
InvalidResponse(String),
#[error("No cookie provided")]
NoCookie,
}
pub type Result<T> = std::result::Result<T, RugplayError>;
#[derive(Debug, Error)]
pub enum ClientCreateError {
#[error("Invalid URL")]
InvalidUrl(#[from] url::ParseError),
#[error("Could not build HTTP Client")]
ReqwestError(#[from] reqwest::Error),
}
impl RugplayClient {
pub fn new(
token: impl Into<String>,
cookie: Option<String>,
url: Option<&str>,
) -> std::result::Result<Self, ClientCreateError> {
let jar = Arc::new(Jar::default());
let base_url: Url = Url::from_str(url.unwrap_or("https://rugplay.com/api/v1"))?;
if let Some(cookie) = cookie {
let cookie_value = cookie;
jar.add_cookie_str(&cookie_value, &base_url);
}
let http = HttpClient::builder().cookie_provider(jar).build()?;
Ok(Self {
base_url: url.unwrap_or("https://rugplay.com/api/v1").into(),
token: token.into(),
http,
})
}
async fn get<T: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
params: Option<&[(&str, &str)]>,
) -> Result<T> {
let url = format!("{}/{}", self.base_url, endpoint);
let req = self
.http
.request(Method::GET, &url)
.bearer_auth(&self.token)
.query(params.unwrap_or_default());
let resp = req.send().await?.error_for_status()?;
let text = resp.text().await?;
match serde_json::from_str(&text) {
Ok(data) => Ok(data),
Err(e) => Err(RugplayError::Deserialize { source: e, text }),
}
}
async fn post<T: for<'de> Deserialize<'de>, J: Serialize>(
&self,
endpoint: &str,
params: Option<&[(&str, &str)]>,
json: Option<J>,
) -> Result<T> {
let url = format!("{}/{}", self.base_url, endpoint);
let mut req = self
.http
.request(Method::POST, &url)
.bearer_auth(&self.token)
.query(params.unwrap_or_default());
if let Some(body) = json {
req = req.json(&body);
}
let resp = req.send().await?.error_for_status()?;
let text = resp.text().await?;
match serde_json::from_str(&text) {
Ok(data) => Ok(data),
Err(e) => Err(RugplayError::Deserialize { source: e, text }),
}
}
pub async fn get_top_coins(&self) -> Result<TopCoinsResponse> {
self.get("top", None).await
}
pub async fn get_market(&self, params: &[(&str, &str)]) -> Result<MarketResponse> {
self.get("market", Some(params)).await
}
pub async fn get_coin_details(
&self,
symbol: &str,
timeframe: Option<&str>,
) -> Result<CoinDetailsResponse> {
let endpoint = format!("coin/{}", symbol);
let params = timeframe.map(|t| [("timeframe", t)]).unwrap_or_default();
self.get(
&endpoint,
if timeframe.is_some() {
Some(¶ms)
} else {
None
},
)
.await
}
pub async fn get_holders(&self, symbol: &str, limit: Option<u32>) -> Result<HoldersResponse> {
let endpoint = format!("holders/{}", symbol);
if let Some(ref limit) = limit {
let params = &[("limit", &limit.to_string()[..])];
self.get(&endpoint, Some(params)).await
} else {
self.get(&endpoint, None).await
}
}
pub async fn get_hopium(
&self,
hopium_status: HopiumFilter,
limit: Option<u32>,
page: Option<u32>,
) -> Result<HopiumResponse> {
let params = &[
("limit", &limit.unwrap_or(20).to_string()[..]),
("status", &hopium_status.to_string()),
("page", &page.unwrap_or(1).to_string()),
];
self.get("hopium", Some(params)).await
}
pub async fn get_hopium_details(&self, question_id: u32) -> Result<HopiumDetailsResponse> {
let endpoint = format!("hopium/{question_id}");
self.get(&endpoint, None).await
}
pub async fn get_claim_info(&self) -> Result<ClaimInfo> {
self.get("../rewards/claim", None).await
}
pub async fn claim_reward(&self) -> Result<RewardStatus> {
self.post::<RewardStatus, ()>("../rewards/claim", None, None)
.await
}
pub async fn trade(
&self,
coin: &str,
trade_type: CoinTradeType,
amount: u32,
) -> Result<TradeResponse> {
let trade_request = TradeRequest {
amount,
r#type: trade_type,
};
let endpoint = format!("../coin/{coin}/trade");
self.post::<TradeResponse, TradeRequest>(&endpoint, None, Some(trade_request))
.await
}
pub async fn get_recent_trades(&self, limit: u32) -> Result<RecentTradeResponse> {
self.get::<RecentTradeResponse>(
"../trades/recent",
Some(&[("limit", limit.to_string().as_str())]),
)
.await
}
}