oil-api 0.7.8

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

use crate::state::refinery_pda;

use super::OilAccount;

/// Rig configuration (stats for each rig type)
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
pub struct RigConfig {
    /// Mining power for this rig type
    pub mining_power: u64,

    /// Fuel requirement for placement
    pub fuel_requirement: u64,

    /// Fuel consumption rate per block (in atomic units with 11 decimals)
    pub fuel_consumption_rate: u64,
}

/// Refinery global state (singleton account)
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
pub struct Refinery {
    /// Sum of all players' Mining Power
    pub total_network_mining_power: u64,

    /// Current emission rate per block (in atomic units with 11 decimals)
    pub emission_per_block: u64,

    /// Block when emission rate was last updated (tracks initialization time for day calculation)
    pub last_emission_update_block: u64,

    /// Max supply per rig type (u64::MAX = unlimited, otherwise the max supply)
    pub rig_supplies: [u64; 15],

    /// Current supply per rig type (number minted)
    pub rig_current: [u64; 15],

    /// Stats configuration for each rig type (0-14)
    pub rig_configs: [RigConfig; 15],

    /// Admin authority that can update rig_configs
    pub admin: Pubkey,

    /// Cumulative reward per unit of mining power (updated when advancing emission; used for claim math)
    pub refinery_rewards_factor: Numeric,

    /// Block through which refinery_rewards_factor has been applied (for lazy updates)
    pub last_rewards_factor_block: u64,

    /// Init timestamp (unix seconds) for emission schedule; set once in initialize.
    pub init_timestamp: i64,
    /// Last emission rate update timestamp (unix seconds); used for 24h decay and day calculation.
    pub last_emission_update_timestamp: i64,

    /// Reserved for future use (do not use for new features; add new named fields or more buffers).
    pub reserved_a: u64,
    /// Reserved for future use.
    pub reserved_b: u64,
    /// Buffer for future extensions.
    pub buffer_a: u64,
    /// Buffer for future extensions.
    pub buffer_b: u64,
    /// Buffer for future extensions.
    pub buffer_c: u64,
    /// Buffer for future extensions.
    pub buffer_d: u64,
}

impl Refinery {
    pub fn pda() -> (Pubkey, u8) {
        refinery_pda()
    }

    pub fn initialize(&mut self, admin: Pubkey, clock: &Clock, initial_emission: u64) {
        self.total_network_mining_power = 0;
        self.emission_per_block = initial_emission; // In atomic units (e.g., 7,100,000,000 for 0.071 OIL/block)
        self.last_emission_update_block = clock.slot;
        self.rig_supplies = [u64::MAX; 15]; // u64::MAX = unlimited
        self.rig_current = [0; 15];
        // rig_configs should be set by caller with initial values
        self.admin = admin;
        self.refinery_rewards_factor = Numeric::ZERO;
        self.last_rewards_factor_block = clock.slot;
        self.init_timestamp = clock.unix_timestamp;
        self.last_emission_update_timestamp = clock.unix_timestamp;
        self.reserved_a = 0;
        self.reserved_b = 0;
        self.buffer_a = 0;
        self.buffer_b = 0;
        self.buffer_c = 0;
        self.buffer_d = 0;
    }

    /// Update emission rate based on days elapsed (timestamp-based; robust to block rate drift).
    /// Uses Clock::unix_timestamp and init_timestamp / last_emission_update_timestamp.
    pub fn update_emission_rate(&mut self, clock: &Clock, _blocks_per_day: u64) {
        const SECONDS_PER_DAY: i64 = 86400;
        let now = clock.unix_timestamp;
        let days_elapsed = (now - self.init_timestamp).max(0) / SECONDS_PER_DAY;

        // Growth phase (Day 1-7): Linear interpolation from 0.02 → 0.05
        if days_elapsed <= 7 {
            let start_emission = 2_000_000_000u64;  // 0.02 OIL/block in atomic units
            let peak_emission = 5_000_000_000u64;   // 0.05 OIL/block in atomic units
            
            if days_elapsed == 0 {
                self.emission_per_block = start_emission;
            } else if days_elapsed >= 7 {
                self.emission_per_block = peak_emission;
            } else {
                let days_progress = (days_elapsed - 1) as u128;
                let emission_range = (peak_emission - start_emission) as u128;
                let interpolated = start_emission as u128
                    + (emission_range * days_progress) / 6;
                self.emission_per_block = interpolated as u64;
            }
            self.last_emission_update_block = clock.slot;
            self.last_emission_update_timestamp = now;
            return;
        }

        // Peak phase (Day 8-14): Maintain peak
        if days_elapsed <= 14 {
            self.emission_per_block = 5_000_000_000u64; // 0.05 OIL/block
            self.last_emission_update_block = clock.slot;
            self.last_emission_update_timestamp = now;
            return;
        }

        // Decreasing phase (Day 15+): Decrease by 2% per 24 hours (timestamp-based)
        let seconds_since_update = (now - self.last_emission_update_timestamp).max(0);
        if seconds_since_update >= SECONDS_PER_DAY {
            let min_emission = 1_000_000_000u64; // 0.01 OIL/block minimum
            let new_emission = (self.emission_per_block as u128 * 98) / 100;
            self.emission_per_block = new_emission.max(min_emission as u128) as u64;
            self.last_emission_update_block = clock.slot;
            self.last_emission_update_timestamp = now;
        }
    }

    /// Check if rig can be minted (supply limit check)
    pub fn can_mint_rig(&self, rig_type: u64) -> bool {
        let idx = rig_type as usize;
        if idx >= 15 {
            return false;
        }
        
        let max_supply = self.rig_supplies[idx];
        if max_supply == u64::MAX {
            true // Unlimited supply
        } else {
            self.rig_current[idx] < max_supply
        }
    }

    /// Increment rig supply after minting
    pub fn increment_rig_supply(&mut self, rig_type: u64) {
        let idx = rig_type as usize;
        if idx < 15 {
            self.rig_current[idx] = self.rig_current[idx].saturating_add(1);
        }
    }

    /// Update mining power when rig is placed/removed
    pub fn update_mining_power(&mut self, mining_power_change: i64) {
        if mining_power_change > 0 {
            self.total_network_mining_power = self.total_network_mining_power
                .saturating_add(mining_power_change as u64);
        } else {
            self.total_network_mining_power = self.total_network_mining_power
                .saturating_sub((-mining_power_change) as u64);
        }
    }

    /// Advance refinery_rewards_factor up to current block (call before claim or when reading claimable).
    /// Claimable for a plot = plot.total_mining_power * (refinery.rewards_factor - plot.last_refinery_rewards_factor).
    pub fn advance_rewards_factor(&mut self, clock: &Clock) {
        let blocks = clock.slot.saturating_sub(self.last_rewards_factor_block);
        if self.total_network_mining_power > 0 && blocks > 0 {
            let total_emission = self.emission_per_block.saturating_mul(blocks);
            self.refinery_rewards_factor +=
                Numeric::from_fraction(total_emission, self.total_network_mining_power);
        }
        self.last_rewards_factor_block = clock.slot;
    }
}

account!(OilAccount, Refinery);