Skip to main content
  1. Mint accounts uniquely represent a token on Solana and store its global metadata.
  2. Mints for light-token accounts are compressed accounts and rent-free.

Get Started

The example creates a light-mint with token metadata.
  1. Derive the mint address from the mint signer and address tree
  2. Fetch a from your RPC that proves the address does not exist yet.
  3. Configure mint and your token metadata (name, symbol, URI, additional metadata)
  4. Build the instruction with CreateCMint::new() and send the transaction.
use light_ctoken_sdk::ctoken::CreateCMint;

let create_cmint = CreateCMint::new(
    params,
    mint_signer.pubkey(),
    payer.pubkey(),
    address_tree.tree,
    output_queue,
);
let instruction = create_cmint.instruction()?;
1

Prerequisites

Cargo.toml
[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
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 Mint with Token Metadata

use light_client::indexer::{AddressWithTree, Indexer};
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
use light_ctoken_sdk::ctoken::{CreateCMint, CreateCMintParams};
use light_ctoken_interface::instructions::extensions::token_metadata::TokenMetadataInstructionData;
use light_ctoken_interface::instructions::extensions::ExtensionInstructionData;
use light_ctoken_interface::state::AdditionalMetadata;
use serde_json;
use solana_sdk::{bs58, 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_compressed_mint_with_metadata() {
    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");

    // Create compressed mint with metadata
    let (mint_pda, compression_address) = create_compressed_mint(&mut rpc, &payer, 9).await;

    println!("\n=== Created Compressed Mint ===");
    println!("Mint PDA: {}", mint_pda);
    println!("Compression Address: {}", bs58::encode(compression_address).into_string());
    println!("Decimals: 9");
    println!("Name: Example Token");
    println!("Symbol: EXT");
    println!("URI: https://example.com/metadata.json");
}

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 with token metadata
    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: Some(vec![ExtensionInstructionData::TokenMetadata(
            TokenMetadataInstructionData {
                update_authority: Some(payer.pubkey().to_bytes().into()),
                name: b"Example Token".to_vec(),
                symbol: b"EXT".to_vec(),
                uri: b"https://example.com/metadata.json".to_vec(),
                additional_metadata: Some(vec![AdditionalMetadata {
                    key: b"type".to_vec(),
                    value: b"compressed".to_vec(),
                }]),
            },
        )]),
    };

    // 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.