polymarket_client/secure/
account.rs1use 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}