- Associated light-token accounts are Solana accounts that hold token balances of light, SPL, or Token 2022 mints.
- The address for light-ATAs is deterministically derived with the owner’s address, compressed token program ID, and mint address.
- Associated light-token accounts implement a default rent config:
- At account creation, you pay ~17,208 lamports
and (the rent-exemption is sponsored by the protocol) - Transfers keep the account funded via top-ups. The transaction payer tops up 776 lamports when the account’s rent is below 3h.
- At account creation, you pay ~17,208 lamports
Light Rent Config Explained
Light Rent Config Explained
- The rent-exemption for light-token account creation is sponsored by Light Protocol.
- Transaction payer’s pay rent
to keep accounts “active”. - “Inactive” accounts, where rent is below one epoch, are compressed
and the rent-exemption can be claimed by the rent sponsor. - Transfers to inactive accounts (decompress).
| Event | Total Cost | Payer | Time of Rent funded |
|---|---|---|---|
| Account Creation | Transaction payer | Funds 24h rent | |
| Automatic Top ups (when rent < 3h) | 776 lamports | Transaction payer | Funds 3h rent |
| Load Account (when inactive) | Transaction payer | Funds 24h rent |
Get Started
- Rust Client
- Program Guide
- The example creates a test light-mint. You can use existing light-mints, SPL or Token 2022 mints as well.
- Derive the address from mint and owner pubkey.
- Build the instruction with
CreateAssociatedCTokenAccount. It automatically includes the default rent config:
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccount;
let instruction = CreateAssociatedCTokenAccount::new(
payer.pubkey(),
owner,
mint,
)
.instruction()?;
- Send transaction & verify light-ATA creation with
get_account.
1
Prerequisites
Dependencies
Dependencies
Cargo.toml
Report incorrect code
Copy
Ask AI
[dependencies]
light-compressed-token-sdk = "0.1"
light-client = "0.1"
light-ctoken-types = "0.1"
solana-sdk = "2.2"
borsh = "0.10"
tokio = { version = "1.36", features = ["full"] }
[dev-dependencies]
light-program-test = "0.1" # For in-memory tests with LiteSVM
Developer Environment
Developer Environment
- In-Memory (LightProgramTest)
- Localnet (LightClient)
- Devnet (LightClient)
Test with Lite-SVM (…)
Report incorrect code
Copy
Ask AI
# Initialize project
cargo init my-light-project
cd my-light-project
# Run tests
cargo test
Report incorrect code
Copy
Ask AI
use light_program_test::{LightProgramTest, ProgramTestConfig};
use solana_sdk::signer::Signer;
#[tokio::test]
async fn test_example() {
// In-memory test environment
let mut rpc = LightProgramTest::new(ProgramTestConfig::default())
.await
.unwrap();
let payer = rpc.get_payer().insecure_clone();
println!("Payer: {}", payer.pubkey());
}
Connects to a local test validator.
- npm
- yarn
- pnpm
Report incorrect code
Copy
Ask AI
npm install -g @lightprotocol/zk-compression-cli@0.27.1-alpha.2
Report incorrect code
Copy
Ask AI
yarn global add @lightprotocol/zk-compression-cli@0.27.1-alpha.2
Report incorrect code
Copy
Ask AI
pnpm add -g @lightprotocol/zk-compression-cli@0.27.1-alpha.2
Report incorrect code
Copy
Ask AI
# Initialize project
cargo init my-light-project
cd my-light-project
# Start local test validator (in separate terminal)
light test-validator
Report incorrect code
Copy
Ask AI
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connects to http://localhost:8899
let rpc = LightClient::new(LightClientConfig::local()).await?;
let slot = rpc.get_slot().await?;
println!("Current slot: {}", slot);
Ok(())
}
Replace
<your-api-key> with your actual API key. Get your API key here.Report incorrect code
Copy
Ask AI
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let rpc_url = "https://devnet.helius-rpc.com?api-key=<your_api_key>";
let rpc = LightClient::new(
LightClientConfig::new(rpc_url.to_string(), None, None)
).await?;
println!("Connected to Devnet");
Ok(())
}
2
Create ATA
Report incorrect code
Copy
Ask AI
use borsh::BorshDeserialize;
use light_client::indexer::{AddressWithTree, Indexer};
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
use light_ctoken_sdk::ctoken::{
derive_ctoken_ata, CreateAssociatedCTokenAccount, CreateCMint,
CreateCMintParams,
};
use light_ctoken_interface::state::CToken;
use serde_json;
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer};
use std::convert::TryFrom;
use std::env;
use std::fs;
#[tokio::test(flavor = "multi_thread")]
async fn test_create_associated_token_account() {
dotenvy::dotenv().ok();
let keypair_path = env::var("KEYPAIR_PATH")
.unwrap_or_else(|_| format!("{}/.config/solana/id.json", env::var("HOME").unwrap()));
let payer = load_keypair(&keypair_path).expect("Failed to load keypair");
let api_key = env::var("api_key")
.expect("api_key environment variable must be set");
let config = LightClientConfig::devnet(
Some("https://devnet.helius-rpc.com".to_string()),
Some(api_key),
);
let mut rpc = LightClient::new_with_retry(config, None)
.await
.expect("Failed to initialize LightClient");
// Step 1: Create compressed mint (prerequisite)
let (mint, _compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await;
// Step 2: Define owner and derive ATA address
let owner = payer.pubkey();
let (ata_address, _bump) = derive_ctoken_ata(&owner, &mint);
// Step 3: Build instruction using SDK builder
let instruction = CreateAssociatedCTokenAccount::new(
payer.pubkey(),
owner,
mint,
)
.instruction()
.unwrap();
// Step 4: Send transaction (only payer signs, no account keypair needed)
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer])
.await
.unwrap();
// Step 5: Verify light-ATA creation
let account_data = rpc.get_account(ata_address).await.unwrap().unwrap();
let ctoken_state = CToken::deserialize(&mut &account_data.data[..]).unwrap();
assert_eq!(ctoken_state.mint, mint.to_bytes(), "Mint should match");
assert_eq!(ctoken_state.owner, owner.to_bytes(), "Owner should match");
assert_eq!(ctoken_state.amount, 0, "Initial amount should be 0");
}
pub async fn create_compressed_mint<R: Rpc + Indexer>(
rpc: &mut R,
payer: &Keypair,
decimals: u8,
) -> (Pubkey, [u8; 32]) {
let mint_signer = Keypair::new();
let address_tree = rpc.get_address_tree_v2();
let _ = rpc.get_latest_active_state_trees().await;
let output_pubkey = match rpc
.get_random_state_tree_info()
.ok()
.or_else(|| rpc.get_random_state_tree_info_v1().ok())
{
Some(info) => info
.get_output_pubkey()
.expect("Invalid state tree type for output"),
None => {
let queues = rpc
.indexer_mut()
.expect("IndexerNotInitialized")
.get_queue_info(None)
.await
.expect("Failed to fetch queue info")
.value
.queues;
queues
.get(0)
.map(|q| q.queue)
.expect("NoStateTreesAvailable")
}
};
// Derive compression address
let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address(
&mint_signer.pubkey(),
&address_tree.tree,
);
let mint_pda =
light_ctoken_sdk::ctoken::find_cmint_address(&mint_signer.pubkey()).0;
// Get validity proof for the address
let rpc_result = rpc
.get_validity_proof(
vec![],
vec![AddressWithTree {
address: compression_address,
tree: address_tree.tree,
}],
None,
)
.await
.unwrap()
.value;
// Build params
let params = CreateCMintParams {
decimals,
address_merkle_tree_root_index: rpc_result.addresses[0].root_index,
mint_authority: payer.pubkey(),
proof: rpc_result.proof.0.unwrap(),
compression_address,
mint: mint_pda,
freeze_authority: None,
extensions: None,
};
// Create instruction
let create_cmint = CreateCMint::new(
params,
mint_signer.pubkey(),
payer.pubkey(),
address_tree.tree,
output_pubkey,
);
let instruction = create_cmint.instruction().unwrap();
// Send transaction
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_signer])
.await
.unwrap();
(mint_pda, compression_address)
}
fn load_keypair(path: &str) -> Result<Keypair, Box<dyn std::error::Error>> {
let path = if path.starts_with("~") {
path.replace("~", &env::var("HOME").unwrap_or_default())
} else {
path.to_string()
};
let file = fs::read_to_string(&path)?;
let bytes: Vec<u8> = serde_json::from_str(&file)?;
Ok(Keypair::try_from(&bytes[..])?)
}
1
Define Rent Config Accounts
Report incorrect code
Copy
Ask AI
use light_compressed_token_sdk::ctoken::CompressibleParamsInfos;
let compressible_params = CompressibleParamsInfos::new(
compressible_config.clone(),
rent_sponsor.clone(),
system_program.clone(),
);
| Protocol PDA that stores account rent config. | |
| |
| Solana System Program to create the on-chain account. |
2
Build Account Infos and CPI the Compressed Token Program
- Pass the required accounts that include the rent config.
- Use
invokeorinvoke_signed, when a CPI requires a PDA signer.
The light-ATA address is derived from
[owner, ctoken_program_id, mint]. Unlike light-token accounts, owner and mint are passed as accounts, not in instruction data.- invoke (External signer)
- invoke_signed (PDA is signer)
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccountCpi;
CreateAssociatedCTokenAccountCpi {
owner: owner.clone(),
mint: mint.clone(),
payer: payer.clone(),
associated_token_account: associated_token_account.clone(),
system_program: system_program.clone(),
bump: data.bump,
compressible: Some(compressible_params),
idempotent: false,
}
.invoke()?;
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateAssociatedCTokenAccountCpi;
let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]];
CreateAssociatedCTokenAccountCpi {
owner: owner.clone(),
mint: mint.clone(),
payer: payer.clone(),
associated_token_account: associated_token_account.clone(),
system_program: system_program.clone(),
bump: data.bump,
compressible: Some(compressible_params),
idempotent: false,
}
.invoke_signed(&[signer_seeds])?;
| Owner | - |
|
| Mint | - |
|
| Payer | signer, mutable |
|
| light-ATA Account | mutable |
|
| - | Solana System Program. Required for CPI to create the on-chain account. | |
| Bump | u8 | The PDA bump seed for the light-ATA address derivation. |
| Idempotent | bool |
|
Full Code Example
Find the source code here.
Report incorrect code
Copy
Ask AI
use borsh::{BorshDeserialize, BorshSerialize};
use light_ctoken_sdk::ctoken::{CompressibleParamsCpi, CreateAssociatedCTokenAccountCpi};
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use crate::{ATA_SEED, ID};
/// Instruction data for create ATA V2 (owner/mint as accounts)
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateAta2Data {
pub bump: u8,
pub pre_pay_num_epochs: u8,
pub lamports_per_write: u32,
}
/// Handler for creating ATA using V2 variant (invoke)
///
/// Account order:
/// - accounts[0]: owner (readonly)
/// - accounts[1]: mint (readonly)
/// - accounts[2]: payer (signer, writable)
/// - accounts[3]: associated_token_account (writable)
/// - accounts[4]: system_program
/// - accounts[5]: compressible_config
/// - accounts[6]: rent_sponsor (writable)
pub fn process_create_ata2_invoke(
accounts: &[AccountInfo],
data: CreateAta2Data,
) -> Result<(), ProgramError> {
if accounts.len() < 7 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let compressible_params = CompressibleParamsCpi::new(
accounts[5].clone(),
accounts[6].clone(),
accounts[4].clone(),
);
CreateAssociatedCTokenAccountCpi {
owner: accounts[0].clone(),
mint: accounts[1].clone(),
payer: accounts[2].clone(),
associated_token_account: accounts[3].clone(),
system_program: accounts[4].clone(),
bump: data.bump,
compressible: Some(compressible_params),
idempotent: false,
}
.invoke()?;
Ok(())
}
/// Handler for creating ATA using V2 variant with PDA ownership (invoke_signed)
///
/// Account order:
/// - accounts[0]: owner (PDA, readonly)
/// - accounts[1]: mint (readonly)
/// - accounts[2]: payer (PDA, writable, not signer - program signs)
/// - accounts[3]: associated_token_account (writable)
/// - accounts[4]: system_program
/// - accounts[5]: compressible_config
/// - accounts[6]: rent_sponsor (writable)
pub fn process_create_ata2_invoke_signed(
accounts: &[AccountInfo],
data: CreateAta2Data,
) -> Result<(), ProgramError> {
if accounts.len() < 7 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Derive the PDA that will act as payer
let (pda, bump) = Pubkey::find_program_address(&[ATA_SEED], &ID);
// Verify the payer is the PDA
if &pda != accounts[2].key {
return Err(ProgramError::InvalidSeeds);
}
let compressible_params = CompressibleParamsCpi::new(
accounts[5].clone(),
accounts[6].clone(),
accounts[4].clone(),
);
let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]];
CreateAssociatedCTokenAccountCpi {
owner: accounts[0].clone(),
mint: accounts[1].clone(),
payer: accounts[2].clone(), // PDA
associated_token_account: accounts[3].clone(),
system_program: accounts[4].clone(),
bump: data.bump,
compressible: Some(compressible_params),
idempotent: false,
}
.invoke_signed(&[signer_seeds])?;
Ok(())
}