Skip to main content

Documentation Index

Fetch the complete documentation index at: https://luminouslabs-cc5545c6-indexing-tokens.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.


  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