Skip to content
Open
Show file tree
Hide file tree
Changes from all 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);
}
}
119 changes: 107 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,73 @@ 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>> {
if limit <= 0 {
return Ok(Vec::new());
}
let page_size = limit;
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 +155,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 +214,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