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) -> Result<Self::Packed, ProgramError>;
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) -> Result<&CompressionInfo, ProgramError>;
38 fn compression_info_mut(&mut self) -> Result<&mut CompressionInfo, ProgramError>;
39 fn compression_info_mut_opt(&mut self) -> &mut Option<CompressionInfo>;
40 fn set_compression_info_none(&mut self) -> Result<(), ProgramError>;
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, PartialEq, 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(cfg: &crate::interface::LightConfig, current_slot: u64) -> Self {
90 Self {
91 config_version: cfg.version as u16,
92 lamports_per_write: cfg.write_top_up,
93 last_claimed_slot: current_slot,
94 rent_config: cfg.rent_config,
95 state: CompressionState::Decompressed,
96 }
97 }
98
99 pub fn new_decompressed() -> Result<Self, crate::ProgramError> {
102 Ok(Self {
103 config_version: 0,
104 lamports_per_write: 0,
105 last_claimed_slot: Clock::get()?.slot,
106 rent_config: RentConfig::default(),
107 state: CompressionState::Decompressed,
108 })
109 }
110
111 pub fn bump_last_claimed_slot(&mut self) -> Result<(), crate::ProgramError> {
113 self.last_claimed_slot = Clock::get()?.slot;
114 Ok(())
115 }
116
117 pub fn set_last_claimed_slot(&mut self, slot: u64) {
119 self.last_claimed_slot = slot;
120 }
121
122 pub fn last_claimed_slot(&self) -> u64 {
124 self.last_claimed_slot
125 }
126
127 pub fn set_compressed(&mut self) {
128 self.state = CompressionState::Compressed;
129 }
130
131 pub fn is_compressed(&self) -> bool {
132 self.state == CompressionState::Compressed
133 }
134}
135
136impl CompressionInfo {
137 pub fn calculate_top_up_lamports(
144 &self,
145 num_bytes: u64,
146 current_slot: u64,
147 current_lamports: u64,
148 rent_exemption_lamports: u64,
149 ) -> u64 {
150 use light_compressible::rent::AccountRentState;
151
152 let state = AccountRentState {
153 num_bytes,
154 current_slot,
155 current_lamports,
156 last_claimed_slot: self.last_claimed_slot(),
157 };
158
159 if let Some(rent_deficit) =
161 state.is_compressible(&self.rent_config, rent_exemption_lamports)
162 {
163 return self.lamports_per_write as u64 + rent_deficit;
164 }
165
166 let epochs_funded_ahead =
167 state.epochs_funded_ahead(&self.rent_config, rent_exemption_lamports);
168
169 if epochs_funded_ahead >= self.rent_config.max_funded_epochs as u64 {
171 return 0;
172 }
173
174 self.lamports_per_write as u64
176 }
177
178 pub fn top_up_rent<'a>(
190 &self,
191 account_info: &AccountInfo<'a>,
192 payer_info: &AccountInfo<'a>,
193 system_program_info: &AccountInfo<'a>,
194 ) -> Result<(), crate::ProgramError> {
195 use solana_clock::Clock;
196 use solana_sysvar::{rent::Rent, Sysvar};
197
198 let bytes = account_info.data_len() as u64;
199 let current_lamports = account_info.lamports();
200 let current_slot = Clock::get()?.slot;
201 let rent_exemption_lamports = Rent::get()?.minimum_balance(bytes as usize);
202
203 let top_up = self.calculate_top_up_lamports(
204 bytes,
205 current_slot,
206 current_lamports,
207 rent_exemption_lamports,
208 );
209
210 if top_up > 0 {
211 transfer_lamports_cpi(payer_info, account_info, system_program_info, top_up)?;
215 }
216
217 Ok(())
218 }
219}
220
221pub trait Space {
222 const INIT_SPACE: usize;
223}
224
225impl Space for CompressionInfo {
226 const INIT_SPACE: usize = 2 + 4 + 8 + core::mem::size_of::<RentConfig>() + 1;
228}
229
230#[cfg(feature = "anchor")]
231impl anchor_lang::Space for CompressionInfo {
232 const INIT_SPACE: usize = <Self as Space>::INIT_SPACE;
233}
234
235pub const OPTION_COMPRESSION_INFO_SPACE: usize = 1 + CompressionInfo::INIT_SPACE;
238
239#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
241pub struct CompressedAccountData<T> {
242 pub meta: CompressedAccountMetaNoLamportsNoAddress,
243 pub data: T,
244}
245
246pub fn claim_completed_epoch_rent<'info, A>(
249 account_info: &AccountInfo<'info>,
250 account_data: &mut A,
251 rent_sponsor: &AccountInfo<'info>,
252) -> Result<Option<u64>, ProgramError>
253where
254 A: HasCompressionInfo,
255{
256 use light_compressible::rent::{AccountRentState, SLOTS_PER_EPOCH};
257 use solana_sysvar::rent::Rent;
258
259 let current_slot = Clock::get()?.slot;
260 let bytes = account_info.data_len() as u64;
261 let current_lamports = account_info.lamports();
262 let rent_exemption_lamports = Rent::get()
263 .map_err(|_| ProgramError::Custom(0))?
264 .minimum_balance(bytes as usize);
265
266 let ci = account_data.compression_info_mut()?;
267 let state = AccountRentState {
268 num_bytes: bytes,
269 current_slot,
270 current_lamports,
271 last_claimed_slot: ci.last_claimed_slot(),
272 };
273
274 if state
276 .is_compressible(&ci.rent_config, rent_exemption_lamports)
277 .is_some()
278 {
279 return Ok(None);
280 }
281
282 let claimable = state.calculate_claimable_rent(&ci.rent_config, rent_exemption_lamports);
284 if let Some(amount) = claimable {
285 if amount > 0 {
286 let completed_epochs = state.get_completed_epochs();
288 ci.set_last_claimed_slot(
289 ci.last_claimed_slot()
290 .saturating_add(completed_epochs * SLOTS_PER_EPOCH),
291 );
292
293 {
295 let mut src = account_info
296 .try_borrow_mut_lamports()
297 .map_err(|_| ProgramError::Custom(0))?;
298 let mut dst = rent_sponsor
299 .try_borrow_mut_lamports()
300 .map_err(|_| ProgramError::Custom(0))?;
301 let new_src = src
302 .checked_sub(amount)
303 .ok_or(ProgramError::InsufficientFunds)?;
304 let new_dst = dst.checked_add(amount).ok_or(ProgramError::Custom(0))?;
305 **src = new_src;
306 **dst = new_dst;
307 }
308 return Ok(Some(amount));
309 }
310 }
311 Ok(Some(0))
312}
313
314fn transfer_lamports_cpi<'a>(
323 from: &AccountInfo<'a>,
324 to: &AccountInfo<'a>,
325 system_program: &AccountInfo<'a>,
326 lamports: u64,
327) -> Result<(), ProgramError> {
328 use solana_cpi::invoke;
329 use solana_instruction::{AccountMeta, Instruction};
330
331 const SYSTEM_PROGRAM_ID: [u8; 32] = [
333 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,
334 0, 0,
335 ];
336
337 let mut instruction_data = vec![2, 0, 0, 0];
339 instruction_data.extend_from_slice(&lamports.to_le_bytes());
340
341 let transfer_instruction = Instruction {
342 program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
343 accounts: vec![
344 AccountMeta::new(*from.key, true),
345 AccountMeta::new(*to.key, false),
346 ],
347 data: instruction_data,
348 };
349
350 invoke(
351 &transfer_instruction,
352 &[from.clone(), to.clone(), system_program.clone()],
353 )
354}