1use polyoxide_core::{HttpClient, QueryBuilder};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 account::{Credentials, Signer, Wallet},
6 error::ClobError,
7 request::{AuthMode, Request},
8 types::SignatureType,
9};
10
11#[derive(Clone)]
13pub struct Rewards {
14 pub(crate) http_client: HttpClient,
15 pub(crate) wallet: Wallet,
16 pub(crate) credentials: Credentials,
17 pub(crate) signer: Signer,
18 pub(crate) chain_id: u64,
19 pub(crate) signature_type: SignatureType,
20}
21
22impl Rewards {
23 fn l2_auth(&self) -> AuthMode {
24 AuthMode::L2 {
25 address: self.wallet.address(),
26 credentials: self.credentials.clone(),
27 signer: self.signer.clone(),
28 }
29 }
30
31 pub fn earnings(&self, date: impl Into<String>) -> UserEarningsRequest {
36 UserEarningsRequest {
37 request: Request::get(
38 self.http_client.clone(),
39 "/rewards/user",
40 self.l2_auth(),
41 self.chain_id,
42 )
43 .query("date", date.into())
44 .query("signature_type", self.signature_type as u8),
45 }
46 }
47
48 pub fn total_earnings(&self, date: impl Into<String>) -> UserTotalEarningsRequest {
53 UserTotalEarningsRequest {
54 request: Request::get(
55 self.http_client.clone(),
56 "/rewards/user/total",
57 self.l2_auth(),
58 self.chain_id,
59 )
60 .query("date", date.into())
61 .query("signature_type", self.signature_type as u8),
62 }
63 }
64
65 pub fn percentages(&self) -> UserPercentagesRequest {
67 UserPercentagesRequest {
68 request: Request::get(
69 self.http_client.clone(),
70 "/rewards/user/percentages",
71 self.l2_auth(),
72 self.chain_id,
73 )
74 .query("signature_type", self.signature_type as u8),
75 }
76 }
77
78 pub fn market_earnings(&self) -> ListUserRewardMarkets {
84 ListUserRewardMarkets {
85 request: Request::get(
86 self.http_client.clone(),
87 "/rewards/user/markets",
88 self.l2_auth(),
89 self.chain_id,
90 )
91 .query("signature_type", self.signature_type as u8),
92 }
93 }
94
95 fn public(&self) -> PublicRewards {
97 PublicRewards {
98 http_client: self.http_client.clone(),
99 chain_id: self.chain_id,
100 }
101 }
102
103 pub fn current_markets(&self) -> ListRewardMarkets {
109 self.public().current_markets()
110 }
111
112 pub fn market(&self, condition_id: impl Into<String>) -> RewardMarketRequest {
117 self.public().market(condition_id)
118 }
119
120 pub fn multi_markets(&self) -> ListMultiRewardMarkets {
126 self.public().multi_markets()
127 }
128
129 pub fn current_rebates(
135 &self,
136 date: impl Into<String>,
137 maker_address: impl Into<String>,
138 ) -> Request<Vec<RebatedFees>> {
139 self.public().current_rebates(date, maker_address)
140 }
141}
142
143#[derive(Clone)]
151pub struct PublicRewards {
152 pub(crate) http_client: HttpClient,
153 pub(crate) chain_id: u64,
154}
155
156impl PublicRewards {
157 pub fn current_markets(&self) -> ListRewardMarkets {
162 ListRewardMarkets {
163 request: Request::get(
164 self.http_client.clone(),
165 "/rewards/markets/current",
166 AuthMode::None,
167 self.chain_id,
168 ),
169 }
170 }
171
172 pub fn market(&self, condition_id: impl Into<String>) -> RewardMarketRequest {
174 RewardMarketRequest {
175 request: Request::get(
176 self.http_client.clone(),
177 format!(
178 "/rewards/markets/{}",
179 urlencoding::encode(&condition_id.into())
180 ),
181 AuthMode::None,
182 self.chain_id,
183 ),
184 }
185 }
186
187 pub fn multi_markets(&self) -> ListMultiRewardMarkets {
194 ListMultiRewardMarkets {
195 request: Request::get(
196 self.http_client.clone(),
197 "/rewards/markets/multi",
198 AuthMode::None,
199 self.chain_id,
200 ),
201 }
202 }
203
204 pub fn current_rebates(
210 &self,
211 date: impl Into<String>,
212 maker_address: impl Into<String>,
213 ) -> Request<Vec<RebatedFees>> {
214 Request::get(
215 self.http_client.clone(),
216 "/rebates/current",
217 AuthMode::None,
218 self.chain_id,
219 )
220 .query("date", date.into())
221 .query("maker_address", maker_address.into())
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "UPPERCASE")]
228pub enum SortPosition {
229 Asc,
231 Desc,
233}
234
235impl SortPosition {
236 pub fn as_str(&self) -> &'static str {
238 match self {
239 Self::Asc => "ASC",
240 Self::Desc => "DESC",
241 }
242 }
243}
244
245impl std::fmt::Display for SortPosition {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 f.write_str(self.as_str())
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum MultiMarketOrderBy {
255 MarketId,
257 CreatedAt,
259 Volume24hr,
261 Spread,
263 Competitiveness,
265 MaxSpread,
267 MinSize,
269 Question,
271 OneDayPriceChange,
273 RatePerDay,
275 Price,
277 EndDate,
279 StartDate,
281 RewardEndDate,
283}
284
285impl MultiMarketOrderBy {
286 pub fn as_str(&self) -> &'static str {
288 match self {
289 Self::MarketId => "market_id",
290 Self::CreatedAt => "created_at",
291 Self::Volume24hr => "volume_24hr",
292 Self::Spread => "spread",
293 Self::Competitiveness => "competitiveness",
294 Self::MaxSpread => "max_spread",
295 Self::MinSize => "min_size",
296 Self::Question => "question",
297 Self::OneDayPriceChange => "one_day_price_change",
298 Self::RatePerDay => "rate_per_day",
299 Self::Price => "price",
300 Self::EndDate => "end_date",
301 Self::StartDate => "start_date",
302 Self::RewardEndDate => "reward_end_date",
303 }
304 }
305}
306
307impl std::fmt::Display for MultiMarketOrderBy {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 f.write_str(self.as_str())
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "snake_case")]
316pub enum UserRewardMarketOrderBy {
317 MaxSpread,
319 MinSize,
321 EndDate,
323 EarningPercentage,
325 RatePerDay,
327 Earnings,
329 Spread,
331 Competitiveness,
333 Question,
335 Price,
337 Market,
339 Volume24hr,
341}
342
343impl UserRewardMarketOrderBy {
344 pub fn as_str(&self) -> &'static str {
346 match self {
347 Self::MaxSpread => "max_spread",
348 Self::MinSize => "min_size",
349 Self::EndDate => "end_date",
350 Self::EarningPercentage => "earning_percentage",
351 Self::RatePerDay => "rate_per_day",
352 Self::Earnings => "earnings",
353 Self::Spread => "spread",
354 Self::Competitiveness => "competitiveness",
355 Self::Question => "question",
356 Self::Price => "price",
357 Self::Market => "market",
358 Self::Volume24hr => "volume_24hr",
359 }
360 }
361}
362
363impl std::fmt::Display for UserRewardMarketOrderBy {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 f.write_str(self.as_str())
366 }
367}
368
369pub struct UserEarningsRequest {
371 request: Request<RewardEarnings>,
372}
373
374impl UserEarningsRequest {
375 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
377 self.request = self.request.query("maker_address", address.into());
378 self
379 }
380
381 pub fn sponsored(mut self, sponsored: bool) -> Self {
383 self.request = self.request.query("sponsored", sponsored);
384 self
385 }
386
387 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
389 self.request = self.request.query("next_cursor", cursor.into());
390 self
391 }
392
393 pub async fn send(self) -> Result<RewardEarnings, ClobError> {
395 self.request.send().await
396 }
397}
398
399pub struct UserTotalEarningsRequest {
401 request: Request<Vec<RewardTotalEarnings>>,
402}
403
404impl UserTotalEarningsRequest {
405 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
407 self.request = self.request.query("maker_address", address.into());
408 self
409 }
410
411 pub fn sponsored(mut self, sponsored: bool) -> Self {
413 self.request = self.request.query("sponsored", sponsored);
414 self
415 }
416
417 pub async fn send(self) -> Result<Vec<RewardTotalEarnings>, ClobError> {
419 self.request.send().await
420 }
421}
422
423pub struct UserPercentagesRequest {
425 request: Request<RewardPercentages>,
426}
427
428impl UserPercentagesRequest {
429 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
431 self.request = self.request.query("maker_address", address.into());
432 self
433 }
434
435 pub async fn send(self) -> Result<RewardPercentages, ClobError> {
437 self.request.send().await
438 }
439}
440
441pub struct ListRewardMarkets {
443 request: Request<Paginated<RewardMarket>>,
444}
445
446impl ListRewardMarkets {
447 pub fn sponsored(mut self, sponsored: bool) -> Self {
449 self.request = self.request.query("sponsored", sponsored);
450 self
451 }
452
453 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
455 self.request = self.request.query("next_cursor", cursor.into());
456 self
457 }
458
459 pub async fn send(self) -> Result<Paginated<RewardMarket>, ClobError> {
461 self.request.send().await
462 }
463}
464
465pub struct RewardMarketRequest {
467 request: Request<RewardMarket>,
468}
469
470impl RewardMarketRequest {
471 pub fn sponsored(mut self, sponsored: bool) -> Self {
473 self.request = self.request.query("sponsored", sponsored);
474 self
475 }
476
477 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
479 self.request = self.request.query("next_cursor", cursor.into());
480 self
481 }
482
483 pub async fn send(self) -> Result<RewardMarket, ClobError> {
485 self.request.send().await
486 }
487}
488
489pub struct ListMultiRewardMarkets {
491 request: Request<Paginated<RewardMarket>>,
492}
493
494impl ListMultiRewardMarkets {
495 pub fn query_text(mut self, q: impl Into<String>) -> Self {
497 self.request = self.request.query("q", q.into());
498 self
499 }
500
501 pub fn tag_slug(mut self, slug: impl Into<String>) -> Self {
503 self.request = self.request.query("tag_slug", slug.into());
504 self
505 }
506
507 pub fn event_id(mut self, event_id: impl Into<String>) -> Self {
509 self.request = self.request.query("event_id", event_id.into());
510 self
511 }
512
513 pub fn event_title(mut self, title: impl Into<String>) -> Self {
515 self.request = self.request.query("event_title", title.into());
516 self
517 }
518
519 pub fn order_by(mut self, order_by: MultiMarketOrderBy) -> Self {
521 self.request = self.request.query("order_by", order_by.as_str());
522 self
523 }
524
525 pub fn position(mut self, position: SortPosition) -> Self {
527 self.request = self.request.query("position", position.as_str());
528 self
529 }
530
531 pub fn min_volume_24hr(mut self, value: f64) -> Self {
533 self.request = self.request.query("min_volume_24hr", value);
534 self
535 }
536
537 pub fn max_volume_24hr(mut self, value: f64) -> Self {
539 self.request = self.request.query("max_volume_24hr", value);
540 self
541 }
542
543 pub fn min_spread(mut self, value: f64) -> Self {
545 self.request = self.request.query("min_spread", value);
546 self
547 }
548
549 pub fn max_spread(mut self, value: f64) -> Self {
551 self.request = self.request.query("max_spread", value);
552 self
553 }
554
555 pub fn min_price(mut self, value: f64) -> Self {
557 self.request = self.request.query("min_price", value);
558 self
559 }
560
561 pub fn max_price(mut self, value: f64) -> Self {
563 self.request = self.request.query("max_price", value);
564 self
565 }
566
567 pub fn page_size(mut self, page_size: u32) -> Self {
569 self.request = self.request.query("page_size", page_size);
570 self
571 }
572
573 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
575 self.request = self.request.query("next_cursor", cursor.into());
576 self
577 }
578
579 pub async fn send(self) -> Result<Paginated<RewardMarket>, ClobError> {
581 self.request.send().await
582 }
583}
584
585pub struct ListUserRewardMarkets {
587 request: Request<Paginated<RewardMarketEarning>>,
588}
589
590impl ListUserRewardMarkets {
591 pub fn date(mut self, date: impl Into<String>) -> Self {
593 self.request = self.request.query("date", date.into());
594 self
595 }
596
597 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
599 self.request = self.request.query("maker_address", address.into());
600 self
601 }
602
603 pub fn sponsored(mut self, sponsored: bool) -> Self {
605 self.request = self.request.query("sponsored", sponsored);
606 self
607 }
608
609 pub fn query_text(mut self, q: impl Into<String>) -> Self {
611 self.request = self.request.query("q", q.into());
612 self
613 }
614
615 pub fn tag_slug(mut self, slug: impl Into<String>) -> Self {
617 self.request = self.request.query("tag_slug", slug.into());
618 self
619 }
620
621 pub fn favorite_markets(mut self, value: bool) -> Self {
623 self.request = self.request.query("favorite_markets", value);
624 self
625 }
626
627 pub fn no_competition(mut self, value: bool) -> Self {
629 self.request = self.request.query("no_competition", value);
630 self
631 }
632
633 pub fn only_mergeable(mut self, value: bool) -> Self {
635 self.request = self.request.query("only_mergeable", value);
636 self
637 }
638
639 pub fn only_open_orders(mut self, value: bool) -> Self {
641 self.request = self.request.query("only_open_orders", value);
642 self
643 }
644
645 pub fn only_open_positions(mut self, value: bool) -> Self {
647 self.request = self.request.query("only_open_positions", value);
648 self
649 }
650
651 pub fn order_by(mut self, order_by: UserRewardMarketOrderBy) -> Self {
653 self.request = self.request.query("order_by", order_by.as_str());
654 self
655 }
656
657 pub fn position(mut self, position: SortPosition) -> Self {
659 self.request = self.request.query("position", position.as_str());
660 self
661 }
662
663 pub fn page_size(mut self, page_size: u32) -> Self {
665 self.request = self.request.query("page_size", page_size);
666 self
667 }
668
669 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
671 self.request = self.request.query("next_cursor", cursor.into());
672 self
673 }
674
675 pub async fn send(self) -> Result<Paginated<RewardMarketEarning>, ClobError> {
677 self.request.send().await
678 }
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct RebatedFees {
685 pub date: String,
687 pub condition_id: String,
689 pub asset_address: String,
691 pub maker_address: String,
693 pub rebated_fees_usdc: String,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct RewardEarnings {
700 #[serde(flatten)]
701 pub data: serde_json::Value,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct RewardTotalEarnings {
707 #[serde(flatten)]
708 pub data: serde_json::Value,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct RewardPercentages {
714 #[serde(flatten)]
715 pub data: serde_json::Value,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct RewardMarketEarning {
721 #[serde(flatten)]
722 pub data: serde_json::Value,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize)]
727pub struct RewardMarket {
728 #[serde(flatten)]
729 pub data: serde_json::Value,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct Paginated<T> {
739 #[serde(default = "Vec::new")]
741 pub data: Vec<T>,
742 #[serde(default)]
744 pub next_cursor: Option<String>,
745 #[serde(default)]
747 pub limit: Option<u32>,
748 #[serde(default)]
750 pub count: Option<u32>,
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn reward_earnings_deserializes() {
759 let json = r#"{"amount": "1.5", "day": "2024-01-15"}"#;
760 let resp: RewardEarnings = serde_json::from_str(json).unwrap();
761 assert_eq!(resp.data["amount"], "1.5");
762 assert_eq!(resp.data["day"], "2024-01-15");
763 }
764
765 #[test]
766 fn reward_total_earnings_deserializes() {
767 let json = r#"{"total": "42.0"}"#;
768 let resp: RewardTotalEarnings = serde_json::from_str(json).unwrap();
769 assert_eq!(resp.data["total"], "42.0");
770 }
771
772 #[test]
773 fn reward_total_earnings_list_deserializes() {
774 let json = r#"[{"asset_address": "0xabc", "total": "42.0"}]"#;
777 let resp: Vec<RewardTotalEarnings> = serde_json::from_str(json).unwrap();
778 assert_eq!(resp.len(), 1);
779 assert_eq!(resp[0].data["total"], "42.0");
780 }
781
782 #[test]
783 fn reward_percentages_deserializes() {
784 let json = r#"{"maker": "0.5", "taker": "0.3"}"#;
785 let resp: RewardPercentages = serde_json::from_str(json).unwrap();
786 assert_eq!(resp.data["maker"], "0.5");
787 }
788
789 #[test]
790 fn reward_market_earning_list_deserializes() {
791 let json = r#"[
792 {"condition_id": "0xabc", "amount": "10.0"},
793 {"condition_id": "0xdef", "amount": "5.0"}
794 ]"#;
795 let resp: Vec<RewardMarketEarning> = serde_json::from_str(json).unwrap();
796 assert_eq!(resp.len(), 2);
797 assert_eq!(resp[0].data["condition_id"], "0xabc");
798 }
799
800 #[test]
801 fn reward_market_deserializes() {
802 let json = r#"{"condition_id": "0xabc", "reward_rate": "0.01"}"#;
803 let resp: RewardMarket = serde_json::from_str(json).unwrap();
804 assert_eq!(resp.data["condition_id"], "0xabc");
805 }
806
807 #[test]
808 fn reward_market_list_deserializes() {
809 let json = r#"[{"condition_id": "0xabc"}, {"condition_id": "0xdef"}]"#;
810 let resp: Vec<RewardMarket> = serde_json::from_str(json).unwrap();
811 assert_eq!(resp.len(), 2);
812 }
813
814 #[test]
815 fn current_markets_paginated_response_deserializes() {
816 let json = r#"{
819 "limit": 500,
820 "count": 1,
821 "next_cursor": "LTE=",
822 "data": [
823 {"condition_id": "0xabc", "rewards_max_spread": 99}
824 ]
825 }"#;
826 let page: Paginated<RewardMarket> =
827 serde_json::from_str(json).expect("paginated reward markets should deserialize");
828 assert_eq!(page.data.len(), 1);
829 assert_eq!(page.count, Some(1));
830 assert_eq!(page.next_cursor.as_deref(), Some("LTE="));
831 assert_eq!(page.data[0].data["condition_id"], "0xabc");
832 }
833
834 #[test]
835 fn market_earnings_paginated_response_deserializes() {
836 let json = r#"{
838 "limit": 100,
839 "count": 1,
840 "next_cursor": "LTE=",
841 "data": [
842 {"condition_id": "0xabc", "earnings": 0.237519}
843 ]
844 }"#;
845 let page: Paginated<RewardMarketEarning> =
846 serde_json::from_str(json).expect("paginated market earnings should deserialize");
847 assert_eq!(page.data.len(), 1);
848 assert_eq!(page.count, Some(1));
849 assert_eq!(page.data[0].data["condition_id"], "0xabc");
850 }
851
852 #[test]
853 fn reward_earnings_empty_object_deserializes() {
854 let json = r#"{}"#;
855 let resp: RewardEarnings = serde_json::from_str(json).unwrap();
856 assert!(resp.data.is_object());
857 }
858}