hypercall_competition/
lib.rs1use axum::http::StatusCode;
13use chrono::{DateTime, Utc};
14pub use hypercall_db::{CompetitionRecord, CompetitionUpdateInput, CompetitionUpsertInput};
15use hypercall_types::api_models::CompetitionData;
16use hypercall_types::WalletAddress;
17use rust_decimal::Decimal;
18use serde::Deserialize;
19
20use hypercall_runtime_api::error::ApiError;
21
22mod recorder;
23mod service;
24pub use recorder::CompetitionFillRecorder;
25pub use service::*;
26
27pub const DEFAULT_LIMIT: usize = 50;
29pub const MAX_LIMIT: usize = 200;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CompetitionState {
34 Pre,
35 Active,
36 Post,
37}
38
39impl CompetitionState {
40 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Pre => "pre",
43 Self::Active => "active",
44 Self::Post => "post",
45 }
46 }
47
48 pub fn parse(input: &str) -> Option<Self> {
49 match input {
50 "pre" => Some(Self::Pre),
51 "active" => Some(Self::Active),
52 "post" => Some(Self::Post),
53 _ => None,
54 }
55 }
56
57 pub fn from_bounds(start_ts_ms: i64, end_ts_ms: i64, now_ts_ms: i64) -> Self {
58 if now_ts_ms < start_ts_ms {
59 Self::Pre
60 } else if now_ts_ms < end_ts_ms {
61 Self::Active
62 } else {
63 Self::Post
64 }
65 }
66}
67
68pub fn competition_state(record: &CompetitionRecord, now_ts_ms: i64) -> CompetitionState {
70 CompetitionState::from_bounds(record.start_ts_ms, record.end_ts_ms, now_ts_ms)
71}
72
73pub fn competition_to_api(row: CompetitionRecord, now_ts_ms: i64) -> CompetitionData {
74 let state = competition_state(&row, now_ts_ms).as_str().to_string();
75 CompetitionData {
76 id: row.id,
77 name: row.name,
78 description: row.description,
79 rules_url: row.rules_url,
80 rules_content: row.rules_content,
81 win_conditions: row.win_conditions,
82 primary_win_condition: row.primary_win_condition,
83 start_ts_ms: row.start_ts_ms,
84 end_ts_ms: row.end_ts_ms,
85 state,
86 created_at: row.created_at,
87 updated_at: row.updated_at,
88 }
89}
90
91pub fn clamp_limit(limit: Option<usize>) -> usize {
92 limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT)
93}
94
95#[derive(Debug, Deserialize)]
97pub struct Fill {
98 pub fill_id: i64,
99 pub trade_id: i64,
100 pub wallet_address: WalletAddress,
101 pub symbol: String,
102 pub price: Decimal,
103 pub size: Decimal,
104 pub fee: Decimal,
105 pub is_taker: bool,
106 pub timestamp: i64,
107 pub builder_code_address: Option<WalletAddress>,
108 pub builder_code_fee: Option<Decimal>,
109 pub realized_pnl: Option<Decimal>,
110 pub created_at: DateTime<Utc>,
111}
112
113impl Fill {
114 pub fn to_api_response(&self) -> hypercall_types::api_models::FillApiResponse {
115 let converted_size = hypercall_types::to_human_readable_decimal(&self.symbol, self.size);
116 hypercall_types::api_models::FillApiResponse {
117 fill_id: self.fill_id,
118 trade_id: self.trade_id,
119 wallet_address: self.wallet_address,
120 symbol: self.symbol.clone(),
121 price: self.price,
122 size: converted_size,
123 fee: self.fee,
124 is_taker: self.is_taker,
125 timestamp: self.timestamp,
126 created_at: self.created_at,
127 builder_code_address: self.builder_code_address,
128 builder_code_fee: self.builder_code_fee,
129 realized_pnl: self.realized_pnl,
130 explorer_url: None,
131 }
132 }
133}
134
135pub type Result<T, E = CompetitionError> = std::result::Result<T, E>;
137
138#[derive(Debug, thiserror::Error)]
145pub enum CompetitionError {
146 #[error("{0}")]
147 BadRequest(String),
148 #[error("{0}")]
149 NotFound(String),
150 #[error("{0}")]
151 Conflict(String),
152 #[error("{0}")]
153 Unavailable(String),
154 #[error(transparent)]
155 Internal(#[from] anyhow::Error),
156}
157
158impl From<CompetitionError> for ApiError {
159 fn from(err: CompetitionError) -> Self {
160 match err {
161 CompetitionError::BadRequest(msg) => ApiError::bad_request(msg),
162 CompetitionError::NotFound(msg) => ApiError::not_found(msg),
163 CompetitionError::Conflict(msg) => ApiError::new(StatusCode::CONFLICT, "conflict", msg),
164 CompetitionError::Unavailable(msg) => {
165 ApiError::new(StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", msg)
166 }
167 CompetitionError::Internal(inner) => {
168 tracing::error!("competition service internal error: {:#}", inner);
169 ApiError::internal_error("internal server error")
170 }
171 }
172 }
173}