riglr_web_tools/
rugcheck.rs

1//! RugCheck integration for Solana token security analysis and risk assessment
2//!
3//! This module provides tools for accessing RugCheck API to analyze Solana tokens
4//! for potential rug pull risks, security issues, and insider trading patterns.
5
6use 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/// Configuration for RugCheck API access
15#[derive(Debug, Clone)]
16pub struct RugCheckConfig {
17    /// API base URL (default: https://api.rugcheck.xyz/v1)
18    pub base_url: String,
19    /// Rate limit requests per minute (default: 60)
20    pub rate_limit_per_minute: u32,
21    /// Timeout for API requests in seconds (default: 30)
22    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/// Main token check report from RugCheck
36#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37pub struct TokenCheck {
38    /// Token mint address
39    pub mint: String,
40    /// Creator wallet address
41    pub creator: Option<String>,
42    /// When the token was first detected
43    pub detected_at: Option<DateTime<Utc>>,
44    /// Token events history
45    pub events: Option<Vec<TokenEvent>>,
46    /// File metadata from the token
47    pub file_meta: Option<FileMetadata>,
48    /// Freeze authority address
49    pub freeze_authority: Option<String>,
50    /// Graph-based insider detection report
51    pub graph_insider_report: Option<GraphDetectedData>,
52    /// Number of graph insiders detected
53    pub graph_insiders_detected: Option<i32>,
54    /// Insider networks information
55    pub insider_networks: Option<Vec<InsiderNetwork>>,
56    /// Known accounts mapping
57    pub known_accounts: Option<HashMap<String, KnownAccount>>,
58    /// Locker owners mapping
59    pub locker_owners: Option<HashMap<String, bool>>,
60    /// Token lockers information
61    pub lockers: Option<HashMap<String, Locker>>,
62    /// LP token lockers information
63    pub lp_lockers: Option<HashMap<String, Locker>>,
64    /// Market trading pairs
65    pub markets: Option<Vec<Market>>,
66    /// Mint authority address
67    pub mint_authority: Option<String>,
68    /// Current price in USD
69    pub price: Option<f64>,
70    /// Risk factors identified
71    pub risks: Option<Vec<Risk>>,
72    /// Whether the token has been rugged
73    pub rugged: Option<bool>,
74    /// Overall risk score (0-100, lower is better)
75    pub score: Option<i32>,
76    /// Normalized risk score
77    pub score_normalised: Option<i32>,
78    /// Token information
79    pub token: Option<TokenInfo>,
80    /// Token metadata
81    pub token_meta: Option<TokenMetadata>,
82    /// Token program ID
83    pub token_program: Option<String>,
84    /// Token type (e.g., "SPL", "Token22")
85    pub token_type: Option<String>,
86    /// Token extensions if any
87    pub token_extensions: Option<String>,
88    /// Creator's token balance
89    pub creator_tokens: Option<String>,
90    /// Creator's balance
91    pub creator_balance: Option<i64>,
92    /// Top token holders
93    pub top_holders: Option<Vec<TokenHolder>>,
94    /// Total number of holders
95    pub total_holders: Option<i32>,
96    /// Total LP providers
97    pub total_lp_providers: Option<i32>,
98    /// Total market liquidity in USD
99    pub total_market_liquidity: Option<f64>,
100    /// Transfer fee configuration
101    pub transfer_fee: Option<TransferFee>,
102    /// Token verification status
103    pub verification: Option<VerifiedToken>,
104}
105
106/// Token event in history
107#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
108pub struct TokenEvent {
109    /// When the event occurred
110    pub created_at: DateTime<Utc>,
111    /// Event type identifier
112    pub event: i32,
113    /// New value after the event
114    pub new_value: Option<String>,
115    /// Old value before the event
116    pub old_value: Option<String>,
117}
118
119/// File metadata associated with the token
120#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
121pub struct FileMetadata {
122    /// Token description
123    pub description: Option<String>,
124    /// Token image URL
125    pub image: Option<String>,
126    /// Token name
127    pub name: Option<String>,
128    /// Token symbol
129    pub symbol: Option<String>,
130}
131
132/// Graph-based insider detection data
133#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
134pub struct GraphDetectedData {
135    /// Whether the token is blacklisted
136    pub blacklisted: Option<bool>,
137    /// Raw graph analysis data
138    pub raw_graph_data: Option<Vec<Account>>,
139    /// Detected receivers
140    pub receivers: Option<Vec<InsiderDetectedData>>,
141    /// Detected senders
142    pub senders: Option<Vec<InsiderDetectedData>>,
143    /// Total amount sent
144    pub total_sent: Option<i64>,
145}
146
147/// Account information in graph analysis
148#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149pub struct Account {
150    /// Account address
151    pub address: String,
152    /// Sent transfers
153    pub sent: Option<Vec<Transfer>>,
154}
155
156/// Transfer information
157#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
158pub struct Transfer {
159    /// Transfer amount
160    pub amount: i64,
161    /// Token mint
162    pub mint: String,
163    /// Transfer receiver
164    pub receiver: Option<Receiver>,
165}
166
167/// Transfer receiver
168#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
169pub struct Receiver {
170    /// Receiver address
171    pub address: String,
172}
173
174/// Insider detection data
175#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
176pub struct InsiderDetectedData {
177    /// Insider address
178    pub address: String,
179    /// Amount held or transferred
180    pub amount: i64,
181}
182
183/// Insider network information
184#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
185pub struct InsiderNetwork {
186    /// Network identifier
187    pub id: String,
188    /// Network size
189    pub size: i32,
190    /// Network type
191    #[serde(rename = "type")]
192    pub network_type: String,
193    /// Token amount in network
194    pub token_amount: i64,
195    /// Active accounts in network
196    pub active_accounts: i32,
197}
198
199/// Known account information
200#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
201pub struct KnownAccount {
202    /// Account name
203    pub name: String,
204    /// Account type
205    #[serde(rename = "type")]
206    pub account_type: String,
207}
208
209/// Token locker information
210#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
211pub struct Locker {
212    /// Locker owner
213    pub owner: Option<String>,
214    /// Program ID
215    pub program_id: Option<String>,
216    /// Token account
217    pub token_account: Option<String>,
218    /// Locker type
219    #[serde(rename = "type")]
220    pub locker_type: Option<String>,
221    /// Unlock date timestamp
222    pub unlock_date: Option<i64>,
223    /// Locker URI
224    pub uri: Option<String>,
225    /// USDC value locked
226    pub usdc_locked: Option<f64>,
227}
228
229/// Market trading pair information
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
231pub struct Market {
232    /// Market address
233    pub pubkey: String,
234    /// Market type (e.g., "Raydium", "Orca")
235    pub market_type: String,
236    /// Mint A address
237    pub mint_a: Option<String>,
238    /// Mint A account
239    pub mint_a_account: Option<String>,
240    /// Mint B address
241    pub mint_b: Option<String>,
242    /// Mint B account
243    pub mint_b_account: Option<String>,
244    /// Liquidity A amount
245    pub liquidity_a: Option<String>,
246    /// Liquidity A account
247    pub liquidity_a_account: Option<String>,
248    /// Liquidity B amount
249    pub liquidity_b: Option<String>,
250    /// Liquidity B account
251    pub liquidity_b_account: Option<String>,
252    /// LP mint address
253    pub mint_lp: Option<String>,
254    /// LP mint account
255    pub mint_lp_account: Option<String>,
256    /// LP token information
257    pub lp: Option<MarketLP>,
258}
259
260/// Market LP token information
261#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
262pub struct MarketLP {
263    /// Base token amount
264    pub base: Option<f64>,
265    /// Base token mint
266    pub base_mint: Option<String>,
267    /// Base token price
268    pub base_price: Option<f64>,
269    /// Base token USD value
270    pub base_usd: Option<f64>,
271    /// Current supply
272    pub current_supply: Option<i64>,
273    /// LP token holders
274    pub holders: Option<Vec<TokenHolder>>,
275    /// LP current supply
276    pub lp_current_supply: Option<i64>,
277    /// LP locked amount
278    pub lp_locked: Option<i64>,
279    /// LP locked percentage
280    pub lp_locked_pct: Option<f64>,
281    /// LP locked USD value
282    pub lp_locked_usd: Option<f64>,
283    /// LP max supply
284    pub lp_max_supply: Option<i64>,
285    /// LP mint address
286    pub lp_mint: Option<String>,
287    /// LP total supply
288    pub lp_total_supply: Option<i64>,
289    /// LP unlocked amount
290    pub lp_unlocked: Option<i64>,
291    /// Percentage of reserve
292    pub pct_reserve: Option<f64>,
293    /// Percentage of supply
294    pub pct_supply: Option<f64>,
295    /// Quote token amount
296    pub quote: Option<f64>,
297    /// Quote token mint
298    pub quote_mint: Option<String>,
299    /// Quote token price
300    pub quote_price: Option<f64>,
301    /// Quote token USD value
302    pub quote_usd: Option<f64>,
303    /// Reserve supply
304    pub reserve_supply: Option<i64>,
305    /// Token supply
306    pub token_supply: Option<i64>,
307    /// Total tokens unlocked
308    pub total_tokens_unlocked: Option<i64>,
309}
310
311/// Risk factor information
312#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
313pub struct Risk {
314    /// Risk name
315    pub name: String,
316    /// Risk description
317    pub description: String,
318    /// Risk level (e.g., "High", "Medium", "Low")
319    pub level: String,
320    /// Risk score contribution
321    pub score: i32,
322    /// Risk value or details
323    pub value: Option<String>,
324}
325
326/// Token information
327#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
328pub struct TokenInfo {
329    /// Mint authority
330    pub mint_authority: Option<String>,
331    /// Token supply
332    pub supply: Option<i64>,
333    /// Token decimals
334    pub decimals: Option<i32>,
335    /// Whether token is initialized
336    pub is_initialized: Option<bool>,
337    /// Freeze authority
338    pub freeze_authority: Option<String>,
339}
340
341/// Token metadata
342#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
343pub struct TokenMetadata {
344    /// Whether metadata is mutable
345    pub mutable: Option<bool>,
346    /// Token name
347    pub name: Option<String>,
348    /// Token symbol
349    pub symbol: Option<String>,
350    /// Update authority
351    pub update_authority: Option<String>,
352    /// Metadata URI
353    pub uri: Option<String>,
354}
355
356/// Token holder information
357#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
358pub struct TokenHolder {
359    /// Holder's wallet address
360    pub address: String,
361    /// Token amount held
362    pub amount: i64,
363    /// Token decimals
364    pub decimals: Option<i32>,
365    /// Whether holder is an insider
366    pub insider: Option<bool>,
367    /// Account owner
368    pub owner: Option<String>,
369    /// Percentage of total supply
370    pub pct: Option<f64>,
371    /// UI formatted amount
372    pub ui_amount: Option<f64>,
373    /// UI amount as string
374    pub ui_amount_string: Option<String>,
375}
376
377/// Transfer fee configuration
378#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
379pub struct TransferFee {
380    /// Fee authority
381    pub authority: Option<String>,
382    /// Maximum fee amount
383    pub max_amount: Option<f64>,
384    /// Fee percentage
385    pub pct: Option<f64>,
386}
387
388/// Verified token information
389#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
390pub struct VerifiedToken {
391    /// Token description
392    pub description: Option<String>,
393    /// Whether verified by Jupiter
394    pub jup_verified: Option<bool>,
395    /// Social and other links
396    pub links: Option<Vec<VerifiedTokenLinks>>,
397    /// Token mint
398    pub mint: String,
399    /// Token name
400    pub name: String,
401    /// Payer address
402    pub payer: Option<String>,
403    /// Token symbol
404    pub symbol: String,
405}
406
407/// Verified token links
408#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
409pub struct VerifiedTokenLinks {
410    /// Link provider (e.g., "twitter", "website", "telegram")
411    pub provider: String,
412    /// Link value/URL
413    pub value: String,
414}
415
416/// Risk analysis summary
417#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
418pub struct RiskAnalysis {
419    /// Token being analyzed
420    pub token_mint: String,
421    /// Overall risk score (0-100)
422    pub risk_score: i32,
423    /// Risk level classification
424    pub risk_level: RiskLevel,
425    /// Whether token has been rugged
426    pub is_rugged: bool,
427    /// Critical risks found
428    pub critical_risks: Vec<Risk>,
429    /// High risks found
430    pub high_risks: Vec<Risk>,
431    /// Medium risks found
432    pub medium_risks: Vec<Risk>,
433    /// Low risks found
434    pub low_risks: Vec<Risk>,
435    /// Insider trading analysis
436    pub insider_analysis: Option<InsiderAnalysis>,
437    /// Liquidity analysis
438    pub liquidity_analysis: Option<LiquidityAnalysis>,
439    /// Holder concentration analysis
440    pub concentration_analysis: Option<ConcentrationAnalysis>,
441    /// Summary recommendation
442    pub recommendation: String,
443    /// Analysis timestamp
444    pub analyzed_at: DateTime<Utc>,
445}
446
447/// Risk level classification
448#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
449pub enum RiskLevel {
450    /// Critical risk - Extreme danger, avoid at all costs
451    #[serde(rename = "critical")]
452    Critical,
453    /// High risk - Significant concerns present
454    #[serde(rename = "high")]
455    High,
456    /// Medium risk - Some concerns, proceed with caution
457    #[serde(rename = "medium")]
458    Medium,
459    /// Low risk - Minor concerns only
460    #[serde(rename = "low")]
461    Low,
462    /// Safe - Minimal to no risk detected
463    #[serde(rename = "safe")]
464    Safe,
465}
466
467/// Insider trading analysis
468#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
469pub struct InsiderAnalysis {
470    /// Number of insider networks detected
471    pub networks_detected: i32,
472    /// Total insider accounts
473    pub insider_accounts: i32,
474    /// Percentage of supply held by insiders
475    pub insider_supply_pct: f64,
476    /// Suspicious transfer patterns detected
477    pub suspicious_patterns: Vec<String>,
478}
479
480/// Liquidity analysis
481#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
482pub struct LiquidityAnalysis {
483    /// Total liquidity in USD
484    pub total_liquidity_usd: f64,
485    /// Percentage of LP tokens locked
486    pub lp_locked_pct: f64,
487    /// Number of liquidity providers
488    pub provider_count: i32,
489    /// Liquidity concentration
490    pub concentration: String,
491}
492
493/// Holder concentration analysis
494#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
495pub struct ConcentrationAnalysis {
496    /// Top 10 holders percentage
497    pub top_10_pct: f64,
498    /// Top 25 holders percentage
499    pub top_25_pct: f64,
500    /// Number of holders
501    pub holder_count: i32,
502    /// Whale dominance level
503    pub whale_dominance: String,
504}
505
506/// Get a comprehensive security report for a Solana token from RugCheck.
507/// This tool analyzes tokens for rug pull risks, insider trading, and other security concerns.
508#[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
538/// Categorize risks by severity level
539fn 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
558/// Determine risk level from score
559fn 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
569/// Analyze insider trading patterns
570fn 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, // Would need to calculate from holders
587        suspicious_patterns,
588    })
589}
590
591/// Analyze market liquidity characteristics
592fn 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, // Would need to calculate from markets
607        provider_count: lp_providers,
608        concentration: concentration.to_string(),
609    })
610}
611
612/// Analyze token holder concentration patterns
613fn 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
638/// Generate risk-based recommendation
639fn 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/// Analyze a Solana token's security risks and provide a comprehensive risk assessment.
657/// This tool provides a simplified risk analysis based on RugCheck data.
658#[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    // Get the full report first
666    let report = get_token_report(context, mint.clone()).await?;
667
668    // Categorize risks by level
669    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    // Determine overall risk level
676    let risk_score = report.score.unwrap_or(0);
677    let risk_level = calculate_risk_level(risk_score);
678
679    // Analyze different aspects
680    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    // Generate recommendation
685    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/// Check if a Solana token has been rugged or shows signs of a rug pull.
705/// Returns a simple boolean result with basic risk information.
706#[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/// Simple rug check result
742#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
743pub struct RugCheckResult {
744    /// Token mint address
745    pub mint: String,
746    /// Whether token has been rugged
747    pub is_rugged: bool,
748    /// Risk score (0-100)
749    pub risk_score: i32,
750    /// Number of risk factors detected
751    pub risk_factors: i32,
752    /// Status message
753    pub status: String,
754    /// Check timestamp
755    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}