oil-api 0.7.8

API for interacting with the OIL protocol on Solana
Documentation
use serde::{Deserialize, Serialize};
use steel::*;

use crate::state::plot_pda;

use super::OilAccount;

/// Plot account (one per wallet/authority)
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
pub struct Plot {
    /// The authority (owner) of this plot account.
    pub authority: Pubkey,

    /// Plot level (0-9: Basic to Master)
    pub plot_level: u64,

    /// Maximum fuel capacity (tank size)
    pub fuel_capacity: u64,

    /// Current fuel remaining in tank
    pub current_fuel: u64,

    /// Maximum number of rig slots (1-5)
    pub max_rig_slots: u64,

    /// Total mining power from all staked rigs
    pub total_mining_power: u64,

    /// Last block when rewards were claimed
    pub last_claim_block: u64,

    /// Last block when fuel was updated (for consumption calculation)
    pub last_fuel_update_block: u64,

    /// Timestamp of last upgrade (for 24h cooldown)
    pub last_upgrade_timestamp: i64,

    /// Sum of fuel_requirement of all rigs currently placed on this plot (placement constraint)
    pub total_fuel_requirement: u64,

    /// Number of rigs currently placed on this plot (≤ max_rig_slots)
    pub num_rigs_staked: u64,

    /// Refinery rewards factor at this plot's last claim (for claimable = power * (current - this))
    pub last_refinery_rewards_factor: Numeric,

    /// Sum of fuel_consumption_rate of all staked rigs (used for fuel consumption on claim).
    pub total_fuel_consumption_rate: u64,
    /// Buffer for future extensions
    pub buffer_b: u64,
    /// Buffer for future extensions
    pub buffer_c: u64,
}

impl Plot {
    pub fn pda(&self) -> (Pubkey, u8) {
        plot_pda(self.authority)
    }

    pub fn initialize(&mut self, authority: Pubkey, clock: &Clock) {
        self.authority = authority;
        self.plot_level = 0;
        self.fuel_capacity = 20;
        self.current_fuel = 20; // Start with full tank
        self.max_rig_slots = 1;
        self.total_mining_power = 0;
        self.last_claim_block = clock.slot;
        self.last_fuel_update_block = clock.slot;
        self.last_upgrade_timestamp = 0; // No upgrade yet, cooldown doesn't apply to first upgrade
        self.total_fuel_requirement = 0;
        self.num_rigs_staked = 0;
        self.last_refinery_rewards_factor = Numeric::ZERO;
        self.total_fuel_consumption_rate = 0;
        self.buffer_b = 0;
        self.buffer_c = 0;
    }

    /// Check if plot can be upgraded (24h cooldown)
    pub fn can_upgrade(&self, clock: &Clock) -> bool {
        if self.last_upgrade_timestamp == 0 {
            return true; // First upgrade, no cooldown
        }
        let elapsed = clock.unix_timestamp - self.last_upgrade_timestamp;
        elapsed >= 86400 // 24 hours in seconds
    }

    /// Get upgrade cost for the next level (in atomic units, 11 decimals)
    /// Returns None if already at max level (9)
    pub fn get_upgrade_cost(&self) -> Option<u64> {
        if self.plot_level >= 9 {
            return None; // Already at max level
        }
        let next_level = self.plot_level + 1;
        Some(Self::upgrade_cost_for_level(next_level))
    }
    
    /// Get upgrade cost for a specific level (in atomic units, 11 decimals)
    pub fn upgrade_cost_for_level(level: u64) -> u64 {
        const UPGRADE_COSTS: [u64; 9] = [
            210,      // Level 1: 210 OIL
            1_125,    // Level 2: 1,125 OIL
            888,      // Level 3: 888 OIL
            1_065,    // Level 4: 1,065 OIL
            1_275,    // Level 5: 1,275 OIL
            2_550,    // Level 6: 2,550 OIL
            5_000,    // Level 7: 5,000 OIL
            10_000,   // Level 8: 10,000 OIL
            20_000,   // Level 9: 20,000 OIL
        ];
        if level == 0 || level > 9 {
            return 0;
        }
        UPGRADE_COSTS[(level - 1) as usize] * crate::consts::ONE_OIL
    }
    
    /// Get level stats (slots, fuel_capacity) for a specific level
    pub fn level_stats(level: u64) -> (u64, u64) {
        const LEVEL_STATS: [(u64, u64); 10] = [
            (1, 20),      // Level 0
            (1, 50),      // Level 1
            (2, 120),     // Level 2
            (2, 300),     // Level 3
            (3, 750),     // Level 4
            (3, 1_800),   // Level 5
            (4, 4_500),   // Level 6
            (4, 11_000),  // Level 7
            (5, 27_000),  // Level 8
            (5, 65_000),  // Level 9
        ];
        if level > 9 {
            return LEVEL_STATS[9];
        }
        LEVEL_STATS[level as usize]
    }
    
    /// Upgrade plot to next level
    /// Returns the upgrade cost if successful, None if already at max level
    pub fn upgrade(&mut self, clock: &Clock) -> Option<u64> {
        if !self.can_upgrade(clock) {
            return None; // Cooldown not met
        }
        if self.plot_level >= 9 {
            return None; // Already at max level
        }
        let new_level = self.plot_level + 1;
        let (slots, fuel_capacity) = Self::level_stats(new_level);
        let cost = Self::upgrade_cost_for_level(new_level);
        
        self.plot_level = new_level;
        self.max_rig_slots = slots;
        self.fuel_capacity = fuel_capacity;
        self.last_upgrade_timestamp = clock.unix_timestamp;
        
        Some(cost)
    }

    /// Apply fuel consumption since last_fuel_update_block using total_fuel_consumption_rate.
    /// Uses fuel-supported blocks: rewards accrue only for blocks where fuel was available;
    /// fuel can deplete mid-interval so we cap at fuel_supported_blocks and consume proportionally.
    /// Returns effective mining power for the period (0 if no fuel; else scaled by fuel_supported_blocks/blocks_elapsed).
    pub fn apply_fuel_consumption(&mut self, clock: &Clock) -> u64 {
        let blocks_elapsed = clock.slot.saturating_sub(self.last_fuel_update_block);
        if blocks_elapsed == 0 {
            return if self.current_fuel > 0 {
                self.total_mining_power
            } else {
                0
            };
        }
        let total_rate = self.total_fuel_consumption_rate; // atomic units per block
        if total_rate == 0 {
            // No consumption; full power for full period
            return self.total_mining_power;
        }
        // consumption_per_block in fuel units = total_rate / ONE_OIL
        // fuel_supported_blocks = min(blocks_elapsed, current_fuel / consumption_per_block)
        //   = min(blocks_elapsed, current_fuel * ONE_OIL / total_rate)
        let fuel_supported_blocks = {
            let max_fuel_blocks = (self.current_fuel as u128)
                .saturating_mul(crate::consts::ONE_OIL as u128)
                .checked_div(total_rate as u128)
                .unwrap_or(u64::MAX as u128) as u64;
            blocks_elapsed.min(max_fuel_blocks)
        };
        // consumed = consumption_per_block * fuel_supported_blocks = (total_rate * fuel_supported_blocks) / ONE_OIL
        let consumed = (total_rate as u128)
            .saturating_mul(fuel_supported_blocks as u128)
            .checked_div(crate::consts::ONE_OIL as u128)
            .unwrap_or(0) as u64;
        self.current_fuel = self.current_fuel.saturating_sub(consumed);
        self.last_fuel_update_block = self.last_fuel_update_block.saturating_add(fuel_supported_blocks);
        // Rewards accrue only for fuel_supported_blocks; effective power = total_mining_power * (fuel_supported_blocks / blocks_elapsed)
        if fuel_supported_blocks == 0 {
            return 0;
        }
        ((self.total_mining_power as u128) * (fuel_supported_blocks as u128) / (blocks_elapsed as u128)) as u64
    }

    /// Update fuel consumption based on staked rig consumption rates (alternative when rig list available).
    /// Uses same fuel-supported-blocks logic as apply_fuel_consumption.
    pub fn update_fuel_consumption(
        &mut self,
        staked_rig_consumption_rates: &[u64],
        clock: &Clock,
    ) -> u64 {
        let blocks_elapsed = clock.slot.saturating_sub(self.last_fuel_update_block);
        if blocks_elapsed == 0 {
            return if self.current_fuel > 0 {
                self.total_mining_power
            } else {
                0
            };
        }
        let total_rate: u64 = staked_rig_consumption_rates.iter().copied().sum();
        if total_rate == 0 {
            return self.total_mining_power;
        }
        let fuel_supported_blocks = {
            let max_fuel_blocks = (self.current_fuel as u128)
                .saturating_mul(crate::consts::ONE_OIL as u128)
                .checked_div(total_rate as u128)
                .unwrap_or(u64::MAX as u128) as u64;
            blocks_elapsed.min(max_fuel_blocks)
        };
        let mut consumed = 0u64;
        for &consumption_rate in staked_rig_consumption_rates {
            let rig_consumption = (consumption_rate as u128)
                .saturating_mul(fuel_supported_blocks as u128)
                .checked_div(crate::consts::ONE_OIL as u128)
                .unwrap_or(0) as u64;
            consumed = consumed.saturating_add(rig_consumption);
        }
        self.current_fuel = self.current_fuel.saturating_sub(consumed);
        self.last_fuel_update_block = self.last_fuel_update_block.saturating_add(fuel_supported_blocks);
        if fuel_supported_blocks == 0 {
            return 0;
        }
        ((self.total_mining_power as u128) * (fuel_supported_blocks as u128) / (blocks_elapsed as u128)) as u64
    }
}

account!(OilAccount, Plot);