Skip to main content

oil_api/state/
plot.rs

1use serde::{Deserialize, Serialize};
2use steel::*;
3
4use crate::state::plot_pda;
5
6use super::OilAccount;
7
8/// Plot account (one per wallet/authority)
9#[repr(C)]
10#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
11pub struct Plot {
12    /// The authority (owner) of this plot account.
13    pub authority: Pubkey,
14
15    /// Plot level (0-9: Basic to Master)
16    pub plot_level: u64,
17
18    /// Maximum fuel capacity (tank size)
19    pub fuel_capacity: u64,
20
21    /// Current fuel remaining in tank
22    pub current_fuel: u64,
23
24    /// Maximum number of rig slots (1-5)
25    pub max_rig_slots: u64,
26
27    /// Total mining power from all staked rigs
28    pub total_mining_power: u64,
29
30    /// Last block when rewards were claimed
31    pub last_claim_block: u64,
32
33    /// Last block when fuel was updated (for consumption calculation)
34    pub last_fuel_update_block: u64,
35
36    /// Timestamp of last upgrade (for 24h cooldown)
37    pub last_upgrade_timestamp: i64,
38
39    /// Sum of fuel_requirement of all rigs currently placed on this plot (placement constraint)
40    pub total_fuel_requirement: u64,
41
42    /// Number of rigs currently placed on this plot (≤ max_rig_slots)
43    pub num_rigs_staked: u64,
44
45    /// Refinery rewards factor at this plot's last claim (for claimable = power * (current - this))
46    pub last_refinery_rewards_factor: Numeric,
47
48    /// Sum of fuel_consumption_rate of all staked rigs (used for fuel consumption on claim).
49    pub total_fuel_consumption_rate: u64,
50    /// Buffer for future extensions
51    pub buffer_b: u64,
52    /// Buffer for future extensions
53    pub buffer_c: u64,
54}
55
56impl Plot {
57    pub fn pda(&self) -> (Pubkey, u8) {
58        plot_pda(self.authority)
59    }
60
61    pub fn initialize(&mut self, authority: Pubkey, clock: &Clock) {
62        self.authority = authority;
63        self.plot_level = 0;
64        self.fuel_capacity = 20;
65        self.current_fuel = 20; // Start with full tank
66        self.max_rig_slots = 1;
67        self.total_mining_power = 0;
68        self.last_claim_block = clock.slot;
69        self.last_fuel_update_block = clock.slot;
70        self.last_upgrade_timestamp = 0; // No upgrade yet, cooldown doesn't apply to first upgrade
71        self.total_fuel_requirement = 0;
72        self.num_rigs_staked = 0;
73        self.last_refinery_rewards_factor = Numeric::ZERO;
74        self.total_fuel_consumption_rate = 0;
75        self.buffer_b = 0;
76        self.buffer_c = 0;
77    }
78
79    /// Check if plot can be upgraded (24h cooldown)
80    pub fn can_upgrade(&self, clock: &Clock) -> bool {
81        if self.last_upgrade_timestamp == 0 {
82            return true; // First upgrade, no cooldown
83        }
84        let elapsed = clock.unix_timestamp - self.last_upgrade_timestamp;
85        elapsed >= 86400 // 24 hours in seconds
86    }
87
88    /// Get upgrade cost for the next level (in atomic units, 11 decimals)
89    /// Returns None if already at max level (9)
90    pub fn get_upgrade_cost(&self) -> Option<u64> {
91        if self.plot_level >= 9 {
92            return None; // Already at max level
93        }
94        let next_level = self.plot_level + 1;
95        Some(Self::upgrade_cost_for_level(next_level))
96    }
97    
98    /// Get upgrade cost for a specific level (in atomic units, 11 decimals)
99    pub fn upgrade_cost_for_level(level: u64) -> u64 {
100        const UPGRADE_COSTS: [u64; 9] = [
101            210,      // Level 1: 210 OIL
102            1_125,    // Level 2: 1,125 OIL
103            888,      // Level 3: 888 OIL
104            1_065,    // Level 4: 1,065 OIL
105            1_275,    // Level 5: 1,275 OIL
106            2_550,    // Level 6: 2,550 OIL
107            5_000,    // Level 7: 5,000 OIL
108            10_000,   // Level 8: 10,000 OIL
109            20_000,   // Level 9: 20,000 OIL
110        ];
111        if level == 0 || level > 9 {
112            return 0;
113        }
114        UPGRADE_COSTS[(level - 1) as usize] * crate::consts::ONE_OIL
115    }
116    
117    /// Get level stats (slots, fuel_capacity) for a specific level
118    pub fn level_stats(level: u64) -> (u64, u64) {
119        const LEVEL_STATS: [(u64, u64); 10] = [
120            (1, 20),      // Level 0
121            (1, 50),      // Level 1
122            (2, 120),     // Level 2
123            (2, 300),     // Level 3
124            (3, 750),     // Level 4
125            (3, 1_800),   // Level 5
126            (4, 4_500),   // Level 6
127            (4, 11_000),  // Level 7
128            (5, 27_000),  // Level 8
129            (5, 65_000),  // Level 9
130        ];
131        if level > 9 {
132            return LEVEL_STATS[9];
133        }
134        LEVEL_STATS[level as usize]
135    }
136    
137    /// Upgrade plot to next level
138    /// Returns the upgrade cost if successful, None if already at max level
139    pub fn upgrade(&mut self, clock: &Clock) -> Option<u64> {
140        if !self.can_upgrade(clock) {
141            return None; // Cooldown not met
142        }
143        if self.plot_level >= 9 {
144            return None; // Already at max level
145        }
146        let new_level = self.plot_level + 1;
147        let (slots, fuel_capacity) = Self::level_stats(new_level);
148        let cost = Self::upgrade_cost_for_level(new_level);
149        
150        self.plot_level = new_level;
151        self.max_rig_slots = slots;
152        self.fuel_capacity = fuel_capacity;
153        self.last_upgrade_timestamp = clock.unix_timestamp;
154        
155        Some(cost)
156    }
157
158    /// Apply fuel consumption since last_fuel_update_block using total_fuel_consumption_rate.
159    /// Uses fuel-supported blocks: rewards accrue only for blocks where fuel was available;
160    /// fuel can deplete mid-interval so we cap at fuel_supported_blocks and consume proportionally.
161    /// Returns effective mining power for the period (0 if no fuel; else scaled by fuel_supported_blocks/blocks_elapsed).
162    pub fn apply_fuel_consumption(&mut self, clock: &Clock) -> u64 {
163        let blocks_elapsed = clock.slot.saturating_sub(self.last_fuel_update_block);
164        if blocks_elapsed == 0 {
165            return if self.current_fuel > 0 {
166                self.total_mining_power
167            } else {
168                0
169            };
170        }
171        let total_rate = self.total_fuel_consumption_rate; // atomic units per block
172        if total_rate == 0 {
173            // No consumption; full power for full period
174            return self.total_mining_power;
175        }
176        // consumption_per_block in fuel units = total_rate / ONE_OIL
177        // fuel_supported_blocks = min(blocks_elapsed, current_fuel / consumption_per_block)
178        //   = min(blocks_elapsed, current_fuel * ONE_OIL / total_rate)
179        let fuel_supported_blocks = {
180            let max_fuel_blocks = (self.current_fuel as u128)
181                .saturating_mul(crate::consts::ONE_OIL as u128)
182                .checked_div(total_rate as u128)
183                .unwrap_or(u64::MAX as u128) as u64;
184            blocks_elapsed.min(max_fuel_blocks)
185        };
186        // consumed = consumption_per_block * fuel_supported_blocks = (total_rate * fuel_supported_blocks) / ONE_OIL
187        let consumed = (total_rate as u128)
188            .saturating_mul(fuel_supported_blocks as u128)
189            .checked_div(crate::consts::ONE_OIL as u128)
190            .unwrap_or(0) as u64;
191        self.current_fuel = self.current_fuel.saturating_sub(consumed);
192        self.last_fuel_update_block = self.last_fuel_update_block.saturating_add(fuel_supported_blocks);
193        // Rewards accrue only for fuel_supported_blocks; effective power = total_mining_power * (fuel_supported_blocks / blocks_elapsed)
194        if fuel_supported_blocks == 0 {
195            return 0;
196        }
197        ((self.total_mining_power as u128) * (fuel_supported_blocks as u128) / (blocks_elapsed as u128)) as u64
198    }
199
200    /// Update fuel consumption based on staked rig consumption rates (alternative when rig list available).
201    /// Uses same fuel-supported-blocks logic as apply_fuel_consumption.
202    pub fn update_fuel_consumption(
203        &mut self,
204        staked_rig_consumption_rates: &[u64],
205        clock: &Clock,
206    ) -> u64 {
207        let blocks_elapsed = clock.slot.saturating_sub(self.last_fuel_update_block);
208        if blocks_elapsed == 0 {
209            return if self.current_fuel > 0 {
210                self.total_mining_power
211            } else {
212                0
213            };
214        }
215        let total_rate: u64 = staked_rig_consumption_rates.iter().copied().sum();
216        if total_rate == 0 {
217            return self.total_mining_power;
218        }
219        let fuel_supported_blocks = {
220            let max_fuel_blocks = (self.current_fuel as u128)
221                .saturating_mul(crate::consts::ONE_OIL as u128)
222                .checked_div(total_rate as u128)
223                .unwrap_or(u64::MAX as u128) as u64;
224            blocks_elapsed.min(max_fuel_blocks)
225        };
226        let mut consumed = 0u64;
227        for &consumption_rate in staked_rig_consumption_rates {
228            let rig_consumption = (consumption_rate as u128)
229                .saturating_mul(fuel_supported_blocks as u128)
230                .checked_div(crate::consts::ONE_OIL as u128)
231                .unwrap_or(0) as u64;
232            consumed = consumed.saturating_add(rig_consumption);
233        }
234        self.current_fuel = self.current_fuel.saturating_sub(consumed);
235        self.last_fuel_update_block = self.last_fuel_update_block.saturating_add(fuel_supported_blocks);
236        if fuel_supported_blocks == 0 {
237            return 0;
238        }
239        ((self.total_mining_power as u128) * (fuel_supported_blocks as u128) / (blocks_elapsed as u128)) as u64
240    }
241}
242
243account!(OilAccount, Plot);