1use std::collections::HashMap;
2
3use borsh::BorshDeserialize;
4use light_client::rpc::{Rpc, RpcError};
5use light_compressible::{
6 compression_info::CompressionInfo,
7 config::CompressibleConfig as CtokenCompressibleConfig,
8 rent::{RentConfig, SLOTS_PER_EPOCH},
9};
10use light_sdk::interface::LightConfig;
11use light_token_interface::{
12 state::{Mint, Token, ACCOUNT_TYPE_MINT, ACCOUNT_TYPE_TOKEN_ACCOUNT},
13 LIGHT_TOKEN_PROGRAM_ID,
14};
15use solana_pubkey::Pubkey;
16
17use crate::{
18 litesvm_extensions::LiteSvmExtensions, registry_sdk::REGISTRY_PROGRAM_ID, LightProgramTest,
19};
20
21fn determine_account_type(data: &[u8]) -> Option<u8> {
26 const ACCOUNT_TYPE_OFFSET: usize = 165;
27
28 match data.len().cmp(&ACCOUNT_TYPE_OFFSET) {
29 std::cmp::Ordering::Less => None,
30 std::cmp::Ordering::Equal => Some(ACCOUNT_TYPE_TOKEN_ACCOUNT), std::cmp::Ordering::Greater => Some(data[ACCOUNT_TYPE_OFFSET]),
32 }
33}
34
35fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8, bool)> {
38 use light_zero_copy::traits::ZeroCopyAt;
39
40 let account_type = determine_account_type(data)?;
41
42 match account_type {
43 ACCOUNT_TYPE_TOKEN_ACCOUNT => {
44 let (ctoken, _) = Token::zero_copy_at(data).ok()?;
45 let ext = ctoken.get_compressible_extension()?;
46
47 let compression_info = CompressionInfo {
48 config_account_version: ext.info.config_account_version.into(),
49 compress_to_pubkey: ext.info.compress_to_pubkey,
50 account_version: ext.info.account_version,
51 lamports_per_write: ext.info.lamports_per_write.into(),
52 compression_authority: ext.info.compression_authority,
53 rent_sponsor: ext.info.rent_sponsor,
54 last_claimed_slot: ext.info.last_claimed_slot.into(),
55 rent_exemption_paid: ext.info.rent_exemption_paid.into(),
56 _reserved: ext.info._reserved.into(),
57 rent_config: RentConfig {
58 base_rent: ext.info.rent_config.base_rent.into(),
59 compression_cost: ext.info.rent_config.compression_cost.into(),
60 lamports_per_byte_per_epoch: ext.info.rent_config.lamports_per_byte_per_epoch,
61 max_funded_epochs: ext.info.rent_config.max_funded_epochs,
62 max_top_up: ext.info.rent_config.max_top_up.into(),
63 },
64 };
65 let compression_only = ext.compression_only != 0;
66 Some((compression_info, account_type, compression_only))
67 }
68 ACCOUNT_TYPE_MINT => {
69 let mint = Mint::deserialize(&mut &data[..]).ok()?;
70 Some((mint.compression, account_type, false))
72 }
73 _ => None,
74 }
75}
76
77pub type CompressibleAccountStore = HashMap<Pubkey, StoredCompressibleAccount>;
78
79#[derive(Eq, Hash, PartialEq)]
80pub struct StoredCompressibleAccount {
81 pub pubkey: Pubkey,
82 pub last_paid_slot: u64,
83 pub compression: CompressionInfo,
84 pub account_type: u8,
86 pub compression_only: bool,
88}
89
90#[derive(Debug, PartialEq, Copy, Clone)]
91pub struct FundingPoolConfig {
92 pub compressible_config_pda: Pubkey,
93 pub compression_authority_pda: Pubkey,
94 pub compression_authority_pda_bump: u8,
95 pub rent_sponsor_pda: Pubkey,
97 pub rent_sponsor_pda_bump: u8,
98}
99
100impl FundingPoolConfig {
101 pub fn new(version: u16) -> Self {
102 let config = CtokenCompressibleConfig::new_light_token(
103 version,
104 true,
105 Pubkey::default(),
106 Pubkey::default(),
107 RentConfig::default(),
108 );
109 let compressible_config =
110 CtokenCompressibleConfig::derive_pda(®ISTRY_PROGRAM_ID, version).0;
111 Self {
112 compressible_config_pda: compressible_config,
113 rent_sponsor_pda: config.rent_sponsor,
114 rent_sponsor_pda_bump: config.rent_sponsor_bump,
115 compression_authority_pda: config.compression_authority,
116 compression_authority_pda_bump: config.compression_authority_bump,
117 }
118 }
119
120 pub fn get_v1() -> Self {
121 Self::new(1)
122 }
123}
124
125pub async fn claim_and_compress(
126 rpc: &mut LightProgramTest,
127 stored_compressible_accounts: &mut CompressibleAccountStore,
128) -> Result<(), RpcError> {
129 use crate::forester::{claim_forester, compress_and_close_forester};
130
131 let forester_keypair = rpc.test_accounts.protocol.forester.insecure_clone();
132 let payer = rpc.get_payer().insecure_clone();
133
134 let compressible_ctoken_accounts = rpc
136 .context
137 .get_program_accounts(&Pubkey::from(LIGHT_TOKEN_PROGRAM_ID));
138
139 for account in compressible_ctoken_accounts
141 .iter()
142 .filter(|e| e.1.data.len() >= 165 && e.1.lamports > 0)
143 {
144 let Some((compression, account_type, compression_only)) =
146 extract_compression_info(&account.1.data)
147 else {
148 continue;
149 };
150
151 let base_lamports = rpc
152 .get_minimum_balance_for_rent_exemption(account.1.data.len())
153 .await
154 .unwrap();
155 let last_funded_epoch = compression
156 .get_last_funded_epoch(
157 account.1.data.len() as u64,
158 account.1.lamports,
159 base_lamports,
160 )
161 .unwrap();
162 let last_funded_slot = last_funded_epoch * SLOTS_PER_EPOCH;
163 stored_compressible_accounts.insert(
164 account.0,
165 StoredCompressibleAccount {
166 pubkey: account.0,
167 last_paid_slot: last_funded_slot,
168 compression,
169 account_type,
170 compression_only,
171 },
172 );
173 }
174
175 let current_slot = rpc.get_slot().await?;
176
177 let mut compress_accounts_compression_only = Vec::new();
179 let mut compress_accounts_normal = Vec::new();
180 let mut compress_mint_accounts = Vec::new();
181 let mut claim_accounts = Vec::new();
182
183 for (pubkey, stored_account) in stored_compressible_accounts.iter() {
185 let account = rpc.get_account(*pubkey).await?.unwrap();
186 let rent_exemption = rpc
187 .get_minimum_balance_for_rent_exemption(account.data.len())
188 .await?;
189
190 use light_compressible::rent::AccountRentState;
191
192 let compression = &stored_account.compression;
193
194 let state = AccountRentState {
196 num_bytes: account.data.len() as u64,
197 current_slot,
198 current_lamports: account.lamports,
199 last_claimed_slot: compression.last_claimed_slot,
200 };
201
202 match state.calculate_claimable_rent(&compression.rent_config, rent_exemption) {
204 None => {
205 if stored_account.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT {
207 if stored_account.compression_only {
209 compress_accounts_compression_only.push(*pubkey);
210 } else {
211 compress_accounts_normal.push(*pubkey);
212 }
213 } else if stored_account.account_type == ACCOUNT_TYPE_MINT {
214 compress_mint_accounts.push(*pubkey);
216 }
217 }
218 Some(claimable_amount) if claimable_amount > 0 => {
219 claim_accounts.push(*pubkey);
222 }
223 Some(_) => {
224 }
227 }
228 }
229
230 for token_accounts in claim_accounts.as_slice().chunks(20) {
232 claim_forester(rpc, token_accounts, &forester_keypair, &payer).await?;
233 }
234
235 const BATCH_SIZE: usize = 10;
238
239 for chunk in compress_accounts_compression_only.chunks(BATCH_SIZE) {
241 compress_and_close_forester(rpc, chunk, &forester_keypair, &payer, None).await?;
242 for account_pubkey in chunk {
243 stored_compressible_accounts.remove(account_pubkey);
244 }
245 }
246
247 for chunk in compress_accounts_normal.chunks(BATCH_SIZE) {
249 compress_and_close_forester(rpc, chunk, &forester_keypair, &payer, None).await?;
250 for account_pubkey in chunk {
251 stored_compressible_accounts.remove(account_pubkey);
252 }
253 }
254
255 for mint_pubkey in compress_mint_accounts {
257 compress_mint_forester(rpc, mint_pubkey, &payer).await?;
258 stored_compressible_accounts.remove(&mint_pubkey);
259 }
260
261 Ok(())
262}
263
264pub async fn auto_compress_program_pdas(
265 rpc: &mut LightProgramTest,
266 program_id: Pubkey,
267) -> Result<(), RpcError> {
268 use solana_instruction::AccountMeta;
269 use solana_sdk::signature::Signer;
270
271 let payer = rpc.get_payer().insecure_clone();
272
273 let config_pda = LightConfig::derive_pda(&program_id, 0).0;
274
275 let cfg_acc_opt = rpc.get_account(config_pda).await?;
276 let Some(cfg_acc) = cfg_acc_opt else {
277 return Ok(());
278 };
279 let cfg = LightConfig::try_from_slice(&cfg_acc.data)
280 .map_err(|e| RpcError::CustomError(format!("config deserialize: {e:?}")))?;
281 let rent_sponsor = cfg.rent_sponsor;
282 let compression_authority = payer.pubkey();
284 let address_tree = cfg.address_space[0];
285
286 let program_accounts = rpc.context.get_program_accounts(&program_id);
287
288 if program_accounts.is_empty() {
289 return Ok(());
290 }
291
292 let program_metas = vec![
298 AccountMeta::new(payer.pubkey(), true),
299 AccountMeta::new_readonly(config_pda, false),
300 AccountMeta::new(rent_sponsor, false),
301 AccountMeta::new(compression_authority, false),
302 ];
303
304 const BATCH_SIZE: usize = 5;
305 let mut chunk = Vec::with_capacity(BATCH_SIZE);
306 for (pubkey, account) in program_accounts
307 .into_iter()
308 .filter(|(_, acc)| acc.lamports > 0 && !acc.data.is_empty())
309 {
310 chunk.push((pubkey, account));
311 if chunk.len() == BATCH_SIZE {
312 try_compress_chunk(rpc, &program_id, &chunk, &program_metas, &address_tree).await;
313 chunk.clear();
314 }
315 }
316
317 if !chunk.is_empty() {
318 try_compress_chunk(rpc, &program_id, &chunk, &program_metas, &address_tree).await;
319 }
320
321 Ok(())
322}
323
324async fn try_compress_chunk(
325 rpc: &mut LightProgramTest,
326 program_id: &Pubkey,
327 chunk: &[(Pubkey, solana_sdk::account::Account)],
328 program_metas: &[solana_instruction::AccountMeta],
329 address_tree: &Pubkey,
330) {
331 use light_client::{indexer::Indexer, interface::instructions};
332 use light_compressed_account::address::derive_address;
333 use solana_sdk::signature::Signer;
334
335 for (pda, _acc) in chunk.iter() {
337 let addr = derive_address(
339 &pda.to_bytes(),
340 &address_tree.to_bytes(),
341 &program_id.to_bytes(),
342 );
343
344 let Ok(resp) = rpc.get_compressed_account(addr, None).await else {
346 continue;
347 };
348 let Some(cacc) = resp.value else {
349 continue;
350 };
351
352 let Ok(proof_with_context) = rpc
354 .get_validity_proof(vec![cacc.hash], vec![], None)
355 .await
356 .map(|r| r.value)
357 else {
358 continue;
359 };
360
361 let Ok(ix) = instructions::build_compress_accounts_idempotent(
363 program_id,
364 &instructions::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR,
365 &[*pda],
366 program_metas,
367 proof_with_context,
368 )
369 .map_err(|e| e.to_string()) else {
370 continue;
371 };
372
373 let payer = rpc.get_payer().insecure_clone();
374 let payer_pubkey = payer.pubkey();
375
376 let _ = rpc
378 .create_and_send_transaction(std::slice::from_ref(&ix), &payer_pubkey, &[&payer])
379 .await;
380 }
381}
382
383async fn compress_mint_forester(
386 rpc: &mut LightProgramTest,
387 mint_pubkey: Pubkey,
388 payer: &solana_sdk::signature::Keypair,
389) -> Result<(), RpcError> {
390 use light_client::indexer::Indexer;
391 use light_compressed_account::instruction_data::traits::LightInstructionData;
392 use light_compressible::config::CompressibleConfig;
393 use light_token::compressed_token::mint_action::MintActionMetaConfig;
394 use light_token_interface::instructions::mint_action::{
395 CompressAndCloseMintAction, MintActionCompressedInstructionData, MintWithContext,
396 };
397 use solana_sdk::signature::Signer;
398
399 let mint_account = rpc
401 .get_account(mint_pubkey)
402 .await?
403 .ok_or_else(|| RpcError::CustomError(format!("Mint account {} not found", mint_pubkey)))?;
404
405 let mint: Mint = BorshDeserialize::deserialize(&mut mint_account.data.as_slice())
407 .map_err(|e| RpcError::CustomError(format!("Failed to deserialize Mint: {:?}", e)))?;
408
409 let compressed_mint_address = mint.metadata.compressed_address();
410 let rent_sponsor = Pubkey::from(mint.compression.rent_sponsor);
411
412 let compressed_mint_account = rpc
414 .get_compressed_account(compressed_mint_address, None)
415 .await?
416 .value
417 .ok_or(RpcError::AccountDoesNotExist(format!(
418 "Compressed mint {:?}",
419 compressed_mint_address
420 )))?;
421
422 let rpc_proof_result = rpc
424 .get_validity_proof(vec![compressed_mint_account.hash], vec![], None)
425 .await?
426 .value;
427
428 let compressed_mint_inputs = MintWithContext {
433 prove_by_index: rpc_proof_result.accounts[0].root_index.proof_by_index(),
434 leaf_index: compressed_mint_account.leaf_index,
435 root_index: rpc_proof_result.accounts[0]
436 .root_index
437 .root_index()
438 .unwrap_or_default(),
439 address: compressed_mint_address,
440 mint: None, };
442
443 let instruction_data = MintActionCompressedInstructionData::new(
445 compressed_mint_inputs,
446 rpc_proof_result.proof.into(),
447 )
448 .with_compress_and_close_mint(CompressAndCloseMintAction { idempotent: 1 });
449
450 let state_tree_info = rpc_proof_result.accounts[0].tree_info;
452
453 let config_address = CompressibleConfig::light_token_v1_config_pda();
455 let meta_config = MintActionMetaConfig::new(
456 payer.pubkey(),
457 payer.pubkey(), state_tree_info.tree,
459 state_tree_info.queue,
460 state_tree_info.queue,
461 )
462 .with_compressible_mint(mint_pubkey, config_address, rent_sponsor);
463
464 let account_metas = meta_config.to_account_metas();
465
466 let data = instruction_data
468 .data()
469 .map_err(|e| RpcError::CustomError(format!("Failed to serialize instruction: {:?}", e)))?;
470
471 let instruction = solana_instruction::Instruction {
473 program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID),
474 accounts: account_metas,
475 data,
476 };
477
478 rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer])
480 .await?;
481
482 Ok(())
483}