use serde::{Deserialize, Serialize};
use super::auth::ApiCredentials;
pub const WS_MARKET_URL: &str = "wss://ws-subscriptions-clob.polymarket.com/ws/market";
pub const WS_USER_URL: &str = "wss://ws-subscriptions-clob.polymarket.com/ws/user";
pub const WS_SPORTS_URL: &str = "wss://sports-api.polymarket.com/ws";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChannelType {
Market,
User,
Sports,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "u8", try_from = "u8")]
pub enum SubscriptionLevel {
One,
Two,
Three,
}
impl From<SubscriptionLevel> for u8 {
fn from(level: SubscriptionLevel) -> Self {
match level {
SubscriptionLevel::One => 1,
SubscriptionLevel::Two => 2,
SubscriptionLevel::Three => 3,
}
}
}
impl TryFrom<u8> for SubscriptionLevel {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::One),
2 => Ok(Self::Two),
3 => Ok(Self::Three),
other => Err(format!("invalid subscription level {other}, expected 1-3")),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MarketSubscriptionOptions {
pub custom_feature_enabled: bool,
pub initial_dump: Option<bool>,
pub level: Option<SubscriptionLevel>,
}
impl MarketSubscriptionOptions {
pub fn with_custom_features(mut self) -> Self {
self.custom_feature_enabled = true;
self
}
pub fn initial_dump(mut self, initial_dump: bool) -> Self {
self.initial_dump = Some(initial_dump);
self
}
pub fn level(mut self, level: SubscriptionLevel) -> Self {
self.level = Some(level);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketSubscription {
pub assets_ids: Vec<String>,
#[serde(rename = "type")]
pub channel_type: ChannelType,
#[serde(default, skip_serializing_if = "is_false")]
pub custom_feature_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_dump: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<SubscriptionLevel>,
}
fn is_false(value: &bool) -> bool {
!*value
}
impl MarketSubscription {
pub fn new(assets_ids: Vec<String>) -> Self {
Self::with_options(assets_ids, MarketSubscriptionOptions::default())
}
pub fn with_options(assets_ids: Vec<String>, options: MarketSubscriptionOptions) -> Self {
Self {
assets_ids,
channel_type: ChannelType::Market,
custom_feature_enabled: options.custom_feature_enabled,
initial_dump: options.initial_dump,
level: options.level,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSubscription {
pub markets: Vec<String>,
pub auth: ApiCredentials,
#[serde(rename = "type")]
pub channel_type: ChannelType,
}
impl UserSubscription {
pub fn new(markets: Vec<String>, credentials: ApiCredentials) -> Self {
Self {
markets,
auth: credentials,
channel_type: ChannelType::User,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_type_serialization() {
let market = serde_json::to_value(ChannelType::Market).unwrap();
let user = serde_json::to_value(ChannelType::User).unwrap();
assert_eq!(market, "market");
assert_eq!(user, "user");
}
#[test]
fn channel_type_deserialization() {
let market: ChannelType = serde_json::from_str("\"market\"").unwrap();
let user: ChannelType = serde_json::from_str("\"user\"").unwrap();
assert_eq!(market, ChannelType::Market);
assert_eq!(user, ChannelType::User);
}
#[test]
fn channel_type_rejects_uppercase() {
let result = serde_json::from_str::<ChannelType>("\"MARKET\"");
assert!(result.is_err(), "Should reject uppercase channel type");
}
#[test]
fn market_subscription_new_sets_channel_type() {
let sub = MarketSubscription::new(vec!["asset1".into(), "asset2".into()]);
assert_eq!(sub.channel_type, ChannelType::Market);
assert_eq!(sub.assets_ids.len(), 2);
assert_eq!(sub.assets_ids[0], "asset1");
assert_eq!(sub.assets_ids[1], "asset2");
}
#[test]
fn market_subscription_serialization() {
let sub = MarketSubscription::new(vec!["token123".into()]);
let json = serde_json::to_value(&sub).unwrap();
assert_eq!(json["type"], "market");
assert_eq!(json["assets_ids"][0], "token123");
}
#[test]
fn market_subscription_empty_assets() {
let sub = MarketSubscription::new(vec![]);
let json = serde_json::to_value(&sub).unwrap();
assert_eq!(json["type"], "market");
assert!(json["assets_ids"].as_array().unwrap().is_empty());
}
#[test]
fn user_subscription_new_sets_channel_type() {
let creds = ApiCredentials::new("key", "secret", "pass");
let sub = UserSubscription::new(vec!["cond1".into()], creds);
assert_eq!(sub.channel_type, ChannelType::User);
assert_eq!(sub.markets.len(), 1);
assert_eq!(sub.markets[0], "cond1");
}
#[test]
fn user_subscription_serialization() {
let creds = ApiCredentials::new("my_key", "my_secret", "my_pass");
let sub = UserSubscription::new(vec!["market1".into(), "market2".into()], creds);
let json = serde_json::to_value(&sub).unwrap();
assert_eq!(json["type"], "user");
assert_eq!(json["markets"][0], "market1");
assert_eq!(json["markets"][1], "market2");
assert_eq!(json["auth"]["apiKey"], "my_key");
assert_eq!(json["auth"]["secret"], "my_secret");
assert_eq!(json["auth"]["passphrase"], "my_pass");
}
#[test]
fn ws_url_constants() {
assert!(WS_MARKET_URL.starts_with("wss://"));
assert!(WS_MARKET_URL.contains("market"));
assert!(WS_USER_URL.starts_with("wss://"));
assert!(WS_USER_URL.contains("user"));
}
}
#[cfg(test)]
mod options_tests {
use super::*;
#[test]
fn default_options_preserve_the_original_wire_payload() {
let sub = MarketSubscription::new(vec!["token123".into()]);
let json = serde_json::to_value(&sub).unwrap();
assert_eq!(json["type"], "market");
assert_eq!(json["assets_ids"][0], "token123");
assert_eq!(
json.as_object().unwrap().len(),
2,
"unset options must be omitted entirely, got {json}"
);
}
#[test]
fn custom_features_flag_is_sent_when_enabled() {
let sub = MarketSubscription::with_options(
vec!["t".into()],
MarketSubscriptionOptions::default().with_custom_features(),
);
let json = serde_json::to_value(&sub).unwrap();
assert_eq!(json["custom_feature_enabled"], true);
}
#[test]
fn initial_dump_and_level_serialize_when_set() {
let sub = MarketSubscription::with_options(
vec!["t".into()],
MarketSubscriptionOptions::default()
.initial_dump(false)
.level(SubscriptionLevel::Three),
);
let json = serde_json::to_value(&sub).unwrap();
assert_eq!(json["initial_dump"], false);
assert_eq!(json["level"], 3);
}
#[test]
fn subscription_level_rejects_out_of_range() {
assert!(SubscriptionLevel::try_from(0u8).is_err());
assert!(SubscriptionLevel::try_from(4u8).is_err());
assert_eq!(
SubscriptionLevel::try_from(2u8).unwrap(),
SubscriptionLevel::Two
);
}
#[test]
fn sports_url_uses_its_own_host() {
assert!(WS_SPORTS_URL.starts_with("wss://"));
assert!(
!WS_SPORTS_URL.contains("ws-subscriptions-clob"),
"sports is served by sports-api, not the clob subscriptions host"
);
}
}