riglr_web_tools/
trenchbot.rs

1//! TrenchBot integration for Solana token bundle analysis and risk assessment
2//!
3//! This module provides tools for accessing TrenchBot API to analyze token bundles,
4//! detect potential rug pulls, identify sniper wallets, and assess creator risk.
5
6use crate::{client::WebClient, error::WebToolError};
7use riglr_macros::tool;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use tracing::{debug, info};
12
13/// Configuration for TrenchBot API access
14#[derive(Debug, Clone)]
15pub struct TrenchBotConfig {
16    /// API base URL (default: https://trench.bot/api)
17    pub base_url: String,
18    /// Rate limit requests per minute (default: 60)
19    pub rate_limit_per_minute: u32,
20    /// Timeout for API requests in seconds (default: 30)
21    pub request_timeout: u64,
22}
23
24impl Default for TrenchBotConfig {
25    fn default() -> Self {
26        Self {
27            base_url: "https://trench.bot/api".to_string(),
28            rate_limit_per_minute: 60,
29            request_timeout: 30,
30        }
31    }
32}
33
34/// Main bundle response from TrenchBot
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
36pub struct BundleResponse {
37    /// Indicates if the bundle is bonded
38    pub bonded: Option<bool>,
39    /// Collection of bundles, keyed by bundle ID
40    pub bundles: Option<HashMap<String, BundleDetails>>,
41    /// Analysis of the bundle creator
42    pub creator_analysis: Option<CreatorAnalysis>,
43    /// Total amount of tokens distributed
44    pub distributed_amount: Option<i64>,
45    /// Percentage of total tokens distributed
46    pub distributed_percentage: Option<f64>,
47    /// Number of wallets the token has been distributed to
48    pub distributed_wallets: Option<i32>,
49    /// Ticker symbol of the token
50    pub ticker: Option<String>,
51    /// Total number of bundles created for this token
52    pub total_bundles: Option<i32>,
53    /// Total amount of tokens held in bundles
54    pub total_holding_amount: Option<i64>,
55    /// Percentage of the total token supply held in bundles
56    pub total_holding_percentage: Option<f64>,
57    /// Total percentage of tokens that are bundled
58    pub total_percentage_bundled: Option<f64>,
59    /// Total SOL spent on creating the bundles
60    pub total_sol_spent: Option<f64>,
61    /// Total number of tokens included in bundles
62    pub total_tokens_bundled: Option<i64>,
63}
64
65/// Details about a specific bundle
66#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
67pub struct BundleDetails {
68    /// Analysis of the bundle
69    pub bundle_analysis: Option<BundleAnalysis>,
70    /// Amount of tokens held within the bundle
71    pub holding_amount: Option<i64>,
72    /// Percentage of total tokens held within the bundle
73    pub holding_percentage: Option<f64>,
74    /// Percentage of the total token supply within the bundle
75    pub token_percentage: Option<f64>,
76    /// Total SOL value of the bundle
77    pub total_sol: Option<f64>,
78    /// Total number of tokens within the bundle
79    pub total_tokens: Option<i64>,
80    /// Number of unique wallets interacting with the bundle
81    pub unique_wallets: Option<i32>,
82    /// Categories of wallets (e.g., "sniper", "regular"), keyed by wallet address
83    pub wallet_categories: Option<HashMap<String, String>>,
84    /// Information about individual wallets, keyed by wallet address
85    pub wallet_info: Option<HashMap<String, WalletInfo>>,
86}
87
88/// Analysis results for a bundle
89#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
90pub struct BundleAnalysis {
91    /// Breakdown of wallet categories within the bundle
92    pub category_breakdown: Option<HashMap<String, i32>>,
93    /// Indicates if the interaction is likely part of a bundle
94    pub is_likely_bundle: Option<bool>,
95    /// Primary category of the bundle (e.g., "regular", "sniper")
96    pub primary_category: Option<String>,
97}
98
99/// Information about a specific wallet's interaction with a bundle
100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
101pub struct WalletInfo {
102    /// SOL balance of the wallet associated with this bundle
103    pub sol: Option<f64>,
104    /// Percentage of total SOL in the bundle represented by this wallet
105    pub sol_percentage: Option<f64>,
106    /// Percentage of total tokens in the bundle held by this wallet
107    pub token_percentage: Option<f64>,
108    /// Number of tokens held by this wallet within the bundle
109    pub tokens: Option<i64>,
110}
111
112/// Analysis of the token creator
113#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
114pub struct CreatorAnalysis {
115    /// Wallet address of the token creator
116    pub address: Option<String>,
117    /// Creator's current holdings of the token
118    pub current_holdings: Option<i64>,
119    /// Historical data on the creator's activity
120    pub history: Option<CreatorHistory>,
121    /// Percentage of the token supply currently held by the creator
122    pub holding_percentage: Option<f64>,
123    /// Assessed risk level of the creator
124    pub risk_level: Option<RiskLevel>,
125    /// Warning flags associated with this creator
126    pub warning_flags: Option<Vec<String>>,
127}
128
129/// Historical data on the creator's activity
130#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
131pub struct CreatorHistory {
132    /// Average market cap of tokens created by this creator
133    pub average_market_cap: Option<i64>,
134    /// Indicates if the creator is considered high risk
135    pub high_risk: Option<bool>,
136    /// List of previously created coins
137    pub previous_coins: Option<Vec<PreviousCoin>>,
138    /// Number of recent rug pulls by the creator
139    pub recent_rugs: Option<i64>,
140    /// Total number of rug pulls by the creator
141    pub rug_count: Option<i64>,
142    /// Percentage of the creator's tokens that were rug pulls
143    pub rug_percentage: Option<f64>,
144    /// Total number of coins created by this creator
145    pub total_coins_created: Option<i64>,
146}
147
148/// Information about a previously created coin
149#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
150pub struct PreviousCoin {
151    /// Timestamp of when the coin was created
152    pub created_at: Option<i64>,
153    /// Indicates if the coin was a rug pull
154    pub is_rug: Option<bool>,
155    /// Market cap of the coin
156    pub market_cap: Option<i64>,
157    /// Mint address of the coin
158    pub mint: Option<String>,
159    /// Symbol of the coin
160    pub symbol: Option<String>,
161}
162
163/// Risk level enumeration
164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
165pub enum RiskLevel {
166    /// Low risk - Creator has good track record
167    #[serde(rename = "LOW")]
168    Low,
169    /// Medium risk - Some concerning patterns
170    #[serde(rename = "MEDIUM")]
171    Medium,
172    /// High risk - Multiple red flags detected
173    #[serde(rename = "HIGH")]
174    High,
175}
176
177/// Simplified bundle analysis result
178#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
179pub struct BundleAnalysisResult {
180    /// Token address/mint
181    pub token: String,
182    /// Token ticker symbol
183    pub ticker: Option<String>,
184    /// Whether the token is likely bundled
185    pub is_bundled: bool,
186    /// Total percentage of supply bundled
187    pub bundle_percentage: f64,
188    /// Number of bundles detected
189    pub bundle_count: i32,
190    /// Total SOL spent on bundles
191    pub total_sol_spent: f64,
192    /// Number of wallets holding the token
193    pub holder_count: i32,
194    /// Creator risk assessment
195    pub creator_risk: CreatorRiskAssessment,
196    /// Wallet category breakdown
197    pub wallet_categories: WalletCategoryBreakdown,
198    /// Key warning flags
199    pub warnings: Vec<String>,
200    /// Summary recommendation
201    pub recommendation: String,
202}
203
204/// Creator risk assessment
205#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
206pub struct CreatorRiskAssessment {
207    /// Creator wallet address
208    pub address: Option<String>,
209    /// Risk level
210    pub risk_level: Option<RiskLevel>,
211    /// Percentage of tokens that were rugs
212    pub rug_percentage: f64,
213    /// Total coins created
214    pub total_coins: i64,
215    /// Number of rug pulls
216    pub rug_count: i64,
217    /// Creator's current holding percentage
218    pub holding_percentage: f64,
219}
220
221/// Wallet category breakdown
222#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
223pub struct WalletCategoryBreakdown {
224    /// Number of sniper wallets
225    pub snipers: i32,
226    /// Number of regular wallets
227    pub regular: i32,
228    /// Percentage of tokens held by snipers
229    pub sniper_percentage: f64,
230    /// Most common wallet category
231    pub primary_category: String,
232}
233
234/// Get comprehensive bundle information for a Solana token from TrenchBot.
235/// This tool analyzes token distribution patterns to detect bundling and assess risk.
236#[tool]
237pub async fn get_bundle_info(
238    _context: &riglr_core::provider::ApplicationContext,
239    token: String,
240) -> crate::error::Result<BundleResponse> {
241    debug!("Fetching bundle info for token: {}", token);
242
243    let config = TrenchBotConfig::default();
244    let client = WebClient::default();
245
246    let url = format!("{}/bundle/bundle_advanced/{}", config.base_url, token);
247
248    info!("Requesting bundle info from: {}", url);
249
250    let response_text = client
251        .get(&url)
252        .await
253        .map_err(|e| WebToolError::Network(format!("Failed to fetch bundle info: {}", e)))?;
254
255    let bundle_response: BundleResponse = serde_json::from_str(&response_text)
256        .map_err(|e| WebToolError::Parsing(format!("Failed to parse TrenchBot response: {}", e)))?;
257
258    info!(
259        "Successfully fetched bundle info for {} - Bundles: {:?}, Bundled: {:?}%",
260        token, bundle_response.total_bundles, bundle_response.total_percentage_bundled
261    );
262
263    Ok(bundle_response)
264}
265
266/// Extract wallet category statistics from bundle data
267fn calculate_wallet_categories(
268    bundles: &HashMap<String, BundleDetails>,
269) -> (i32, i32, f64, String) {
270    let mut sniper_count = 0;
271    let mut regular_count = 0;
272    let mut sniper_tokens = 0i64;
273    let mut total_tokens = 0i64;
274
275    for bundle in bundles.values() {
276        if let Some(categories) = &bundle.wallet_categories {
277            for category in categories.values() {
278                match category.as_str() {
279                    "sniper" => sniper_count += 1,
280                    "regular" => regular_count += 1,
281                    _ => {}
282                }
283            }
284        }
285
286        if let Some(wallet_info) = &bundle.wallet_info {
287            for (addr, info) in wallet_info {
288                if let Some(tokens) = info.tokens {
289                    total_tokens += tokens;
290                    if let Some(categories) = &bundle.wallet_categories {
291                        if categories.get(addr).map_or(false, |c| c == "sniper") {
292                            sniper_tokens += tokens;
293                        }
294                    }
295                }
296            }
297        }
298    }
299
300    let sniper_percentage = if total_tokens > 0 {
301        (sniper_tokens as f64 / total_tokens as f64) * 100.0
302    } else {
303        0.0
304    };
305
306    let primary_category = if sniper_count > regular_count {
307        "sniper".to_string()
308    } else {
309        "regular".to_string()
310    };
311
312    (
313        sniper_count,
314        regular_count,
315        sniper_percentage,
316        primary_category,
317    )
318}
319
320/// Build creator risk assessment from creator analysis data
321fn build_creator_risk_assessment(
322    creator_analysis: Option<&CreatorAnalysis>,
323) -> CreatorRiskAssessment {
324    if let Some(creator) = creator_analysis {
325        CreatorRiskAssessment {
326            address: creator.address.clone(),
327            risk_level: creator.risk_level.clone(),
328            rug_percentage: creator
329                .history
330                .as_ref()
331                .and_then(|h| h.rug_percentage)
332                .unwrap_or(0.0),
333            total_coins: creator
334                .history
335                .as_ref()
336                .and_then(|h| h.total_coins_created)
337                .unwrap_or(0),
338            rug_count: creator
339                .history
340                .as_ref()
341                .and_then(|h| h.rug_count)
342                .unwrap_or(0),
343            holding_percentage: creator.holding_percentage.unwrap_or(0.0),
344        }
345    } else {
346        CreatorRiskAssessment {
347            address: None,
348            risk_level: None,
349            rug_percentage: 0.0,
350            total_coins: 0,
351            rug_count: 0,
352            holding_percentage: 0.0,
353        }
354    }
355}
356
357/// Collect warning flags based on bundle data and analysis
358fn collect_warnings(bundle_data: &BundleResponse, sniper_percentage: f64) -> Vec<String> {
359    let mut warnings = Vec::new();
360
361    if bundle_data.total_percentage_bundled.unwrap_or(0.0) > 50.0 {
362        warnings.push("High bundle concentration (>50% bundled)".to_string());
363    }
364
365    if sniper_percentage > 30.0 {
366        warnings.push("High sniper wallet presence".to_string());
367    }
368
369    if let Some(creator) = &bundle_data.creator_analysis {
370        if let Some(flags) = &creator.warning_flags {
371            warnings.extend(flags.clone());
372        }
373
374        if matches!(creator.risk_level, Some(RiskLevel::High)) {
375            warnings.push("Creator has high risk profile".to_string());
376        }
377
378        if creator.holding_percentage.unwrap_or(0.0) > 20.0 {
379            warnings.push("Creator holds >20% of supply".to_string());
380        }
381    }
382
383    warnings
384}
385
386/// Generate recommendation based on bundle percentage
387fn generate_recommendation(bundle_percentage: f64) -> String {
388    if bundle_percentage > 70.0 {
389        "EXTREME CAUTION: Very high bundle concentration detected. High risk of coordinated dump."
390    } else if bundle_percentage > 50.0 {
391        "HIGH RISK: Significant bundling detected. Potential for price manipulation."
392    } else if bundle_percentage > 30.0 {
393        "MODERATE RISK: Notable bundling present. Monitor closely for unusual activity."
394    } else if bundle_percentage > 10.0 {
395        "LOW-MODERATE RISK: Some bundling detected but within acceptable range."
396    } else {
397        "LOW RISK: Minimal bundling detected. Distribution appears organic."
398    }
399    .to_string()
400}
401
402/// Analyze a Solana token for bundling patterns and provide risk assessment.
403/// This tool provides a simplified analysis based on TrenchBot data.
404#[tool]
405pub async fn analyze_token_bundles(
406    context: &riglr_core::provider::ApplicationContext,
407    token: String,
408) -> crate::error::Result<BundleAnalysisResult> {
409    debug!("Analyzing token bundles for: {}", token);
410
411    let bundle_data = get_bundle_info(context, token.clone()).await?;
412
413    let (sniper_count, regular_count, sniper_percentage, primary_category) =
414        if let Some(bundles) = &bundle_data.bundles {
415            calculate_wallet_categories(bundles)
416        } else {
417            (0, 0, 0.0, "regular".to_string())
418        };
419
420    let creator_risk = build_creator_risk_assessment(bundle_data.creator_analysis.as_ref());
421    let warnings = collect_warnings(&bundle_data, sniper_percentage);
422    let bundle_percentage = bundle_data.total_percentage_bundled.unwrap_or(0.0);
423    let recommendation = generate_recommendation(bundle_percentage);
424
425    Ok(BundleAnalysisResult {
426        token,
427        ticker: bundle_data.ticker,
428        is_bundled: bundle_data.total_bundles.unwrap_or(0) > 0,
429        bundle_percentage,
430        bundle_count: bundle_data.total_bundles.unwrap_or(0),
431        total_sol_spent: bundle_data.total_sol_spent.unwrap_or(0.0),
432        holder_count: bundle_data.distributed_wallets.unwrap_or(0),
433        creator_risk,
434        wallet_categories: WalletCategoryBreakdown {
435            snipers: sniper_count,
436            regular: regular_count,
437            sniper_percentage,
438            primary_category,
439        },
440        warnings,
441        recommendation,
442    })
443}
444
445/// Check if a Solana token shows signs of bundling manipulation.
446/// Returns a simple assessment of bundling risk.
447#[tool]
448pub async fn check_bundle_risk(
449    context: &riglr_core::provider::ApplicationContext,
450    token: String,
451) -> crate::error::Result<BundleRiskCheck> {
452    debug!("Checking bundle risk for token: {}", token);
453
454    let bundle_data = get_bundle_info(context, token.clone()).await?;
455
456    let bundle_percentage = bundle_data.total_percentage_bundled.unwrap_or(0.0);
457    let bundle_count = bundle_data.total_bundles.unwrap_or(0);
458
459    let risk_level = if bundle_percentage > 70.0 {
460        "EXTREME"
461    } else if bundle_percentage > 50.0 {
462        "HIGH"
463    } else if bundle_percentage > 30.0 {
464        "MODERATE"
465    } else if bundle_percentage > 10.0 {
466        "LOW-MODERATE"
467    } else {
468        "LOW"
469    };
470
471    let is_high_risk = bundle_percentage > 50.0;
472
473    let message = format!(
474        "{} risk: {:.1}% of supply is bundled across {} bundles",
475        risk_level, bundle_percentage, bundle_count
476    );
477
478    Ok(BundleRiskCheck {
479        token,
480        is_bundled: bundle_count > 0,
481        bundle_percentage,
482        bundle_count,
483        risk_level: risk_level.to_string(),
484        is_high_risk,
485        message,
486    })
487}
488
489/// Simple bundle risk check result
490#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
491pub struct BundleRiskCheck {
492    /// Token address
493    pub token: String,
494    /// Whether bundling is detected
495    pub is_bundled: bool,
496    /// Percentage of supply bundled
497    pub bundle_percentage: f64,
498    /// Number of bundles
499    pub bundle_count: i32,
500    /// Risk level assessment
501    pub risk_level: String,
502    /// Whether this represents high risk
503    pub is_high_risk: bool,
504    /// Summary message
505    pub message: String,
506}
507
508/// Analyze the creator of a Solana token for historical rug pull activity.
509/// Provides detailed creator risk assessment based on their track record.
510#[tool]
511pub async fn analyze_creator_risk(
512    context: &riglr_core::provider::ApplicationContext,
513    token: String,
514) -> crate::error::Result<CreatorAnalysisResult> {
515    debug!("Analyzing creator risk for token: {}", token);
516
517    let bundle_data = get_bundle_info(context, token.clone()).await?;
518
519    if let Some(creator) = bundle_data.creator_analysis {
520        let mut red_flags = Vec::new();
521
522        // Check for red flags
523        if let Some(history) = &creator.history {
524            if history.rug_count.unwrap_or(0) > 0 {
525                red_flags.push(format!(
526                    "Creator has {} previous rug pulls",
527                    history.rug_count.unwrap_or(0)
528                ));
529            }
530
531            if history.rug_percentage.unwrap_or(0.0) > 20.0 {
532                red_flags.push(format!(
533                    "{:.1}% of creator's tokens were rugs",
534                    history.rug_percentage.unwrap_or(0.0)
535                ));
536            }
537
538            if history.high_risk.unwrap_or(false) {
539                red_flags.push("Creator flagged as high risk".to_string());
540            }
541
542            if history.recent_rugs.unwrap_or(0) > 0 {
543                red_flags.push(format!(
544                    "{} recent rug pulls detected",
545                    history.recent_rugs.unwrap_or(0)
546                ));
547            }
548        }
549
550        if creator.holding_percentage.unwrap_or(0.0) > 30.0 {
551            red_flags.push(format!(
552                "Creator holds {:.1}% of supply",
553                creator.holding_percentage.unwrap_or(0.0)
554            ));
555        }
556
557        let risk_assessment = match &creator.risk_level {
558            Some(RiskLevel::High) => "HIGH RISK: Creator has concerning history",
559            Some(RiskLevel::Medium) => "MODERATE RISK: Some red flags in creator history",
560            Some(RiskLevel::Low) => "LOW RISK: Creator appears legitimate",
561            None => "UNKNOWN: Unable to assess creator risk",
562        }
563        .to_string();
564
565        Ok(CreatorAnalysisResult {
566            token,
567            creator_address: creator.address,
568            risk_level: creator.risk_level,
569            current_holdings: creator.current_holdings,
570            holding_percentage: creator.holding_percentage.unwrap_or(0.0),
571            total_coins_created: creator
572                .history
573                .as_ref()
574                .and_then(|h| h.total_coins_created)
575                .unwrap_or(0),
576            rug_count: creator
577                .history
578                .as_ref()
579                .and_then(|h| h.rug_count)
580                .unwrap_or(0),
581            rug_percentage: creator
582                .history
583                .as_ref()
584                .and_then(|h| h.rug_percentage)
585                .unwrap_or(0.0),
586            average_market_cap: creator.history.as_ref().and_then(|h| h.average_market_cap),
587            previous_coins: creator
588                .history
589                .and_then(|h| h.previous_coins)
590                .unwrap_or_default(),
591            red_flags,
592            risk_assessment,
593        })
594    } else {
595        Ok(CreatorAnalysisResult {
596            token,
597            creator_address: None,
598            risk_level: None,
599            current_holdings: None,
600            holding_percentage: 0.0,
601            total_coins_created: 0,
602            rug_count: 0,
603            rug_percentage: 0.0,
604            average_market_cap: None,
605            previous_coins: Vec::new(),
606            red_flags: vec!["No creator analysis available".to_string()],
607            risk_assessment: "UNKNOWN: Creator information not available".to_string(),
608        })
609    }
610}
611
612/// Creator analysis result
613#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
614pub struct CreatorAnalysisResult {
615    /// Token being analyzed
616    pub token: String,
617    /// Creator wallet address
618    pub creator_address: Option<String>,
619    /// Risk level
620    pub risk_level: Option<RiskLevel>,
621    /// Creator's current token holdings
622    pub current_holdings: Option<i64>,
623    /// Percentage of supply held by creator
624    pub holding_percentage: f64,
625    /// Total number of coins created
626    pub total_coins_created: i64,
627    /// Number of rug pulls
628    pub rug_count: i64,
629    /// Percentage of tokens that were rugs
630    pub rug_percentage: f64,
631    /// Average market cap of created tokens
632    pub average_market_cap: Option<i64>,
633    /// List of previous coins
634    pub previous_coins: Vec<PreviousCoin>,
635    /// Red flags identified
636    pub red_flags: Vec<String>,
637    /// Risk assessment summary
638    pub risk_assessment: String,
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    #[test]
646    fn test_trenchbot_config_default() {
647        let config = TrenchBotConfig::default();
648        assert_eq!(config.base_url, "https://trench.bot/api");
649        assert_eq!(config.rate_limit_per_minute, 60);
650        assert_eq!(config.request_timeout, 30);
651    }
652
653    #[test]
654    fn test_risk_level_serialization() {
655        let risk = RiskLevel::High;
656        let json = serde_json::to_string(&risk).unwrap();
657        assert_eq!(json, "\"HIGH\"");
658
659        let risk: RiskLevel = serde_json::from_str("\"MEDIUM\"").unwrap();
660        assert!(matches!(risk, RiskLevel::Medium));
661    }
662
663    #[test]
664    fn test_bundle_response_deserialization() {
665        let json = r#"{
666            "bonded": true,
667            "ticker": "TEST",
668            "total_bundles": 5,
669            "total_percentage_bundled": 25.5
670        }"#;
671
672        let response: BundleResponse = serde_json::from_str(json).unwrap();
673        assert_eq!(response.bonded, Some(true));
674        assert_eq!(response.ticker, Some("TEST".to_string()));
675        assert_eq!(response.total_bundles, Some(5));
676        assert_eq!(response.total_percentage_bundled, Some(25.5));
677    }
678
679    #[test]
680    fn test_creator_analysis_deserialization() {
681        let json = r#"{
682            "address": "SomeWalletAddress",
683            "risk_level": "HIGH",
684            "holding_percentage": 15.5,
685            "history": {
686                "rug_count": 3,
687                "total_coins_created": 10,
688                "rug_percentage": 30.0
689            }
690        }"#;
691
692        let creator: CreatorAnalysis = serde_json::from_str(json).unwrap();
693        assert_eq!(creator.address, Some("SomeWalletAddress".to_string()));
694        assert!(matches!(creator.risk_level, Some(RiskLevel::High)));
695        assert_eq!(creator.holding_percentage, Some(15.5));
696        assert!(creator.history.is_some());
697
698        let history = creator.history.unwrap();
699        assert_eq!(history.rug_count, Some(3));
700        assert_eq!(history.total_coins_created, Some(10));
701        assert_eq!(history.rug_percentage, Some(30.0));
702    }
703}