Skip to main content

polymarket_client/
account.rs

1//! Account data types, requests, and pagination helpers.
2
3#![allow(clippy::redundant_pub_crate)]
4
5use polymarket_types::PaginationCursor;
6
7use crate::error::{user_input, UserInputError};
8
9macro_rules! account_error {
10    ($name:ident) => {
11        #[derive(Debug, thiserror::Error, Clone)]
12        pub enum $name {
13            #[error(transparent)]
14            UserInput(#[from] UserInputError),
15            #[error("data API error: {0}")]
16            Data(String),
17        }
18
19        impl $name {
20            #[must_use]
21            pub fn is_error(err: &(dyn std::error::Error + 'static)) -> bool {
22                err.downcast_ref::<Self>().is_some()
23                    || err.downcast_ref::<UserInputError>().is_some()
24            }
25        }
26    };
27}
28
29account_error!(ListPositionsError);
30account_error!(ListActivityError);
31account_error!(FetchPortfolioValueError);
32
33#[derive(Clone, Debug)]
34pub struct Position {
35    pub token_id: String,
36    pub condition_id: String,
37    pub size: String,
38    pub avg_price: String,
39    pub current_value: String,
40    pub cash_pnl: String,
41    pub percent_pnl: String,
42    pub title: String,
43    pub outcome: String,
44    pub redeemable: bool,
45    pub mergeable: bool,
46}
47
48#[derive(Clone, Debug)]
49pub struct Activity {
50    pub timestamp: i64,
51    pub activity_type: String,
52    pub condition_id: Option<String>,
53    pub size: String,
54    pub usdc_size: String,
55    pub transaction_hash: String,
56    pub title: Option<String>,
57    pub outcome: Option<String>,
58}
59
60#[derive(Clone, Debug)]
61pub struct PortfolioValue {
62    pub user: String,
63    pub value: String,
64}
65
66#[derive(Clone, Debug, Default)]
67pub struct ListPositionsRequest {
68    pub user: String,
69    pub markets: Vec<String>,
70    pub page_size: Option<u32>,
71    pub cursor: Option<PaginationCursor>,
72    pub redeemable: Option<bool>,
73    pub mergeable: Option<bool>,
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct ListActivityRequest {
78    pub user: String,
79    pub page_size: Option<u32>,
80    pub cursor: Option<PaginationCursor>,
81}
82
83#[derive(Clone, Debug, Default)]
84pub struct FetchPortfolioValueRequest {
85    pub user: String,
86    pub markets: Vec<String>,
87}
88
89pub(crate) struct OffsetCursorState {
90    pub(crate) offset: u32,
91    pub(crate) page_size: u32,
92}
93
94pub(crate) fn decode_offset_cursor(
95    cursor: Option<&PaginationCursor>,
96    default_page_size: u32,
97) -> Result<OffsetCursorState, UserInputError> {
98    let Some(cursor) = cursor else {
99        return Ok(OffsetCursorState {
100            offset: 0,
101            page_size: default_page_size,
102        });
103    };
104
105    let parts: Vec<&str> = cursor.as_str().split(':').collect();
106    if parts.len() != 4 || parts[0] != "offset" || parts[2] != "page_size" {
107        return Err(user_input("invalid pagination cursor"));
108    }
109    let offset = parts[1]
110        .parse()
111        .map_err(|_| user_input("invalid pagination cursor offset"))?;
112    let page_size = parts[3]
113        .parse()
114        .map_err(|_| user_input("invalid pagination cursor page_size"))?;
115    Ok(OffsetCursorState { offset, page_size })
116}
117
118pub(crate) fn encode_offset_cursor(state: OffsetCursorState) -> PaginationCursor {
119    PaginationCursor::parse(format!(
120        "offset:{}:page_size:{}",
121        state.offset, state.page_size
122    ))
123    .expect("offset cursor is valid")
124}
125
126pub(crate) fn next_offset_cursor(state: OffsetCursorState) -> PaginationCursor {
127    encode_offset_cursor(OffsetCursorState {
128        offset: state.offset + state.page_size,
129        page_size: state.page_size,
130    })
131}
132
133pub(crate) fn validate_page_size(page_size: Option<u32>) -> Result<u32, UserInputError> {
134    let size = page_size.unwrap_or(20);
135    if !(1..=500).contains(&size) {
136        return Err(user_input("page_size must be between 1 and 500"));
137    }
138    Ok(size)
139}
140
141pub(crate) fn validate_user(user: &str) -> Result<(), UserInputError> {
142    if user.trim().is_empty() {
143        return Err(user_input("user address cannot be empty"));
144    }
145    Ok(())
146}
147
148#[cfg(feature = "account")]
149pub(crate) mod mappers {
150    use polymarket_client_sdk_v2::data::types::response::{
151        Activity as SdkActivity, Position as SdkPosition, Value as SdkValue,
152    };
153
154    use super::{Activity, PortfolioValue, Position};
155
156    pub fn map_position(position: SdkPosition) -> Position {
157        Position {
158            token_id: position.asset.to_string(),
159            condition_id: position.condition_id.to_string(),
160            size: position.size.to_string(),
161            avg_price: position.avg_price.to_string(),
162            current_value: position.current_value.to_string(),
163            cash_pnl: position.cash_pnl.to_string(),
164            percent_pnl: position.percent_pnl.to_string(),
165            title: position.title,
166            outcome: position.outcome,
167            redeemable: position.redeemable,
168            mergeable: position.mergeable,
169        }
170    }
171
172    pub fn map_activity(activity: SdkActivity) -> Activity {
173        Activity {
174            timestamp: activity.timestamp,
175            activity_type: format!("{:?}", activity.activity_type),
176            condition_id: activity.condition_id.map(|id| id.to_string()),
177            size: activity.size.to_string(),
178            usdc_size: activity.usdc_size.to_string(),
179            transaction_hash: activity.transaction_hash.to_string(),
180            title: activity.title,
181            outcome: activity.outcome,
182        }
183    }
184
185    pub fn map_portfolio_value(value: SdkValue) -> PortfolioValue {
186        PortfolioValue {
187            user: value.user.to_string(),
188            value: value.value.to_string(),
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn offset_cursor_round_trip() {
199        let encoded = encode_offset_cursor(OffsetCursorState {
200            offset: 40,
201            page_size: 20,
202        });
203        assert_eq!(encoded.as_str(), "offset:40:page_size:20");
204        let decoded = decode_offset_cursor(Some(&encoded), 20).unwrap();
205        assert_eq!(decoded.offset, 40);
206        assert_eq!(decoded.page_size, 20);
207    }
208}