light_sdk/compressible/
decompress_runtime.rs

1//! Traits and processor for decompress_accounts_idempotent instruction.
2use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo;
3#[cfg(feature = "cpi-context")]
4use light_sdk_types::cpi_context_write::CpiContextWriteAccounts;
5use light_sdk_types::{
6    cpi_accounts::CpiAccountsConfig,
7    instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, CpiSigner,
8};
9use solana_account_info::AccountInfo;
10use solana_msg::msg;
11use solana_program_error::ProgramError;
12use solana_pubkey::Pubkey;
13
14use crate::{
15    cpi::{
16        v2::{CpiAccounts, LightSystemProgramCpi},
17        InvokeLightSystemProgram, LightCpiInstruction,
18    },
19    AnchorDeserialize, AnchorSerialize, LightDiscriminator,
20};
21
22/// Trait for account variants that can be checked for token vs PDA type.
23pub trait HasTokenVariant {
24    /// Returns true if this variant represents a token account (PackedCTokenData).
25    fn is_packed_ctoken(&self) -> bool;
26}
27
28/// Trait for CToken seed providers.
29///
30/// Also defined in compressed-token-sdk for token-specific runtime helpers.
31pub trait CTokenSeedProvider: Copy {
32    /// Type of accounts struct needed for seed derivation.
33    type Accounts<'info>;
34
35    /// Get seeds for the token account PDA (used for decompression).
36    fn get_seeds<'a, 'info>(
37        &self,
38        accounts: &'a Self::Accounts<'info>,
39        remaining_accounts: &'a [AccountInfo<'info>],
40    ) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;
41
42    /// Get authority seeds for signing during compression.
43    fn get_authority_seeds<'a, 'info>(
44        &self,
45        accounts: &'a Self::Accounts<'info>,
46        remaining_accounts: &'a [AccountInfo<'info>],
47    ) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;
48}
49
50/// Context trait for decompression.
51pub trait DecompressContext<'info> {
52    /// The compressed account data type (wraps program's variant enum)
53    type CompressedData: HasTokenVariant;
54
55    /// Packed token data type
56    type PackedTokenData;
57
58    /// Compressed account metadata type (standardized)
59    type CompressedMeta: Clone;
60
61    /// Seed parameters type containing data.* field values from instruction data
62    type SeedParams;
63
64    // Account accessors
65    fn fee_payer(&self) -> &AccountInfo<'info>;
66    fn config(&self) -> &AccountInfo<'info>;
67    fn rent_sponsor(&self) -> &AccountInfo<'info>;
68    fn ctoken_rent_sponsor(&self) -> Option<&AccountInfo<'info>>;
69    fn ctoken_program(&self) -> Option<&AccountInfo<'info>>;
70    fn ctoken_cpi_authority(&self) -> Option<&AccountInfo<'info>>;
71    fn ctoken_config(&self) -> Option<&AccountInfo<'info>>;
72
73    /// Collect and unpack compressed accounts into PDAs and tokens.
74    ///
75    /// Caller program-specific: handles variant matching and PDA seed derivation.
76    #[allow(clippy::type_complexity)]
77    #[allow(clippy::too_many_arguments)]
78    fn collect_pda_and_token<'b>(
79        &self,
80        cpi_accounts: &CpiAccounts<'b, 'info>,
81        address_space: Pubkey,
82        compressed_accounts: Vec<Self::CompressedData>,
83        solana_accounts: &[AccountInfo<'info>],
84        seed_params: Option<&Self::SeedParams>,
85    ) -> Result<(
86        Vec<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
87        Vec<(Self::PackedTokenData, Self::CompressedMeta)>
88    ), ProgramError>;
89
90    /// Process token decompression.
91    ///
92    /// Caller program-specific: handles token account creation and seed derivation.
93    #[allow(clippy::too_many_arguments)]
94    fn process_tokens<'b>(
95        &self,
96        remaining_accounts: &[AccountInfo<'info>],
97        fee_payer: &AccountInfo<'info>,
98        ctoken_program: &AccountInfo<'info>,
99        ctoken_rent_sponsor: &AccountInfo<'info>,
100        ctoken_cpi_authority: &AccountInfo<'info>,
101        ctoken_config: &AccountInfo<'info>,
102        config: &AccountInfo<'info>,
103        ctoken_accounts: Vec<(Self::PackedTokenData, Self::CompressedMeta)>,
104        proof: crate::instruction::ValidityProof,
105        cpi_accounts: &CpiAccounts<'b, 'info>,
106        post_system_accounts: &[AccountInfo<'info>],
107        has_pdas: bool,
108    ) -> Result<(), ProgramError>;
109}
110
111/// Trait for PDA types that can derive seeds with full account context access.
112///
113/// - A: The accounts struct type (typically DecompressAccountsIdempotent<'info>)
114/// - S: The SeedParams struct containing data.* field values from instruction data
115///
116/// This allows PDA seeds to reference:
117/// - `data.*` fields from instruction parameters (seed_params.field)
118/// - `ctx.*` accounts from the instruction context (accounts.field)
119///
120/// For off-chain PDA derivation, use the generated client helper functions (get_*_seeds).
121pub trait PdaSeedDerivation<A, S> {
122    fn derive_pda_seeds_with_accounts(
123        &self,
124        program_id: &Pubkey,
125        accounts: &A,
126        seed_params: &S,
127    ) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;
128}
129
130/// Check compressed accounts to determine if we have tokens and/or PDAs.
131#[inline(never)]
132pub fn check_account_types<T: HasTokenVariant>(compressed_accounts: &[T]) -> (bool, bool) {
133    let (mut has_tokens, mut has_pdas) = (false, false);
134    for account in compressed_accounts {
135        if account.is_packed_ctoken() {
136            has_tokens = true;
137        } else {
138            has_pdas = true;
139        }
140        if has_tokens && has_pdas {
141            break;
142        }
143    }
144    (has_tokens, has_pdas)
145}
146
147/// Handler for unpacking and preparing a single PDA variant for decompression.
148#[inline(never)]
149#[allow(clippy::too_many_arguments)]
150pub fn handle_packed_pda_variant<'a, 'b, 'info, T, P, A, S>(
151    accounts_rent_sponsor: &AccountInfo<'info>,
152    cpi_accounts: &CpiAccounts<'b, 'info>,
153    address_space: Pubkey,
154    solana_account: &AccountInfo<'info>,
155    index: usize,
156    packed: &P,
157    meta: &CompressedAccountMetaNoLamportsNoAddress,
158    post_system_accounts: &[AccountInfo<'info>],
159    compressed_pda_infos: &mut Vec<CompressedAccountInfo>,
160    program_id: &Pubkey,
161    seed_accounts: &A,
162    seed_params: Option<&S>,
163) -> Result<(), ProgramError>
164where
165    T: PdaSeedDerivation<A, S>
166        + Clone
167        + crate::account::Size
168        + LightDiscriminator
169        + Default
170        + AnchorSerialize
171        + AnchorDeserialize
172        + crate::compressible::HasCompressionInfo
173        + 'info,
174    P: crate::compressible::Unpack<Unpacked = T>,
175    S: Default,
176{
177    let data: T = P::unpack(packed, post_system_accounts)?;
178
179    // CHECK: pda match
180    // Call the method with account context and seed params
181    // Note: Some implementations may use S::default() when seed_params is None for static seeds
182    let (seeds_vec, derived_pda) = if let Some(params) = seed_params {
183        data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)?
184    } else {
185        let default_params = S::default();
186        data.derive_pda_seeds_with_accounts(program_id, seed_accounts, &default_params)?
187    };
188    if derived_pda != *solana_account.key {
189        msg!(
190            "Derived PDA does not match account at index {}: expected {:?}, got {:?}, seeds: {:?}",
191            index,
192            solana_account.key,
193            derived_pda,
194            seeds_vec
195        );
196        return Err(ProgramError::from(
197            crate::error::LightSdkError::ConstraintViolation,
198        ));
199    }
200
201    // prepare decompression
202    let compressed_infos = {
203        let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v| v.as_slice()).collect();
204        crate::compressible::decompress_idempotent::prepare_account_for_decompression_idempotent::<T>(
205            program_id,
206            data,
207            crate::compressible::decompress_idempotent::into_compressed_meta_with_address(
208                meta,
209                solana_account,
210                address_space,
211                program_id,
212            ),
213            solana_account,
214            accounts_rent_sponsor,
215            cpi_accounts,
216            seed_refs.as_slice(),
217        )?
218    };
219    compressed_pda_infos.extend(compressed_infos);
220    Ok(())
221}
222
223/// Processor for decompress_accounts_idempotent.
224#[inline(never)]
225#[allow(clippy::too_many_arguments)]
226pub fn process_decompress_accounts_idempotent<'info, Ctx>(
227    ctx: &Ctx,
228    remaining_accounts: &[AccountInfo<'info>],
229    compressed_accounts: Vec<Ctx::CompressedData>,
230    proof: crate::instruction::ValidityProof,
231    system_accounts_offset: u8,
232    cpi_signer: CpiSigner,
233    program_id: &Pubkey,
234    seed_params: Option<&Ctx::SeedParams>,
235) -> Result<(), ProgramError>
236where
237    Ctx: DecompressContext<'info>,
238{
239    let compression_config =
240        crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
241    let address_space = compression_config.address_space[0];
242
243    let (has_tokens, has_pdas) = check_account_types(&compressed_accounts);
244    if !has_tokens && !has_pdas {
245        return Ok(());
246    }
247
248    let system_accounts_offset_usize = system_accounts_offset as usize;
249    if system_accounts_offset_usize > remaining_accounts.len() {
250        return Err(ProgramError::NotEnoughAccountKeys);
251    }
252
253    let cpi_accounts = if has_tokens {
254        CpiAccounts::new_with_config(
255            ctx.fee_payer(),
256            &remaining_accounts[system_accounts_offset_usize..],
257            CpiAccountsConfig::new_with_cpi_context(cpi_signer),
258        )
259    } else {
260        CpiAccounts::new(
261            ctx.fee_payer(),
262            &remaining_accounts[system_accounts_offset_usize..],
263            cpi_signer,
264        )
265    };
266
267    let pda_accounts_start = remaining_accounts
268        .len()
269        .checked_sub(compressed_accounts.len())
270        .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?;
271    let solana_accounts = remaining_accounts
272        .get(pda_accounts_start..)
273        .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?;
274    let post_system_offset = cpi_accounts.system_accounts_end_offset();
275    let all_infos = cpi_accounts.account_infos();
276    let post_system_accounts = all_infos
277        .get(post_system_offset..)
278        .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?;
279
280    // Call trait method for program-specific collection
281    let (compressed_pda_infos, compressed_token_accounts) = ctx.collect_pda_and_token(
282        &cpi_accounts,
283        address_space,
284        compressed_accounts,
285        solana_accounts,
286        seed_params,
287    )?;
288
289    let has_pdas = !compressed_pda_infos.is_empty();
290    let has_tokens = !compressed_token_accounts.is_empty();
291    if !has_pdas && !has_tokens {
292        return Ok(());
293    }
294
295    let fee_payer = ctx.fee_payer();
296
297    // Decompress PDAs via LightSystemProgram
298    #[cfg(feature = "cpi-context")]
299    if has_pdas && has_tokens {
300        let authority = cpi_accounts
301            .authority()
302            .map_err(|_| ProgramError::MissingRequiredSignature)?;
303        let cpi_context = cpi_accounts
304            .cpi_context()
305            .map_err(|_| ProgramError::MissingRequiredSignature)?;
306        let system_cpi_accounts = CpiContextWriteAccounts {
307            fee_payer,
308            authority,
309            cpi_context,
310            cpi_signer,
311        };
312
313        LightSystemProgramCpi::new_cpi(cpi_signer, proof)
314            .with_account_infos(&compressed_pda_infos)
315            .write_to_cpi_context_first()
316            .invoke_write_to_cpi_context_first(system_cpi_accounts)?;
317    } else if has_pdas {
318        LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof)
319            .with_account_infos(&compressed_pda_infos)
320            .invoke(cpi_accounts.clone())?;
321    }
322
323    // TODO: fix this
324    #[cfg(not(feature = "cpi-context"))]
325    if has_pdas {
326        LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof)
327            .with_account_infos(&compressed_pda_infos)
328            .invoke(cpi_accounts.clone())?;
329    }
330
331    // Decompress tokens via trait method
332    if has_tokens {
333        let ctoken_program = ctx
334            .ctoken_program()
335            .ok_or(ProgramError::NotEnoughAccountKeys)?;
336        let ctoken_rent_sponsor = ctx
337            .ctoken_rent_sponsor()
338            .ok_or(ProgramError::NotEnoughAccountKeys)?;
339        let ctoken_cpi_authority = ctx
340            .ctoken_cpi_authority()
341            .ok_or(ProgramError::NotEnoughAccountKeys)?;
342        let ctoken_config = ctx
343            .ctoken_config()
344            .ok_or(ProgramError::NotEnoughAccountKeys)?;
345
346        ctx.process_tokens(
347            remaining_accounts,
348            fee_payer,
349            ctoken_program,
350            ctoken_rent_sponsor,
351            ctoken_cpi_authority,
352            ctoken_config,
353            ctx.config(),
354            compressed_token_accounts,
355            proof,
356            &cpi_accounts,
357            post_system_accounts,
358            has_pdas,
359        )?;
360    }
361
362    Ok(())
363}