@@ -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) ]
34128mod 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