| light-token -> light-token Account |
|
| SPL token -> light-token Account |
|
| light-token -> SPL Account |
|
- For example, SPL -> light-token account can be used for transfers from Alice’s SPL token account to her own light-token account.
- You can use this to use the sponsored rent-exemption for existing SPL tokens.
Get Started
- Rust Client
- Program Guide
The example transfers SPL token -> light-token and light-token -> light-token:
- Create SPL mint, SPL token accounts, and mint SPL tokens
- Send SPL tokens to light-token account to mint light-tokens.
- Transfer light-tokens to another light-token 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
Transfer Interface
Report incorrect code
Copy
Ask AI
use anchor_spl::token::{spl_token, Mint};
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
use light_ctoken_sdk::{
ctoken::{
derive_ctoken_ata as derive_token_ata, CreateAssociatedCTokenAccount as CreateAssociatedTokenAccount,
TransferCToken as TransferToken, TransferSplToCtoken as TransferSplToToken,
},
spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda},
};
use serde_json;
use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::program_pack::Pack;
use solana_sdk::{signature::Keypair, signer::Signer};
use spl_token_2022::pod::PodAccount;
use std::convert::TryFrom;
use std::env;
use std::fs;
/// Test SPL → light-token → light-token
// with ATA creation + transfer in one transaction
#[tokio::test(flavor = "multi_thread")]
async fn test_spl_to_token_to_token() {
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");
// 2. Create SPL mint
let mint_keypair = Keypair::new();
let mint = mint_keypair.pubkey();
let decimals = 2u8;
let mint_rent = rpc
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.await
.unwrap();
let create_mint_account_ix = solana_sdk::system_instruction::create_account(
&payer.pubkey(),
&mint,
mint_rent,
Mint::LEN as u64,
&spl_token::ID,
);
let initialize_mint_ix = spl_token::instruction::initialize_mint(
&spl_token::ID,
&mint,
&payer.pubkey(),
None,
decimals,
)
.unwrap();
rpc.create_and_send_transaction(
&[create_mint_account_ix, initialize_mint_ix],
&payer.pubkey(),
&[&payer, &mint_keypair],
)
.await
.unwrap();
// 3. Create SPL interface PDA
let create_spl_interface_pda_ix =
CreateSplInterfacePda::new(payer.pubkey(), mint, anchor_spl::token::ID).instruction();
rpc.create_and_send_transaction(&[create_spl_interface_pda_ix], &payer.pubkey(), &[&payer])
.await
.unwrap();
let mint_amount = 10_000u64;
let spl_to_token_amount = 7_000u64;
let token_transfer_amount = 3_000u64;
// 4. Create SPL token account
let spl_token_account_keypair = Keypair::new();
let token_account_rent = rpc
.get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)
.await
.unwrap();
let create_token_account_ix = solana_sdk::system_instruction::create_account(
&payer.pubkey(),
&spl_token_account_keypair.pubkey(),
token_account_rent,
spl_token::state::Account::LEN as u64,
&spl_token::ID,
);
let init_token_account_ix = spl_token::instruction::initialize_account(
&spl_token::ID,
&spl_token_account_keypair.pubkey(),
&mint,
&payer.pubkey(),
)
.unwrap();
rpc.create_and_send_transaction(
&[create_token_account_ix, init_token_account_ix],
&payer.pubkey(),
&[&spl_token_account_keypair, &payer],
)
.await
.unwrap();
// 5. Mint SPL tokens to the SPL account
let mint_to_ix = spl_token::instruction::mint_to(
&spl_token::ID,
&mint,
&spl_token_account_keypair.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
mint_amount,
)
.unwrap();
rpc.create_and_send_transaction(&[mint_to_ix], &payer.pubkey(), &[&payer])
.await
.unwrap();
// Verify SPL account has tokens
let spl_account_data = rpc
.get_account(spl_token_account_keypair.pubkey())
.await
.unwrap()
.unwrap();
let spl_account =
spl_pod::bytemuck::pod_from_bytes::<PodAccount>(&spl_account_data.data).unwrap();
let initial_spl_balance: u64 = spl_account.amount.into();
assert_eq!(initial_spl_balance, mint_amount);
// 6. Create sender's token ATA
let (sender_token_ata, _bump) = derive_token_ata(&payer.pubkey(), &mint);
let create_ata_instruction =
CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint)
.instruction()
.unwrap();
rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer])
.await
.unwrap();
// Verify sender's token ATA was created
let token_account_data = rpc.get_account(sender_token_ata).await.unwrap().unwrap();
assert!(
!token_account_data.data.is_empty(),
"Sender token ATA should exist"
);
// 7. Transfer SPL tokens to sender's token account
let (spl_interface_pda, spl_interface_pda_bump) = find_spl_interface_pda_with_index(&mint, 0);
let spl_to_token_ix = TransferSplToToken {
amount: spl_to_token_amount,
spl_interface_pda_bump,
source_spl_token_account: spl_token_account_keypair.pubkey(),
destination_ctoken_account: sender_token_ata,
authority: payer.pubkey(),
mint,
payer: payer.pubkey(),
spl_interface_pda,
spl_token_program: anchor_spl::token::ID,
}
.instruction()
.unwrap();
rpc.create_and_send_transaction(&[spl_to_token_ix], &payer.pubkey(), &[&payer])
.await
.unwrap();
// 8. Create recipient ATA + transfer token→token in one transaction
let recipient = Keypair::new();
let (recipient_token_ata, _) = derive_token_ata(&recipient.pubkey(), &mint);
let create_recipient_ata_ix = CreateAssociatedTokenAccount::new(
payer.pubkey(),
recipient.pubkey(),
mint,
)
.instruction()
.unwrap();
let token_transfer_ix = TransferToken {
source: sender_token_ata,
destination: recipient_token_ata,
amount: token_transfer_amount,
authority: payer.pubkey(),
max_top_up: None,
}
.instruction()
.unwrap();
let compute_unit_ix = ComputeBudgetInstruction::set_compute_unit_limit(10_000);
rpc.create_and_send_transaction(
&[compute_unit_ix, create_recipient_ata_ix, token_transfer_ix],
&payer.pubkey(),
&[&payer],
)
.await
.unwrap();
}
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
Light-Token Transfer Interface
Define the number of light-tokens / SPL tokens to transfer- from which SPL or light-token account, and
- to which SPL or light-token account.
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::TransferInterfaceCpi;
let mut transfer = TransferInterfaceCpi::new(
data.amount,
source_account.clone(),
destination_account.clone(),
authority.clone(),
payer.clone(),
compressed_token_program_authority.clone(),
);
light-token Interface Accounts
light-token Interface Accounts
| Source Account | mutable | The source account (SPL token account or light-token account). |
| Destination Account | mutable | The destination account (SPL token account or light-token account). |
| Authority | signer* |
|
| Payer | signer, mutable | Pays transaction fees. |
| Compressed Token Program Authority | - | The light-token program authority PDA. |
2
SPL Transfer Interface (Optional)
The SPL transfer interface is only needed for SPL ↔ light-token transfers.Report incorrect code
Copy
Ask AI
transfer = transfer.with_spl_interface(
Some(mint.clone()),
Some(spl_token_program.clone()),
Some(spl_interface_pda.clone()),
data.spl_interface_pda_bump,
)?;
spl_interface_pda:- SPL → light-token: SPL tokens are locked by the light-token Program in the PDA, light-tokens are minted to light-token accounts
- light-token → SPL: light-tokens are burned, SPL tokens transferred to SPL token accounts
The interface PDA is derived from the
mint pubkey and pool seed.SPL Interface Accounts
SPL Interface Accounts
| Mint | - | The SPL token mint. |
| SPL Token Program | - | The SPL Token program. |
| Interface PDA | mutable | Interface PDA for SPL ↔ light-token transfers. |
3
CPI
CPI the Light Token program to execute the transfer. Useinvoke(), or invoke_signed() when a CPI requires a PDA signer.- invoke (External signer)
- invoke_signed (PDA is signer)
Report incorrect code
Copy
Ask AI
transfer.invoke()?;
Report incorrect code
Copy
Ask AI
let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]];
transfer.invoke_signed(&[authority_seeds])?;
Full Code Example
Find the source code here.
Report incorrect code
Copy
Ask AI
use borsh::{BorshDeserialize, BorshSerialize};
use light_ctoken_sdk::ctoken::TransferInterfaceCpi;
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use crate::ID;
/// PDA seed for authority in invoke_signed variants
pub const TRANSFER_INTERFACE_AUTHORITY_SEED: &[u8] = b"transfer_interface_authority";
/// Instruction data for TransferInterfaceCpi
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct TransferInterfaceData {
pub amount: u64,
/// Required for SPL<->CToken transfers, None for CToken->CToken
pub token_pool_pda_bump: Option<u8>,
}
/// Handler for TransferInterfaceCpi (invoke)
///
/// This unified interface automatically detects account types and routes to:
/// - CToken -> CToken transfer
/// - CToken -> SPL transfer
/// - SPL -> CToken transfer
///
/// Account order:
/// - accounts[0]: compressed_token_program (for CPI)
/// - accounts[1]: source_account (SPL or CToken)
/// - accounts[2]: destination_account (SPL or CToken)
/// - accounts[3]: authority (signer)
/// - accounts[4]: payer (signer)
/// - accounts[5]: compressed_token_program_authority
/// For SPL bridge (optional, required for SPL<->CToken):
/// - accounts[6]: mint
/// - accounts[7]: token_pool_pda
/// - accounts[8]: spl_token_program
pub fn process_transfer_interface_invoke(
accounts: &[AccountInfo],
data: TransferInterfaceData,
) -> Result<(), ProgramError> {
if accounts.len() < 6 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let mut transfer = TransferInterfaceCpi::new(
data.amount,
accounts[1].clone(), // source_account
accounts[2].clone(), // destination_account
accounts[3].clone(), // authority
accounts[4].clone(), // payer
accounts[5].clone(), // compressed_token_program_authority
);
// Add SPL bridge config if provided
if accounts.len() >= 9 && data.token_pool_pda_bump.is_some() {
transfer = transfer.with_spl_interface(
Some(accounts[6].clone()), // mint
Some(accounts[8].clone()), // spl_token_program
Some(accounts[7].clone()), // token_pool_pda
data.token_pool_pda_bump,
)?;
}
transfer.invoke()?;
Ok(())
}
/// Handler for TransferInterfaceCpi with PDA authority (invoke_signed)
///
/// The authority is a PDA derived from TRANSFER_INTERFACE_AUTHORITY_SEED.
///
/// Account order:
/// - accounts[0]: compressed_token_program (for CPI)
/// - accounts[1]: source_account (SPL or CToken)
/// - accounts[2]: destination_account (SPL or CToken)
/// - accounts[3]: authority (PDA, not signer - program signs)
/// - accounts[4]: payer (signer)
/// - accounts[5]: compressed_token_program_authority
/// For SPL bridge (optional, required for SPL<->CToken):
/// - accounts[6]: mint
/// - accounts[7]: token_pool_pda
/// - accounts[8]: spl_token_program
pub fn process_transfer_interface_invoke_signed(
accounts: &[AccountInfo],
data: TransferInterfaceData,
) -> Result<(), ProgramError> {
if accounts.len() < 6 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Derive the PDA for the authority
let (authority_pda, authority_bump) =
Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &ID);
// Verify the authority account is the PDA we expect
if &authority_pda != accounts[3].key {
return Err(ProgramError::InvalidSeeds);
}
let mut transfer = TransferInterfaceCpi::new(
data.amount,
accounts[1].clone(), // source_account
accounts[2].clone(), // destination_account
accounts[3].clone(), // authority (PDA)
accounts[4].clone(), // payer
accounts[5].clone(), // compressed_token_program_authority
);
// Add SPL bridge config if provided
if accounts.len() >= 9 && data.token_pool_pda_bump.is_some() {
transfer = transfer.with_spl_interface(
Some(accounts[6].clone()), // mint
Some(accounts[8].clone()), // spl_token_program
Some(accounts[7].clone()), // token_pool_pda
data.token_pool_pda_bump,
)?;
}
// Invoke with PDA signing
let authority_seeds: &[&[u8]] = &[TRANSFER_INTERFACE_AUTHORITY_SEED, &[authority_bump]];
transfer.invoke_signed(&[authority_seeds])?;
Ok(())
}