1use crate::{client::WebClient, error::WebToolError};
7use chrono::{DateTime, Utc};
8use riglr_macros::tool;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use tracing::{debug, info};
13
14#[derive(Debug, Clone)]
16pub struct RugCheckConfig {
17 pub base_url: String,
19 pub rate_limit_per_minute: u32,
21 pub request_timeout: u64,
23}
24
25impl Default for RugCheckConfig {
26 fn default() -> Self {
27 Self {
28 base_url: "https://api.rugcheck.xyz/v1".to_string(),
29 rate_limit_per_minute: 60,
30 request_timeout: 30,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37pub struct TokenCheck {
38 pub mint: String,
40 pub creator: Option<String>,
42 pub detected_at: Option<DateTime<Utc>>,
44 pub events: Option<Vec<TokenEvent>>,
46 pub file_meta: Option<FileMetadata>,
48 pub freeze_authority: Option<String>,
50 pub graph_insider_report: Option<GraphDetectedData>,
52 pub graph_insiders_detected: Option<i32>,
54 pub insider_networks: Option<Vec<InsiderNetwork>>,
56 pub known_accounts: Option<HashMap<String, KnownAccount>>,
58 pub locker_owners: Option<HashMap<String, bool>>,
60 pub lockers: Option<HashMap<String, Locker>>,
62 pub lp_lockers: Option<HashMap<String, Locker>>,
64 pub markets: Option<Vec<Market>>,
66 pub mint_authority: Option<String>,
68 pub price: Option<f64>,
70 pub risks: Option<Vec<Risk>>,
72 pub rugged: Option<bool>,
74 pub score: Option<i32>,
76 pub score_normalised: Option<i32>,
78 pub token: Option<TokenInfo>,
80 pub token_meta: Option<TokenMetadata>,
82 pub token_program: Option<String>,
84 pub token_type: Option<String>,
86 pub token_extensions: Option<String>,
88 pub creator_tokens: Option<String>,
90 pub creator_balance: Option<i64>,
92 pub top_holders: Option<Vec<TokenHolder>>,
94 pub total_holders: Option<i32>,
96 pub total_lp_providers: Option<i32>,
98 pub total_market_liquidity: Option<f64>,
100 pub transfer_fee: Option<TransferFee>,
102 pub verification: Option<VerifiedToken>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
108pub struct TokenEvent {
109 pub created_at: DateTime<Utc>,
111 pub event: i32,
113 pub new_value: Option<String>,
115 pub old_value: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
121pub struct FileMetadata {
122 pub description: Option<String>,
124 pub image: Option<String>,
126 pub name: Option<String>,
128 pub symbol: Option<String>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
134pub struct GraphDetectedData {
135 pub blacklisted: Option<bool>,
137 pub raw_graph_data: Option<Vec<Account>>,
139 pub receivers: Option<Vec<InsiderDetectedData>>,
141 pub senders: Option<Vec<InsiderDetectedData>>,
143 pub total_sent: Option<i64>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149pub struct Account {
150 pub address: String,
152 pub sent: Option<Vec<Transfer>>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
158pub struct Transfer {
159 pub amount: i64,
161 pub mint: String,
163 pub receiver: Option<Receiver>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
169pub struct Receiver {
170 pub address: String,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
176pub struct InsiderDetectedData {
177 pub address: String,
179 pub amount: i64,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
185pub struct InsiderNetwork {
186 pub id: String,
188 pub size: i32,
190 #[serde(rename = "type")]
192 pub network_type: String,
193 pub token_amount: i64,
195 pub active_accounts: i32,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
201pub struct KnownAccount {
202 pub name: String,
204 #[serde(rename = "type")]
206 pub account_type: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
211pub struct Locker {
212 pub owner: Option<String>,
214 pub program_id: Option<String>,
216 pub token_account: Option<String>,
218 #[serde(rename = "type")]
220 pub locker_type: Option<String>,
221 pub unlock_date: Option<i64>,
223 pub uri: Option<String>,
225 pub usdc_locked: Option<f64>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
231pub struct Market {
232 pub pubkey: String,
234 pub market_type: String,
236 pub mint_a: Option<String>,
238 pub mint_a_account: Option<String>,
240 pub mint_b: Option<String>,
242 pub mint_b_account: Option<String>,
244 pub liquidity_a: Option<String>,
246 pub liquidity_a_account: Option<String>,
248 pub liquidity_b: Option<String>,
250 pub liquidity_b_account: Option<String>,
252 pub mint_lp: Option<String>,
254 pub mint_lp_account: Option<String>,
256 pub lp: Option<MarketLP>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
262pub struct MarketLP {
263 pub base: Option<f64>,
265 pub base_mint: Option<String>,
267 pub base_price: Option<f64>,
269 pub base_usd: Option<f64>,
271 pub current_supply: Option<i64>,
273 pub holders: Option<Vec<TokenHolder>>,
275 pub lp_current_supply: Option<i64>,
277 pub lp_locked: Option<i64>,
279 pub lp_locked_pct: Option<f64>,
281 pub lp_locked_usd: Option<f64>,
283 pub lp_max_supply: Option<i64>,
285 pub lp_mint: Option<String>,
287 pub lp_total_supply: Option<i64>,
289 pub lp_unlocked: Option<i64>,
291 pub pct_reserve: Option<f64>,
293 pub pct_supply: Option<f64>,
295 pub quote: Option<f64>,
297 pub quote_mint: Option<String>,
299 pub quote_price: Option<f64>,
301 pub quote_usd: Option<f64>,
303 pub reserve_supply: Option<i64>,
305 pub token_supply: Option<i64>,
307 pub total_tokens_unlocked: Option<i64>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
313pub struct Risk {
314 pub name: String,
316 pub description: String,
318 pub level: String,
320 pub score: i32,
322 pub value: Option<String>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
328pub struct TokenInfo {
329 pub mint_authority: Option<String>,
331 pub supply: Option<i64>,
333 pub decimals: Option<i32>,
335 pub is_initialized: Option<bool>,
337 pub freeze_authority: Option<String>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
343pub struct TokenMetadata {
344 pub mutable: Option<bool>,
346 pub name: Option<String>,
348 pub symbol: Option<String>,
350 pub update_authority: Option<String>,
352 pub uri: Option<String>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
358pub struct TokenHolder {
359 pub address: String,
361 pub amount: i64,
363 pub decimals: Option<i32>,
365 pub insider: Option<bool>,
367 pub owner: Option<String>,
369 pub pct: Option<f64>,
371 pub ui_amount: Option<f64>,
373 pub ui_amount_string: Option<String>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
379pub struct TransferFee {
380 pub authority: Option<String>,
382 pub max_amount: Option<f64>,
384 pub pct: Option<f64>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
390pub struct VerifiedToken {
391 pub description: Option<String>,
393 pub jup_verified: Option<bool>,
395 pub links: Option<Vec<VerifiedTokenLinks>>,
397 pub mint: String,
399 pub name: String,
401 pub payer: Option<String>,
403 pub symbol: String,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
409pub struct VerifiedTokenLinks {
410 pub provider: String,
412 pub value: String,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
418pub struct RiskAnalysis {
419 pub token_mint: String,
421 pub risk_score: i32,
423 pub risk_level: RiskLevel,
425 pub is_rugged: bool,
427 pub critical_risks: Vec<Risk>,
429 pub high_risks: Vec<Risk>,
431 pub medium_risks: Vec<Risk>,
433 pub low_risks: Vec<Risk>,
435 pub insider_analysis: Option<InsiderAnalysis>,
437 pub liquidity_analysis: Option<LiquidityAnalysis>,
439 pub concentration_analysis: Option<ConcentrationAnalysis>,
441 pub recommendation: String,
443 pub analyzed_at: DateTime<Utc>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
449pub enum RiskLevel {
450 #[serde(rename = "critical")]
452 Critical,
453 #[serde(rename = "high")]
455 High,
456 #[serde(rename = "medium")]
458 Medium,
459 #[serde(rename = "low")]
461 Low,
462 #[serde(rename = "safe")]
464 Safe,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
469pub struct InsiderAnalysis {
470 pub networks_detected: i32,
472 pub insider_accounts: i32,
474 pub insider_supply_pct: f64,
476 pub suspicious_patterns: Vec<String>,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
482pub struct LiquidityAnalysis {
483 pub total_liquidity_usd: f64,
485 pub lp_locked_pct: f64,
487 pub provider_count: i32,
489 pub concentration: String,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
495pub struct ConcentrationAnalysis {
496 pub top_10_pct: f64,
498 pub top_25_pct: f64,
500 pub holder_count: i32,
502 pub whale_dominance: String,
504}
505
506#[tool]
509pub async fn get_token_report(
510 _context: &riglr_core::provider::ApplicationContext,
511 mint: String,
512) -> crate::error::Result<TokenCheck> {
513 debug!("Fetching RugCheck report for token: {}", mint);
514
515 let config = RugCheckConfig::default();
516 let client = WebClient::default();
517
518 let url = format!("{}/tokens/{}/report", config.base_url, mint);
519
520 info!("Requesting RugCheck report from: {}", url);
521
522 let response_text = client
523 .get(&url)
524 .await
525 .map_err(|e| WebToolError::Network(format!("Failed to fetch RugCheck report: {}", e)))?;
526
527 let report: TokenCheck = serde_json::from_str(&response_text)
528 .map_err(|e| WebToolError::Parsing(format!("Failed to parse RugCheck response: {}", e)))?;
529
530 info!(
531 "Successfully fetched RugCheck report for {} - Score: {:?}, Rugged: {:?}",
532 mint, report.score, report.rugged
533 );
534
535 Ok(report)
536}
537
538fn categorize_risks(risks: &[Risk]) -> (Vec<Risk>, Vec<Risk>, Vec<Risk>, Vec<Risk>) {
540 let mut critical_risks = Vec::new();
541 let mut high_risks = Vec::new();
542 let mut medium_risks = Vec::new();
543 let mut low_risks = Vec::new();
544
545 for risk in risks {
546 match risk.level.to_lowercase().as_str() {
547 "critical" | "danger" => critical_risks.push(risk.clone()),
548 "high" | "warning" => high_risks.push(risk.clone()),
549 "medium" | "caution" => medium_risks.push(risk.clone()),
550 "low" | "info" => low_risks.push(risk.clone()),
551 _ => medium_risks.push(risk.clone()),
552 }
553 }
554
555 (critical_risks, high_risks, medium_risks, low_risks)
556}
557
558fn calculate_risk_level(risk_score: i32) -> RiskLevel {
560 match risk_score {
561 0..=20 => RiskLevel::Safe,
562 21..=40 => RiskLevel::Low,
563 41..=60 => RiskLevel::Medium,
564 61..=80 => RiskLevel::High,
565 _ => RiskLevel::Critical,
566 }
567}
568
569fn analyze_insider_activity(report: &TokenCheck) -> Option<InsiderAnalysis> {
571 let networks = report.insider_networks.as_ref()?;
572 let total_insiders = report.graph_insiders_detected.unwrap_or(0);
573 let mut suspicious_patterns = Vec::new();
574
575 if total_insiders > 10 {
576 suspicious_patterns.push("High number of insider accounts detected".to_string());
577 }
578
579 if networks.len() > 3 {
580 suspicious_patterns.push("Multiple insider networks identified".to_string());
581 }
582
583 Some(InsiderAnalysis {
584 networks_detected: networks.len() as i32,
585 insider_accounts: total_insiders,
586 insider_supply_pct: 0.0, suspicious_patterns,
588 })
589}
590
591fn analyze_market_liquidity(report: &TokenCheck) -> Option<LiquidityAnalysis> {
593 let liquidity = report.total_market_liquidity?;
594 let lp_providers = report.total_lp_providers.unwrap_or(0);
595
596 let concentration = if lp_providers < 10 {
597 "High concentration"
598 } else if lp_providers < 50 {
599 "Moderate concentration"
600 } else {
601 "Well distributed"
602 };
603
604 Some(LiquidityAnalysis {
605 total_liquidity_usd: liquidity,
606 lp_locked_pct: 0.0, provider_count: lp_providers,
608 concentration: concentration.to_string(),
609 })
610}
611
612fn analyze_holder_concentration(report: &TokenCheck) -> Option<ConcentrationAnalysis> {
614 let holders = report.top_holders.as_ref()?;
615 let total_holders = report.total_holders.unwrap_or(0);
616
617 let top_10_supply: f64 = holders.iter().take(10).filter_map(|h| h.pct).sum();
618 let top_25_supply: f64 = holders.iter().take(25).filter_map(|h| h.pct).sum();
619
620 let whale_dominance = if top_10_supply > 50.0 {
621 "Very High"
622 } else if top_10_supply > 30.0 {
623 "High"
624 } else if top_10_supply > 15.0 {
625 "Moderate"
626 } else {
627 "Low"
628 };
629
630 Some(ConcentrationAnalysis {
631 top_10_pct: top_10_supply,
632 top_25_pct: top_25_supply,
633 holder_count: total_holders,
634 whale_dominance: whale_dominance.to_string(),
635 })
636}
637
638fn generate_recommendation(risk_level: &RiskLevel) -> String {
640 match risk_level {
641 RiskLevel::Critical => {
642 "EXTREME CAUTION: This token shows critical risk factors. Avoid trading."
643 }
644 RiskLevel::High => {
645 "HIGH RISK: Significant security concerns detected. Trade with extreme caution."
646 }
647 RiskLevel::Medium => {
648 "MODERATE RISK: Some concerns present. Perform additional due diligence."
649 }
650 RiskLevel::Low => "LOW RISK: Token appears relatively safe but always DYOR.",
651 RiskLevel::Safe => "MINIMAL RISK: Token shows good security characteristics.",
652 }
653 .to_string()
654}
655
656#[tool]
659pub async fn analyze_token_risks(
660 context: &riglr_core::provider::ApplicationContext,
661 mint: String,
662) -> crate::error::Result<RiskAnalysis> {
663 debug!("Analyzing token risks for: {}", mint);
664
665 let report = get_token_report(context, mint.clone()).await?;
667
668 let (critical_risks, high_risks, medium_risks, low_risks) = if let Some(risks) = &report.risks {
670 categorize_risks(risks)
671 } else {
672 (Vec::new(), Vec::new(), Vec::new(), Vec::new())
673 };
674
675 let risk_score = report.score.unwrap_or(0);
677 let risk_level = calculate_risk_level(risk_score);
678
679 let insider_analysis = analyze_insider_activity(&report);
681 let liquidity_analysis = analyze_market_liquidity(&report);
682 let concentration_analysis = analyze_holder_concentration(&report);
683
684 let recommendation = generate_recommendation(&risk_level);
686
687 Ok(RiskAnalysis {
688 token_mint: mint,
689 risk_score,
690 risk_level,
691 is_rugged: report.rugged.unwrap_or(false),
692 critical_risks,
693 high_risks,
694 medium_risks,
695 low_risks,
696 insider_analysis,
697 liquidity_analysis,
698 concentration_analysis,
699 recommendation,
700 analyzed_at: Utc::now(),
701 })
702}
703
704#[tool]
707pub async fn check_if_rugged(
708 context: &riglr_core::provider::ApplicationContext,
709 mint: String,
710) -> crate::error::Result<RugCheckResult> {
711 debug!("Checking rug status for token: {}", mint);
712
713 let report = get_token_report(context, mint.clone()).await?;
714
715 let is_rugged = report.rugged.unwrap_or(false);
716 let risk_score = report.score.unwrap_or(0);
717 let risk_count = report.risks.as_ref().map(|r| r.len()).unwrap_or(0);
718
719 let status = if is_rugged {
720 "RUGGED: This token has been identified as a rug pull"
721 } else if risk_score > 80 {
722 "EXTREME RISK: Very high likelihood of rug pull"
723 } else if risk_score > 60 {
724 "HIGH RISK: Significant rug pull indicators present"
725 } else if risk_score > 40 {
726 "MODERATE RISK: Some concerning factors detected"
727 } else {
728 "LOW RISK: No major rug pull indicators found"
729 };
730
731 Ok(RugCheckResult {
732 mint,
733 is_rugged,
734 risk_score,
735 risk_factors: risk_count as i32,
736 status: status.to_string(),
737 check_time: Utc::now(),
738 })
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
743pub struct RugCheckResult {
744 pub mint: String,
746 pub is_rugged: bool,
748 pub risk_score: i32,
750 pub risk_factors: i32,
752 pub status: String,
754 pub check_time: DateTime<Utc>,
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
763 fn test_rugcheck_config_default() {
764 let config = RugCheckConfig::default();
765 assert_eq!(config.base_url, "https://api.rugcheck.xyz/v1");
766 assert_eq!(config.rate_limit_per_minute, 60);
767 assert_eq!(config.request_timeout, 30);
768 }
769
770 #[test]
771 fn test_risk_level_serialization() {
772 let risk = RiskLevel::Critical;
773 let json = serde_json::to_string(&risk).unwrap();
774 assert_eq!(json, "\"critical\"");
775
776 let risk: RiskLevel = serde_json::from_str("\"high\"").unwrap();
777 assert!(matches!(risk, RiskLevel::High));
778 }
779
780 #[test]
781 fn test_token_check_deserialization() {
782 let json = r#"{
783 "mint": "So11111111111111111111111111111111111111112",
784 "score": 25,
785 "rugged": false
786 }"#;
787
788 let token: TokenCheck = serde_json::from_str(json).unwrap();
789 assert_eq!(token.mint, "So11111111111111111111111111111111111111112");
790 assert_eq!(token.score, Some(25));
791 assert_eq!(token.rugged, Some(false));
792 }
793}