1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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);