Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions js/compressed-token/src/v3/actions/create-mint-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ export async function createMintInterface(

// Default: light-token mint creation
if (!('secretKey' in mintAuthority)) {
throw new Error(
'mintAuthority must be a Signer for light-token mints',
);
throw new Error('mintAuthority must be a Signer for light-token mints');
}
if (
addressTreeInfo &&
Expand Down
4 changes: 1 addition & 3 deletions js/compressed-token/src/v3/get-mint-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ export async function getMintInterface(
);

if (!compressedAccount?.data?.data) {
throw new Error(
`Light mint not found for ${address.toString()}`,
);
throw new Error(`Light mint not found for ${address.toString()}`);
}

const compressedData = Buffer.from(compressedAccount.data.data);
Expand Down
4 changes: 4 additions & 0 deletions sdk-libs/sdk-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ thiserror = { workspace = true }

[dev-dependencies]
solana-pubkey = { workspace = true }
solana-account-info = { workspace = true }
light-account-checks = { workspace = true, features = ["solana", "test-only"] }
light-compressed-account = { workspace = true, features = ["keccak"] }
rand = { workspace = true }

[lints.rust.unexpected_cfgs]
level = "allow"
Expand Down
72 changes: 52 additions & 20 deletions sdk-libs/sdk-types/src/interface/program/compression/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,33 @@ pub type CompressDispatchFn<AI> = fn(
ctx: &mut CompressCtx<'_, AI>,
) -> Result<(), LightSdkTypesError>;

/// Process compress-and-close for PDA accounts (idempotent).
///
/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`,
/// then invokes the Light system program CPI to commit compressed state,
/// and closes the PDA accounts (transferring lamports to rent_sponsor).
///
/// Idempotent: if any account is not yet compressible (rent function check fails),
/// the entire batch is silently skipped.
#[inline(never)]
pub fn process_compress_pda_accounts_idempotent<AI: AccountInfoTrait + Clone>(
remaining_accounts: &[AI],
/// Result of building CPI data for compress-and-close.
pub struct CompressPdaBuilt<'a, AI: AccountInfoTrait + Clone> {
pub cpi_ix_data: InstructionDataInvokeCpiWithAccountInfo,
pub cpi_accounts: CpiAccounts<'a, AI>,
pub pda_indices_to_close: Vec<usize>,
}

/// Validates accounts and builds CPI data for compress-and-close.
/// Returns None when any account is non-compressible (idempotent skip, no CPI needed).
pub fn build_compress_pda_cpi_data<'a, AI: AccountInfoTrait + Clone>(
remaining_accounts: &'a [AI],
params: &CompressAndCloseParams,
dispatch_fn: CompressDispatchFn<AI>,
cpi_signer: CpiSigner,
program_id: &[u8; 32],
) -> Result<(), LightSdkTypesError> {
) -> Result<Option<CompressPdaBuilt<'a, AI>>, LightSdkTypesError> {
let system_accounts_offset = params.system_accounts_offset as usize;
let num_pdas = params.compressed_accounts.len();

if num_pdas == 0 {
return Err(LightSdkTypesError::InvalidInstructionData);
}

if system_accounts_offset > remaining_accounts.len() {
return Err(LightSdkTypesError::InvalidInstructionData);
}

// 2. Load and validate config
let config = LightConfig::load_checked(&remaining_accounts[CONFIG_INDEX], program_id)?;

Expand Down Expand Up @@ -120,7 +124,7 @@ pub fn process_compress_pda_accounts_idempotent<AI: AccountInfoTrait + Clone>(

// 6. Idempotent: if any account is not yet compressible, skip entire batch
if has_non_compressible {
return Ok(());
return Ok(None);
}

// 7. Build CPI instruction data
Expand All @@ -138,14 +142,42 @@ pub fn process_compress_pda_accounts_idempotent<AI: AccountInfoTrait + Clone>(
cpi_signer,
);

// 9. Invoke Light system program CPI
cpi_ix_data.invoke::<AI>(cpi_accounts)?;
Ok(Some(CompressPdaBuilt {
cpi_ix_data,
cpi_accounts,
pda_indices_to_close,
}))
}

// 10. Close PDA accounts, transferring lamports to rent_sponsor
for pda_index in &pda_indices_to_close {
light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor)
.map_err(LightSdkTypesError::AccountError)?;
/// Process compress-and-close for PDA accounts (idempotent).
///
/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`,
/// then invokes the Light system program CPI to commit compressed state,
/// and closes the PDA accounts (transferring lamports to rent_sponsor).
///
/// Idempotent: if any account is not yet compressible (rent function check fails),
/// the entire batch is silently skipped.
#[inline(never)]
pub fn process_compress_pda_accounts_idempotent<AI: AccountInfoTrait + Clone>(
remaining_accounts: &[AI],
params: &CompressAndCloseParams,
dispatch_fn: CompressDispatchFn<AI>,
cpi_signer: CpiSigner,
program_id: &[u8; 32],
) -> Result<(), LightSdkTypesError> {
if let Some(built) = build_compress_pda_cpi_data(
remaining_accounts,
params,
dispatch_fn,
cpi_signer,
program_id,
)? {
let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX];
built.cpi_ix_data.invoke::<AI>(built.cpi_accounts)?;
for pda_index in &built.pda_indices_to_close {
light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor)
.map_err(LightSdkTypesError::AccountError)?;
}
}

Ok(())
}
167 changes: 130 additions & 37 deletions sdk-libs/sdk-types/src/interface/program/decompression/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,21 @@ pub struct DecompressCtx<'a, AI: AccountInfoTrait + Clone> {
// PDA-only Processor
// ============================================================================

/// Process decompression for PDA accounts (idempotent, PDA-only).
///
/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`,
/// then invokes the Light system program CPI to commit compressed state.
///
/// Idempotent: if a PDA is already initialized, it is silently skipped.
///
/// # Account layout in remaining_accounts:
/// - `[0]`: fee_payer (Signer, mut)
/// - `[1]`: config (LightConfig PDA)
/// - `[2]`: rent_sponsor (mut)
/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts
/// - `[hot_accounts_start..]`: PDA accounts to decompress into
#[inline(never)]
pub fn process_decompress_pda_accounts_idempotent<AI, V>(
remaining_accounts: &[AI],
/// Result of building CPI data for PDA decompression.
pub struct DecompressPdaBuilt<'a, AI: AccountInfoTrait + Clone> {
pub cpi_ix_data: InstructionDataInvokeCpiWithAccountInfo,
pub cpi_accounts: CpiAccounts<'a, AI>,
}

/// Validates accounts and builds CPI data for PDA decompression.
/// Returns None when all accounts already initialized (idempotent skip, no CPI needed).
pub fn build_decompress_pda_cpi_data<'a, AI, V>(
remaining_accounts: &'a [AI],
params: &DecompressIdempotentParams<V>,
cpi_signer: CpiSigner,
program_id: &[u8; 32],
current_slot: u64,
) -> Result<(), LightSdkTypesError>
) -> Result<Option<DecompressPdaBuilt<'a, AI>>, LightSdkTypesError>
where
AI: AccountInfoTrait + Clone,
V: DecompressVariant<AI>,
Expand Down Expand Up @@ -234,51 +228,90 @@ where

// 6. If no compressed accounts were produced (all already initialized), skip CPI
if compressed_account_infos.is_empty() {
return Ok(());
return Ok(None);
}

// 7. Build and invoke Light system program CPI
// 7. Build CPI instruction data
let mut cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo::new(
program_id.into(),
cpi_signer.bump,
params.proof.into(),
);
cpi_ix_data.account_infos = compressed_account_infos;
cpi_ix_data.invoke::<AI>(cpi_accounts)?;

Ok(())
Ok(Some(DecompressPdaBuilt {
cpi_ix_data,
cpi_accounts,
}))
}

// ============================================================================
// Full Processor (PDA + Token)
// ============================================================================

/// Process decompression for both PDA and token accounts (idempotent).
/// Process decompression for PDA accounts (idempotent, PDA-only).
///
/// Handles the combined PDA + token decompression flow:
/// - PDA accounts are decompressed first
/// - If both PDAs and tokens exist, PDA data is written to CPI context first
/// - Token accounts are decompressed via Transfer2 CPI to the light token program
/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`,
/// then invokes the Light system program CPI to commit compressed state.
///
/// Idempotent: if a PDA is already initialized, it is silently skipped.
///
/// # Account layout in remaining_accounts:
/// - `[0]`: fee_payer (Signer, mut)
/// - `[1]`: config (LightConfig PDA)
/// - `[2]`: rent_sponsor (mut)
/// - `[3]`: ctoken_rent_sponsor (mut)
/// - `[4]`: light_token_program
/// - `[5]`: cpi_authority
/// - `[6]`: ctoken_compressible_config
/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts
/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens)
#[cfg(feature = "token")]
/// - `[hot_accounts_start..]`: PDA accounts to decompress into
#[inline(never)]
pub fn process_decompress_accounts_idempotent<AI, V>(
pub fn process_decompress_pda_accounts_idempotent<AI, V>(
remaining_accounts: &[AI],
params: &DecompressIdempotentParams<V>,
cpi_signer: CpiSigner,
program_id: &[u8; 32],
current_slot: u64,
) -> Result<(), LightSdkTypesError>
where
AI: AccountInfoTrait + Clone,
V: DecompressVariant<AI>,
{
if let Some(built) = build_decompress_pda_cpi_data(
remaining_accounts,
params,
cpi_signer,
program_id,
current_slot,
)? {
built.cpi_ix_data.invoke::<AI>(built.cpi_accounts)?;
}
Ok(())
}

// ============================================================================
// Full Processor (PDA + Token)
// ============================================================================

/// Result of building CPI data for the combined PDA + token decompression.
#[cfg(feature = "token")]
pub struct DecompressAccountsBuilt<'a, AI: AccountInfoTrait + Clone> {
pub cpi_accounts: CpiAccounts<'a, AI>,
pub compressed_account_infos: Vec<CompressedAccountInfo>,
pub has_pda_accounts: bool,
pub has_token_accounts: bool,
pub cpi_context: bool,
pub in_token_data: Vec<MultiInputTokenDataWithContext>,
pub in_tlv: Option<Vec<Vec<ExtensionInstructionData>>>,
pub token_seeds: Vec<Vec<u8>>,
}

/// Validates accounts, dispatches all variants, and collects CPI inputs for
/// the combined PDA + token decompression.
///
/// Returns the assembled [`DecompressAccountsBuilt`] on success.
/// The caller is responsible for executing the actual CPIs.
#[cfg(feature = "token")]
pub fn build_decompress_accounts_cpi_data<'a, AI, V>(
remaining_accounts: &'a [AI],
params: &DecompressIdempotentParams<V>,
cpi_signer: CpiSigner,
program_id: &[u8; 32],
current_slot: u64,
) -> Result<DecompressAccountsBuilt<'a, AI>, LightSdkTypesError>
where
AI: AccountInfoTrait + Clone,
V: DecompressVariant<AI>,
Expand Down Expand Up @@ -388,6 +421,66 @@ where
)
};

Ok(DecompressAccountsBuilt {
cpi_accounts,
compressed_account_infos,
has_pda_accounts,
has_token_accounts,
cpi_context,
in_token_data,
in_tlv,
token_seeds,
})
}

/// Process decompression for both PDA and token accounts (idempotent).
///
/// Handles the combined PDA + token decompression flow:
/// - PDA accounts are decompressed first
/// - If both PDAs and tokens exist, PDA data is written to CPI context first
/// - Token accounts are decompressed via Transfer2 CPI to the light token program
///
/// # Account layout in remaining_accounts:
/// - `[0]`: fee_payer (Signer, mut)
/// - `[1]`: config (LightConfig PDA)
/// - `[2]`: rent_sponsor (mut)
/// - `[3]`: ctoken_rent_sponsor (mut)
/// - `[4]`: light_token_program
/// - `[5]`: cpi_authority
/// - `[6]`: ctoken_compressible_config
/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts
/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens)
#[cfg(feature = "token")]
#[inline(never)]
pub fn process_decompress_accounts_idempotent<AI, V>(
remaining_accounts: &[AI],
params: &DecompressIdempotentParams<V>,
cpi_signer: CpiSigner,
program_id: &[u8; 32],
current_slot: u64,
) -> Result<(), LightSdkTypesError>
where
AI: AccountInfoTrait + Clone,
V: DecompressVariant<AI>,
{
let DecompressAccountsBuilt {
cpi_accounts,
compressed_account_infos,
has_pda_accounts,
has_token_accounts,
cpi_context,
in_token_data,
in_tlv,
token_seeds,
} = build_decompress_accounts_cpi_data(
remaining_accounts,
params,
cpi_signer,
program_id,
current_slot,
)?;
let system_accounts_offset = params.system_accounts_offset as usize;

// 7. PDA CPI (Light system program)
if has_pda_accounts {
let pda_only = !cpi_context;
Expand Down
Loading