riglr_web_tools/
lib.rs

1//! # riglr-web-tools
2//!
3//! Web-based data tools for riglr agents, providing access to social media, market data,
4//! and web search capabilities.
5//!
6//! This crate bridges the gap between on-chain data and off-chain information sources,
7//! enabling AI agents to gather comprehensive market intelligence and social sentiment.
8//!
9//! ## Features
10//!
11//! - **Social Media Tools**: Twitter/X integration for sentiment analysis
12//! - **Market Data Tools**: DexScreener integration for token metrics
13//! - **Web Search Tools**: Exa API integration for intelligent web search
14//! - **Rate Limiting**: Built-in rate limiting and API quota management
15//! - **Caching**: Optional response caching to improve performance
16//!
17//! ## Quick Start
18//!
19//! ```ignore
20//! // Example usage (requires rig-core dependency):
21//! use riglr_web_tools::twitter::search_tweets;
22//! use rig_core::Agent;
23//!
24//! # async fn example() -> anyhow::Result<()> {
25//! let agent = Agent::builder()
26//!     .preamble("You are a market sentiment analyst.")
27//!     .tool(search_tweets)
28//!     .build();
29//!
30//! let response = agent.prompt("What's the current sentiment on Twitter about $SOL?").await?;
31//! println!("Agent response: {}", response);
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! ## API Configuration
37//!
38//! Most tools require API keys. Set the following environment variables:
39//!
40//! - `TWITTER_BEARER_TOKEN` - For Twitter API access
41//! - `EXA_API_KEY` - For Exa web search
42//! - `DEXSCREENER_API_KEY` - For DexScreener (if required)
43//! - `LUNARCRUSH_API_KEY` - For LunarCrush social analytics
44//! - `FASTER100X_API_KEY` - For Faster100x holder analysis
45//!
46//! ## Tool Categories
47//!
48//! - [`twitter`] - Twitter/X integration for social sentiment
49//! - [`dexscreener`] - Token market data and trading metrics
50//! - [`web_search`] - Intelligent web search capabilities
51//! - [`news`] - Cryptocurrency news aggregation
52//! - [`lunarcrush`] - LunarCrush social analytics and sentiment tracking
53//! - [`faster100x`] - Token holder analysis and whale activity tracking
54//! - [`rugcheck`] - Solana token security analysis and rug pull detection
55//! - [`trenchbot`] - Solana token bundle analysis and sniper detection
56//! - [`pocketuniverse`] - Solana token rug pull detection based on wallet history
57//! - [`tweetscout`] - Twitter/X account credibility scoring and social network analysis
58
59pub mod client;
60pub mod dexscreener;
61pub mod dexscreener_api;
62pub mod error;
63pub mod faster100x;
64pub mod lunarcrush;
65pub mod news;
66pub mod pocketuniverse;
67pub mod price;
68pub mod rugcheck;
69pub mod trenchbot;
70pub mod tweetscout;
71pub mod twitter;
72pub mod web_search;
73
74// Re-export commonly used tools - be selective to avoid name conflicts
75// From dexscreener
76pub use dexscreener::{
77    analyze_token_market, get_token_info, get_top_pairs, get_trending_tokens, search_tokens,
78    ChainInfo, MarketAnalysis, TokenInfo, TokenPair,
79};
80
81// From news
82pub use news::{
83    analyze_market_sentiment, get_crypto_news, get_trending_news, monitor_breaking_news,
84    LexiconSentimentAnalyzer, NewsAggregationResult, NewsArticle, NewsSource, SentimentAnalyzer,
85};
86
87// From twitter
88pub use twitter::{
89    analyze_crypto_sentiment, get_user_tweets, search_tweets, SentimentAnalysis,
90    SentimentBreakdown, TwitterPost, TwitterSearchResult, TwitterUser,
91};
92
93// From web_search
94pub use web_search::{
95    find_similar_pages, search_recent_news, search_web, summarize_web_content, ContentSummary,
96    SearchResult, WebSearchResult,
97};
98
99// From lunarcrush
100pub use lunarcrush::{
101    get_influencer_mentions, get_social_sentiment, get_trending_cryptos, InfluencerMention,
102    InfluencerMentionsResult, SentimentData, TrendingCrypto,
103};
104
105// From faster100x
106pub use faster100x::{
107    analyze_token_holders, get_holder_trends, get_whale_activity, ConcentrationRisk, HolderTrends,
108    TokenHolderAnalysis, WalletHolding, WhaleActivity,
109};
110
111// From price
112pub use price::{get_token_price, get_token_prices_batch, TokenPriceResult};
113
114// From rugcheck
115pub use rugcheck::{
116    analyze_token_risks, check_if_rugged, get_token_report, RiskAnalysis, RiskLevel,
117    RugCheckResult, TokenCheck, TokenHolder as RugCheckTokenHolder,
118};
119
120// From trenchbot
121pub use trenchbot::{
122    analyze_creator_risk, analyze_token_bundles, check_bundle_risk, get_bundle_info,
123    BundleAnalysisResult, BundleResponse, BundleRiskCheck, CreatorAnalysisResult,
124};
125
126// From pocketuniverse
127pub use pocketuniverse::{
128    analyze_rug_risk, check_rug_pull, check_rug_pull_raw, is_token_safe, DetailedRugAnalysis,
129    RugApiResponse, RugCheckResult as PocketUniverseRugCheck, SafetyCheck,
130};
131
132// From tweetscout
133pub use tweetscout::{
134    analyze_account, analyze_social_network, get_account_info, get_account_score,
135    get_top_followers, get_top_friends, is_account_credible, AccountAnalysis, AccountInfo,
136    CredibilityCheck, SocialNetworkAnalysis,
137};
138
139// Re-export client and error types
140pub use client::WebClient;
141pub use error::{Result, WebToolError};
142
143/// Current version of riglr-web-tools
144pub const VERSION: &str = env!("CARGO_PKG_VERSION");
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_version_when_valid_should_start_with_semver_digit() {
152        // Happy Path: VERSION should be a valid semver
153        assert!(
154            VERSION.starts_with("0.") || VERSION.starts_with("1."),
155            "VERSION should be a valid semver"
156        );
157    }
158
159    #[test]
160    fn test_version_when_called_should_not_be_empty() {
161        // Edge Case: VERSION should not be empty
162        assert!(!VERSION.is_empty(), "VERSION should not be empty");
163    }
164
165    #[test]
166    fn test_version_when_called_should_contain_dots() {
167        // Edge Case: VERSION should contain dots for semver format
168        assert!(
169            VERSION.contains('.'),
170            "VERSION should contain dots for semver format"
171        );
172    }
173
174    #[test]
175    fn test_version_when_called_should_be_valid_utf8() {
176        // Edge Case: VERSION should be valid UTF-8
177        assert!(VERSION.is_ascii(), "VERSION should be valid ASCII");
178    }
179
180    #[test]
181    fn test_version_when_called_should_match_cargo_version() {
182        // Integration test: VERSION should match what's in Cargo.toml
183        let version = env!("CARGO_PKG_VERSION");
184        assert_eq!(VERSION, version, "VERSION should match CARGO_PKG_VERSION");
185    }
186
187    #[test]
188    fn test_version_when_parsed_should_have_major_minor_patch() {
189        // Edge Case: VERSION should have at least major.minor format
190        let parts: Vec<&str> = VERSION.split('.').collect();
191        assert!(
192            parts.len() >= 2,
193            "VERSION should have at least major.minor format"
194        );
195
196        // Verify major version is numeric
197        assert!(
198            parts[0].parse::<u32>().is_ok(),
199            "Major version should be numeric"
200        );
201
202        // Verify minor version is numeric (may contain pre-release info)
203        let minor_part = parts[1].split('-').next().unwrap_or(parts[1]);
204        assert!(
205            minor_part.parse::<u32>().is_ok(),
206            "Minor version should be numeric"
207        );
208    }
209
210    #[test]
211    fn test_module_re_exports_are_accessible() {
212        // Test that re-exported types are accessible
213        // This ensures the pub use statements work correctly
214
215        // Test a few key re-exports from different modules
216        use crate::{Result, WebClient, WebToolError, VERSION};
217
218        // These should compile without issues, proving the re-exports work
219        let _version: &str = VERSION;
220        let _error_type = std::marker::PhantomData::<WebToolError>;
221        let _result_type = std::marker::PhantomData::<Result<()>>;
222        let _client_type = std::marker::PhantomData::<WebClient>;
223    }
224
225    #[test]
226    fn test_dexscreener_re_exports_are_accessible() {
227        // Test dexscreener re-exports
228        use crate::{ChainInfo, MarketAnalysis, TokenInfo, TokenPair};
229
230        let _chain_info_type = std::marker::PhantomData::<ChainInfo>;
231        let _market_analysis_type = std::marker::PhantomData::<MarketAnalysis>;
232        let _token_info_type = std::marker::PhantomData::<TokenInfo>;
233        let _token_pair_type = std::marker::PhantomData::<TokenPair>;
234    }
235
236    #[test]
237    fn test_news_re_exports_are_accessible() {
238        // Test news re-exports
239        use crate::{NewsAggregationResult, NewsArticle, NewsSource};
240
241        let _news_aggregation_type = std::marker::PhantomData::<NewsAggregationResult>;
242        let _news_article_type = std::marker::PhantomData::<NewsArticle>;
243        let _news_source_type = std::marker::PhantomData::<NewsSource>;
244    }
245
246    #[test]
247    fn test_twitter_re_exports_are_accessible() {
248        // Test twitter re-exports
249        use crate::{
250            SentimentAnalysis, SentimentBreakdown, TwitterPost, TwitterSearchResult, TwitterUser,
251        };
252
253        let _sentiment_analysis_type = std::marker::PhantomData::<SentimentAnalysis>;
254        let _sentiment_breakdown_type = std::marker::PhantomData::<SentimentBreakdown>;
255        let _twitter_post_type = std::marker::PhantomData::<TwitterPost>;
256        let _twitter_search_result_type = std::marker::PhantomData::<TwitterSearchResult>;
257        let _twitter_user_type = std::marker::PhantomData::<TwitterUser>;
258    }
259
260    #[test]
261    fn test_web_search_re_exports_are_accessible() {
262        // Test web_search re-exports
263        use crate::{ContentSummary, SearchResult, WebSearchResult};
264
265        let _content_summary_type = std::marker::PhantomData::<ContentSummary>;
266        let _search_result_type = std::marker::PhantomData::<SearchResult>;
267        let _web_search_result_type = std::marker::PhantomData::<WebSearchResult>;
268    }
269
270    #[test]
271    fn test_lunarcrush_re_exports_are_accessible() {
272        // Test lunarcrush re-exports
273        use crate::{InfluencerMention, InfluencerMentionsResult, SentimentData, TrendingCrypto};
274
275        let _influencer_mention_type = std::marker::PhantomData::<InfluencerMention>;
276        let _influencer_mentions_result_type = std::marker::PhantomData::<InfluencerMentionsResult>;
277        let _sentiment_data_type = std::marker::PhantomData::<SentimentData>;
278        let _trending_crypto_type = std::marker::PhantomData::<TrendingCrypto>;
279    }
280
281    #[test]
282    fn test_faster100x_re_exports_are_accessible() {
283        // Test faster100x re-exports
284        use crate::{
285            ConcentrationRisk, HolderTrends, TokenHolderAnalysis, WalletHolding, WhaleActivity,
286        };
287
288        let _concentration_risk_type = std::marker::PhantomData::<ConcentrationRisk>;
289        let _holder_trends_type = std::marker::PhantomData::<HolderTrends>;
290        let _token_holder_analysis_type = std::marker::PhantomData::<TokenHolderAnalysis>;
291        let _wallet_holding_type = std::marker::PhantomData::<WalletHolding>;
292        let _whale_activity_type = std::marker::PhantomData::<WhaleActivity>;
293    }
294
295    #[test]
296    fn test_price_re_exports_are_accessible() {
297        // Test price re-exports
298        use crate::TokenPriceResult;
299
300        let _token_price_result_type = std::marker::PhantomData::<TokenPriceResult>;
301    }
302
303    #[test]
304    fn test_rugcheck_re_exports_are_accessible() {
305        // Test rugcheck re-exports
306        use crate::{RiskAnalysis, RiskLevel, RugCheckResult, RugCheckTokenHolder, TokenCheck};
307
308        let _risk_analysis_type = std::marker::PhantomData::<RiskAnalysis>;
309        let _risk_level_type = std::marker::PhantomData::<RiskLevel>;
310        let _rugcheck_result_type = std::marker::PhantomData::<RugCheckResult>;
311        let _token_check_type = std::marker::PhantomData::<TokenCheck>;
312        let _rugcheck_token_holder_type = std::marker::PhantomData::<RugCheckTokenHolder>;
313    }
314
315    #[test]
316    fn test_trenchbot_re_exports_are_accessible() {
317        // Test trenchbot re-exports
318        use crate::{BundleAnalysisResult, BundleResponse, BundleRiskCheck, CreatorAnalysisResult};
319
320        let _bundle_analysis_result_type = std::marker::PhantomData::<BundleAnalysisResult>;
321        let _bundle_response_type = std::marker::PhantomData::<BundleResponse>;
322        let _bundle_risk_check_type = std::marker::PhantomData::<BundleRiskCheck>;
323        let _creator_analysis_result_type = std::marker::PhantomData::<CreatorAnalysisResult>;
324    }
325
326    #[test]
327    fn test_pocketuniverse_re_exports_are_accessible() {
328        // Test pocketuniverse re-exports
329        use crate::{DetailedRugAnalysis, PocketUniverseRugCheck, RugApiResponse, SafetyCheck};
330
331        let _detailed_rug_analysis_type = std::marker::PhantomData::<DetailedRugAnalysis>;
332        let _pocket_universe_rug_check_type = std::marker::PhantomData::<PocketUniverseRugCheck>;
333        let _rug_api_response_type = std::marker::PhantomData::<RugApiResponse>;
334        let _safety_check_type = std::marker::PhantomData::<SafetyCheck>;
335    }
336
337    #[test]
338    fn test_tweetscout_re_exports_are_accessible() {
339        // Test tweetscout re-exports
340        use crate::{AccountAnalysis, AccountInfo, CredibilityCheck, SocialNetworkAnalysis};
341
342        let _account_analysis_type = std::marker::PhantomData::<AccountAnalysis>;
343        let _account_info_type = std::marker::PhantomData::<AccountInfo>;
344        let _credibility_check_type = std::marker::PhantomData::<CredibilityCheck>;
345        let _social_network_analysis_type = std::marker::PhantomData::<SocialNetworkAnalysis>;
346    }
347
348    #[test]
349    fn test_all_function_re_exports_are_accessible() {
350        // Test that function re-exports are accessible (compile-time check)
351        use crate::{
352            analyze_account, analyze_creator_risk, analyze_crypto_sentiment,
353            analyze_market_sentiment, analyze_rug_risk, analyze_social_network,
354            analyze_token_bundles, analyze_token_holders, analyze_token_market,
355            analyze_token_risks, check_bundle_risk, check_if_rugged, check_rug_pull,
356            check_rug_pull_raw, find_similar_pages, get_account_info, get_account_score,
357            get_bundle_info, get_crypto_news, get_holder_trends, get_influencer_mentions,
358            get_social_sentiment, get_token_info, get_token_price, get_token_prices_batch,
359            get_token_report, get_top_followers, get_top_friends, get_top_pairs,
360            get_trending_cryptos, get_trending_news, get_trending_tokens, get_user_tweets,
361            get_whale_activity, is_account_credible, is_token_safe, monitor_breaking_news,
362            search_recent_news, search_tokens, search_tweets, search_web, summarize_web_content,
363        };
364
365        // These functions are async and return impl Future, so we can't cast them to simple function pointers.
366        // Instead, we verify they exist by referencing them as function items
367        let _analyze_token_market = analyze_token_market;
368        let _get_token_info = get_token_info;
369        let _get_top_pairs = get_top_pairs;
370        let _get_trending_tokens = get_trending_tokens;
371        let _search_tokens = search_tokens;
372
373        let _analyze_market_sentiment = analyze_market_sentiment;
374        let _get_crypto_news = get_crypto_news;
375        let _get_trending_news = get_trending_news;
376        let _monitor_breaking_news = monitor_breaking_news;
377
378        let _analyze_crypto_sentiment = analyze_crypto_sentiment;
379        let _get_user_tweets = get_user_tweets;
380        let _search_tweets = search_tweets;
381
382        let _find_similar_pages = find_similar_pages;
383        let _search_recent_news = search_recent_news;
384        let _search_web = search_web;
385        let _summarize_web_content = summarize_web_content;
386
387        let _get_influencer_mentions = get_influencer_mentions;
388        let _get_social_sentiment = get_social_sentiment;
389        let _get_trending_cryptos = get_trending_cryptos;
390
391        let _analyze_token_holders = analyze_token_holders;
392        let _get_holder_trends = get_holder_trends;
393        let _get_whale_activity = get_whale_activity;
394
395        let _get_token_price = get_token_price;
396        let _get_token_prices_batch = get_token_prices_batch;
397
398        let _analyze_token_risks = analyze_token_risks;
399        let _check_if_rugged = check_if_rugged;
400        let _get_token_report = get_token_report;
401
402        let _analyze_creator_risk = analyze_creator_risk;
403        let _analyze_token_bundles = analyze_token_bundles;
404        let _check_bundle_risk = check_bundle_risk;
405        let _get_bundle_info = get_bundle_info;
406
407        let _analyze_rug_risk = analyze_rug_risk;
408        let _check_rug_pull = check_rug_pull;
409        let _check_rug_pull_raw = check_rug_pull_raw;
410        let _is_token_safe = is_token_safe;
411
412        let _analyze_account = analyze_account;
413        let _analyze_social_network = analyze_social_network;
414        let _get_account_info = get_account_info;
415        let _get_account_score = get_account_score;
416        let _get_top_followers = get_top_followers;
417        let _get_top_friends = get_top_friends;
418        let _is_account_credible = is_account_credible;
419    }
420
421    #[test]
422    fn test_version_constant_is_static() {
423        // Test that VERSION is a static string reference
424        let version_ref: &'static str = VERSION;
425        assert!(!version_ref.is_empty(), "VERSION should not be empty");
426    }
427
428    #[test]
429    fn test_module_declarations_are_public() {
430        // This test verifies that all the modules are properly declared as public
431        // by attempting to access module paths (compile-time verification)
432
433        // Test that all modules can be referenced
434        let _client_mod = crate::client::WebClient::default;
435        let _dexscreener_mod = crate::dexscreener::get_token_info;
436        // dexscreener_api module contains utility functions and types, not a main struct
437        let _error_mod = std::marker::PhantomData::<crate::error::WebToolError>;
438        let _faster100x_mod = crate::faster100x::analyze_token_holders;
439        let _lunarcrush_mod = crate::lunarcrush::get_social_sentiment;
440        let _news_mod = crate::news::get_crypto_news;
441        let _pocketuniverse_mod = crate::pocketuniverse::check_rug_pull;
442        let _price_mod = crate::price::get_token_price;
443        let _rugcheck_mod = crate::rugcheck::get_token_report;
444        let _trenchbot_mod = crate::trenchbot::get_bundle_info;
445        let _tweetscout_mod = crate::tweetscout::get_account_info;
446        let _twitter_mod = crate::twitter::search_tweets;
447        let _web_search_mod = crate::web_search::search_web;
448    }
449}