1use {
2 crate::{cache::FileCache, error::Result},
3 serde::{Deserialize, Deserializer, Serialize},
4};
5
6#[cfg(feature = "tracing")]
8macro_rules! log_info {
9 ($($arg:tt)*) => { tracing::info!($($arg)*) };
10}
11
12#[cfg(not(feature = "tracing"))]
13macro_rules! log_info {
14 ($($arg:tt)*) => {};
15}
16
17#[cfg(feature = "tracing")]
19macro_rules! log_debug {
20 ($($arg:tt)*) => { tracing::debug!($($arg)*) };
21}
22
23#[cfg(not(feature = "tracing"))]
24macro_rules! log_debug {
25 ($($arg:tt)*) => {};
26}
27
28#[cfg(feature = "tracing")]
30macro_rules! log_warn {
31 ($($arg:tt)*) => { tracing::warn!($($arg)*) };
32}
33
34#[cfg(not(feature = "tracing"))]
35macro_rules! log_warn {
36 ($($arg:tt)*) => {};
37}
38
39#[cfg(feature = "tracing")]
41macro_rules! log_error {
42 ($($arg:tt)*) => { tracing::error!($($arg)*) };
43}
44
45#[cfg(not(feature = "tracing"))]
46macro_rules! log_error {
47 ($($arg:tt)*) => {};
48}
49
50const GAMMA_API_BASE: &str = "https://gamma-api.polymarket.com";
51
52fn deserialize_clob_token_ids<'de, D>(
54 deserializer: D,
55) -> std::result::Result<Option<Vec<String>>, D::Error>
56where
57 D: Deserializer<'de>,
58{
59 use serde::de::Error;
60
61 let opt: Option<serde_json::Value> = Option::deserialize(deserializer)?;
63
64 let value = match opt {
65 Some(v) => v,
66 None => return Ok(None),
67 };
68
69 if value.is_null() {
70 return Ok(None);
71 }
72
73 match value {
74 serde_json::Value::String(s) => {
75 serde_json::from_str(&s).map(Some).map_err(Error::custom)
77 },
78 serde_json::Value::Array(arr) => {
79 Ok(Some(
81 arr.into_iter()
82 .map(|v| {
83 if let serde_json::Value::String(s) = v {
84 s
85 } else {
86 v.to_string()
87 }
88 })
89 .collect(),
90 ))
91 },
92 _ => Ok(None),
93 }
94}
95
96fn deserialize_string_array<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
98where
99 D: Deserializer<'de>,
100{
101 use serde::de::Error;
102
103 let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
104
105 match value {
106 serde_json::Value::String(s) => {
107 serde_json::from_str(&s).map_err(Error::custom)
109 },
110 serde_json::Value::Array(arr) => {
111 Ok(arr
113 .into_iter()
114 .map(|v| {
115 if let serde_json::Value::String(s) = v {
116 s
117 } else {
118 v.to_string()
119 }
120 })
121 .collect())
122 },
123 _ => Ok(vec![]),
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct Event {
129 pub id: String,
130 pub slug: String,
131 pub title: String,
132 pub active: bool,
133 pub closed: bool,
134 #[serde(default)]
135 pub tags: Vec<Tag>,
136 pub markets: Vec<Market>,
137 #[serde(rename = "endDate", default)]
138 pub end_date: Option<String>, #[serde(default)]
140 pub image: Option<String>, }
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct Tag {
145 pub id: String,
146 pub label: String,
147 pub slug: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct Market {
152 #[serde(default)]
153 pub id: Option<String>,
154 pub question: String,
155 #[serde(rename = "groupItemTitle", default)]
157 pub group_item_title: Option<String>,
158 #[serde(
159 rename = "clobTokenIds",
160 deserialize_with = "deserialize_clob_token_ids",
161 default
162 )]
163 pub clob_token_ids: Option<Vec<String>>,
164 #[serde(deserialize_with = "deserialize_string_array", default)]
165 pub outcomes: Vec<String>,
166 #[serde(
167 rename = "outcomePrices",
168 deserialize_with = "deserialize_string_array",
169 default
170 )]
171 pub outcome_prices: Vec<String>,
172 #[serde(rename = "volume24hr", default)]
173 pub volume_24hr: Option<f64>,
174 #[serde(rename = "volumeTotal", default)]
175 pub volume_total: Option<f64>,
176 #[serde(default)]
178 pub active: bool,
179 #[serde(default)]
181 pub closed: bool,
182 #[serde(default)]
184 pub slug: Option<String>,
185 #[serde(rename = "acceptingOrders", default)]
187 pub accepting_orders: bool,
188 #[serde(rename = "umaResolutionStatuses", default)]
190 pub uma_resolution_statuses: Option<String>,
191 #[serde(default)]
193 pub events: Vec<MarketEventRef>,
194}
195
196impl Market {
197 pub fn event(&self) -> Option<&MarketEventRef> {
199 self.events.first()
200 }
201
202 pub fn is_in_review(&self) -> bool {
204 if let Some(ref statuses) = self.uma_resolution_statuses {
205 statuses.contains("proposed") || statuses.contains("disputed")
206 } else {
207 false
208 }
209 }
210
211 pub fn status(&self) -> &'static str {
213 if self.closed {
214 "closed"
215 } else if self.is_in_review() {
216 "in-review"
217 } else if self.active {
218 "open"
219 } else {
220 "paused"
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct MarketEventRef {
228 pub id: String,
229 pub slug: String,
230 pub title: String,
231 #[serde(rename = "endDate")]
232 pub end_date: Option<String>,
233 #[serde(default)]
234 pub active: bool,
235 #[serde(default)]
236 pub closed: bool,
237}
238
239impl MarketEventRef {
240 pub fn status(&self) -> &'static str {
242 if self.closed {
243 "closed"
244 } else if self.active {
245 "active"
246 } else {
247 "inactive"
248 }
249 }
250}
251
252pub struct GammaClient {
253 client: reqwest::Client,
254 cache: Option<FileCache>,
255}
256
257impl GammaClient {
258 pub fn new() -> Self {
259 Self {
260 client: reqwest::Client::new(),
261 cache: None,
262 }
263 }
264
265 pub fn with_cache<P: AsRef<std::path::Path>>(cache_dir: P) -> Result<Self> {
267 let cache = FileCache::new(cache_dir)?;
268 Ok(Self {
269 client: reqwest::Client::new(),
270 cache: Some(cache),
271 })
272 }
273
274 pub fn set_cache_ttl(&mut self, ttl_seconds: u64) -> Result<()> {
276 if let Some(ref mut cache) = self.cache {
277 *cache = cache.clone().with_default_ttl(ttl_seconds);
278 }
279 Ok(())
280 }
281
282 pub fn set_cache(&mut self, cache: FileCache) {
284 self.cache = Some(cache);
285 }
286
287 pub async fn get_active_events(&self, limit: Option<usize>) -> Result<Vec<Event>> {
288 let limit = limit.unwrap_or(100);
289 let url = format!(
290 "{}/events?active=true&closed=false&limit={}",
291 GAMMA_API_BASE, limit
292 );
293 let events: Vec<Event> = self.client.get(&url).send().await?.json().await?;
294 Ok(events)
295 }
296
297 pub async fn get_trending_events(
304 &self,
305 order_by: Option<&str>,
306 ascending: Option<bool>,
307 limit: Option<usize>,
308 ) -> Result<Vec<Event>> {
309 let limit = limit.unwrap_or(50);
310 let order_by = order_by.unwrap_or("volume24hr");
311 let ascending = ascending.unwrap_or(false);
312
313 let url = format!(
314 "{}/events?active=true&closed=false&order={}&ascending={}&limit={}",
315 GAMMA_API_BASE, order_by, ascending, limit
316 );
317
318 log_info!("GET {}", url);
319
320 let response = self.client.get(&url).send().await?;
321 let _status = response.status();
322
323 log_info!("GET {} -> status: {}", url, _status);
324
325 let events: Vec<Event> = response.json().await?;
326 Ok(events)
327 }
328
329 pub async fn get_market_by_slug(&self, slug: &str) -> Result<Vec<Market>> {
330 let url = format!("{}/markets?slug={}", GAMMA_API_BASE, slug);
331 let response: serde_json::Value = self.client.get(&url).send().await?.json().await?;
332
333 let markets = if response.is_array() {
335 serde_json::from_value(response)?
336 } else {
337 vec![serde_json::from_value(response)?]
338 };
339
340 Ok(markets)
341 }
342
343 pub async fn get_all_active_asset_ids(&self) -> Result<Vec<String>> {
344 let events = self.get_active_events(None).await?;
345 let mut asset_ids = Vec::new();
346
347 for event in events {
348 for market in event.markets {
349 if let Some(token_ids) = market.clob_token_ids {
350 asset_ids.extend(token_ids);
351 }
352 }
353 }
354
355 Ok(asset_ids)
356 }
357
358 pub async fn get_event_by_id(&self, event_id: &str) -> Result<Option<Event>> {
360 let url = format!("{}/events/{}", GAMMA_API_BASE, event_id);
361 let response = self.client.get(&url).send().await?;
362
363 if response.status() == 404 {
364 return Ok(None);
365 }
366
367 let event: Event = response.json().await?;
368 Ok(Some(event))
369 }
370
371 pub async fn get_event_by_slug(&self, slug: &str) -> Result<Option<Event>> {
373 let url = format!("{}/events?slug={}", GAMMA_API_BASE, slug);
374 let events: Vec<Event> = self.client.get(&url).send().await?.json().await?;
375 Ok(events.into_iter().next())
376 }
377
378 pub async fn get_market_by_id(&self, market_id: &str) -> Result<Option<Market>> {
380 let url = format!("{}/markets/{}", GAMMA_API_BASE, market_id);
381 let response = self.client.get(&url).send().await?;
382
383 if response.status() == 404 {
384 return Ok(None);
385 }
386
387 let market: Market = response.json().await?;
388 Ok(Some(market))
389 }
390
391 pub async fn get_markets(
393 &self,
394 active: Option<bool>,
395 closed: Option<bool>,
396 limit: Option<usize>,
397 ) -> Result<Vec<Market>> {
398 let url = format!("{}/markets", GAMMA_API_BASE);
399 let mut params = Vec::new();
400
401 if let Some(active) = active {
402 params.push(("active", active.to_string()));
403 }
404 if let Some(closed) = closed {
405 params.push(("closed", closed.to_string()));
406 }
407 if let Some(limit) = limit {
408 params.push(("limit", limit.to_string()));
409 }
410
411 let markets: Vec<Market> = self
412 .client
413 .get(&url)
414 .query(¶ms)
415 .send()
416 .await?
417 .json()
418 .await?;
419 Ok(markets)
420 }
421
422 pub async fn get_categories(&self) -> Result<Vec<Tag>> {
424 let url = format!("{}/categories", GAMMA_API_BASE);
425 let categories: Vec<Tag> = self.client.get(&url).send().await?.json().await?;
426 Ok(categories)
427 }
428
429 pub async fn get_events_by_category(
431 &self,
432 category_slug: &str,
433 limit: Option<usize>,
434 ) -> Result<Vec<Event>> {
435 let limit = limit.unwrap_or(100);
436 let url = format!(
437 "{}/events?category={}&limit={}",
438 GAMMA_API_BASE, category_slug, limit
439 );
440 let events: Vec<Event> = self.client.get(&url).send().await?.json().await?;
441 Ok(events)
442 }
443
444 pub async fn search_events(&self, query: &str, limit: Option<usize>) -> Result<Vec<Event>> {
446 let limit_per_type = limit.unwrap_or(50);
447 let url = format!(
448 "{}/public-search?q={}&optimized=true&limit_per_type={}&type=events&search_tags=true&search_profiles=true&cache=true",
449 GAMMA_API_BASE,
450 urlencoding::encode(query),
451 limit_per_type
452 );
453
454 log_info!("GET {}", url);
456
457 let response = self.client.get(&url).send().await.inspect_err(|_e| {
458 log_error!("Failed to send search request: {}", _e);
459 })?;
460
461 let status = response.status();
462 log_info!("GET {} -> status: {}", url, status);
463
464 let response_text = response.text().await.inspect_err(|_e| {
465 log_error!("Failed to read search response body: {}", _e);
466 })?;
467
468 if !status.is_success() {
470 log_debug!(
471 "Search API response body (first 500 chars): {}",
472 if response_text.len() > 500 {
473 &response_text[..500]
474 } else {
475 &response_text
476 }
477 );
478 }
479
480 if !status.is_success() {
481 log_warn!(
482 "Search API error: status={}, body={}",
483 status,
484 response_text
485 );
486 return Err(crate::error::PolymarketError::InvalidData(format!(
487 "Search API returned status {}: {}",
488 status, response_text
489 )));
490 }
491
492 #[derive(Deserialize)]
493 struct SearchResponse {
494 events: Vec<Event>,
495 #[allow(dead_code)]
496 profiles: Option<serde_json::Value>,
497 #[allow(dead_code)]
498 tags: Option<serde_json::Value>,
499 #[allow(dead_code)]
500 has_more: Option<bool>,
501 }
502
503 let search_response: SearchResponse =
504 serde_json::from_str(&response_text).map_err(|e| {
505 log_error!(
506 "Failed to parse search response: {}, body (first 1000 chars): {}",
507 e,
508 if response_text.len() > 1000 {
509 &response_text[..1000]
510 } else {
511 &response_text
512 }
513 );
514 crate::error::PolymarketError::Serialization(e)
515 })?;
516
517 log_info!("Search returned {} events", search_response.events.len());
518
519 let mut full_events = Vec::with_capacity(search_response.events.len());
522 for event in &search_response.events {
523 match self.get_event_by_slug(&event.slug).await {
524 Ok(Some(full_event)) => full_events.push(full_event),
525 Ok(None) => {
526 log_debug!("Event not found by slug: {}", event.slug);
528 full_events.push(event.clone());
529 },
530 Err(_e) => {
531 log_debug!("Failed to fetch event {}: {}", event.slug, _e);
533 full_events.push(event.clone());
534 },
535 }
536 }
537
538 log_info!(
539 "Enriched {} search results with full event data",
540 full_events.len()
541 );
542
543 Ok(full_events)
544 }
545
546 pub async fn get_market_info_by_asset_id(&self, asset_id: &str) -> Result<Option<MarketInfo>> {
547 if let Some(ref cache) = self.cache {
549 let cache_key = format!("market_info_{}", asset_id);
550 if let Some(cached_info) = cache.get::<MarketInfo>(&cache_key)? {
551 return Ok(Some(cached_info));
552 }
553 }
554
555 let events = self.get_active_events(Some(1000)).await?;
556
557 for event in events {
558 for market in event.markets {
559 if let Some(ref token_ids) = market.clob_token_ids
560 && token_ids.contains(&asset_id.to_string())
561 {
562 let outcomes = market.outcomes.clone();
563 let prices = market.outcome_prices.clone();
564
565 let market_info = MarketInfo {
566 event_title: event.title,
567 event_slug: event.slug,
568 market_question: market.question,
569 market_id: market.id.clone().unwrap_or_default(),
570 asset_id: asset_id.to_string(),
571 outcomes,
572 prices,
573 };
574
575 if let Some(ref cache) = self.cache {
577 let cache_key = format!("market_info_{}", asset_id);
578 let _ = cache.set(&cache_key, &market_info);
579 }
580
581 return Ok(Some(market_info));
582 }
583 }
584 }
585
586 Ok(None)
587 }
588
589 pub async fn get_status(&self) -> Result<StatusResponse> {
591 let url = format!("{}/status", GAMMA_API_BASE);
592 let status: StatusResponse = self.client.get(&url).send().await?.json().await?;
593 Ok(status)
594 }
595
596 pub async fn get_tag_by_id(&self, tag_id: &str) -> Result<Option<Tag>> {
598 let url = format!("{}/tags/{}", GAMMA_API_BASE, tag_id);
599 let response = self.client.get(&url).send().await?;
600
601 if response.status() == 404 {
602 return Ok(None);
603 }
604
605 let tag: Tag = response.json().await?;
606 Ok(Some(tag))
607 }
608
609 pub async fn get_tag_by_slug(&self, slug: &str) -> Result<Option<Tag>> {
611 let url = format!("{}/tags/slug/{}", GAMMA_API_BASE, slug);
612 let response = self.client.get(&url).send().await?;
613
614 if response.status() == 404 {
615 return Ok(None);
616 }
617
618 let tag: Tag = response.json().await?;
619 Ok(Some(tag))
620 }
621
622 pub async fn get_related_tags(&self, tag_id: &str) -> Result<Vec<Tag>> {
624 let url = format!("{}/tags/{}/related-tags", GAMMA_API_BASE, tag_id);
625 let tags: Vec<Tag> = self.client.get(&url).send().await?.json().await?;
626 Ok(tags)
627 }
628
629 pub async fn get_series(&self, limit: Option<usize>) -> Result<Vec<Series>> {
631 let limit = limit.unwrap_or(100);
632 let url = format!("{}/series?limit={}", GAMMA_API_BASE, limit);
633 let series: Vec<Series> = self.client.get(&url).send().await?.json().await?;
634 Ok(series)
635 }
636
637 pub async fn get_series_by_id(&self, series_id: &str) -> Result<Option<Series>> {
639 let url = format!("{}/series/{}", GAMMA_API_BASE, series_id);
640 let response = self.client.get(&url).send().await?;
641
642 if response.status() == 404 {
643 return Ok(None);
644 }
645
646 let series: Series = response.json().await?;
647 Ok(Some(series))
648 }
649
650 pub async fn get_public_profile(&self, address: &str) -> Result<Option<PublicProfile>> {
652 let url = format!("{}/public-profile", GAMMA_API_BASE);
653 let params = [("address", address)];
654 let response = self.client.get(&url).query(¶ms).send().await?;
655
656 if response.status() == 404 {
657 return Ok(None);
658 }
659
660 let profile: PublicProfile = response.json().await?;
661 Ok(Some(profile))
662 }
663
664 pub async fn get_event_tags(&self, event_id: &str) -> Result<Vec<Tag>> {
666 let url = format!("{}/events/{}/tags", GAMMA_API_BASE, event_id);
667 let tags: Vec<Tag> = self.client.get(&url).send().await?.json().await?;
668 Ok(tags)
669 }
670
671 pub async fn get_market_tags(&self, market_id: &str) -> Result<Vec<Tag>> {
673 let url = format!("{}/markets/{}/tags", GAMMA_API_BASE, market_id);
674 let tags: Vec<Tag> = self.client.get(&url).send().await?.json().await?;
675 Ok(tags)
676 }
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct MarketInfo {
681 pub event_title: String,
682 pub event_slug: String,
683 pub market_question: String,
684 pub market_id: String,
685 pub asset_id: String,
686 pub outcomes: Vec<String>,
687 pub prices: Vec<String>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct StatusResponse {
693 pub status: String,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct Series {
699 pub id: String,
700 #[serde(default)]
701 pub title: Option<String>,
702 #[serde(default)]
703 pub slug: Option<String>,
704 #[serde(default)]
705 pub description: Option<String>,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct PublicProfile {
711 #[serde(default)]
712 pub address: Option<String>,
713 #[serde(default)]
714 pub name: Option<String>,
715 #[serde(default)]
716 pub pseudonym: Option<String>,
717 #[serde(default)]
718 pub bio: Option<String>,
719 #[serde(rename = "profileImage", default)]
720 pub profile_image: Option<String>,
721 #[serde(rename = "profileImageOptimized", default)]
722 pub profile_image_optimized: Option<String>,
723}
724
725impl Default for GammaClient {
726 fn default() -> Self {
727 Self::new()
728 }
729}