Skip to main content

  1. Light token accounts are Solana accounts that hold token balances of light, SPL, or Token 2022 mints.
  2. Light token accounts implement a default rent config:
    1. At account creation, you pay ~17,208 lamports
      and (the rent-exemption is sponsored by the protocol)
    2. Transfers keep the account funded via top-ups. The transaction payer tops up 776 lamports when the account’s rent is below 3h.
  1. The Light Token Program pays the rent-exemption cost for the account.
  2. Transaction fee payers bump a virtual rent balance when writing to the account, which keeps the account “hot”.
  3. “Cold” accounts virtual rent balance below threshold (eg 24h without write bump) get auto-compressed.
  4. The cold account’s state is cryptographically preserved on the Solana ledger. Users can load a cold account into hot state in-flight when using the account again.

Get Started

  1. The example creates a test mint for light-tokens. You can use existing light, SPL or Token 2022 mints as well.
  2. Build the instruction with CreateTokenAccount. It automatically includes the default rent config.
use light_token_sdk::token::{CreateTokenAccount};

let instruction = CreateTokenAccount::new(
    payer.pubkey(),
    account.pubkey(),
    mint,
    owner,
)
.instruction()?;
  1. Send transaction & verify light-token account creation with get_account.
1

Prerequisites

Cargo.toml
[dependencies]
light-compressed-token-sdk = "0.1"
light-client = "0.1"
light-token-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
Test with Lite-SVM (…)
# Initialize project
cargo init my-light-project
cd my-light-project

# Run tests
cargo test
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());
}
2

Create Token Account

use borsh::BorshDeserialize;
use light_client::indexer::{AddressWithTree, Indexer};
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
use light_token_sdk::token::{CreateCMint, CreateCMintParams, CreateTokenAccount};
use light_token_interface::state::Token;
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_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") // 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 = CreateTokenAccount::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 token_state = Token::deserialize(&mut &account_data.data[..]).unwrap();

    assert_eq!(token_state.mint, mint.to_bytes(), "Mint should match");
    assert_eq!(token_state.owner, owner.to_bytes(), "Owner should match");
    assert_eq!(token_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_token_sdk::token::derive_cmint_compressed_address(
        &mint_signer.pubkey(),
        &address_tree.tree,
    );

    let mint_pda = light_token_sdk::token::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[..])?)
}

Next Steps

Learn how to mint tokens to light-token accounts