use polyoxide_core::{HttpClient, QueryBuilder};
use serde::{Deserialize, Serialize};
use crate::{
account::{Credentials, Signer, Wallet},
error::ClobError,
request::{AuthMode, Request},
types::SignatureType,
};
#[derive(Clone)]
pub struct Rewards {
pub(crate) http_client: HttpClient,
pub(crate) wallet: Wallet,
pub(crate) credentials: Credentials,
pub(crate) signer: Signer,
pub(crate) chain_id: u64,
pub(crate) signature_type: SignatureType,
}
impl Rewards {
fn l2_auth(&self) -> AuthMode {
AuthMode::L2 {
address: self.wallet.address(),
credentials: self.credentials.clone(),
signer: self.signer.clone(),
}
}
pub fn earnings(&self, date: impl Into<String>) -> UserEarningsRequest {
UserEarningsRequest {
request: Request::get(
self.http_client.clone(),
"/rewards/user",
self.l2_auth(),
self.chain_id,
)
.query("date", date.into())
.query("signature_type", self.signature_type as u8),
}
}
pub fn total_earnings(&self, date: impl Into<String>) -> UserTotalEarningsRequest {
UserTotalEarningsRequest {
request: Request::get(
self.http_client.clone(),
"/rewards/user/total",
self.l2_auth(),
self.chain_id,
)
.query("date", date.into())
.query("signature_type", self.signature_type as u8),
}
}
pub fn percentages(&self) -> UserPercentagesRequest {
UserPercentagesRequest {
request: Request::get(
self.http_client.clone(),
"/rewards/user/percentages",
self.l2_auth(),
self.chain_id,
)
.query("signature_type", self.signature_type as u8),
}
}
pub fn market_earnings(&self) -> ListUserRewardMarkets {
ListUserRewardMarkets {
request: Request::get(
self.http_client.clone(),
"/rewards/user/markets",
self.l2_auth(),
self.chain_id,
)
.query("signature_type", self.signature_type as u8),
}
}
fn public(&self) -> PublicRewards {
PublicRewards {
http_client: self.http_client.clone(),
chain_id: self.chain_id,
}
}
pub fn current_markets(&self) -> ListRewardMarkets {
self.public().current_markets()
}
pub fn market(&self, condition_id: impl Into<String>) -> RewardMarketRequest {
self.public().market(condition_id)
}
pub fn multi_markets(&self) -> ListMultiRewardMarkets {
self.public().multi_markets()
}
pub fn current_rebates(
&self,
date: impl Into<String>,
maker_address: impl Into<String>,
) -> Request<Vec<RebatedFees>> {
self.public().current_rebates(date, maker_address)
}
}
#[derive(Clone)]
pub struct PublicRewards {
pub(crate) http_client: HttpClient,
pub(crate) chain_id: u64,
}
impl PublicRewards {
pub fn current_markets(&self) -> ListRewardMarkets {
ListRewardMarkets {
request: Request::get(
self.http_client.clone(),
"/rewards/markets/current",
AuthMode::None,
self.chain_id,
),
}
}
pub fn market(&self, condition_id: impl Into<String>) -> RewardMarketRequest {
RewardMarketRequest {
request: Request::get(
self.http_client.clone(),
format!(
"/rewards/markets/{}",
urlencoding::encode(&condition_id.into())
),
AuthMode::None,
self.chain_id,
),
}
}
pub fn multi_markets(&self) -> ListMultiRewardMarkets {
ListMultiRewardMarkets {
request: Request::get(
self.http_client.clone(),
"/rewards/markets/multi",
AuthMode::None,
self.chain_id,
),
}
}
pub fn current_rebates(
&self,
date: impl Into<String>,
maker_address: impl Into<String>,
) -> Request<Vec<RebatedFees>> {
Request::get(
self.http_client.clone(),
"/rebates/current",
AuthMode::None,
self.chain_id,
)
.query("date", date.into())
.query("maker_address", maker_address.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum SortPosition {
Asc,
Desc,
}
impl SortPosition {
pub fn as_str(&self) -> &'static str {
match self {
Self::Asc => "ASC",
Self::Desc => "DESC",
}
}
}
impl std::fmt::Display for SortPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MultiMarketOrderBy {
MarketId,
CreatedAt,
Volume24hr,
Spread,
Competitiveness,
MaxSpread,
MinSize,
Question,
OneDayPriceChange,
RatePerDay,
Price,
EndDate,
StartDate,
RewardEndDate,
}
impl MultiMarketOrderBy {
pub fn as_str(&self) -> &'static str {
match self {
Self::MarketId => "market_id",
Self::CreatedAt => "created_at",
Self::Volume24hr => "volume_24hr",
Self::Spread => "spread",
Self::Competitiveness => "competitiveness",
Self::MaxSpread => "max_spread",
Self::MinSize => "min_size",
Self::Question => "question",
Self::OneDayPriceChange => "one_day_price_change",
Self::RatePerDay => "rate_per_day",
Self::Price => "price",
Self::EndDate => "end_date",
Self::StartDate => "start_date",
Self::RewardEndDate => "reward_end_date",
}
}
}
impl std::fmt::Display for MultiMarketOrderBy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UserRewardMarketOrderBy {
MaxSpread,
MinSize,
EndDate,
EarningPercentage,
RatePerDay,
Earnings,
Spread,
Competitiveness,
Question,
Price,
Market,
Volume24hr,
}
impl UserRewardMarketOrderBy {
pub fn as_str(&self) -> &'static str {
match self {
Self::MaxSpread => "max_spread",
Self::MinSize => "min_size",
Self::EndDate => "end_date",
Self::EarningPercentage => "earning_percentage",
Self::RatePerDay => "rate_per_day",
Self::Earnings => "earnings",
Self::Spread => "spread",
Self::Competitiveness => "competitiveness",
Self::Question => "question",
Self::Price => "price",
Self::Market => "market",
Self::Volume24hr => "volume_24hr",
}
}
}
impl std::fmt::Display for UserRewardMarketOrderBy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub struct UserEarningsRequest {
request: Request<RewardEarnings>,
}
impl UserEarningsRequest {
pub fn maker_address(mut self, address: impl Into<String>) -> Self {
self.request = self.request.query("maker_address", address.into());
self
}
pub fn sponsored(mut self, sponsored: bool) -> Self {
self.request = self.request.query("sponsored", sponsored);
self
}
pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request = self.request.query("next_cursor", cursor.into());
self
}
pub async fn send(self) -> Result<RewardEarnings, ClobError> {
self.request.send().await
}
}
pub struct UserTotalEarningsRequest {
request: Request<Vec<RewardTotalEarnings>>,
}
impl UserTotalEarningsRequest {
pub fn maker_address(mut self, address: impl Into<String>) -> Self {
self.request = self.request.query("maker_address", address.into());
self
}
pub fn sponsored(mut self, sponsored: bool) -> Self {
self.request = self.request.query("sponsored", sponsored);
self
}
pub async fn send(self) -> Result<Vec<RewardTotalEarnings>, ClobError> {
self.request.send().await
}
}
pub struct UserPercentagesRequest {
request: Request<RewardPercentages>,
}
impl UserPercentagesRequest {
pub fn maker_address(mut self, address: impl Into<String>) -> Self {
self.request = self.request.query("maker_address", address.into());
self
}
pub async fn send(self) -> Result<RewardPercentages, ClobError> {
self.request.send().await
}
}
pub struct ListRewardMarkets {
request: Request<Paginated<RewardMarket>>,
}
impl ListRewardMarkets {
pub fn sponsored(mut self, sponsored: bool) -> Self {
self.request = self.request.query("sponsored", sponsored);
self
}
pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request = self.request.query("next_cursor", cursor.into());
self
}
pub async fn send(self) -> Result<Paginated<RewardMarket>, ClobError> {
self.request.send().await
}
}
pub struct RewardMarketRequest {
request: Request<RewardMarket>,
}
impl RewardMarketRequest {
pub fn sponsored(mut self, sponsored: bool) -> Self {
self.request = self.request.query("sponsored", sponsored);
self
}
pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request = self.request.query("next_cursor", cursor.into());
self
}
pub async fn send(self) -> Result<RewardMarket, ClobError> {
self.request.send().await
}
}
pub struct ListMultiRewardMarkets {
request: Request<Paginated<RewardMarket>>,
}
impl ListMultiRewardMarkets {
pub fn query_text(mut self, q: impl Into<String>) -> Self {
self.request = self.request.query("q", q.into());
self
}
pub fn tag_slug(mut self, slug: impl Into<String>) -> Self {
self.request = self.request.query("tag_slug", slug.into());
self
}
pub fn event_id(mut self, event_id: impl Into<String>) -> Self {
self.request = self.request.query("event_id", event_id.into());
self
}
pub fn event_title(mut self, title: impl Into<String>) -> Self {
self.request = self.request.query("event_title", title.into());
self
}
pub fn order_by(mut self, order_by: MultiMarketOrderBy) -> Self {
self.request = self.request.query("order_by", order_by.as_str());
self
}
pub fn position(mut self, position: SortPosition) -> Self {
self.request = self.request.query("position", position.as_str());
self
}
pub fn min_volume_24hr(mut self, value: f64) -> Self {
self.request = self.request.query("min_volume_24hr", value);
self
}
pub fn max_volume_24hr(mut self, value: f64) -> Self {
self.request = self.request.query("max_volume_24hr", value);
self
}
pub fn min_spread(mut self, value: f64) -> Self {
self.request = self.request.query("min_spread", value);
self
}
pub fn max_spread(mut self, value: f64) -> Self {
self.request = self.request.query("max_spread", value);
self
}
pub fn min_price(mut self, value: f64) -> Self {
self.request = self.request.query("min_price", value);
self
}
pub fn max_price(mut self, value: f64) -> Self {
self.request = self.request.query("max_price", value);
self
}
pub fn page_size(mut self, page_size: u32) -> Self {
self.request = self.request.query("page_size", page_size);
self
}
pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request = self.request.query("next_cursor", cursor.into());
self
}
pub async fn send(self) -> Result<Paginated<RewardMarket>, ClobError> {
self.request.send().await
}
}
pub struct ListUserRewardMarkets {
request: Request<Paginated<RewardMarketEarning>>,
}
impl ListUserRewardMarkets {
pub fn date(mut self, date: impl Into<String>) -> Self {
self.request = self.request.query("date", date.into());
self
}
pub fn maker_address(mut self, address: impl Into<String>) -> Self {
self.request = self.request.query("maker_address", address.into());
self
}
pub fn sponsored(mut self, sponsored: bool) -> Self {
self.request = self.request.query("sponsored", sponsored);
self
}
pub fn query_text(mut self, q: impl Into<String>) -> Self {
self.request = self.request.query("q", q.into());
self
}
pub fn tag_slug(mut self, slug: impl Into<String>) -> Self {
self.request = self.request.query("tag_slug", slug.into());
self
}
pub fn favorite_markets(mut self, value: bool) -> Self {
self.request = self.request.query("favorite_markets", value);
self
}
pub fn no_competition(mut self, value: bool) -> Self {
self.request = self.request.query("no_competition", value);
self
}
pub fn only_mergeable(mut self, value: bool) -> Self {
self.request = self.request.query("only_mergeable", value);
self
}
pub fn only_open_orders(mut self, value: bool) -> Self {
self.request = self.request.query("only_open_orders", value);
self
}
pub fn only_open_positions(mut self, value: bool) -> Self {
self.request = self.request.query("only_open_positions", value);
self
}
pub fn order_by(mut self, order_by: UserRewardMarketOrderBy) -> Self {
self.request = self.request.query("order_by", order_by.as_str());
self
}
pub fn position(mut self, position: SortPosition) -> Self {
self.request = self.request.query("position", position.as_str());
self
}
pub fn page_size(mut self, page_size: u32) -> Self {
self.request = self.request.query("page_size", page_size);
self
}
pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
self.request = self.request.query("next_cursor", cursor.into());
self
}
pub async fn send(self) -> Result<Paginated<RewardMarketEarning>, ClobError> {
self.request.send().await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RebatedFees {
pub date: String,
pub condition_id: String,
pub asset_address: String,
pub maker_address: String,
pub rebated_fees_usdc: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewardEarnings {
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewardTotalEarnings {
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewardPercentages {
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewardMarketEarning {
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewardMarket {
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Paginated<T> {
#[serde(default = "Vec::new")]
pub data: Vec<T>,
#[serde(default)]
pub next_cursor: Option<String>,
#[serde(default)]
pub limit: Option<u32>,
#[serde(default)]
pub count: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reward_earnings_deserializes() {
let json = r#"{"amount": "1.5", "day": "2024-01-15"}"#;
let resp: RewardEarnings = serde_json::from_str(json).unwrap();
assert_eq!(resp.data["amount"], "1.5");
assert_eq!(resp.data["day"], "2024-01-15");
}
#[test]
fn reward_total_earnings_deserializes() {
let json = r#"{"total": "42.0"}"#;
let resp: RewardTotalEarnings = serde_json::from_str(json).unwrap();
assert_eq!(resp.data["total"], "42.0");
}
#[test]
fn reward_total_earnings_list_deserializes() {
let json = r#"[{"asset_address": "0xabc", "total": "42.0"}]"#;
let resp: Vec<RewardTotalEarnings> = serde_json::from_str(json).unwrap();
assert_eq!(resp.len(), 1);
assert_eq!(resp[0].data["total"], "42.0");
}
#[test]
fn reward_percentages_deserializes() {
let json = r#"{"maker": "0.5", "taker": "0.3"}"#;
let resp: RewardPercentages = serde_json::from_str(json).unwrap();
assert_eq!(resp.data["maker"], "0.5");
}
#[test]
fn reward_market_earning_list_deserializes() {
let json = r#"[
{"condition_id": "0xabc", "amount": "10.0"},
{"condition_id": "0xdef", "amount": "5.0"}
]"#;
let resp: Vec<RewardMarketEarning> = serde_json::from_str(json).unwrap();
assert_eq!(resp.len(), 2);
assert_eq!(resp[0].data["condition_id"], "0xabc");
}
#[test]
fn reward_market_deserializes() {
let json = r#"{"condition_id": "0xabc", "reward_rate": "0.01"}"#;
let resp: RewardMarket = serde_json::from_str(json).unwrap();
assert_eq!(resp.data["condition_id"], "0xabc");
}
#[test]
fn reward_market_list_deserializes() {
let json = r#"[{"condition_id": "0xabc"}, {"condition_id": "0xdef"}]"#;
let resp: Vec<RewardMarket> = serde_json::from_str(json).unwrap();
assert_eq!(resp.len(), 2);
}
#[test]
fn current_markets_paginated_response_deserializes() {
let json = r#"{
"limit": 500,
"count": 1,
"next_cursor": "LTE=",
"data": [
{"condition_id": "0xabc", "rewards_max_spread": 99}
]
}"#;
let page: Paginated<RewardMarket> =
serde_json::from_str(json).expect("paginated reward markets should deserialize");
assert_eq!(page.data.len(), 1);
assert_eq!(page.count, Some(1));
assert_eq!(page.next_cursor.as_deref(), Some("LTE="));
assert_eq!(page.data[0].data["condition_id"], "0xabc");
}
#[test]
fn market_earnings_paginated_response_deserializes() {
let json = r#"{
"limit": 100,
"count": 1,
"next_cursor": "LTE=",
"data": [
{"condition_id": "0xabc", "earnings": 0.237519}
]
}"#;
let page: Paginated<RewardMarketEarning> =
serde_json::from_str(json).expect("paginated market earnings should deserialize");
assert_eq!(page.data.len(), 1);
assert_eq!(page.count, Some(1));
assert_eq!(page.data[0].data["condition_id"], "0xabc");
}
#[test]
fn reward_earnings_empty_object_deserializes() {
let json = r#"{}"#;
let resp: RewardEarnings = serde_json::from_str(json).unwrap();
assert!(resp.data.is_object());
}
}