Skip to content

Commit b977baa

Browse files
committed
feat: accept Polymarket URLs as input for markets/events get
Commands `markets get` and `events get` now accept full Polymarket URLs (e.g. https://polymarket.com/event/my-event/my-market) in addition to numeric IDs and plain slugs. The CLI extracts the appropriate slug automatically — market slug for `markets get`, event slug for `events get`.
1 parent 3ba646b commit b977baa

File tree

4 files changed

+327
-18
lines changed

4 files changed

+327
-18
lines changed

src/commands/events.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use polymarket_client_sdk::gamma::{
55
types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
66
};
77

8-
use super::is_numeric_id;
8+
use super::{ResolvedId, resolve_id};
99
use crate::output::events::{print_event_detail, print_events_table};
1010
use crate::output::tags::print_tags_table;
1111
use crate::output::{OutputFormat, print_json};
@@ -51,7 +51,7 @@ pub enum EventsCommand {
5151

5252
/// Get a single event by ID or slug
5353
Get {
54-
/// Event ID (numeric) or slug
54+
/// Event ID (numeric), slug, or Polymarket URL
5555
id: String,
5656
},
5757

@@ -93,13 +93,15 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
9393
}
9494

9595
EventsCommand::Get { id } => {
96-
let is_numeric = is_numeric_id(&id);
97-
let event = if is_numeric {
98-
let req = EventByIdRequest::builder().id(id).build();
99-
client.event_by_id(&req).await?
100-
} else {
101-
let req = EventBySlugRequest::builder().slug(id).build();
102-
client.event_by_slug(&req).await?
96+
let event = match resolve_id(&id, false) {
97+
ResolvedId::Numeric(n) => {
98+
let req = EventByIdRequest::builder().id(n).build();
99+
client.event_by_id(&req).await?
100+
}
101+
ResolvedId::Slug(slug) => {
102+
let req = EventBySlugRequest::builder().slug(slug).build();
103+
client.event_by_slug(&req).await?
104+
}
103105
};
104106

105107
match output {

src/commands/markets.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use polymarket_client_sdk::gamma::{
1111
},
1212
};
1313

14-
use super::is_numeric_id;
14+
use super::{ResolvedId, resolve_id};
1515
use crate::output::markets::{print_market_detail, print_markets_table};
1616
use crate::output::tags::print_tags_table;
1717
use crate::output::{OutputFormat, print_json};
@@ -53,7 +53,7 @@ pub enum MarketsCommand {
5353

5454
/// Get a single market by ID or slug
5555
Get {
56-
/// Market ID (numeric) or slug
56+
/// Market ID (numeric), slug, or Polymarket URL
5757
id: String,
5858
},
5959

@@ -107,13 +107,15 @@ pub async fn execute(
107107
}
108108

109109
MarketsCommand::Get { id } => {
110-
let is_numeric = is_numeric_id(&id);
111-
let market = if is_numeric {
112-
let req = MarketByIdRequest::builder().id(id).build();
113-
client.market_by_id(&req).await?
114-
} else {
115-
let req = MarketBySlugRequest::builder().slug(id).build();
116-
client.market_by_slug(&req).await?
110+
let market = match resolve_id(&id, true) {
111+
ResolvedId::Numeric(n) => {
112+
let req = MarketByIdRequest::builder().id(n).build();
113+
client.market_by_id(&req).await?
114+
}
115+
ResolvedId::Slug(slug) => {
116+
let req = MarketBySlugRequest::builder().slug(slug).build();
117+
client.market_by_slug(&req).await?
118+
}
117119
};
118120

119121
match output {

src/commands/mod.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,106 @@ pub fn parse_condition_id(s: &str) -> anyhow::Result<B256> {
3030
.map_err(|_| anyhow::anyhow!("Invalid condition ID: must be a 0x-prefixed 32-byte hex"))
3131
}
3232

33+
/// Parsed Polymarket URL with event slug and optional market slug.
34+
#[derive(Debug, PartialEq)]
35+
pub struct PolymarketUrl {
36+
pub event_slug: String,
37+
pub market_slug: Option<String>,
38+
}
39+
40+
/// Parse a Polymarket URL into its event and optional market slugs.
41+
///
42+
/// Accepts URLs with or without scheme (`https://`, `http://`), with or without
43+
/// `www.`, and strips query strings, fragments, and trailing slashes.
44+
///
45+
/// Returns `None` for non-Polymarket URLs or URLs missing `/event/<slug>`.
46+
pub fn parse_polymarket_url(input: &str) -> Option<PolymarketUrl> {
47+
// Strip scheme if present
48+
let without_scheme = input
49+
.strip_prefix("https://")
50+
.or_else(|| input.strip_prefix("http://"))
51+
.unwrap_or(input);
52+
53+
// Split host from path at the first '/'
54+
let (host, path) = match without_scheme.find('/') {
55+
Some(i) => (&without_scheme[..i], &without_scheme[i..]),
56+
None => return None, // No path at all
57+
};
58+
59+
// Verify it's a polymarket.com host
60+
let host_lower = host.to_ascii_lowercase();
61+
if host_lower != "polymarket.com" && host_lower != "www.polymarket.com" {
62+
return None;
63+
}
64+
65+
// Strip query string and fragment
66+
let path = path.split('?').next().unwrap_or(path);
67+
let path = path.split('#').next().unwrap_or(path);
68+
69+
// Strip trailing slash
70+
let path = path.strip_suffix('/').unwrap_or(path);
71+
72+
// Expect /event/<event_slug>[/<market_slug>]
73+
let path = path.strip_prefix("/event/")?;
74+
if path.is_empty() {
75+
return None;
76+
}
77+
78+
let mut segments = path.split('/');
79+
let event_slug = segments.next()?.to_string();
80+
if event_slug.is_empty() {
81+
return None;
82+
}
83+
84+
let market_slug = segments
85+
.next()
86+
.filter(|s| !s.is_empty())
87+
.map(|s| s.to_string());
88+
89+
Some(PolymarketUrl {
90+
event_slug,
91+
market_slug,
92+
})
93+
}
94+
95+
/// What `resolve_id` determined the input to be.
96+
#[derive(Debug, PartialEq)]
97+
pub enum ResolvedId {
98+
/// A numeric API id (e.g. "12345").
99+
Numeric(String),
100+
/// A slug extracted from a Polymarket URL or passed directly.
101+
Slug(String),
102+
}
103+
104+
/// Resolve a user-provided identifier that may be a Polymarket URL, a numeric
105+
/// ID, or a plain slug.
106+
///
107+
/// Accepts URLs like `https://polymarket.com/event/<event>[/<market>]`.
108+
/// When `prefer_market` is true and the URL contains a market slug, the market
109+
/// slug is used; otherwise the event slug is used.
110+
pub fn resolve_id(input: &str, prefer_market: bool) -> ResolvedId {
111+
if let Some(parsed) = parse_polymarket_url(input) {
112+
let slug = if prefer_market {
113+
parsed.market_slug.unwrap_or(parsed.event_slug)
114+
} else {
115+
parsed.event_slug
116+
};
117+
return ResolvedId::Slug(slug);
118+
}
119+
120+
if is_numeric_id(input) {
121+
ResolvedId::Numeric(input.to_string())
122+
} else {
123+
ResolvedId::Slug(input.to_string())
124+
}
125+
}
126+
33127
#[cfg(test)]
34128
mod tests {
35129
use super::*;
36130

131+
// ── is_numeric_id ──────────────────────────────────────────────
132+
37133
#[test]
38134
fn is_numeric_id_pure_digits() {
39135
assert!(is_numeric_id("12345"));
@@ -52,6 +148,8 @@ mod tests {
52148
assert!(!is_numeric_id(""));
53149
}
54150

151+
// ── parse_address / parse_condition_id ──────────────────────────
152+
55153
#[test]
56154
fn parse_address_valid_hex() {
57155
let addr = "0x0000000000000000000000000000000000000001";
@@ -87,4 +185,180 @@ mod tests {
87185
let err = parse_condition_id("garbage").unwrap_err().to_string();
88186
assert!(err.contains("32-byte"), "got: {err}");
89187
}
188+
189+
// ── parse_polymarket_url ───────────────────────────────────────
190+
191+
#[test]
192+
fn parse_url_standard_event() {
193+
let url = "https://polymarket.com/event/will-bitcoin-hit-100k";
194+
let parsed = parse_polymarket_url(url).unwrap();
195+
assert_eq!(parsed.event_slug, "will-bitcoin-hit-100k");
196+
assert_eq!(parsed.market_slug, None);
197+
}
198+
199+
#[test]
200+
fn parse_url_event_with_market() {
201+
let url = "https://polymarket.com/event/will-bitcoin-hit-100k/bitcoin-100k-by-march";
202+
let parsed = parse_polymarket_url(url).unwrap();
203+
assert_eq!(parsed.event_slug, "will-bitcoin-hit-100k");
204+
assert_eq!(parsed.market_slug.as_deref(), Some("bitcoin-100k-by-march"));
205+
}
206+
207+
#[test]
208+
fn parse_url_http_scheme() {
209+
let url = "http://polymarket.com/event/some-event";
210+
let parsed = parse_polymarket_url(url).unwrap();
211+
assert_eq!(parsed.event_slug, "some-event");
212+
}
213+
214+
#[test]
215+
fn parse_url_no_scheme() {
216+
let url = "polymarket.com/event/some-event/some-market";
217+
let parsed = parse_polymarket_url(url).unwrap();
218+
assert_eq!(parsed.event_slug, "some-event");
219+
assert_eq!(parsed.market_slug.as_deref(), Some("some-market"));
220+
}
221+
222+
#[test]
223+
fn parse_url_www_prefix() {
224+
let url = "https://www.polymarket.com/event/some-event";
225+
let parsed = parse_polymarket_url(url).unwrap();
226+
assert_eq!(parsed.event_slug, "some-event");
227+
}
228+
229+
#[test]
230+
fn parse_url_www_no_scheme() {
231+
let url = "www.polymarket.com/event/some-event";
232+
let parsed = parse_polymarket_url(url).unwrap();
233+
assert_eq!(parsed.event_slug, "some-event");
234+
}
235+
236+
#[test]
237+
fn parse_url_trailing_slash() {
238+
let url = "https://polymarket.com/event/some-event/";
239+
let parsed = parse_polymarket_url(url).unwrap();
240+
assert_eq!(parsed.event_slug, "some-event");
241+
assert_eq!(parsed.market_slug, None);
242+
}
243+
244+
#[test]
245+
fn parse_url_trailing_slash_with_market() {
246+
let url = "https://polymarket.com/event/some-event/some-market/";
247+
let parsed = parse_polymarket_url(url).unwrap();
248+
assert_eq!(parsed.event_slug, "some-event");
249+
assert_eq!(parsed.market_slug.as_deref(), Some("some-market"));
250+
}
251+
252+
#[test]
253+
fn parse_url_with_query_string() {
254+
let url = "https://polymarket.com/event/some-event/some-market?tid=abc123";
255+
let parsed = parse_polymarket_url(url).unwrap();
256+
assert_eq!(parsed.event_slug, "some-event");
257+
assert_eq!(parsed.market_slug.as_deref(), Some("some-market"));
258+
}
259+
260+
#[test]
261+
fn parse_url_with_fragment() {
262+
let url = "https://polymarket.com/event/some-event#comments";
263+
let parsed = parse_polymarket_url(url).unwrap();
264+
assert_eq!(parsed.event_slug, "some-event");
265+
}
266+
267+
#[test]
268+
fn parse_url_with_query_and_fragment() {
269+
let url = "https://polymarket.com/event/some-event/some-market?tid=1#top";
270+
let parsed = parse_polymarket_url(url).unwrap();
271+
assert_eq!(parsed.event_slug, "some-event");
272+
assert_eq!(parsed.market_slug.as_deref(), Some("some-market"));
273+
}
274+
275+
#[test]
276+
fn parse_url_extra_path_segments_ignored() {
277+
let url = "https://polymarket.com/event/my-event/my-market/extra/stuff";
278+
let parsed = parse_polymarket_url(url).unwrap();
279+
assert_eq!(parsed.event_slug, "my-event");
280+
assert_eq!(parsed.market_slug.as_deref(), Some("my-market"));
281+
}
282+
283+
#[test]
284+
fn parse_url_rejects_non_polymarket_domain() {
285+
assert!(parse_polymarket_url("https://example.com/event/foo").is_none());
286+
assert!(parse_polymarket_url("https://notpolymarket.com/event/foo").is_none());
287+
}
288+
289+
#[test]
290+
fn parse_url_rejects_missing_event_prefix() {
291+
assert!(parse_polymarket_url("https://polymarket.com/markets/foo").is_none());
292+
assert!(parse_polymarket_url("https://polymarket.com/foo").is_none());
293+
}
294+
295+
#[test]
296+
fn parse_url_rejects_empty_slug() {
297+
assert!(parse_polymarket_url("https://polymarket.com/event/").is_none());
298+
}
299+
300+
#[test]
301+
fn parse_url_rejects_plain_slug() {
302+
assert!(parse_polymarket_url("will-bitcoin-hit-100k").is_none());
303+
}
304+
305+
#[test]
306+
fn parse_url_rejects_numeric_id() {
307+
assert!(parse_polymarket_url("12345").is_none());
308+
}
309+
310+
#[test]
311+
fn parse_url_rejects_no_path() {
312+
assert!(parse_polymarket_url("https://polymarket.com").is_none());
313+
assert!(parse_polymarket_url("polymarket.com").is_none());
314+
}
315+
316+
// ── resolve_id ─────────────────────────────────────────────────
317+
318+
#[test]
319+
fn resolve_id_numeric() {
320+
assert_eq!(
321+
resolve_id("12345", false),
322+
ResolvedId::Numeric("12345".to_string())
323+
);
324+
assert_eq!(
325+
resolve_id("12345", true),
326+
ResolvedId::Numeric("12345".to_string())
327+
);
328+
}
329+
330+
#[test]
331+
fn resolve_id_plain_slug() {
332+
assert_eq!(
333+
resolve_id("will-bitcoin-hit-100k", false),
334+
ResolvedId::Slug("will-bitcoin-hit-100k".to_string())
335+
);
336+
}
337+
338+
#[test]
339+
fn resolve_id_url_prefer_market_true() {
340+
let url = "https://polymarket.com/event/my-event/my-market";
341+
assert_eq!(
342+
resolve_id(url, true),
343+
ResolvedId::Slug("my-market".to_string())
344+
);
345+
}
346+
347+
#[test]
348+
fn resolve_id_url_prefer_market_false() {
349+
let url = "https://polymarket.com/event/my-event/my-market";
350+
assert_eq!(
351+
resolve_id(url, false),
352+
ResolvedId::Slug("my-event".to_string())
353+
);
354+
}
355+
356+
#[test]
357+
fn resolve_id_url_no_market_prefer_market_true() {
358+
let url = "https://polymarket.com/event/my-event";
359+
assert_eq!(
360+
resolve_id(url, true),
361+
ResolvedId::Slug("my-event".to_string())
362+
);
363+
}
90364
}

0 commit comments

Comments
 (0)