1use std::borrow::Cow;
2
3use light_compressible::rent::RentConfig;
4use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress;
5use solana_account_info::AccountInfo;
6use solana_clock::Clock;
7use solana_pubkey::Pubkey;
8use solana_sysvar::Sysvar;
9
10use crate::{instruction::PackedAccounts, AnchorDeserialize, AnchorSerialize, ProgramError};
11
12pub trait Pack {
15 type Packed: AnchorSerialize + Clone + std::fmt::Debug;
16
17 fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed;
18}
19
20pub trait Unpack {
21 type Unpacked;
22
23 fn unpack(
24 &self,
25 remaining_accounts: &[AccountInfo],
26 ) -> Result<Self::Unpacked, crate::ProgramError>;
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)]
30#[repr(u8)]
31pub enum AccountState {
32 Initialized,
33 Frozen,
34}
35
36pub trait HasCompressionInfo {
37 fn compression_info(&self) -> &CompressionInfo;
38 fn compression_info_mut(&mut self) -> &mut CompressionInfo;
39 fn compression_info_mut_opt(&mut self) -> &mut Option<CompressionInfo>;
40 fn set_compression_info_none(&mut self);
41}
42
43pub trait CompressedInitSpace {
45 const COMPRESSED_INIT_SPACE: usize;
46}
47
48pub trait CompressAs {
50 type Output: crate::AnchorSerialize
51 + crate::AnchorDeserialize
52 + crate::LightDiscriminator
53 + crate::account::Size
54 + HasCompressionInfo
55 + Default
56 + Clone;
57
58 fn compress_as(&self) -> Cow<'_, Self::Output>;
59}
60
61#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)]
62pub struct CompressionInfo {
63 pub config_version: u16,
65 pub lamports_per_write: u32,
67 pub last_claimed_slot: u64,
69 pub rent_config: RentConfig,
71 pub state: CompressionState,
73}
74
75#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize, PartialEq)]
76pub enum CompressionState {
77 #[default]
78 Uninitialized,
79 Decompressed,
80 Compressed,
81}
82
83impl CompressionInfo {
84 pub fn new_from_config(
90 cfg: &crate::compressible::CompressibleConfig,
91 current_slot: u64,
92 ) -> Self {
93 Self {
94 config_version: cfg.version as u16,
95 lamports_per_write: cfg.write_top_up,
96 last_claimed_slot: current_slot,
97 rent_config: cfg.rent_config,
98 state: CompressionState::Decompressed,
99 }
100 }
101
102 pub fn new_decompressed() -> Result<Self, crate::ProgramError> {
105 Ok(Self {
106 config_version: 0,
107 lamports_per_write: 0,
108 last_claimed_slot: Clock::get()?.slot,
109 rent_config: RentConfig::default(),
110 state: CompressionState::Decompressed,
111 })
112 }
113
114 pub fn bump_last_claimed_slot(&mut self) -> Result<(), crate::ProgramError> {
116 self.last_claimed_slot = Clock::get()?.slot;
117 Ok(())
118 }
119
120 pub fn set_last_claimed_slot(&mut self, slot: u64) {
122 self.last_claimed_slot = slot;
123 }
124
125 pub fn last_claimed_slot(&self) -> u64 {
127 self.last_claimed_slot
128 }
129
130 pub fn set_compressed(&mut self) {
131 self.state = CompressionState::Compressed;
132 }
133
134 pub fn is_compressed(&self) -> bool {
135 self.state == CompressionState::Compressed
136 }
137}
138
139impl CompressionInfo {
140 pub fn calculate_top_up_lamports(
147 &self,
148 num_bytes: u64,
149 current_slot: u64,
150 current_lamports: u64,
151 rent_exemption_lamports: u64,
152 ) -> u64 {
153 use light_compressible::rent::AccountRentState;
154
155 let state = AccountRentState {
156 num_bytes,
157 current_slot,
158 current_lamports,
159 last_claimed_slot: self.last_claimed_slot(),
160 };
161
162 if let Some(rent_deficit) =
164 state.is_compressible(&self.rent_config, rent_exemption_lamports)
165 {
166 return self.lamports_per_write as u64 + rent_deficit;
167 }
168
169 let epochs_funded_ahead =
170 state.epochs_funded_ahead(&self.rent_config, rent_exemption_lamports);
171
172 if epochs_funded_ahead >= self.rent_config.max_funded_epochs as u64 {
174 return 0;
175 }
176
177 self.lamports_per_write as u64
179 }
180
181 pub fn top_up_rent<'a>(
193 &self,
194 account_info: &AccountInfo<'a>,
195 payer_info: &AccountInfo<'a>,
196 system_program_info: &AccountInfo<'a>,
197 ) -> Result<(), crate::ProgramError> {
198 use solana_clock::Clock;
199 use solana_sysvar::{rent::Rent, Sysvar};
200
201 let bytes = account_info.data_len() as u64;
202 let current_lamports = account_info.lamports();
203 let current_slot = Clock::get()?.slot;
204 let rent_exemption_lamports = Rent::get()?.minimum_balance(bytes as usize);
205
206 let top_up = self.calculate_top_up_lamports(
207 bytes,
208 current_slot,
209 current_lamports,
210 rent_exemption_lamports,
211 );
212
213 if top_up > 0 {
214 transfer_lamports_cpi(payer_info, account_info, system_program_info, top_up)?;
218 }
219
220 Ok(())
221 }
222}
223
224pub trait Space {
225 const INIT_SPACE: usize;
226}
227
228impl Space for CompressionInfo {
229 const INIT_SPACE: usize = 2 + 4 + 8 + core::mem::size_of::<RentConfig>() + 1;
231}
232
233#[cfg(feature = "anchor")]
234impl anchor_lang::Space for CompressionInfo {
235 const INIT_SPACE: usize = <Self as Space>::INIT_SPACE;
236}
237
238pub const OPTION_COMPRESSION_INFO_SPACE: usize = 1 + CompressionInfo::INIT_SPACE;
241
242#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
244pub struct CompressedAccountData<T> {
245 pub meta: CompressedAccountMetaNoLamportsNoAddress,
246 pub data: T,
247}
248
249pub fn claim_completed_epoch_rent<'info, A>(
252 account_info: &AccountInfo<'info>,
253 account_data: &mut A,
254 rent_sponsor: &AccountInfo<'info>,
255) -> Result<Option<u64>, ProgramError>
256where
257 A: HasCompressionInfo,
258{
259 use light_compressible::rent::{AccountRentState, SLOTS_PER_EPOCH};
260 use solana_sysvar::rent::Rent;
261
262 let current_slot = Clock::get()?.slot;
263 let bytes = account_info.data_len() as u64;
264 let current_lamports = account_info.lamports();
265 let rent_exemption_lamports = Rent::get()
266 .map_err(|_| ProgramError::Custom(0))?
267 .minimum_balance(bytes as usize);
268
269 let ci = account_data.compression_info_mut();
270 let state = AccountRentState {
271 num_bytes: bytes,
272 current_slot,
273 current_lamports,
274 last_claimed_slot: ci.last_claimed_slot(),
275 };
276
277 if state
279 .is_compressible(&ci.rent_config, rent_exemption_lamports)
280 .is_some()
281 {
282 return Ok(None);
283 }
284
285 let claimable = state.calculate_claimable_rent(&ci.rent_config, rent_exemption_lamports);
287 if let Some(amount) = claimable {
288 if amount > 0 {
289 let completed_epochs = state.get_completed_epochs();
291 ci.set_last_claimed_slot(
292 ci.last_claimed_slot()
293 .saturating_add(completed_epochs * SLOTS_PER_EPOCH),
294 );
295
296 {
298 let mut src = account_info
299 .try_borrow_mut_lamports()
300 .map_err(|_| ProgramError::Custom(0))?;
301 let mut dst = rent_sponsor
302 .try_borrow_mut_lamports()
303 .map_err(|_| ProgramError::Custom(0))?;
304 let new_src = src
305 .checked_sub(amount)
306 .ok_or(ProgramError::InsufficientFunds)?;
307 let new_dst = dst.checked_add(amount).ok_or(ProgramError::Custom(0))?;
308 **src = new_src;
309 **dst = new_dst;
310 }
311 return Ok(Some(amount));
312 }
313 }
314 Ok(Some(0))
315}
316
317fn transfer_lamports_cpi<'a>(
326 from: &AccountInfo<'a>,
327 to: &AccountInfo<'a>,
328 system_program: &AccountInfo<'a>,
329 lamports: u64,
330) -> Result<(), ProgramError> {
331 use solana_cpi::invoke;
332 use solana_instruction::{AccountMeta, Instruction};
333
334 const SYSTEM_PROGRAM_ID: [u8; 32] = [
336 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
337 0, 0,
338 ];
339
340 let mut instruction_data = vec![2, 0, 0, 0];
342 instruction_data.extend_from_slice(&lamports.to_le_bytes());
343
344 let transfer_instruction = Instruction {
345 program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
346 accounts: vec![
347 AccountMeta::new(*from.key, true),
348 AccountMeta::new(*to.key, false),
349 ],
350 data: instruction_data,
351 };
352
353 invoke(
354 &transfer_instruction,
355 &[from.clone(), to.clone(), system_program.clone()],
356 )
357}