1#![allow(unused_imports)]
15use async_trait::async_trait;
16use derive_builder::Builder;
17use reqwest;
18use rust_decimal::prelude::*;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use std::collections::BTreeMap;
22
23use crate::common::{
24 config::ConfigurationRestApi,
25 models::{ParamBuildError, RestApiResponse},
26 utils::send_request,
27};
28use crate::w3w_prediction::rest_api::models;
29
30const HAS_TIME_UNIT: bool = false;
31
32#[async_trait]
33pub trait PositionApi: Send + Sync {
34 async fn get_position_by_token(
35 &self,
36 params: GetPositionByTokenParams,
37 ) -> anyhow::Result<RestApiResponse<models::GetPositionByTokenResponse>>;
38 async fn query_pn_l(
39 &self,
40 params: QueryPnLParams,
41 ) -> anyhow::Result<RestApiResponse<models::QueryPnLResponse>>;
42 async fn query_positions(
43 &self,
44 params: QueryPositionsParams,
45 ) -> anyhow::Result<RestApiResponse<models::QueryPositionsResponse>>;
46 async fn query_positions_by_filter(
47 &self,
48 params: QueryPositionsByFilterParams,
49 ) -> anyhow::Result<RestApiResponse<models::QueryPositionsByFilterResponse>>;
50 async fn query_settled_position_history(
51 &self,
52 params: QuerySettledPositionHistoryParams,
53 ) -> anyhow::Result<RestApiResponse<models::QuerySettledPositionHistoryResponse>>;
54}
55
56#[derive(Debug, Clone)]
57pub struct PositionApiClient {
58 configuration: ConfigurationRestApi,
59}
60
61impl PositionApiClient {
62 pub fn new(configuration: ConfigurationRestApi) -> Self {
63 Self { configuration }
64 }
65}
66
67#[derive(Clone, Debug, Builder, Deserialize)]
72#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
73pub struct GetPositionByTokenParams {
74 #[builder(setter(into))]
78 #[serde(rename = "walletAddress")]
79 pub wallet_address: String,
80 #[builder(setter(into))]
84 #[serde(rename = "tokenId")]
85 pub token_id: String,
86 #[builder(setter(into), default)]
90 #[serde(rename = "recvWindow", default)]
91 pub recv_window: Option<i64>,
92}
93
94impl GetPositionByTokenParams {
95 #[must_use]
103 pub fn builder(wallet_address: String, token_id: String) -> GetPositionByTokenParamsBuilder {
104 GetPositionByTokenParamsBuilder::default()
105 .wallet_address(wallet_address)
106 .token_id(token_id)
107 }
108}
109#[derive(Clone, Debug, Builder, Deserialize)]
114#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
115pub struct QueryPnLParams {
116 #[builder(setter(into))]
120 #[serde(rename = "walletAddress")]
121 pub wallet_address: String,
122 #[builder(setter(into), default)]
126 #[serde(rename = "tokenId", default)]
127 pub token_id: Option<String>,
128 #[builder(setter(into), default)]
132 #[serde(rename = "marketId", default)]
133 pub market_id: Option<i64>,
134 #[builder(setter(into), default)]
138 #[serde(rename = "marketTopicId", default)]
139 pub market_topic_id: Option<i64>,
140 #[builder(setter(into), default)]
144 #[serde(rename = "activeOnly", default)]
145 pub active_only: Option<bool>,
146 #[builder(setter(into), default)]
150 #[serde(rename = "recvWindow", default)]
151 pub recv_window: Option<i64>,
152}
153
154impl QueryPnLParams {
155 #[must_use]
162 pub fn builder(wallet_address: String) -> QueryPnLParamsBuilder {
163 QueryPnLParamsBuilder::default().wallet_address(wallet_address)
164 }
165}
166#[derive(Clone, Debug, Builder, Deserialize)]
171#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
172pub struct QueryPositionsParams {
173 #[builder(setter(into))]
177 #[serde(rename = "walletAddress")]
178 pub wallet_address: String,
179 #[builder(setter(into), default)]
183 #[serde(rename = "tab", default)]
184 pub tab: Option<String>,
185 #[builder(setter(into), default)]
189 #[serde(rename = "offset", default)]
190 pub offset: Option<i32>,
191 #[builder(setter(into), default)]
195 #[serde(rename = "limit", default)]
196 pub limit: Option<i32>,
197 #[builder(setter(into), default)]
201 #[serde(rename = "recvWindow", default)]
202 pub recv_window: Option<i64>,
203}
204
205impl QueryPositionsParams {
206 #[must_use]
213 pub fn builder(wallet_address: String) -> QueryPositionsParamsBuilder {
214 QueryPositionsParamsBuilder::default().wallet_address(wallet_address)
215 }
216}
217#[derive(Clone, Debug, Builder, Deserialize, Default)]
222#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
223pub struct QueryPositionsByFilterParams {
224 #[builder(setter(into), default)]
228 #[serde(rename = "walletAddress", default)]
229 pub wallet_address: Option<String>,
230 #[builder(setter(into), default)]
234 #[serde(rename = "marketTopicId", default)]
235 pub market_topic_id: Option<i64>,
236 #[builder(setter(into), default)]
240 #[serde(rename = "recvWindow", default)]
241 pub recv_window: Option<i64>,
242}
243
244impl QueryPositionsByFilterParams {
245 #[must_use]
248 pub fn builder() -> QueryPositionsByFilterParamsBuilder {
249 QueryPositionsByFilterParamsBuilder::default()
250 }
251}
252#[derive(Clone, Debug, Builder, Deserialize)]
257#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
258pub struct QuerySettledPositionHistoryParams {
259 #[builder(setter(into))]
263 #[serde(rename = "walletAddress")]
264 pub wallet_address: String,
265 #[builder(setter(into), default)]
269 #[serde(rename = "l1Category", default)]
270 pub l1_category: Option<String>,
271 #[builder(setter(into), default)]
275 #[serde(rename = "result", default)]
276 pub result: Option<i32>,
277 #[builder(setter(into), default)]
281 #[serde(rename = "startDate", default)]
282 pub start_date: Option<String>,
283 #[builder(setter(into), default)]
287 #[serde(rename = "endDate", default)]
288 pub end_date: Option<String>,
289 #[builder(setter(into), default)]
293 #[serde(rename = "offset", default)]
294 pub offset: Option<i32>,
295 #[builder(setter(into), default)]
299 #[serde(rename = "limit", default)]
300 pub limit: Option<i32>,
301 #[builder(setter(into), default)]
305 #[serde(rename = "recvWindow", default)]
306 pub recv_window: Option<i64>,
307}
308
309impl QuerySettledPositionHistoryParams {
310 #[must_use]
317 pub fn builder(wallet_address: String) -> QuerySettledPositionHistoryParamsBuilder {
318 QuerySettledPositionHistoryParamsBuilder::default().wallet_address(wallet_address)
319 }
320}
321
322#[async_trait]
323impl PositionApi for PositionApiClient {
324 async fn get_position_by_token(
325 &self,
326 params: GetPositionByTokenParams,
327 ) -> anyhow::Result<RestApiResponse<models::GetPositionByTokenResponse>> {
328 let GetPositionByTokenParams {
329 wallet_address,
330 token_id,
331 recv_window,
332 } = params;
333
334 let mut query_params = BTreeMap::new();
335 let body_params = BTreeMap::new();
336
337 query_params.insert("walletAddress".to_string(), json!(wallet_address));
338
339 query_params.insert("tokenId".to_string(), json!(token_id));
340
341 if let Some(rw) = recv_window {
342 query_params.insert("recvWindow".to_string(), json!(rw));
343 }
344
345 send_request::<models::GetPositionByTokenResponse>(
346 &self.configuration,
347 "/sapi/v1/w3w/wallet/prediction/position/token",
348 reqwest::Method::GET,
349 query_params,
350 body_params,
351 if HAS_TIME_UNIT {
352 self.configuration.time_unit
353 } else {
354 None
355 },
356 true,
357 )
358 .await
359 }
360
361 async fn query_pn_l(
362 &self,
363 params: QueryPnLParams,
364 ) -> anyhow::Result<RestApiResponse<models::QueryPnLResponse>> {
365 let QueryPnLParams {
366 wallet_address,
367 token_id,
368 market_id,
369 market_topic_id,
370 active_only,
371 recv_window,
372 } = params;
373
374 let mut query_params = BTreeMap::new();
375 let body_params = BTreeMap::new();
376
377 query_params.insert("walletAddress".to_string(), json!(wallet_address));
378
379 if let Some(rw) = token_id {
380 query_params.insert("tokenId".to_string(), json!(rw));
381 }
382
383 if let Some(rw) = market_id {
384 query_params.insert("marketId".to_string(), json!(rw));
385 }
386
387 if let Some(rw) = market_topic_id {
388 query_params.insert("marketTopicId".to_string(), json!(rw));
389 }
390
391 if let Some(rw) = active_only {
392 query_params.insert("activeOnly".to_string(), json!(rw));
393 }
394
395 if let Some(rw) = recv_window {
396 query_params.insert("recvWindow".to_string(), json!(rw));
397 }
398
399 send_request::<models::QueryPnLResponse>(
400 &self.configuration,
401 "/sapi/v1/w3w/wallet/prediction/pnl/query",
402 reqwest::Method::GET,
403 query_params,
404 body_params,
405 if HAS_TIME_UNIT {
406 self.configuration.time_unit
407 } else {
408 None
409 },
410 true,
411 )
412 .await
413 }
414
415 async fn query_positions(
416 &self,
417 params: QueryPositionsParams,
418 ) -> anyhow::Result<RestApiResponse<models::QueryPositionsResponse>> {
419 let QueryPositionsParams {
420 wallet_address,
421 tab,
422 offset,
423 limit,
424 recv_window,
425 } = params;
426
427 let mut query_params = BTreeMap::new();
428 let body_params = BTreeMap::new();
429
430 query_params.insert("walletAddress".to_string(), json!(wallet_address));
431
432 if let Some(rw) = tab {
433 query_params.insert("tab".to_string(), json!(rw));
434 }
435
436 if let Some(rw) = offset {
437 query_params.insert("offset".to_string(), json!(rw));
438 }
439
440 if let Some(rw) = limit {
441 query_params.insert("limit".to_string(), json!(rw));
442 }
443
444 if let Some(rw) = recv_window {
445 query_params.insert("recvWindow".to_string(), json!(rw));
446 }
447
448 send_request::<models::QueryPositionsResponse>(
449 &self.configuration,
450 "/sapi/v1/w3w/wallet/prediction/position/list",
451 reqwest::Method::GET,
452 query_params,
453 body_params,
454 if HAS_TIME_UNIT {
455 self.configuration.time_unit
456 } else {
457 None
458 },
459 true,
460 )
461 .await
462 }
463
464 async fn query_positions_by_filter(
465 &self,
466 params: QueryPositionsByFilterParams,
467 ) -> anyhow::Result<RestApiResponse<models::QueryPositionsByFilterResponse>> {
468 let QueryPositionsByFilterParams {
469 wallet_address,
470 market_topic_id,
471 recv_window,
472 } = params;
473
474 let mut query_params = BTreeMap::new();
475 let body_params = BTreeMap::new();
476
477 if let Some(rw) = wallet_address {
478 query_params.insert("walletAddress".to_string(), json!(rw));
479 }
480
481 if let Some(rw) = market_topic_id {
482 query_params.insert("marketTopicId".to_string(), json!(rw));
483 }
484
485 if let Some(rw) = recv_window {
486 query_params.insert("recvWindow".to_string(), json!(rw));
487 }
488
489 send_request::<models::QueryPositionsByFilterResponse>(
490 &self.configuration,
491 "/sapi/v1/w3w/wallet/prediction/position/filter",
492 reqwest::Method::GET,
493 query_params,
494 body_params,
495 if HAS_TIME_UNIT {
496 self.configuration.time_unit
497 } else {
498 None
499 },
500 true,
501 )
502 .await
503 }
504
505 async fn query_settled_position_history(
506 &self,
507 params: QuerySettledPositionHistoryParams,
508 ) -> anyhow::Result<RestApiResponse<models::QuerySettledPositionHistoryResponse>> {
509 let QuerySettledPositionHistoryParams {
510 wallet_address,
511 l1_category,
512 result,
513 start_date,
514 end_date,
515 offset,
516 limit,
517 recv_window,
518 } = params;
519
520 let mut query_params = BTreeMap::new();
521 let body_params = BTreeMap::new();
522
523 query_params.insert("walletAddress".to_string(), json!(wallet_address));
524
525 if let Some(rw) = l1_category {
526 query_params.insert("l1Category".to_string(), json!(rw));
527 }
528
529 if let Some(rw) = result {
530 query_params.insert("result".to_string(), json!(rw));
531 }
532
533 if let Some(rw) = start_date {
534 query_params.insert("startDate".to_string(), json!(rw));
535 }
536
537 if let Some(rw) = end_date {
538 query_params.insert("endDate".to_string(), json!(rw));
539 }
540
541 if let Some(rw) = offset {
542 query_params.insert("offset".to_string(), json!(rw));
543 }
544
545 if let Some(rw) = limit {
546 query_params.insert("limit".to_string(), json!(rw));
547 }
548
549 if let Some(rw) = recv_window {
550 query_params.insert("recvWindow".to_string(), json!(rw));
551 }
552
553 send_request::<models::QuerySettledPositionHistoryResponse>(
554 &self.configuration,
555 "/sapi/v1/w3w/wallet/prediction/position/settled-history",
556 reqwest::Method::GET,
557 query_params,
558 body_params,
559 if HAS_TIME_UNIT {
560 self.configuration.time_unit
561 } else {
562 None
563 },
564 true,
565 )
566 .await
567 }
568}
569
570#[cfg(all(test, feature = "w3w_prediction"))]
571mod tests {
572 use super::*;
573 use crate::TOKIO_SHARED_RT;
574 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
575 use async_trait::async_trait;
576 use std::collections::HashMap;
577
578 struct DummyRestApiResponse<T> {
579 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
580 status: u16,
581 headers: HashMap<String, String>,
582 rate_limits: Option<Vec<RestApiRateLimit>>,
583 }
584
585 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
586 fn from(dummy: DummyRestApiResponse<T>) -> Self {
587 Self {
588 data_fn: dummy.inner,
589 status: dummy.status,
590 headers: dummy.headers,
591 rate_limits: dummy.rate_limits,
592 }
593 }
594 }
595
596 struct MockPositionApiClient {
597 force_error: bool,
598 }
599
600 #[async_trait]
601 impl PositionApi for MockPositionApiClient {
602 async fn get_position_by_token(
603 &self,
604 _params: GetPositionByTokenParams,
605 ) -> anyhow::Result<RestApiResponse<models::GetPositionByTokenResponse>> {
606 if self.force_error {
607 return Err(ConnectorError::ConnectorClientError {
608 msg: "ResponseError".to_string(),
609 code: None,
610 }
611 .into());
612 }
613
614 let resp_json: Value = serde_json::from_str(r#"{"position":{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}}"#).unwrap_or_else(|_| serde_json::json!({}));
615 let dummy_response: models::GetPositionByTokenResponse =
616 serde_json::from_value(resp_json.clone())
617 .expect("should parse into models::GetPositionByTokenResponse");
618
619 let dummy = DummyRestApiResponse {
620 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
621 status: 200,
622 headers: HashMap::new(),
623 rate_limits: None,
624 };
625
626 Ok(dummy.into())
627 }
628
629 async fn query_pn_l(
630 &self,
631 _params: QueryPnLParams,
632 ) -> anyhow::Result<RestApiResponse<models::QueryPnLResponse>> {
633 if self.force_error {
634 return Err(ConnectorError::ConnectorClientError {
635 msg: "ResponseError".to_string(),
636 code: None,
637 }
638 .into());
639 }
640
641 let resp_json: Value = serde_json::from_str(r#"{"chainId":"56","walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","pnl":{},"pnlList":[{"id":10001,"walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","marketTopicId":4229564,"marketId":5567895,"tokenId":"112233","vendor":"PREDICT_FUN","currentShares":"1923.07","avgPrice":"0.52","currentPrice":"0.55","realizedPnl":"0.00","unrealizedPnl":"0.06","totalPnl":"0.06","pnlPercentage":"6.00","isResolved":false}],"totalCount":1,"totalRealizedPnl":"0.00","totalUnrealizedPnl":"0.06","totalPnl":"0.06"}"#).unwrap_or_else(|_| serde_json::json!({}));
642 let dummy_response: models::QueryPnLResponse =
643 serde_json::from_value(resp_json.clone())
644 .expect("should parse into models::QueryPnLResponse");
645
646 let dummy = DummyRestApiResponse {
647 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
648 status: 200,
649 headers: HashMap::new(),
650 rate_limits: None,
651 };
652
653 Ok(dummy.into())
654 }
655
656 async fn query_positions(
657 &self,
658 _params: QueryPositionsParams,
659 ) -> anyhow::Result<RestApiResponse<models::QueryPositionsResponse>> {
660 if self.force_error {
661 return Err(ConnectorError::ConnectorClientError {
662 msg: "ResponseError".to_string(),
663 code: None,
664 }
665 .into());
666 }
667
668 let resp_json: Value = serde_json::from_str(r#"{"summary":{"totalValue":"1523.45","positionValue":"523.45","walletBalance":"1000.00","totalClaimableAmount":"50.00","todayRealizedPnl":"15.30","todayRealizedPnlPercent":"3.10","todayTotalCost":"493.55"},"counts":{"ongoingCount":3,"endedCount":12,"pendingClaimCount":1},"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","toWin":"1923.07","positionStatus":"OPEN","canClaim":false,"endDate":1748134800000,"unrealizedPnl":"0.06","unrealizedPnlPercent":"6.00","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
669 let dummy_response: models::QueryPositionsResponse =
670 serde_json::from_value(resp_json.clone())
671 .expect("should parse into models::QueryPositionsResponse");
672
673 let dummy = DummyRestApiResponse {
674 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
675 status: 200,
676 headers: HashMap::new(),
677 rate_limits: None,
678 };
679
680 Ok(dummy.into())
681 }
682
683 async fn query_positions_by_filter(
684 &self,
685 _params: QueryPositionsByFilterParams,
686 ) -> anyhow::Result<RestApiResponse<models::QueryPositionsByFilterResponse>> {
687 if self.force_error {
688 return Err(ConnectorError::ConnectorClientError {
689 msg: "ResponseError".to_string(),
690 code: None,
691 }
692 .into());
693 }
694
695 let resp_json: Value = serde_json::from_str(r#"{"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
696 let dummy_response: models::QueryPositionsByFilterResponse =
697 serde_json::from_value(resp_json.clone())
698 .expect("should parse into models::QueryPositionsByFilterResponse");
699
700 let dummy = DummyRestApiResponse {
701 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
702 status: 200,
703 headers: HashMap::new(),
704 rate_limits: None,
705 };
706
707 Ok(dummy.into())
708 }
709
710 async fn query_settled_position_history(
711 &self,
712 _params: QuerySettledPositionHistoryParams,
713 ) -> anyhow::Result<RestApiResponse<models::QuerySettledPositionHistoryResponse>> {
714 if self.force_error {
715 return Err(ConnectorError::ConnectorClientError {
716 msg: "ResponseError".to_string(),
717 code: None,
718 }
719 .into());
720 }
721
722 let resp_json: Value = serde_json::from_str(r#"{"totalCount":5,"positions":[{"positionId":998,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112200","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229500,"marketId":5567800,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"5000.00","avgPrice":"0.30","totalCost":"1.50","value":"5000.00","currentPrice":"1.00","toWin":"5000.00","positionStatus":"CLAIMED","isWinner":true,"redeemStatus":"CONFIRMED","endDate":1748040000000,"finalOutcome":"YES","realizedPnl":"4998.50","pnl":"4998.50","claimAmount":"5000.00","settledDate":1748040000000,"createdTime":1748020000000,"updatedTime":1748040500000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
723 let dummy_response: models::QuerySettledPositionHistoryResponse =
724 serde_json::from_value(resp_json.clone())
725 .expect("should parse into models::QuerySettledPositionHistoryResponse");
726
727 let dummy = DummyRestApiResponse {
728 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
729 status: 200,
730 headers: HashMap::new(),
731 rate_limits: None,
732 };
733
734 Ok(dummy.into())
735 }
736 }
737
738 #[test]
739 fn get_position_by_token_required_params_success() {
740 TOKIO_SHARED_RT.block_on(async {
741 let client = MockPositionApiClient { force_error: false };
742
743 let params = GetPositionByTokenParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),"112233".to_string(),).build().unwrap();
744
745 let resp_json: Value = serde_json::from_str(r#"{"position":{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}}"#).unwrap_or_else(|_| serde_json::json!({}));
746 let expected_response : models::GetPositionByTokenResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetPositionByTokenResponse");
747
748 let resp = client.get_position_by_token(params).await.expect("Expected a response");
749 let data_future = resp.data();
750 let actual_response = data_future.await.unwrap();
751 assert_eq!(actual_response, expected_response);
752 });
753 }
754
755 #[test]
756 fn get_position_by_token_optional_params_success() {
757 TOKIO_SHARED_RT.block_on(async {
758 let client = MockPositionApiClient { force_error: false };
759
760 let params = GetPositionByTokenParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),"112233".to_string(),).recv_window(5000).build().unwrap();
761
762 let resp_json: Value = serde_json::from_str(r#"{"position":{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}}"#).unwrap_or_else(|_| serde_json::json!({}));
763 let expected_response : models::GetPositionByTokenResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetPositionByTokenResponse");
764
765 let resp = client.get_position_by_token(params).await.expect("Expected a response");
766 let data_future = resp.data();
767 let actual_response = data_future.await.unwrap();
768 assert_eq!(actual_response, expected_response);
769 });
770 }
771
772 #[test]
773 fn get_position_by_token_response_error() {
774 TOKIO_SHARED_RT.block_on(async {
775 let client = MockPositionApiClient { force_error: true };
776
777 let params = GetPositionByTokenParams::builder(
778 "0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),
779 "112233".to_string(),
780 )
781 .build()
782 .unwrap();
783
784 match client.get_position_by_token(params).await {
785 Ok(_) => panic!("Expected an error"),
786 Err(err) => {
787 assert_eq!(err.to_string(), "Connector client error: ResponseError");
788 }
789 }
790 });
791 }
792
793 #[test]
794 fn query_pn_l_required_params_success() {
795 TOKIO_SHARED_RT.block_on(async {
796 let client = MockPositionApiClient { force_error: false };
797
798 let params = QueryPnLParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).build().unwrap();
799
800 let resp_json: Value = serde_json::from_str(r#"{"chainId":"56","walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","pnl":{},"pnlList":[{"id":10001,"walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","marketTopicId":4229564,"marketId":5567895,"tokenId":"112233","vendor":"PREDICT_FUN","currentShares":"1923.07","avgPrice":"0.52","currentPrice":"0.55","realizedPnl":"0.00","unrealizedPnl":"0.06","totalPnl":"0.06","pnlPercentage":"6.00","isResolved":false}],"totalCount":1,"totalRealizedPnl":"0.00","totalUnrealizedPnl":"0.06","totalPnl":"0.06"}"#).unwrap_or_else(|_| serde_json::json!({}));
801 let expected_response : models::QueryPnLResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPnLResponse");
802
803 let resp = client.query_pn_l(params).await.expect("Expected a response");
804 let data_future = resp.data();
805 let actual_response = data_future.await.unwrap();
806 assert_eq!(actual_response, expected_response);
807 });
808 }
809
810 #[test]
811 fn query_pn_l_optional_params_success() {
812 TOKIO_SHARED_RT.block_on(async {
813 let client = MockPositionApiClient { force_error: false };
814
815 let params = QueryPnLParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).token_id("112233".to_string()).market_id(5567895).market_topic_id(4229564).active_only(false).recv_window(5000).build().unwrap();
816
817 let resp_json: Value = serde_json::from_str(r#"{"chainId":"56","walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","pnl":{},"pnlList":[{"id":10001,"walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","marketTopicId":4229564,"marketId":5567895,"tokenId":"112233","vendor":"PREDICT_FUN","currentShares":"1923.07","avgPrice":"0.52","currentPrice":"0.55","realizedPnl":"0.00","unrealizedPnl":"0.06","totalPnl":"0.06","pnlPercentage":"6.00","isResolved":false}],"totalCount":1,"totalRealizedPnl":"0.00","totalUnrealizedPnl":"0.06","totalPnl":"0.06"}"#).unwrap_or_else(|_| serde_json::json!({}));
818 let expected_response : models::QueryPnLResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPnLResponse");
819
820 let resp = client.query_pn_l(params).await.expect("Expected a response");
821 let data_future = resp.data();
822 let actual_response = data_future.await.unwrap();
823 assert_eq!(actual_response, expected_response);
824 });
825 }
826
827 #[test]
828 fn query_pn_l_response_error() {
829 TOKIO_SHARED_RT.block_on(async {
830 let client = MockPositionApiClient { force_error: true };
831
832 let params =
833 QueryPnLParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string())
834 .build()
835 .unwrap();
836
837 match client.query_pn_l(params).await {
838 Ok(_) => panic!("Expected an error"),
839 Err(err) => {
840 assert_eq!(err.to_string(), "Connector client error: ResponseError");
841 }
842 }
843 });
844 }
845
846 #[test]
847 fn query_positions_required_params_success() {
848 TOKIO_SHARED_RT.block_on(async {
849 let client = MockPositionApiClient { force_error: false };
850
851 let params = QueryPositionsParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).build().unwrap();
852
853 let resp_json: Value = serde_json::from_str(r#"{"summary":{"totalValue":"1523.45","positionValue":"523.45","walletBalance":"1000.00","totalClaimableAmount":"50.00","todayRealizedPnl":"15.30","todayRealizedPnlPercent":"3.10","todayTotalCost":"493.55"},"counts":{"ongoingCount":3,"endedCount":12,"pendingClaimCount":1},"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","toWin":"1923.07","positionStatus":"OPEN","canClaim":false,"endDate":1748134800000,"unrealizedPnl":"0.06","unrealizedPnlPercent":"6.00","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
854 let expected_response : models::QueryPositionsResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsResponse");
855
856 let resp = client.query_positions(params).await.expect("Expected a response");
857 let data_future = resp.data();
858 let actual_response = data_future.await.unwrap();
859 assert_eq!(actual_response, expected_response);
860 });
861 }
862
863 #[test]
864 fn query_positions_optional_params_success() {
865 TOKIO_SHARED_RT.block_on(async {
866 let client = MockPositionApiClient { force_error: false };
867
868 let params = QueryPositionsParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).tab("ONGOING".to_string()).offset(0).limit(20).recv_window(5000).build().unwrap();
869
870 let resp_json: Value = serde_json::from_str(r#"{"summary":{"totalValue":"1523.45","positionValue":"523.45","walletBalance":"1000.00","totalClaimableAmount":"50.00","todayRealizedPnl":"15.30","todayRealizedPnlPercent":"3.10","todayTotalCost":"493.55"},"counts":{"ongoingCount":3,"endedCount":12,"pendingClaimCount":1},"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","toWin":"1923.07","positionStatus":"OPEN","canClaim":false,"endDate":1748134800000,"unrealizedPnl":"0.06","unrealizedPnlPercent":"6.00","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
871 let expected_response : models::QueryPositionsResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsResponse");
872
873 let resp = client.query_positions(params).await.expect("Expected a response");
874 let data_future = resp.data();
875 let actual_response = data_future.await.unwrap();
876 assert_eq!(actual_response, expected_response);
877 });
878 }
879
880 #[test]
881 fn query_positions_response_error() {
882 TOKIO_SHARED_RT.block_on(async {
883 let client = MockPositionApiClient { force_error: true };
884
885 let params = QueryPositionsParams::builder(
886 "0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),
887 )
888 .build()
889 .unwrap();
890
891 match client.query_positions(params).await {
892 Ok(_) => panic!("Expected an error"),
893 Err(err) => {
894 assert_eq!(err.to_string(), "Connector client error: ResponseError");
895 }
896 }
897 });
898 }
899
900 #[test]
901 fn query_positions_by_filter_required_params_success() {
902 TOKIO_SHARED_RT.block_on(async {
903 let client = MockPositionApiClient { force_error: false };
904
905 let params = QueryPositionsByFilterParams::builder().build().unwrap();
906
907 let resp_json: Value = serde_json::from_str(r#"{"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
908 let expected_response : models::QueryPositionsByFilterResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsByFilterResponse");
909
910 let resp = client.query_positions_by_filter(params).await.expect("Expected a response");
911 let data_future = resp.data();
912 let actual_response = data_future.await.unwrap();
913 assert_eq!(actual_response, expected_response);
914 });
915 }
916
917 #[test]
918 fn query_positions_by_filter_optional_params_success() {
919 TOKIO_SHARED_RT.block_on(async {
920 let client = MockPositionApiClient { force_error: false };
921
922 let params = QueryPositionsByFilterParams::builder().wallet_address("0x12e32db8817e292508c34111cbc4b23340df542c".to_string()).market_topic_id(4229564).recv_window(5000).build().unwrap();
923
924 let resp_json: Value = serde_json::from_str(r#"{"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
925 let expected_response : models::QueryPositionsByFilterResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsByFilterResponse");
926
927 let resp = client.query_positions_by_filter(params).await.expect("Expected a response");
928 let data_future = resp.data();
929 let actual_response = data_future.await.unwrap();
930 assert_eq!(actual_response, expected_response);
931 });
932 }
933
934 #[test]
935 fn query_positions_by_filter_response_error() {
936 TOKIO_SHARED_RT.block_on(async {
937 let client = MockPositionApiClient { force_error: true };
938
939 let params = QueryPositionsByFilterParams::builder().build().unwrap();
940
941 match client.query_positions_by_filter(params).await {
942 Ok(_) => panic!("Expected an error"),
943 Err(err) => {
944 assert_eq!(err.to_string(), "Connector client error: ResponseError");
945 }
946 }
947 });
948 }
949
950 #[test]
951 fn query_settled_position_history_required_params_success() {
952 TOKIO_SHARED_RT.block_on(async {
953 let client = MockPositionApiClient { force_error: false };
954
955 let params = QuerySettledPositionHistoryParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).build().unwrap();
956
957 let resp_json: Value = serde_json::from_str(r#"{"totalCount":5,"positions":[{"positionId":998,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112200","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229500,"marketId":5567800,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"5000.00","avgPrice":"0.30","totalCost":"1.50","value":"5000.00","currentPrice":"1.00","toWin":"5000.00","positionStatus":"CLAIMED","isWinner":true,"redeemStatus":"CONFIRMED","endDate":1748040000000,"finalOutcome":"YES","realizedPnl":"4998.50","pnl":"4998.50","claimAmount":"5000.00","settledDate":1748040000000,"createdTime":1748020000000,"updatedTime":1748040500000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
958 let expected_response : models::QuerySettledPositionHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QuerySettledPositionHistoryResponse");
959
960 let resp = client.query_settled_position_history(params).await.expect("Expected a response");
961 let data_future = resp.data();
962 let actual_response = data_future.await.unwrap();
963 assert_eq!(actual_response, expected_response);
964 });
965 }
966
967 #[test]
968 fn query_settled_position_history_optional_params_success() {
969 TOKIO_SHARED_RT.block_on(async {
970 let client = MockPositionApiClient { force_error: false };
971
972 let params = QuerySettledPositionHistoryParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).l1_category("crypto".to_string()).result(1).start_date("2026-05-01".to_string()).end_date("2026-05-25".to_string()).offset(0).limit(20).recv_window(5000).build().unwrap();
973
974 let resp_json: Value = serde_json::from_str(r#"{"totalCount":5,"positions":[{"positionId":998,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112200","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229500,"marketId":5567800,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"5000.00","avgPrice":"0.30","totalCost":"1.50","value":"5000.00","currentPrice":"1.00","toWin":"5000.00","positionStatus":"CLAIMED","isWinner":true,"redeemStatus":"CONFIRMED","endDate":1748040000000,"finalOutcome":"YES","realizedPnl":"4998.50","pnl":"4998.50","claimAmount":"5000.00","settledDate":1748040000000,"createdTime":1748020000000,"updatedTime":1748040500000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
975 let expected_response : models::QuerySettledPositionHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QuerySettledPositionHistoryResponse");
976
977 let resp = client.query_settled_position_history(params).await.expect("Expected a response");
978 let data_future = resp.data();
979 let actual_response = data_future.await.unwrap();
980 assert_eq!(actual_response, expected_response);
981 });
982 }
983
984 #[test]
985 fn query_settled_position_history_response_error() {
986 TOKIO_SHARED_RT.block_on(async {
987 let client = MockPositionApiClient { force_error: true };
988
989 let params = QuerySettledPositionHistoryParams::builder(
990 "0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),
991 )
992 .build()
993 .unwrap();
994
995 match client.query_settled_position_history(params).await {
996 Ok(_) => panic!("Expected an error"),
997 Err(err) => {
998 assert_eq!(err.to_string(), "Connector client error: ResponseError");
999 }
1000 }
1001 });
1002 }
1003}