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
50 changes: 50 additions & 0 deletions src/commands/events.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand};
use polymarket_client_sdk::gamma::{
self,
types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
};
use rust_decimal::Decimal;

use super::is_numeric_id;
use crate::output::OutputFormat;
Expand Down Expand Up @@ -47,6 +49,38 @@ pub enum EventsCommand {
/// Filter by tag slug (e.g. "politics", "crypto")
#[arg(long)]
tag: Option<String>,

/// Minimum trading volume (e.g. 1000000)
#[arg(long)]
volume_min: Option<Decimal>,

/// Maximum trading volume
#[arg(long)]
volume_max: Option<Decimal>,

/// Minimum liquidity
#[arg(long)]
liquidity_min: Option<Decimal>,

/// Maximum liquidity
#[arg(long)]
liquidity_max: Option<Decimal>,

/// Only events starting after this date (e.g. 2026-03-01T00:00:00Z)
#[arg(long)]
start_date_min: Option<DateTime<Utc>>,

/// Only events starting before this date
#[arg(long)]
start_date_max: Option<DateTime<Utc>>,

/// Only events ending after this date
#[arg(long)]
end_date_min: Option<DateTime<Utc>>,

/// Only events ending before this date
#[arg(long)]
end_date_max: Option<DateTime<Utc>>,
},

/// Get a single event by ID or slug
Expand All @@ -72,6 +106,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
order,
ascending,
tag,
volume_min,
volume_max,
liquidity_min,
liquidity_max,
start_date_min,
start_date_max,
end_date_min,
end_date_max,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

Expand All @@ -83,6 +125,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
.maybe_tag_slug(tag)
// EventsRequest::order is Vec<String>; into_iter on Option yields 0 or 1 items.
.order(order.into_iter().collect())
.maybe_volume_min(volume_min)
.maybe_volume_max(volume_max)
.maybe_liquidity_min(liquidity_min)
.maybe_liquidity_max(liquidity_max)
.maybe_start_date_min(start_date_min)
.maybe_start_date_max(start_date_max)
.maybe_end_date_min(end_date_min)
.maybe_end_date_max(end_date_max)
.build();

let events = client.events(&request).await?;
Expand Down
56 changes: 56 additions & 0 deletions src/commands/markets.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand};
use polymarket_client_sdk::gamma::{
self,
Expand All @@ -10,6 +11,7 @@ use polymarket_client_sdk::gamma::{
response::Market,
},
};
use rust_decimal::Decimal;

use super::is_numeric_id;
use crate::output::OutputFormat;
Expand Down Expand Up @@ -49,6 +51,42 @@ pub enum MarketsCommand {
/// Sort ascending instead of descending
#[arg(long)]
ascending: bool,

/// Minimum trading volume (e.g. 1000000)
#[arg(long)]
volume_min: Option<Decimal>,

/// Maximum trading volume
#[arg(long)]
volume_max: Option<Decimal>,

/// Minimum liquidity
#[arg(long)]
liquidity_min: Option<Decimal>,

/// Maximum liquidity
#[arg(long)]
liquidity_max: Option<Decimal>,

/// Only markets starting after this date (e.g. 2026-03-01T00:00:00Z)
#[arg(long)]
start_date_min: Option<DateTime<Utc>>,

/// Only markets starting before this date
#[arg(long)]
start_date_max: Option<DateTime<Utc>>,

/// Only markets ending after this date
#[arg(long)]
end_date_min: Option<DateTime<Utc>>,

/// Only markets ending before this date
#[arg(long)]
end_date_max: Option<DateTime<Utc>>,

/// Filter by tag ID
#[arg(long)]
tag: Option<String>,
},

/// Get a single market by ID or slug
Expand Down Expand Up @@ -87,6 +125,15 @@ pub async fn execute(
offset,
order,
ascending,
volume_min,
volume_max,
liquidity_min,
liquidity_max,
start_date_min,
start_date_max,
end_date_min,
end_date_max,
tag,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

Expand All @@ -96,6 +143,15 @@ pub async fn execute(
.maybe_offset(offset)
.maybe_order(order)
.ascending(ascending)
.maybe_volume_num_min(volume_min)
.maybe_volume_num_max(volume_max)
.maybe_liquidity_num_min(liquidity_min)
.maybe_liquidity_num_max(liquidity_max)
.maybe_start_date_min(start_date_min)
.maybe_start_date_max(start_date_max)
.maybe_end_date_min(end_date_min)
.maybe_end_date_max(end_date_max)
.maybe_tag_id(tag)
.build();

let markets = client.markets(&request).await?;
Expand Down
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub(crate) struct Cli {
/// Signature type: eoa, proxy, or gnosis-safe
#[arg(long, global = true)]
signature_type: Option<String>,

/// Comma-separated list of fields to include in JSON output (e.g. question,volume_num,slug)
#[arg(long, global = true, value_delimiter = ',')]
fields: Option<Vec<String>>,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -68,9 +72,13 @@ enum Commands {

#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
let mut cli = Cli::parse();
let output = cli.output;

if let Some(fields) = cli.fields.take() {
output::set_json_fields(fields);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

if let Err(e) = run(cli).await {
output::print_error(&e, output);
return ExitCode::FAILURE;
Expand Down
66 changes: 65 additions & 1 deletion src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ pub(crate) mod series;
pub(crate) mod sports;
pub(crate) mod tags;

use std::sync::OnceLock;

use chrono::{DateTime, Utc};
use polymarket_client_sdk::types::Decimal;
use rust_decimal::prelude::ToPrimitive;
use serde_json::Value;
use tabled::Table;
use tabled::settings::object::Columns;
use tabled::settings::{Modify, Style, Width};

/// field names to keep in JSON output; set once at startup via --fields
static JSON_FIELDS: OnceLock<Vec<String>> = OnceLock::new();

pub(crate) fn set_json_fields(fields: Vec<String>) {
let _ = JSON_FIELDS.set(fields);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent failure in set_json_fields hides broken --fields

Low Severity

set_json_fields silently swallows a RwLock write failure with if let Ok(...), meaning the --fields flag is quietly ignored and the previous filter value persists. Meanwhile, print_json calls .unwrap() on the read lock, which would panic on the same poisoned-lock condition. The writer silently no-ops while the reader crashes — these two call sites need consistent error handling. If the write silently fails, users get unfiltered output with no indication that --fields was ignored.

Additional Locations (1)
Fix in Cursor Fix in Web


pub(crate) const DASH: &str = "—";

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
Expand Down Expand Up @@ -63,10 +73,32 @@ pub(crate) fn active_status(closed: Option<bool>, active: Option<bool>) -> &'sta
}

pub(crate) fn print_json(data: &(impl serde::Serialize + ?Sized)) -> anyhow::Result<()> {
println!("{}", serde_json::to_string_pretty(data)?);
let value = serde_json::to_value(data)?;
let output = match JSON_FIELDS.get() {
Some(fields) => filter_fields(value, fields),
None => value,
};
println!("{}", serde_json::to_string_pretty(&output)?);
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Ok(())
}

/// keep only the requested keys from an object or each object in an array
fn filter_fields(value: Value, fields: &[String]) -> Value {
match value {
Value::Array(arr) => {
Value::Array(arr.into_iter().map(|v| filter_fields(v, fields)).collect())
}
Value::Object(map) => {
let filtered = map
.into_iter()
.filter(|(k, _)| fields.iter().any(|f| f == k))
.collect();
Value::Object(filtered)
}
other => other,
}
}

pub(crate) fn print_error(error: &anyhow::Error, format: OutputFormat) {
match format {
OutputFormat::Json => {
Expand Down Expand Up @@ -185,4 +217,36 @@ mod tests {
fn format_decimal_just_below_million_uses_k() {
assert_eq!(format_decimal(dec!(999_999)), "$1000.0K");
}

#[test]
fn filter_fields_keeps_only_requested_keys() {
let obj = serde_json::json!({"a": 1, "b": 2, "c": 3});
let fields = vec!["a".into(), "c".into()];
let result = filter_fields(obj, &fields);
assert_eq!(result, serde_json::json!({"a": 1, "c": 3}));
}

#[test]
fn filter_fields_applies_to_each_array_element() {
let arr = serde_json::json!([{"a": 1, "b": 2}, {"a": 3, "b": 4}]);
let fields = vec!["a".into()];
let result = filter_fields(arr, &fields);
assert_eq!(result, serde_json::json!([{"a": 1}, {"a": 3}]));
}

#[test]
fn filter_fields_returns_empty_object_when_no_match() {
let obj = serde_json::json!({"a": 1, "b": 2});
let fields = vec!["z".into()];
let result = filter_fields(obj, &fields);
assert_eq!(result, serde_json::json!({}));
}

#[test]
fn filter_fields_passes_through_non_object_values() {
let val = serde_json::json!(42);
let fields = vec!["a".into()];
let result = filter_fields(val, &fields);
assert_eq!(result, serde_json::json!(42));
}
}