- Closing a light-token account transfers remaining lamports to a destination account and the rent sponsor can reclaim sponsored rent.
- Light-token accounts can be closed
- by the account owner at any time.
- by the when the account becomes . The account is - it can be reinstated with the same state (decompressed).
Get Started
- Rust Client
- Program Guide
- The example creates a light-token account and mint.
- Build the instruction with
CloseCTokenAccount:
Report incorrect code
Copy
Ask AI
let close_instruction = CloseCTokenAccount::new(
CTOKEN_PROGRAM_ID,
account.pubkey(),
payer.pubkey(), // Destination for remaining lamports
owner,
)
.instruction()
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
Close light-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::{
CloseCTokenAccount, CreateCMint, CreateCMintParams, CreateCTokenAccount, CTOKEN_PROGRAM_ID,
};
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_close_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: Create cToken account with 0 balance
let account = Keypair::new();
let owner = payer.pubkey();
let create_instruction =
CreateCTokenAccount::new(payer.pubkey(), account.pubkey(), mint, owner)
.instruction()
.unwrap();
rpc.create_and_send_transaction(&[create_instruction], &payer.pubkey(), &[&payer, &account])
.await
.unwrap();
// Step 3: Verify account exists before closing
let account_before_close = rpc.get_account(account.pubkey()).await.unwrap();
assert!(
account_before_close.is_some(),
"Account should exist before closing"
);
let ctoken_state =
CToken::deserialize(&mut &account_before_close.unwrap().data[..]).unwrap();
assert_eq!(ctoken_state.amount, 0, "Account balance must be 0 to close");
// Step 4: Build close instruction using SDK builder
let close_instruction = CloseCTokenAccount::new(
CTOKEN_PROGRAM_ID,
account.pubkey(),
payer.pubkey(), // Destination for remaining lamports
owner,
)
.instruction()
.unwrap();
// Step 5: Send close transaction
rpc.create_and_send_transaction(&[close_instruction], &payer.pubkey(), &[&payer])
.await
.unwrap();
// Step 6: Verify account is closed
let account_after_close = rpc.get_account(account.pubkey()).await.unwrap();
assert!(
account_after_close.is_none(),
"Account should be closed and no longer exist"
);
}
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
Build Account Infos and CPI the light-token Program
- Define the light-token account to close and where remaining lamports should go
- Use
invokeorinvoke_signed, when a CPI requires a PDA signer.
- invoke (External Signer)
- invoke_signed (PDA Owner)
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CloseCTokenAccountCpi;
CloseCTokenAccountCpi {
token_program: token_program.clone(),
account: account.clone(),
destination: destination.clone(),
owner: owner.clone(),
rent_sponsor: Some(rent_sponsor.clone()),
}
.invoke()?;
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CloseCTokenAccountCpi;
let close_account_cpi = CloseCTokenAccountCpi {
token_program: token_program.clone(),
account: account.clone(),
destination: destination.clone(),
owner: owner.clone(),
rent_sponsor: Some(rent_sponsor.clone()),
};
let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]];
close_account_cpi.invoke_signed(&[signer_seeds])?;
Account List
Account List
| Token Program | - | The light-token program for CPI. |
| Account | mutable | The light-token account to close. |
| Destination | mutable | Receives remaining lamports from the closed account. |
| Owner | signer* |
|
| Rent Sponsor | mutable, optional |
|
Full Code Example
Find the source code here.
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CloseCTokenAccountCpi;
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use crate::{ID, TOKEN_ACCOUNT_SEED};
/// Handler for closing a compressed token account (invoke)
///
/// Account order:
/// - accounts[0]: token_program (ctoken program)
/// - accounts[1]: account to close (writable)
/// - accounts[2]: destination for lamports (writable)
/// - accounts[3]: owner/authority (signer)
/// - accounts[4]: rent_sponsor (optional, writable)
pub fn process_close_account_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> {
if accounts.len() < 4 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let rent_sponsor = if accounts.len() > 4 {
Some(accounts[4].clone())
} else {
None
};
CloseCTokenAccountCpi {
token_program: accounts[0].clone(),
account: accounts[1].clone(),
destination: accounts[2].clone(),
owner: accounts[3].clone(),
rent_sponsor,
}
.invoke()?;
Ok(())
}
/// Handler for closing a PDA-owned compressed token account (invoke_signed)
///
/// Account order:
/// - accounts[0]: token_program (ctoken program)
/// - accounts[1]: account to close (writable)
/// - accounts[2]: destination for lamports (writable)
/// - accounts[3]: PDA owner/authority (not signer, program signs)
/// - accounts[4]: rent_sponsor (optional, writable)
pub fn process_close_account_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> {
if accounts.len() < 4 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Derive the PDA for the authority
let (pda, bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID);
// Verify the authority account is the PDA we expect
if &pda != accounts[3].key {
return Err(ProgramError::InvalidSeeds);
}
let rent_sponsor = if accounts.len() > 4 {
Some(accounts[4].clone())
} else {
None
};
let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]];
CloseCTokenAccountCpi {
token_program: accounts[0].clone(),
account: accounts[1].clone(),
destination: accounts[2].clone(),
owner: accounts[3].clone(),
rent_sponsor,
}
.invoke_signed(&[signer_seeds])?;
Ok(())
}