use crate::{client::WebClient, error::WebToolError};
use chrono::{DateTime, Utc};
use riglr_macros::tool;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct RugCheckConfig {
pub base_url: String,
pub rate_limit_per_minute: u32,
pub request_timeout: u64,
}
impl Default for RugCheckConfig {
fn default() -> Self {
Self {
base_url: "https://api.rugcheck.xyz/v1".to_string(),
rate_limit_per_minute: 60,
request_timeout: 30,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TokenCheck {
pub mint: String,
pub creator: Option<String>,
pub detected_at: Option<DateTime<Utc>>,
pub events: Option<Vec<TokenEvent>>,
pub file_meta: Option<FileMetadata>,
pub freeze_authority: Option<String>,
pub graph_insider_report: Option<GraphDetectedData>,
pub graph_insiders_detected: Option<i32>,
pub insider_networks: Option<Vec<InsiderNetwork>>,
pub known_accounts: Option<HashMap<String, KnownAccount>>,
pub locker_owners: Option<HashMap<String, bool>>,
pub lockers: Option<HashMap<String, Locker>>,
pub lp_lockers: Option<HashMap<String, Locker>>,
pub markets: Option<Vec<Market>>,
pub mint_authority: Option<String>,
pub price: Option<f64>,
pub risks: Option<Vec<Risk>>,
pub rugged: Option<bool>,
pub score: Option<i32>,
pub score_normalised: Option<i32>,
pub token: Option<TokenInfo>,
pub token_meta: Option<TokenMetadata>,
pub token_program: Option<String>,
pub token_type: Option<String>,
pub token_extensions: Option<String>,
pub creator_tokens: Option<String>,
pub creator_balance: Option<i64>,
pub top_holders: Option<Vec<TokenHolder>>,
pub total_holders: Option<i32>,
pub total_lp_providers: Option<i32>,
pub total_market_liquidity: Option<f64>,
pub transfer_fee: Option<TransferFee>,
pub verification: Option<VerifiedToken>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TokenEvent {
pub created_at: DateTime<Utc>,
pub event: i32,
pub new_value: Option<String>,
pub old_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FileMetadata {
pub description: Option<String>,
pub image: Option<String>,
pub name: Option<String>,
pub symbol: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GraphDetectedData {
pub blacklisted: Option<bool>,
pub raw_graph_data: Option<Vec<Account>>,
pub receivers: Option<Vec<InsiderDetectedData>>,
pub senders: Option<Vec<InsiderDetectedData>>,
pub total_sent: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Account {
pub address: String,
pub sent: Option<Vec<Transfer>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Transfer {
pub amount: i64,
pub mint: String,
pub receiver: Option<Receiver>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Receiver {
pub address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct InsiderDetectedData {
pub address: String,
pub amount: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct InsiderNetwork {
pub id: String,
pub size: i32,
#[serde(rename = "type")]
pub network_type: String,
pub token_amount: i64,
pub active_accounts: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct KnownAccount {
pub name: String,
#[serde(rename = "type")]
pub account_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Locker {
pub owner: Option<String>,
pub program_id: Option<String>,
pub token_account: Option<String>,
#[serde(rename = "type")]
pub locker_type: Option<String>,
pub unlock_date: Option<i64>,
pub uri: Option<String>,
pub usdc_locked: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Market {
pub pubkey: String,
pub market_type: String,
pub mint_a: Option<String>,
pub mint_a_account: Option<String>,
pub mint_b: Option<String>,
pub mint_b_account: Option<String>,
pub liquidity_a: Option<String>,
pub liquidity_a_account: Option<String>,
pub liquidity_b: Option<String>,
pub liquidity_b_account: Option<String>,
pub mint_lp: Option<String>,
pub mint_lp_account: Option<String>,
pub lp: Option<MarketLP>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MarketLP {
pub base: Option<f64>,
pub base_mint: Option<String>,
pub base_price: Option<f64>,
pub base_usd: Option<f64>,
pub current_supply: Option<i64>,
pub holders: Option<Vec<TokenHolder>>,
pub lp_current_supply: Option<i64>,
pub lp_locked: Option<i64>,
pub lp_locked_pct: Option<f64>,
pub lp_locked_usd: Option<f64>,
pub lp_max_supply: Option<i64>,
pub lp_mint: Option<String>,
pub lp_total_supply: Option<i64>,
pub lp_unlocked: Option<i64>,
pub pct_reserve: Option<f64>,
pub pct_supply: Option<f64>,
pub quote: Option<f64>,
pub quote_mint: Option<String>,
pub quote_price: Option<f64>,
pub quote_usd: Option<f64>,
pub reserve_supply: Option<i64>,
pub token_supply: Option<i64>,
pub total_tokens_unlocked: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Risk {
pub name: String,
pub description: String,
pub level: String,
pub score: i32,
pub value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TokenInfo {
pub mint_authority: Option<String>,
pub supply: Option<i64>,
pub decimals: Option<i32>,
pub is_initialized: Option<bool>,
pub freeze_authority: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TokenMetadata {
pub mutable: Option<bool>,
pub name: Option<String>,
pub symbol: Option<String>,
pub update_authority: Option<String>,
pub uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TokenHolder {
pub address: String,
pub amount: i64,
pub decimals: Option<i32>,
pub insider: Option<bool>,
pub owner: Option<String>,
pub pct: Option<f64>,
pub ui_amount: Option<f64>,
pub ui_amount_string: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TransferFee {
pub authority: Option<String>,
pub max_amount: Option<f64>,
pub pct: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct VerifiedToken {
pub description: Option<String>,
pub jup_verified: Option<bool>,
pub links: Option<Vec<VerifiedTokenLinks>>,
pub mint: String,
pub name: String,
pub payer: Option<String>,
pub symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct VerifiedTokenLinks {
pub provider: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RiskAnalysis {
pub token_mint: String,
pub risk_score: i32,
pub risk_level: RiskLevel,
pub is_rugged: bool,
pub critical_risks: Vec<Risk>,
pub high_risks: Vec<Risk>,
pub medium_risks: Vec<Risk>,
pub low_risks: Vec<Risk>,
pub insider_analysis: Option<InsiderAnalysis>,
pub liquidity_analysis: Option<LiquidityAnalysis>,
pub concentration_analysis: Option<ConcentrationAnalysis>,
pub recommendation: String,
pub analyzed_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum RiskLevel {
#[serde(rename = "critical")]
Critical,
#[serde(rename = "high")]
High,
#[serde(rename = "medium")]
Medium,
#[serde(rename = "low")]
Low,
#[serde(rename = "safe")]
Safe,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct InsiderAnalysis {
pub networks_detected: i32,
pub insider_accounts: i32,
pub insider_supply_pct: f64,
pub suspicious_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LiquidityAnalysis {
pub total_liquidity_usd: f64,
pub lp_locked_pct: f64,
pub provider_count: i32,
pub concentration: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ConcentrationAnalysis {
pub top_10_pct: f64,
pub top_25_pct: f64,
pub holder_count: i32,
pub whale_dominance: String,
}
#[tool]
pub async fn get_token_report(
_context: &riglr_core::provider::ApplicationContext,
mint: String,
) -> crate::error::Result<TokenCheck> {
debug!("Fetching RugCheck report for token: {}", mint);
let config = RugCheckConfig::default();
let client = WebClient::default();
let url = format!("{}/tokens/{}/report", config.base_url, mint);
info!("Requesting RugCheck report from: {}", url);
let response_text = client
.get(&url)
.await
.map_err(|e| WebToolError::Network(format!("Failed to fetch RugCheck report: {}", e)))?;
let report: TokenCheck = serde_json::from_str(&response_text)
.map_err(|e| WebToolError::Parsing(format!("Failed to parse RugCheck response: {}", e)))?;
info!(
"Successfully fetched RugCheck report for {} - Score: {:?}, Rugged: {:?}",
mint, report.score, report.rugged
);
Ok(report)
}
fn categorize_risks(risks: &[Risk]) -> (Vec<Risk>, Vec<Risk>, Vec<Risk>, Vec<Risk>) {
let mut critical_risks = Vec::new();
let mut high_risks = Vec::new();
let mut medium_risks = Vec::new();
let mut low_risks = Vec::new();
for risk in risks {
match risk.level.to_lowercase().as_str() {
"critical" | "danger" => critical_risks.push(risk.clone()),
"high" | "warning" => high_risks.push(risk.clone()),
"medium" | "caution" => medium_risks.push(risk.clone()),
"low" | "info" => low_risks.push(risk.clone()),
_ => medium_risks.push(risk.clone()),
}
}
(critical_risks, high_risks, medium_risks, low_risks)
}
fn calculate_risk_level(risk_score: i32) -> RiskLevel {
match risk_score {
0..=20 => RiskLevel::Safe,
21..=40 => RiskLevel::Low,
41..=60 => RiskLevel::Medium,
61..=80 => RiskLevel::High,
_ => RiskLevel::Critical,
}
}
fn analyze_insider_activity(report: &TokenCheck) -> Option<InsiderAnalysis> {
let networks = report.insider_networks.as_ref()?;
let total_insiders = report.graph_insiders_detected.unwrap_or(0);
let mut suspicious_patterns = Vec::new();
if total_insiders > 10 {
suspicious_patterns.push("High number of insider accounts detected".to_string());
}
if networks.len() > 3 {
suspicious_patterns.push("Multiple insider networks identified".to_string());
}
Some(InsiderAnalysis {
networks_detected: networks.len() as i32,
insider_accounts: total_insiders,
insider_supply_pct: 0.0, suspicious_patterns,
})
}
fn analyze_market_liquidity(report: &TokenCheck) -> Option<LiquidityAnalysis> {
let liquidity = report.total_market_liquidity?;
let lp_providers = report.total_lp_providers.unwrap_or(0);
let concentration = if lp_providers < 10 {
"High concentration"
} else if lp_providers < 50 {
"Moderate concentration"
} else {
"Well distributed"
};
Some(LiquidityAnalysis {
total_liquidity_usd: liquidity,
lp_locked_pct: 0.0, provider_count: lp_providers,
concentration: concentration.to_string(),
})
}
fn analyze_holder_concentration(report: &TokenCheck) -> Option<ConcentrationAnalysis> {
let holders = report.top_holders.as_ref()?;
let total_holders = report.total_holders.unwrap_or(0);
let top_10_supply: f64 = holders.iter().take(10).filter_map(|h| h.pct).sum();
let top_25_supply: f64 = holders.iter().take(25).filter_map(|h| h.pct).sum();
let whale_dominance = if top_10_supply > 50.0 {
"Very High"
} else if top_10_supply > 30.0 {
"High"
} else if top_10_supply > 15.0 {
"Moderate"
} else {
"Low"
};
Some(ConcentrationAnalysis {
top_10_pct: top_10_supply,
top_25_pct: top_25_supply,
holder_count: total_holders,
whale_dominance: whale_dominance.to_string(),
})
}
fn generate_recommendation(risk_level: &RiskLevel) -> String {
match risk_level {
RiskLevel::Critical => {
"EXTREME CAUTION: This token shows critical risk factors. Avoid trading."
}
RiskLevel::High => {
"HIGH RISK: Significant security concerns detected. Trade with extreme caution."
}
RiskLevel::Medium => {
"MODERATE RISK: Some concerns present. Perform additional due diligence."
}
RiskLevel::Low => "LOW RISK: Token appears relatively safe but always DYOR.",
RiskLevel::Safe => "MINIMAL RISK: Token shows good security characteristics.",
}
.to_string()
}
#[tool]
pub async fn analyze_token_risks(
context: &riglr_core::provider::ApplicationContext,
mint: String,
) -> crate::error::Result<RiskAnalysis> {
debug!("Analyzing token risks for: {}", mint);
let report = get_token_report(context, mint.clone()).await?;
let (critical_risks, high_risks, medium_risks, low_risks) = if let Some(risks) = &report.risks {
categorize_risks(risks)
} else {
(Vec::new(), Vec::new(), Vec::new(), Vec::new())
};
let risk_score = report.score.unwrap_or(0);
let risk_level = calculate_risk_level(risk_score);
let insider_analysis = analyze_insider_activity(&report);
let liquidity_analysis = analyze_market_liquidity(&report);
let concentration_analysis = analyze_holder_concentration(&report);
let recommendation = generate_recommendation(&risk_level);
Ok(RiskAnalysis {
token_mint: mint,
risk_score,
risk_level,
is_rugged: report.rugged.unwrap_or(false),
critical_risks,
high_risks,
medium_risks,
low_risks,
insider_analysis,
liquidity_analysis,
concentration_analysis,
recommendation,
analyzed_at: Utc::now(),
})
}
#[tool]
pub async fn check_if_rugged(
context: &riglr_core::provider::ApplicationContext,
mint: String,
) -> crate::error::Result<RugCheckResult> {
debug!("Checking rug status for token: {}", mint);
let report = get_token_report(context, mint.clone()).await?;
let is_rugged = report.rugged.unwrap_or(false);
let risk_score = report.score.unwrap_or(0);
let risk_count = report.risks.as_ref().map(|r| r.len()).unwrap_or(0);
let status = if is_rugged {
"RUGGED: This token has been identified as a rug pull"
} else if risk_score > 80 {
"EXTREME RISK: Very high likelihood of rug pull"
} else if risk_score > 60 {
"HIGH RISK: Significant rug pull indicators present"
} else if risk_score > 40 {
"MODERATE RISK: Some concerning factors detected"
} else {
"LOW RISK: No major rug pull indicators found"
};
Ok(RugCheckResult {
mint,
is_rugged,
risk_score,
risk_factors: risk_count as i32,
status: status.to_string(),
check_time: Utc::now(),
})
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RugCheckResult {
pub mint: String,
pub is_rugged: bool,
pub risk_score: i32,
pub risk_factors: i32,
pub status: String,
pub check_time: DateTime<Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rugcheck_config_default() {
let config = RugCheckConfig::default();
assert_eq!(config.base_url, "https://api.rugcheck.xyz/v1");
assert_eq!(config.rate_limit_per_minute, 60);
assert_eq!(config.request_timeout, 30);
}
#[test]
fn test_risk_level_serialization() {
let risk = RiskLevel::Critical;
let json = serde_json::to_string(&risk).unwrap();
assert_eq!(json, "\"critical\"");
let risk: RiskLevel = serde_json::from_str("\"high\"").unwrap();
assert!(matches!(risk, RiskLevel::High));
}
#[test]
fn test_token_check_deserialization() {
let json = r#"{
"mint": "So11111111111111111111111111111111111111112",
"score": 25,
"rugged": false
}"#;
let token: TokenCheck = serde_json::from_str(json).unwrap();
assert_eq!(token.mint, "So11111111111111111111111111111111111111112");
assert_eq!(token.score, Some(25));
assert_eq!(token.rugged, Some(false));
}
}