1#![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}