Skip to main content

polymarket_client/secure/
account.rs

1//! Authenticated account reads (CLOB + data API defaults).
2
3use std::str::FromStr as _;
4
5use polymarket_client_sdk_v2::clob::types::request::TradesRequest;
6use polymarket_client_sdk_v2::clob::types::response::{
7    CurrentRewardResponse, NotificationResponse, TradeResponse,
8};
9use polymarket_client_sdk_v2::types::{B256, U256};
10
11use crate::account::{
12    FetchPortfolioValueError, FetchPortfolioValueRequest, ListActivityError, ListActivityRequest,
13    ListPositionsError, ListPositionsRequest, PortfolioValue,
14};
15use crate::account_client::{ListActivityPaginator, ListPositionsPaginator};
16use crate::error::{user_input, UserInputError};
17use crate::secure::secure_client::SecureClient;
18
19macro_rules! secure_account_error {
20    ($name:ident) => {
21        #[derive(Debug, thiserror::Error, Clone)]
22        pub enum $name {
23            #[error(transparent)]
24            UserInput(#[from] UserInputError),
25            #[error("SDK error: {0}")]
26            Sdk(String),
27        }
28
29        impl $name {
30            #[must_use]
31            pub fn is_error(err: &(dyn std::error::Error + 'static)) -> bool {
32                err.downcast_ref::<Self>().is_some()
33                    || err.downcast_ref::<UserInputError>().is_some()
34            }
35        }
36    };
37}
38
39secure_account_error!(ListAccountTradesError);
40secure_account_error!(FetchNotificationsError);
41secure_account_error!(FetchOrderScoringError);
42secure_account_error!(ListCurrentRewardsError);
43
44#[derive(Clone, Debug, Default)]
45pub struct ListAccountTradesRequest {
46    pub token_id: Option<String>,
47    pub market: Option<String>,
48}
49
50#[derive(Clone, Debug)]
51pub struct AccountTrade {
52    pub trade_id: String,
53    pub token_id: String,
54    pub market: String,
55    pub side: String,
56    pub price: String,
57    pub size: String,
58    pub status: String,
59}
60
61#[derive(Clone, Debug)]
62pub struct Notification {
63    pub notification_type: u32,
64    pub order_id: String,
65    pub token_id: String,
66    pub market: String,
67    pub side: String,
68    pub price: String,
69    pub matched_size: String,
70}
71
72#[derive(Clone, Debug)]
73pub struct CurrentReward {
74    pub condition_id: String,
75    pub max_spread: String,
76    pub min_size: String,
77}
78
79#[derive(Clone, Debug)]
80pub struct FetchOrderScoringRequest {
81    pub order_id: String,
82}
83
84impl SecureClient {
85    pub fn list_positions(
86        &self,
87        request: ListPositionsRequest,
88    ) -> Result<ListPositionsPaginator, ListPositionsError> {
89        self.public().list_positions(with_wallet(self, request))
90    }
91
92    pub async fn fetch_portfolio_value(
93        &self,
94        request: FetchPortfolioValueRequest,
95    ) -> Result<Vec<PortfolioValue>, FetchPortfolioValueError> {
96        self.public()
97            .fetch_portfolio_value(with_wallet(self, request))
98            .await
99    }
100
101    pub fn list_activity(
102        &self,
103        request: ListActivityRequest,
104    ) -> Result<ListActivityPaginator, ListActivityError> {
105        self.public().list_activity(with_wallet(self, request))
106    }
107
108    pub async fn list_account_trades(
109        &self,
110        request: ListAccountTradesRequest,
111    ) -> Result<Vec<AccountTrade>, ListAccountTradesError> {
112        let mut req = TradesRequest::default();
113        if let Some(token_id) = request.token_id {
114            req.asset_id = Some(parse_token_id(&token_id)?);
115        }
116        if let Some(market) = request.market {
117            req.market = Some(parse_market_id(&market)?);
118        }
119
120        let mut cursor = None;
121        let mut all = Vec::new();
122        loop {
123            let page = self
124                .clob
125                .trades(&req, cursor.clone())
126                .await
127                .map_err(|e| ListAccountTradesError::Sdk(e.to_string()))?;
128            all.extend(page.data.into_iter().map(map_account_trade));
129            if page.next_cursor.is_empty() || page.next_cursor == "LTE=" {
130                break;
131            }
132            cursor = Some(page.next_cursor);
133        }
134        Ok(all)
135    }
136
137    pub async fn fetch_notifications(&self) -> Result<Vec<Notification>, FetchNotificationsError> {
138        let notifications = self
139            .clob
140            .notifications()
141            .await
142            .map_err(|e| FetchNotificationsError::Sdk(e.to_string()))?;
143        Ok(notifications.into_iter().map(map_notification).collect())
144    }
145
146    pub async fn fetch_order_scoring(
147        &self,
148        request: FetchOrderScoringRequest,
149    ) -> Result<bool, FetchOrderScoringError> {
150        if request.order_id.trim().is_empty() {
151            return Err(FetchOrderScoringError::UserInput(user_input(
152                "order_id cannot be empty",
153            )));
154        }
155        let response = self
156            .clob
157            .is_order_scoring(&request.order_id)
158            .await
159            .map_err(|e| FetchOrderScoringError::Sdk(e.to_string()))?;
160        Ok(response.scoring)
161    }
162
163    pub async fn list_current_rewards(
164        &self,
165    ) -> Result<Vec<CurrentReward>, ListCurrentRewardsError> {
166        let mut cursor = None;
167        let mut all = Vec::new();
168        loop {
169            let page = self
170                .clob
171                .current_rewards(cursor.clone())
172                .await
173                .map_err(|e| ListCurrentRewardsError::Sdk(e.to_string()))?;
174            all.extend(page.data.into_iter().map(map_current_reward));
175            if page.next_cursor.is_empty() || page.next_cursor == "LTE=" {
176                break;
177            }
178            cursor = Some(page.next_cursor);
179        }
180        Ok(all)
181    }
182}
183
184fn with_wallet<T>(client: &SecureClient, mut request: T) -> T
185where
186    T: DefaultWallet,
187{
188    if request.user().trim().is_empty() {
189        request.set_user(client.wallet().to_string());
190    }
191    request
192}
193
194trait DefaultWallet {
195    fn user(&self) -> &str;
196    fn set_user(&mut self, user: String);
197}
198
199impl DefaultWallet for ListPositionsRequest {
200    fn user(&self) -> &str {
201        &self.user
202    }
203    fn set_user(&mut self, user: String) {
204        self.user = user;
205    }
206}
207
208impl DefaultWallet for FetchPortfolioValueRequest {
209    fn user(&self) -> &str {
210        &self.user
211    }
212    fn set_user(&mut self, user: String) {
213        self.user = user;
214    }
215}
216
217impl DefaultWallet for ListActivityRequest {
218    fn user(&self) -> &str {
219        &self.user
220    }
221    fn set_user(&mut self, user: String) {
222        self.user = user;
223    }
224}
225
226fn parse_token_id(token_id: &str) -> Result<U256, UserInputError> {
227    U256::from_str(token_id).map_err(|e| user_input(format!("invalid token_id: {e}")))
228}
229
230fn parse_market_id(market: &str) -> Result<B256, UserInputError> {
231    B256::from_str(market).map_err(|e| user_input(format!("invalid market id: {e}")))
232}
233
234fn map_account_trade(trade: TradeResponse) -> AccountTrade {
235    AccountTrade {
236        trade_id: trade.id,
237        token_id: trade.asset_id.to_string(),
238        market: trade.market.to_string(),
239        side: format!("{:?}", trade.side),
240        price: trade.price.to_string(),
241        size: trade.size.to_string(),
242        status: format!("{:?}", trade.status),
243    }
244}
245
246fn map_notification(notification: NotificationResponse) -> Notification {
247    Notification {
248        notification_type: notification.r#type,
249        order_id: notification.payload.order_id,
250        token_id: notification.payload.asset_id.to_string(),
251        market: notification.payload.market.to_string(),
252        side: format!("{:?}", notification.payload.side),
253        price: notification.payload.price.to_string(),
254        matched_size: notification.payload.matched_size.to_string(),
255    }
256}
257
258fn map_current_reward(reward: CurrentRewardResponse) -> CurrentReward {
259    CurrentReward {
260        condition_id: reward.condition_id.to_string(),
261        max_spread: reward.rewards_max_spread.to_string(),
262        min_size: reward.rewards_min_size.to_string(),
263    }
264}