- Light-token accounts are Solana accounts that hold token balances of light, SPL, or Token 2022 mints.
- 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 mint for light-tokens. You can use existing light, SPL or Token 2022 mints as well.
- Build the instruction with
CreateCTokenAccount. It automatically includes the default rent config.
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::{CreateCTokenAccount};
let instruction = CreateCTokenAccount::new(
payer.pubkey(),
account.pubkey(),
mint,
owner,
)
.instruction()?;
- Send transaction & verify light-token account 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 Token Account
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::{CreateCMint, CreateCMintParams, CreateCTokenAccount};
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_ctoken_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") // Set api_key in your .env
.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: Generate new keypair for the cToken account
let account = Keypair::new();
let owner = payer.pubkey();
// Step 3: Build instruction using SDK builder
let instruction = CreateCTokenAccount::new(payer.pubkey(), account.pubkey(), mint, owner)
.instruction()
.unwrap();
// Step 4: Send transaction (account keypair must sign)
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &account])
.await
.unwrap();
// Step 5: Verify account creation
let account_data = rpc.get_account(account.pubkey()).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();
// Fetch active state trees for devnet
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: no active state trees returned")
}
};
// 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
Configure Rent
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
- Pass the required accounts
- Include rent config from
compressible_params - Use
invokeorinvoke_signed, when a CPI requires a PDA signer.
- invoke (External signer)
- invoke_signed (PDA is signer)
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateCTokenAccountCpi;
CreateCTokenAccountCpi {
payer: payer.clone(),
account: account.clone(),
mint: mint.clone(),
owner: data.owner,
compressible: Some(compressible_params),
}
.invoke()?;
| Payer | signer, mutable |
|
| light-token Account | signer*, mutable |
|
| Mint | - | The SPL or light-mint token mint. |
| Owner | Pubkey | The owner of the token account. Controls transfers and other operations. |
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateCTokenAccountCpi;
let account_cpi = CreateCTokenAccountCpi {
payer: payer.clone(),
account: account.clone(),
mint: mint.clone(),
owner: data.owner,
compressible: Some(compressible_params),
};
let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]];
account_cpi.invoke_signed(&[signer_seeds])?;
Full Code Example
Find the source code here.
Report incorrect code
Copy
Ask AI
use borsh::{BorshDeserialize, BorshSerialize};
use light_ctoken_sdk::ctoken::{CompressibleParamsCpi, CreateCTokenAccountCpi};
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use crate::{ID, TOKEN_ACCOUNT_SEED};
/// Instruction data for create token account
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateTokenAccountData {
pub owner: Pubkey,
pub pre_pay_num_epochs: u8,
pub lamports_per_write: u32,
}
/// Handler for creating a compressible token account (invoke)
///
/// Uses the builder pattern from the ctoken module. This demonstrates how to:
/// 1. Build the account infos struct with compressible params
/// 2. Call the invoke() method which handles instruction building and CPI
///
/// Account order:
/// - accounts[0]: payer (signer)
/// - accounts[1]: account to create (signer)
/// - accounts[2]: mint
/// - accounts[3]: compressible_config
/// - accounts[4]: system_program
/// - accounts[5]: rent_sponsor
pub fn process_create_token_account_invoke(
accounts: &[AccountInfo],
data: CreateTokenAccountData,
) -> Result<(), ProgramError> {
if accounts.len() < 6 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Build the compressible params using constructor
let compressible_params = CompressibleParamsCpi::new(
accounts[3].clone(),
accounts[5].clone(),
accounts[4].clone(),
);
// Build the account infos struct
CreateCTokenAccountCpi {
payer: accounts[0].clone(),
account: accounts[1].clone(),
mint: accounts[2].clone(),
owner: data.owner,
compressible: Some(compressible_params),
}
.invoke()?;
Ok(())
}
/// Handler for creating a compressible token account with PDA ownership (invoke_signed)
///
/// Account order:
/// - accounts[0]: payer (signer)
/// - accounts[1]: account to create (PDA, will be derived and verified)
/// - accounts[2]: mint
/// - accounts[3]: compressible_config
/// - accounts[4]: system_program
/// - accounts[5]: rent_sponsor
pub fn process_create_token_account_invoke_signed(
accounts: &[AccountInfo],
data: CreateTokenAccountData,
) -> Result<(), ProgramError> {
if accounts.len() < 6 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Derive the PDA for the token account
let (pda, bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID);
// Verify the account to create is the PDA
if &pda != accounts[1].key {
return Err(ProgramError::InvalidSeeds);
}
// Build the compressible params using constructor
let compressible_params = CompressibleParamsCpi::new(
accounts[3].clone(),
accounts[5].clone(),
accounts[4].clone(),
);
// Build the account infos struct
let account_cpi = CreateCTokenAccountCpi {
payer: accounts[0].clone(),
account: accounts[1].clone(),
mint: accounts[2].clone(),
owner: data.owner,
compressible: Some(compressible_params),
};
// Invoke with PDA signing
let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]];
account_cpi.invoke_signed(&[signer_seeds])?;
Ok(())
}