Skip to main content

oil_api/state/
well.rs

1use serde::{Deserialize, Serialize};
2use steel::*;
3
4use crate::state::well_pda;
5
6use super::{OilAccount, Auction};
7
8/// Well account (one per well)
9#[repr(C)]
10#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
11pub struct Well {
12    /// Well ID (0-3) - which well this is for
13    pub well_id: u64,
14    
15    /// Current epoch ID (increments each auction: 0, 1, 2, 3, etc.)
16    pub epoch_id: u64,
17    
18    /// Current bidder/owner (Pubkey::default() if unowned)
19    pub current_bidder: Pubkey,
20    
21    /// Initial price for current epoch (in lamports)
22    pub init_price: u64,
23    
24    /// Mining per second (MPS) - current mining rate (OIL per second, in atomic units)
25    pub mps: u64,
26    
27    /// Epoch start time (timestamp when current epoch started)
28    pub epoch_start_time: u64,
29    
30    /// Accumulated OIL mined by current owner (not yet claimed)
31    pub accumulated_oil: u64,
32    
33    /// Last time accumulated_oil was updated
34    pub last_update_time: u64,
35    
36    /// Number of halvings that have occurred (for rate calculation)
37    pub halving_count: u64,
38    
39    /// Total OIL ever mined from this well (lifetime)
40    pub lifetime_oil_mined: u64,
41    
42    /// Total OIL mined by current operator (doesn't reset when claimed, only when ownership changes)
43    pub operator_total_oil_mined: u64,
44    
45    /// Buffer field (for future use) - previously is_pool_owned
46    pub buffer_c: u64,
47    
48    /// Total contributed FOGO for current epoch (tracks native SOL balance in Well PDA's system account)
49    /// Incremented on each contribution, decremented when pool bids
50    /// Reset to 0 when epoch ends
51    pub total_contributed: u64,
52    
53    /// Pool bid cost - stores the bid_amount when pool bids
54    /// Used to calculate original_total when pool gets outbid
55    /// Reset to 0 when epoch ends
56    pub pool_bid_cost: u64,
57}
58
59impl Well {
60    pub fn pda(well_id: u64) -> (Pubkey, u8) {
61        well_pda(well_id)
62    }
63
64    pub fn current_price(&self, auction: &Auction, clock: &Clock) -> u64 {
65        use crate::consts::AUCTION_FLOOR_PRICE;
66        
67        // If well has no owner (never been bid on), show starting price
68        use solana_program::pubkey::Pubkey;
69        if self.current_bidder == Pubkey::default() {
70            return self.init_price; // Return starting price for unowned wells
71        }
72        
73        let elapsed = clock.unix_timestamp.saturating_sub(self.epoch_start_time as i64);
74        let duration = auction.auction_duration_seconds as i64;
75        
76        if elapsed >= duration {
77            return AUCTION_FLOOR_PRICE; // Auction expired, price is at floor
78        }
79        
80        // Linear decay: price = floor + (init_price - floor) * (remaining / duration)
81        let remaining = duration - elapsed;
82        let price_range = self.init_price.saturating_sub(AUCTION_FLOOR_PRICE);
83        let decayed_amount = (price_range as u128 * remaining as u128 / duration as u128) as u64;
84        AUCTION_FLOOR_PRICE + decayed_amount
85    }
86
87    /// Calculate the effective mining rate at a given point in time based on base rate and halvings
88    fn calculate_rate_at_time(&self, base_rate: u64, halving_count_at_time: u64) -> u64 {
89        if halving_count_at_time == 0 {
90            return base_rate;
91        }
92        
93        // Apply first halving (50% reduction)
94        let mut rate = base_rate / 2;
95        
96        // Apply subsequent halvings (25% reduction each)
97        for _ in 1..halving_count_at_time {
98            rate = (rate * 3) / 4;
99        }
100        
101        rate
102    }
103    
104    /// Calculate how many halvings had occurred by a given timestamp
105    /// 
106    /// Halving schedule:
107    /// - First halving: 14 days after initialization (at last_halving_time + FIRST_HALVING_PERIOD_SECONDS when halving_count = 0)
108    /// - Subsequent halvings: Every 28 days after the first halving
109    /// 
110    /// When halving_count > 0, last_halving_time is when the most recent halving occurred.
111    fn halving_count_at_time(auction: &Auction, timestamp: u64) -> u64 {
112        if auction.halving_count == 0 {
113            // No halvings have occurred yet
114            // last_halving_time is the initialization time
115            let first_halving_time = auction.last_halving_time + Auction::FIRST_HALVING_PERIOD_SECONDS;
116            if timestamp < first_halving_time {
117                return 0;
118            }
119            // First halving should have occurred but hasn't been applied yet
120            // Return 0 to be safe - it will be applied when someone interacts
121            return 0;
122        }
123        
124        // Calculate when the first halving occurred
125        // If halving_count = 1: first halving was at last_halving_time
126        // If halving_count = 2: first halving was 28 days before last_halving_time
127        // If halving_count = 3: first halving was 56 days before last_halving_time, etc.
128        let first_halving_time = if auction.halving_count == 1 {
129            auction.last_halving_time
130        } else {
131            // First halving was (halving_count - 1) * halving_period_seconds before the last halving
132            auction.last_halving_time.saturating_sub(
133                (auction.halving_count - 1) * auction.halving_period_seconds
134            )
135        };
136        
137        if timestamp < first_halving_time {
138            return 0;
139        }
140        
141        // Calculate how many halvings occurred by this timestamp
142        // First halving is at first_halving_time, subsequent are every halving_period_seconds
143        let time_since_first = timestamp.saturating_sub(first_halving_time);
144        
145        // First halving counts as 1, then add subsequent halvings (every halving_period_seconds)
146        let halving_count = 1 + (time_since_first / auction.halving_period_seconds);
147        
148        // Cap at the actual halving_count (can't have more halvings than have occurred)
149        halving_count.min(auction.halving_count)
150    }
151
152    pub fn update_accumulated_oil(&mut self, auction: &Auction, clock: &Clock) {
153        // Skip if no owner
154        use solana_program::pubkey::Pubkey;
155        if self.current_bidder == Pubkey::default() {
156            return;
157        }
158        
159        let last_update = self.last_update_time as i64;
160        let current_time = clock.unix_timestamp as u64;
161        let elapsed = clock.unix_timestamp.saturating_sub(last_update);
162        if elapsed <= 0 {
163            return;
164        }
165        
166        // Get base rate for this well (we need well_id, but we can derive it from self.well_id)
167        let base_rate = auction.base_mining_rates[self.well_id as usize];
168        
169        // Calculate halving counts at start and end of period
170        let halving_count_at_start = Self::halving_count_at_time(auction, last_update as u64);
171        let halving_count_at_end = Self::halving_count_at_time(auction, current_time);
172        
173        // Calculate rate at start of period
174        let rate_at_start = self.calculate_rate_at_time(base_rate, halving_count_at_start);
175        
176        // If no halving occurred during this period, use simple calculation
177        if halving_count_at_start == halving_count_at_end {
178            let oil_mined = rate_at_start.checked_mul(elapsed as u64).unwrap_or(0);
179            self.accumulated_oil = self.accumulated_oil
180                .checked_add(oil_mined)
181                .unwrap_or(u64::MAX);
182            self.lifetime_oil_mined = self.lifetime_oil_mined
183                .checked_add(oil_mined)
184                .unwrap_or(u64::MAX);
185            self.operator_total_oil_mined = self.operator_total_oil_mined
186                .checked_add(oil_mined)
187                .unwrap_or(u64::MAX);
188            self.last_update_time = current_time;
189            return;
190        }
191        
192        // Halving(s) occurred during this period - calculate in segments
193        // Calculate when first halving occurred (needed for segment calculation)
194        let first_halving_time = if auction.halving_count == 0 {
195            auction.last_halving_time + Auction::FIRST_HALVING_PERIOD_SECONDS
196        } else if auction.halving_count == 1 {
197            auction.last_halving_time
198        } else {
199            auction.last_halving_time.saturating_sub(
200                (auction.halving_count - 1) * auction.halving_period_seconds
201            )
202        };
203        
204        let mut total_oil = 0u64;
205        let mut segment_start = last_update as u64;
206        let mut current_halving_count = halving_count_at_start;
207        
208        while segment_start < current_time && current_halving_count < halving_count_at_end {
209            // Calculate when the next halving occurred
210            let next_halving_time = if current_halving_count == 0 {
211                first_halving_time
212            } else {
213                // Subsequent halvings are every halving_period_seconds after the first
214                first_halving_time + (current_halving_count as u64 * auction.halving_period_seconds)
215            };
216            
217            let segment_end = next_halving_time.min(current_time);
218            let segment_time = segment_end.saturating_sub(segment_start);
219            let segment_rate = self.calculate_rate_at_time(base_rate, current_halving_count);
220            
221            total_oil = total_oil.checked_add(
222                segment_rate.checked_mul(segment_time).unwrap_or(0)
223            ).unwrap_or(u64::MAX);
224            
225            segment_start = segment_end;
226            current_halving_count += 1;
227        }
228        
229        // Calculate remaining time after all halvings in this period
230        if segment_start < current_time {
231            let remaining_time = current_time.saturating_sub(segment_start);
232            let final_rate = self.calculate_rate_at_time(base_rate, halving_count_at_end);
233            total_oil = total_oil.checked_add(
234                final_rate.checked_mul(remaining_time).unwrap_or(0)
235            ).unwrap_or(u64::MAX);
236        }
237        
238        let oil_mined = total_oil;
239        
240        self.accumulated_oil = self.accumulated_oil
241            .checked_add(oil_mined)
242            .unwrap_or(u64::MAX);
243        
244        self.lifetime_oil_mined = self.lifetime_oil_mined
245            .checked_add(oil_mined)
246            .unwrap_or(u64::MAX);
247        
248        // Track total mined by current operator (persists even after claiming)
249        self.operator_total_oil_mined = self.operator_total_oil_mined
250            .checked_add(oil_mined)
251            .unwrap_or(u64::MAX);
252        
253        self.last_update_time = current_time;
254    }
255
256    /// Apply all halvings that have already occurred (based on auction.halving_count)
257    /// This is used when resetting well.mps to base rate in a new epoch
258    /// 
259    /// Safety: This function assumes well.mps has just been reset to base rate.
260    /// It applies all halvings that have occurred according to auction.halving_count.
261    pub fn apply_existing_halvings(&mut self, auction: &Auction) {
262        if auction.halving_count == 0 {
263            // No halvings have occurred yet, keep base rate
264            self.halving_count = 0;
265            return;
266        }
267        
268        // Apply first halving (50% reduction)
269        self.mps = self.mps / 2;
270        
271        // Apply subsequent halvings (25% reduction each)
272        // auction.halving_count includes the first halving, so subtract 1 for subsequent halvings
273        for _ in 0..(auction.halving_count - 1) {
274            self.mps = (self.mps * 3) / 4; // 25% reduction (multiply by 0.75)
275        }
276        
277        // Sync well's halving_count to match auction's
278        self.halving_count = auction.halving_count;
279    }
280
281    pub fn check_and_apply_halving(&mut self, auction: &mut Auction, clock: &Clock) {
282        // First, sync existing halvings if this well is behind
283        // This ensures all wells stay in sync with the global halving state
284        while self.halving_count < auction.halving_count {
285            if self.halving_count == 0 {
286                // Apply first halving (50% reduction)
287                self.mps = self.mps / 2;
288                self.halving_count = 1;
289            } else {
290                // Apply subsequent halvings (25% reduction each)
291                self.mps = (self.mps * 3) / 4;
292                self.halving_count += 1;
293            }
294        }
295        
296        // Then check if we should apply NEW halvings based on current time
297        let current_time = clock.unix_timestamp as u64;
298        let (halvings_to_apply, is_first_halving) = auction.should_apply_halving(current_time);
299        
300        if halvings_to_apply > 0 {
301            if is_first_halving {
302                // First halving: 50% reduction
303                self.mps = self.mps / 2; // 50% reduction
304                self.halving_count += 1;
305                auction.halving_count += 1;
306                auction.last_halving_time = current_time;
307            } else {
308                // Subsequent halvings: 25% reduction each (multiply by 0.75)
309                for _ in 0..halvings_to_apply {
310                    self.mps = (self.mps * 3) / 4; // 25% reduction (multiply by 0.75)
311                    self.halving_count += 1;
312                    auction.halving_count += 1;
313                }
314                // Update auction last_halving_time to current time
315                auction.last_halving_time = current_time;
316            }
317        }
318    }
319}
320
321account!(OilAccount, Well);
322