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