Skip to main content

hypercall_db/traits/
competition.rs

1//! Competition persistence traits (async).
2//!
3//! Used by `CompetitionService` for competition CRUD, leaderboard data,
4//! username requests, profile management, and finalization.
5
6use anyhow::Result;
7use hypercall_types::WalletAddress;
8
9use crate::{
10    CompetitionFillInput, CompetitionFillRecord, CompetitionFinalStatsInput,
11    CompetitionFinalStatsRecord, CompetitionRecord, CompetitionUpsertInput, PlatformWalletMetrics,
12    ProfileFillRecord, SymbolPnlRecord, TheoMarkRecord, WalletLedgerStats, WalletUsernameRecord,
13};
14
15/// Errors from competition write operations that callers can pattern-match on.
16#[derive(Debug)]
17pub enum CompetitionWriteError {
18    /// The competition time window overlaps an existing competition.
19    OverlapViolation,
20    /// A unique constraint was violated (e.g. duplicate username request nonce).
21    UniqueViolation(String),
22    /// The target entity was not found.
23    NotFound(String),
24    /// A business rule was violated (e.g. request already reviewed).
25    BadRequest(String),
26    /// A conflict (e.g. username already taken).
27    Conflict(String),
28    /// Any other internal / database error.
29    Internal(anyhow::Error),
30}
31
32impl std::fmt::Display for CompetitionWriteError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::OverlapViolation => {
36                write!(f, "competition window overlaps existing competition")
37            }
38            Self::UniqueViolation(msg) => write!(f, "{msg}"),
39            Self::NotFound(msg) => write!(f, "{msg}"),
40            Self::BadRequest(msg) => write!(f, "{msg}"),
41            Self::Conflict(msg) => write!(f, "{msg}"),
42            Self::Internal(e) => write!(f, "{e}"),
43        }
44    }
45}
46
47impl std::error::Error for CompetitionWriteError {
48    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49        match self {
50            Self::Internal(e) => Some(e.as_ref()),
51            _ => None,
52        }
53    }
54}
55
56impl From<anyhow::Error> for CompetitionWriteError {
57    fn from(e: anyhow::Error) -> Self {
58        Self::Internal(e)
59    }
60}
61
62/// Read-only competition queries.
63#[async_trait::async_trait]
64pub trait CompetitionReader: Send + Sync {
65    /// List competitions with optional state/time filters and pagination.
66    async fn list_competitions(
67        &self,
68        state_filter: Option<&str>,
69        from_ts_ms: Option<i64>,
70        to_ts_ms: Option<i64>,
71        now_ts_ms: i64,
72        limit: i64,
73        offset: i64,
74    ) -> Result<Vec<CompetitionRecord>>;
75
76    /// Get a single competition by ID.
77    async fn get_competition_by_id(&self, competition_id: i64)
78        -> Result<Option<CompetitionRecord>>;
79
80    /// Get the currently active competition (start <= now < end).
81    async fn get_active_competition(&self, now_ts_ms: i64) -> Result<Option<CompetitionRecord>>;
82
83    /// Get the most recently completed competition (end <= now).
84    async fn get_latest_completed_competition(
85        &self,
86        now_ts_ms: i64,
87    ) -> Result<Option<CompetitionRecord>>;
88
89    /// Get competitions that have ended but not yet been finalized.
90    async fn get_competitions_to_finalize(&self, now_ts_ms: i64) -> Result<Vec<CompetitionRecord>>;
91
92    /// Get finalized leaderboard stats for a competition.
93    async fn get_finalized_stats(
94        &self,
95        competition_id: i64,
96    ) -> Result<Vec<CompetitionFinalStatsRecord>>;
97
98    /// Get all competition fill events up to a cutoff timestamp, excluding
99    /// market maker wallets, ordered by timestamp ascending.
100    async fn get_competition_fills_before(
101        &self,
102        cutoff_ts_ms: i64,
103    ) -> Result<Vec<CompetitionFillRecord>>;
104
105    /// Get historical theoretical marks for symbols at or before a timestamp.
106    async fn get_historical_theo_marks(
107        &self,
108        symbols: &[String],
109        cutoff_ts_ms: i64,
110    ) -> Result<Vec<TheoMarkRecord>>;
111
112    /// Get display usernames for a batch of wallets.
113    async fn get_display_usernames_batch(
114        &self,
115        wallet_strings: &[String],
116    ) -> Result<Vec<WalletUsernameRecord>>;
117
118    /// Get the approved username for a single wallet.
119    async fn get_display_username(&self, wallet: &WalletAddress) -> Result<Option<String>>;
120
121    /// Get profile image URL for a wallet.
122    async fn get_profile_image_url(&self, wallet: &WalletAddress) -> Result<Option<String>>;
123
124    /// Compute ledger stats (deposits, withdrawals, realized PnL, 24h PnL) for a wallet.
125    async fn compute_ledger_profile_stats(
126        &self,
127        wallet: &WalletAddress,
128        now_ts_ms: i64,
129    ) -> Result<WalletLedgerStats>;
130
131    /// Get the first seen timestamp for a wallet from ledger events.
132    async fn get_account_first_seen_ts_ms(&self, wallet: &WalletAddress) -> Result<Option<i64>>;
133
134    /// Get per-symbol realized PnL for a wallet within an optional time window.
135    async fn get_realized_pnl_by_symbol(
136        &self,
137        wallet: &WalletAddress,
138        window_start: Option<i64>,
139        window_end: Option<i64>,
140    ) -> Result<Vec<SymbolPnlRecord>>;
141
142    /// Get profile trade history with optional competition window and filters.
143    async fn get_profile_trade_history(
144        &self,
145        wallet: &WalletAddress,
146        window_start: Option<i64>,
147        window_end: Option<i64>,
148        from_ts_ms: Option<i64>,
149        to_ts_ms: Option<i64>,
150        symbol: Option<&str>,
151        limit: i64,
152        offset: i64,
153    ) -> Result<Vec<ProfileFillRecord>>;
154
155    /// Get platform-wide realized PnL and volume per wallet (for medal computation).
156    async fn get_platform_wallet_metrics(&self) -> Result<Vec<PlatformWalletMetrics>>;
157}
158
159/// Write operations for competitions and related entities.
160#[async_trait::async_trait]
161pub trait CompetitionWriter: CompetitionReader {
162    /// Create a new competition. Returns OverlapViolation if the window conflicts.
163    async fn create_competition(
164        &self,
165        input: &CompetitionUpsertInput,
166    ) -> std::result::Result<CompetitionRecord, CompetitionWriteError>;
167
168    /// Update an existing competition by ID. All fields are pre-resolved by caller.
169    /// Returns OverlapViolation if the new window conflicts.
170    async fn update_competition(
171        &self,
172        competition_id: i64,
173        name: &str,
174        description: Option<&str>,
175        rules_url: Option<&str>,
176        rules_content: Option<&str>,
177        win_conditions: &[String],
178        primary_win_condition: &str,
179        start_ts_ms: i64,
180        end_ts_ms: i64,
181    ) -> std::result::Result<CompetitionRecord, CompetitionWriteError>;
182
183    /// Delete a competition by ID. Returns number of rows deleted.
184    async fn delete_competition(&self, competition_id: i64) -> Result<usize>;
185
186    /// Record a competition fill event (idempotent via ON CONFLICT DO NOTHING).
187    async fn record_competition_fill(&self, input: &CompetitionFillInput) -> Result<()>;
188
189    /// Finalize a competition: insert final stats rows in a transaction.
190    /// Uses pg_advisory_xact_lock to prevent double-finalization.
191    /// Returns the number of rows inserted.
192    async fn finalize_competition(
193        &self,
194        competition_id: i64,
195        stats: &[CompetitionFinalStatsInput],
196    ) -> Result<usize>;
197
198    /// Set profile image URL for a wallet. Returns the previous URL (if any).
199    async fn set_profile_image_url(
200        &self,
201        wallet: &WalletAddress,
202        profile_image_url: &str,
203    ) -> Result<Option<String>>;
204}