Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ polymarket markets list --limit 2
```

```
Question Price (Yes) Volume Liquidity Status
Will Trump win the 2024 election? 52.00¢ $145.2M $1.2M Active
Will BTC hit $100k by Dec 2024? 67.30¢ $89.4M $430.5K Active
Question Price Volume Liquidity Status
Will Trump win the 2024 election? Yes: 52.00¢ $145.2M $1.2M Active
Will BTC hit $100k by Dec 2024? Yes: 67.30¢ $89.4M $430.5K Active
```

```bash
Expand Down
26 changes: 22 additions & 4 deletions src/commands/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ pub struct CommentsArgs {

#[derive(Subcommand)]
pub enum CommentsCommand {
/// List comments on an event, market, or series
/// List comments on an event or series
List {
/// Parent entity type: event, market, or series
/// Parent entity type: event or series
#[arg(long)]
entity_type: EntityType,

Expand Down Expand Up @@ -78,15 +78,13 @@ pub enum CommentsCommand {
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum EntityType {
Event,
Market,
Series,
}

impl From<EntityType> for ParentEntityType {
fn from(e: EntityType) -> Self {
match e {
EntityType::Event => ParentEntityType::Event,
EntityType::Market => ParentEntityType::Market,
EntityType::Series => ParentEntityType::Series,
}
}
Expand Down Expand Up @@ -164,3 +162,23 @@ pub async fn execute(

Ok(())
}

#[cfg(test)]
mod tests {
use super::EntityType;
use clap::ValueEnum;

#[test]
fn entity_type_does_not_expose_market_variant() {
let names: Vec<String> = EntityType::value_variants()
.iter()
.filter_map(|variant| {
variant
.to_possible_value()
.map(|value| value.get_name().to_string())
})
.collect();

assert!(!names.iter().any(|name| name == "market"));
}
}
64 changes: 58 additions & 6 deletions src/commands/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ use anyhow::Result;
use clap::{Args, Subcommand};
use polymarket_client_sdk::gamma::{
self,
types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
types::{
request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
response::Event,
},
};

use super::is_numeric_id;
use super::{flag_matches, is_numeric_id};
use crate::output::events::{print_event_detail, print_events_table};
use crate::output::tags::print_tags_table;
use crate::output::{OutputFormat, print_json};
Expand Down Expand Up @@ -62,6 +65,19 @@ pub enum EventsCommand {
},
}

fn apply_status_filters(
events: Vec<Event>,
active_filter: Option<bool>,
closed_filter: Option<bool>,
) -> Vec<Event> {
events
.into_iter()
.filter(|event| {
flag_matches(event.active, active_filter) && flag_matches(event.closed, closed_filter)
})
.collect()
}

pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFormat) -> Result<()> {
match args.command {
EventsCommand::List {
Expand All @@ -73,18 +89,17 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
ascending,
tag,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

let request = EventsRequest::builder()
.limit(limit)
.maybe_closed(resolved_closed)
.maybe_active(active)
.maybe_closed(closed)
.maybe_offset(offset)
.maybe_ascending(if ascending { Some(true) } else { None })
.maybe_tag_slug(tag)
.order(order.into_iter().collect::<Vec<_>>())
.build();

let events = client.events(&request).await?;
let events = apply_status_filters(client.events(&request).await?, active, closed);

match output {
OutputFormat::Table => print_events_table(&events),
Expand Down Expand Up @@ -121,3 +136,40 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor

Ok(())
}

#[cfg(test)]
mod tests {
use super::apply_status_filters;
use polymarket_client_sdk::gamma::types::response::Event;
use serde_json::json;

fn make_event(value: serde_json::Value) -> Event {
serde_json::from_value(value).unwrap()
}

#[test]
fn status_filters_are_independent() {
let events = vec![
make_event(json!({"id":"1", "active": true, "closed": true})),
make_event(json!({"id":"2", "active": false, "closed": true})),
make_event(json!({"id":"3", "active": false, "closed": false})),
];

let filtered = apply_status_filters(events, Some(false), Some(true));

assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].id, "2");
}

#[test]
fn active_filter_does_not_imply_closed_filter() {
let events = vec![
make_event(json!({"id":"1", "active": false, "closed": true})),
make_event(json!({"id":"2", "active": false, "closed": false})),
];

let filtered = apply_status_filters(events, Some(false), None);

assert_eq!(filtered.len(), 2);
}
}
116 changes: 104 additions & 12 deletions src/commands/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use polymarket_client_sdk::gamma::{
},
};

use super::is_numeric_id;
use super::{flag_matches, is_numeric_id};
use crate::output::markets::{print_market_detail, print_markets_table};
use crate::output::tags::print_tags_table;
use crate::output::{OutputFormat, print_json};
Expand Down Expand Up @@ -74,6 +74,70 @@ pub enum MarketsCommand {
},
}

fn apply_status_filters(
markets: Vec<Market>,
active_filter: Option<bool>,
closed_filter: Option<bool>,
) -> Vec<Market> {
markets
.into_iter()
.filter(|market| {
flag_matches(market.active, active_filter) && flag_matches(market.closed, closed_filter)
})
.collect()
}

async fn list_markets(
client: &gamma::Client,
limit: i32,
offset: Option<i32>,
order: Option<String>,
ascending: bool,
active: Option<bool>,
closed: Option<bool>,
) -> Result<Vec<Market>> {
let page_size = limit.max(1);
let mut next_offset = offset.unwrap_or(0);
let mut collected: Vec<Market> = Vec::new();

loop {
let request = MarketsRequest::builder()
.limit(page_size)
.maybe_closed(closed)
.maybe_offset(Some(next_offset))
.maybe_order(order.clone())
.maybe_ascending(if ascending { Some(true) } else { None })
.build();

let page = client.markets(&request).await?;
if page.is_empty() {
break;
}

let raw_count = page.len();
collected.extend(apply_status_filters(page, active, closed));

if collected.len() >= page_size as usize {
collected.truncate(page_size as usize);
break;
}

// Without an active filter, the API-side limit should be authoritative.
if active.is_none() {
break;
}

// Reached end of available results from the backend.
if raw_count < page_size as usize {
break;
}

next_offset += raw_count as i32;
}

Ok(collected)
}

pub async fn execute(
client: &gamma::Client,
args: MarketsArgs,
Expand All @@ -88,17 +152,8 @@ pub async fn execute(
order,
ascending,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

let request = MarketsRequest::builder()
.limit(limit)
.maybe_closed(resolved_closed)
.maybe_offset(offset)
.maybe_order(order)
.maybe_ascending(if ascending { Some(true) } else { None })
.build();

let markets = client.markets(&request).await?;
let markets =
list_markets(client, limit, offset, order, ascending, active, closed).await?;

match output {
OutputFormat::Table => print_markets_table(&markets),
Expand Down Expand Up @@ -156,3 +211,40 @@ pub async fn execute(

Ok(())
}

#[cfg(test)]
mod tests {
use super::apply_status_filters;
use polymarket_client_sdk::gamma::types::response::Market;
use serde_json::json;

fn make_market(value: serde_json::Value) -> Market {
serde_json::from_value(value).unwrap()
}

#[test]
fn status_filters_are_independent() {
let markets = vec![
make_market(json!({"id":"1", "active": true, "closed": true})),
make_market(json!({"id":"2", "active": false, "closed": true})),
make_market(json!({"id":"3", "active": false, "closed": false})),
];

let filtered = apply_status_filters(markets, Some(false), Some(true));

assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].id, "2");
}

#[test]
fn active_filter_does_not_imply_closed_filter() {
let markets = vec![
make_market(json!({"id":"1", "active": false, "closed": true})),
make_market(json!({"id":"2", "active": false, "closed": false})),
];

let filtered = apply_status_filters(markets, Some(false), None);

assert_eq!(filtered.len(), 2);
}
}
17 changes: 17 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub fn parse_condition_id(s: &str) -> anyhow::Result<B256> {
.map_err(|_| anyhow::anyhow!("Invalid condition ID: must be a 0x-prefixed 32-byte hex"))
}

pub fn flag_matches(value: Option<bool>, filter: Option<bool>) -> bool {
filter.is_none_or(|expected| value == Some(expected))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -87,4 +91,17 @@ mod tests {
let err = parse_condition_id("garbage").unwrap_err().to_string();
assert!(err.contains("32-byte"), "got: {err}");
}

#[test]
fn flag_matches_true_cases() {
assert!(flag_matches(Some(true), Some(true)));
assert!(flag_matches(Some(false), Some(false)));
assert!(flag_matches(Some(true), None));
}

#[test]
fn flag_matches_false_cases() {
assert!(!flag_matches(Some(true), Some(false)));
assert!(!flag_matches(None, Some(true)));
}
}
Loading