- Mint accounts uniquely represent a token on Solana and store its global metadata.
- Mints for light-token accounts are compressed accounts and rent-free.
Try It Live
Get Started
- TypeScript Client
- Rust Client
- Program Guide
createMintInterface is a unified interface that dispatches to different mint creation paths based on programId:TOKEN_PROGRAM_IDorTOKEN_2022_PROGRAM_ID→ delegates to SPL or T22createMint- Otherwise it defaults to
CTOKEN_PROGRAM_ID→ creates a light-token mint
Find the source code
here.
1
Create Mint with Token Metadata
Installations
Installations
- npm
- yarn
- pnpm
Install packages in your working directory:Install the CLI globally:
Report incorrect code
Copy
Ask AI
npm install @lightprotocol/stateless.js@alpha \
@lightprotocol/compressed-token@alpha
Report incorrect code
Copy
Ask AI
npm install -g @lightprotocol/zk-compression-cli@alpha
Install packages in your working directory:Install the CLI globally:
Report incorrect code
Copy
Ask AI
yarn add @lightprotocol/stateless.js@alpha \
@lightprotocol/compressed-token@alpha
Report incorrect code
Copy
Ask AI
yarn global add @lightprotocol/zk-compression-cli@alpha
Install packages in your working directory:Install the CLI globally:
Report incorrect code
Copy
Ask AI
pnpm add @lightprotocol/stateless.js@alpha \
@lightprotocol/compressed-token@alpha
Report incorrect code
Copy
Ask AI
pnpm add -g @lightprotocol/zk-compression-cli@alpha
Report incorrect code
Copy
Ask AI
# Start local test-validator in separate terminal
light test-validator
The
mintAuthority must be a Signer for light-mints but can be just a
PublicKey for SPL/T22.Report incorrect code
Copy
Ask AI
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMintInterface, createTokenMetadata } from "@lightprotocol/compressed-token";
async function main() {
const rpc = createRpc();
const payer = Keypair.generate();
const sig = await rpc.requestAirdrop(payer.publicKey, 10e9);
await rpc.confirmTransaction(sig);
const { mint, transactionSignature } = await createMintInterface(
rpc,
payer,
payer,
null,
9,
undefined,
undefined,
undefined,
createTokenMetadata("Example Token", "EXT", "https://example.com/metadata.json")
);
console.log("Mint:", mint.toBase58());
console.log("Tx:", transactionSignature);
}
main().catch(console.error);
The example creates a light-mint with token metadata.
- Derive the mint address from the mint signer and address tree
- Fetch a from your RPC that proves the address does not exist yet.
- Configure mint and your token metadata (name, symbol, URI, additional metadata)
-
Build the instruction with
CreateCMint::new()and send the transaction.
Report incorrect code
Copy
Ask AI
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
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@alpha
Report incorrect code
Copy
Ask AI
yarn global add @lightprotocol/zk-compression-cli@alpha
Report incorrect code
Copy
Ask AI
pnpm add -g @lightprotocol/zk-compression-cli@alpha
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
Create Mint with Token Metadata
Report incorrect code
Copy
Ask AI
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[..])?)
}
1
Configure Token Metadata
Report incorrect code
Copy
Ask AI
use light_ctoken_interface::{
instructions::extensions::{
token_metadata::TokenMetadataInstructionData,
ExtensionInstructionData,
},
state::AdditionalMetadata,
};
let token_metadata = ExtensionInstructionData::TokenMetadata(
TokenMetadataInstructionData {
update_authority: Some(authority.to_bytes().into()),
name: b"My Token".to_vec(),
symbol: b"MTK".to_vec(),
uri: b"https://example.com/metadata.json".to_vec(),
additional_metadata: Some(vec![
AdditionalMetadata {
key: b"category".to_vec(),
value: b"utility".to_vec(),
},
]),
},
);
Fields must be set at light-mint creation. Standard fields (
name, symbol, uri) can be updated by update_authority. For additional_metadata, only existing keys can be modified or removed. New keys cannot be added after creation.2
Configure Mint
Setdecimals, mint_authority, freeze_authority, and pass the token_metadata from the previous step.Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateCMintParams;
let params = CreateCMintParams {
decimals: data.decimals,
address_merkle_tree_root_index: data.address_merkle_tree_root_index,
mint_authority: data.mint_authority,
proof: data.proof,
compression_address: data.compression_address,
mint: data.mint,
freeze_authority: data.freeze_authority,
extensions: data.extensions,
};
The client passes a validity proof that proves the light-mint address does not exist in the address tree where it will be stored. You can safely ignore
compression_address and address_merkle_tree_root_index. The client passes these for proof verification.3
System Accounts
Include system accounts such as the Light System Program required to interact with compressed state. The client includes them in the instruction.System Accounts List
System Accounts List
| Account | Description | |
|---|---|---|
| 1 | Verifies validity proofs and executes compressed account state transitions. | |
| 2 | CPI Authority PDA | PDA that authorizes CPIs from the Compressed Token Program to the Light System Program. |
| 3 | Registered Program PDA | Proves the Compressed Token Program is registered to use compression. |
| 4 | Signs CPI calls from the Light System Program to the Account Compression Program. | |
| 5 | Writes to state and address Merkle tree accounts. | |
| 6 | Solana System Program. |
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::SystemAccountInfos;
let system_accounts = SystemAccountInfos {
light_system_program: light_system_program.clone(),
cpi_authority_pda: cpi_authority_pda.clone(),
registered_program_pda: registered_program_pda.clone(),
account_compression_authority: account_compression_authority.clone(),
account_compression_program: account_compression_program.clone(),
system_program: system_program.clone(),
};
4
Build Account Infos and CPI the light token program
- Pass the required accounts
- Include
paramsandsystem_accountsfrom the previous steps - Use
invokeorinvoke_signed:- When
mint_seedis an external keypair, useinvoke. - When
mint_seedis a PDA, useinvoke_signedwith its seeds. - When both
mint_seedandauthorityare PDAs, useinvoke_signedwith both seeds.
- When
- invoke (External signer)
- invoke_signed (PDA mint_seed)
- invoke_signed (Two PDA signers)
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateCMintCpi;
CreateCMintCpi {
mint_seed: mint_seed.clone(),
authority: authority.clone(),
payer: payer.clone(),
address_tree: address_tree.clone(),
output_queue: output_queue.clone(),
system_accounts,
cpi_context: None,
cpi_context_account: None,
params,
}
.invoke()?;
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateCMintCpi;
let account_infos = CreateCMintCpi {
mint_seed: mint_seed.clone(),
authority: authority.clone(),
payer: payer.clone(),
address_tree: address_tree.clone(),
output_queue: output_queue.clone(),
system_accounts,
cpi_context: None,
cpi_context_account: None,
params,
};
let signer_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[bump]];
account_infos.invoke_signed(&[signer_seeds])?;
Report incorrect code
Copy
Ask AI
use light_ctoken_sdk::ctoken::CreateCMintCpi;
let account_infos = CreateCMintCpi {
mint_seed: mint_seed.clone(),
authority: authority.clone(),
payer: payer.clone(),
address_tree: address_tree.clone(),
output_queue: output_queue.clone(),
system_accounts,
cpi_context: None,
cpi_context_account: None,
params,
};
let mint_seed_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[mint_seed_bump]];
let authority_seeds: &[&[u8]] = &[MINT_AUTHORITY_SEED, &[authority_bump]];
account_infos.invoke_signed(&[mint_seed_seeds, 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::{
CreateCMintCpi, CreateCMintParams, ExtensionInstructionData, SystemAccountInfos,
},
CompressedProof,
};
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use crate::ID;
/// PDA seed for mint signer in invoke_signed variant
pub const MINT_SIGNER_SEED: &[u8] = b"mint_signer";
/// Instruction data for create compressed mint
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateCmintData {
pub decimals: u8,
pub address_merkle_tree_root_index: u16,
pub mint_authority: Pubkey,
pub proof: CompressedProof,
pub compression_address: [u8; 32],
pub mint: Pubkey,
pub freeze_authority: Option<Pubkey>,
pub extensions: Option<Vec<ExtensionInstructionData>>,
}
/// Handler for creating a compressed mint (invoke)
///
/// Uses the CreateCMintCpi builder pattern. This demonstrates how to:
/// 1. Build the CreateCMintParams struct from instruction data
/// 2. Build the CreateCMintCpi with accounts
/// 3. Call invoke() which handles instruction building and CPI
///
/// Account order:
/// - accounts[0]: compressed_token_program (for CPI)
/// - accounts[1]: light_system_program
/// - accounts[2]: mint_seed (signer)
/// - accounts[3]: payer (signer, also authority)
/// - accounts[4]: payer again (fee_payer in SDK)
/// - accounts[5]: cpi_authority_pda
/// - accounts[6]: registered_program_pda
/// - accounts[7]: account_compression_authority
/// - accounts[8]: account_compression_program
/// - accounts[9]: system_program
/// - accounts[10]: output_queue
/// - accounts[11]: address_tree
/// - accounts[12] (optional): cpi_context_account
pub fn process_create_cmint(
accounts: &[AccountInfo],
data: CreateCmintData,
) -> Result<(), ProgramError> {
if accounts.len() < 12 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Build the params
let params = CreateCMintParams {
decimals: data.decimals,
address_merkle_tree_root_index: data.address_merkle_tree_root_index,
mint_authority: data.mint_authority,
proof: data.proof,
compression_address: data.compression_address,
mint: data.mint,
freeze_authority: data.freeze_authority,
extensions: data.extensions,
};
// Build system accounts struct
let system_accounts = SystemAccountInfos {
light_system_program: accounts[1].clone(),
cpi_authority_pda: accounts[5].clone(),
registered_program_pda: accounts[6].clone(),
account_compression_authority: accounts[7].clone(),
account_compression_program: accounts[8].clone(),
system_program: accounts[9].clone(),
};
// Build the account infos struct
// In this case, payer == authority (accounts[3])
CreateCMintCpi {
mint_seed: accounts[2].clone(),
authority: accounts[3].clone(),
payer: accounts[3].clone(),
address_tree: accounts[11].clone(),
output_queue: accounts[10].clone(),
system_accounts,
cpi_context: None,
cpi_context_account: None,
params,
}
.invoke()?;
Ok(())
}
/// Handler for creating a compressed mint with PDA mint seed (invoke_signed)
///
/// Uses the CreateCMintCpi builder pattern with invoke_signed.
/// The mint_seed is a PDA derived from this program.
///
/// Account order:
/// - accounts[0]: compressed_token_program (for CPI)
/// - accounts[1]: light_system_program
/// - accounts[2]: mint_seed (PDA, not signer - program signs)
/// - accounts[3]: payer (signer, also authority)
/// - accounts[4]: payer again (fee_payer in SDK)
/// - accounts[5]: cpi_authority_pda
/// - accounts[6]: registered_program_pda
/// - accounts[7]: account_compression_authority
/// - accounts[8]: account_compression_program
/// - accounts[9]: system_program
/// - accounts[10]: output_queue
/// - accounts[11]: address_tree
/// - accounts[12] (optional): cpi_context_account
pub fn process_create_cmint_invoke_signed(
accounts: &[AccountInfo],
data: CreateCmintData,
) -> Result<(), ProgramError> {
if accounts.len() < 12 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Derive the PDA for the mint seed
let (pda, bump) = Pubkey::find_program_address(&[MINT_SIGNER_SEED], &ID);
// Verify the mint_seed account is the PDA we expect
if &pda != accounts[2].key {
return Err(ProgramError::InvalidSeeds);
}
// Build the params
let params = CreateCMintParams {
decimals: data.decimals,
address_merkle_tree_root_index: data.address_merkle_tree_root_index,
mint_authority: data.mint_authority,
proof: data.proof,
compression_address: data.compression_address,
mint: data.mint,
freeze_authority: data.freeze_authority,
extensions: data.extensions,
};
// Build system accounts struct
let system_accounts = SystemAccountInfos {
light_system_program: accounts[1].clone(),
cpi_authority_pda: accounts[5].clone(),
registered_program_pda: accounts[6].clone(),
account_compression_authority: accounts[7].clone(),
account_compression_program: accounts[8].clone(),
system_program: accounts[9].clone(),
};
// Build the account infos struct
// In this case, payer == authority (accounts[3])
let account_infos = CreateCMintCpi {
mint_seed: accounts[2].clone(),
authority: accounts[3].clone(),
payer: accounts[3].clone(),
address_tree: accounts[11].clone(),
output_queue: accounts[10].clone(),
system_accounts,
cpi_context: None,
cpi_context_account: None,
params,
};
// Invoke with PDA signing
let signer_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[bump]];
account_infos.invoke_signed(&[signer_seeds])?;
Ok(())
}
/// Handler for creating a compressed mint with PDA mint seed AND PDA authority (invoke_signed)
///
/// Uses the SDK's CreateCMintCpi with separate authority and payer accounts.
/// Both mint_seed and authority are PDAs signed by this program.
///
/// Account order:
/// - accounts[0]: compressed_token_program (for CPI)
/// - accounts[1]: light_system_program
/// - accounts[2]: mint_seed (PDA from MINT_SIGNER_SEED, not signer - program signs)
/// - accounts[3]: authority (PDA from MINT_AUTHORITY_SEED, not signer - program signs)
/// - accounts[4]: fee_payer (signer)
/// - accounts[5]: cpi_authority_pda
/// - accounts[6]: registered_program_pda
/// - accounts[7]: account_compression_authority
/// - accounts[8]: account_compression_program
/// - accounts[9]: system_program
/// - accounts[10]: output_queue
/// - accounts[11]: address_tree
/// - accounts[12] (optional): cpi_context_account
pub fn process_create_cmint_with_pda_authority(
accounts: &[AccountInfo],
data: CreateCmintData,
) -> Result<(), ProgramError> {
use crate::mint_to::MINT_AUTHORITY_SEED;
if accounts.len() < 12 {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Derive the PDA for the mint seed
let (mint_seed_pda, mint_seed_bump) =
Pubkey::find_program_address(&[MINT_SIGNER_SEED], &ID);
// Derive the PDA for the authority
let (authority_pda, authority_bump) = Pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &ID);
// Verify the mint_seed account is the PDA we expect
if &mint_seed_pda != accounts[2].key {
return Err(ProgramError::InvalidSeeds);
}
// Verify the authority account is the PDA we expect
if &authority_pda != accounts[3].key {
return Err(ProgramError::InvalidSeeds);
}
// Build the params - authority is the PDA
let params = CreateCMintParams {
decimals: data.decimals,
address_merkle_tree_root_index: data.address_merkle_tree_root_index,
mint_authority: authority_pda, // Use the derived PDA as authority
proof: data.proof,
compression_address: data.compression_address,
mint: data.mint,
freeze_authority: data.freeze_authority,
extensions: data.extensions,
};
// Build system accounts struct
let system_accounts = SystemAccountInfos {
light_system_program: accounts[1].clone(),
cpi_authority_pda: accounts[5].clone(),
registered_program_pda: accounts[6].clone(),
account_compression_authority: accounts[7].clone(),
account_compression_program: accounts[8].clone(),
system_program: accounts[9].clone(),
};
// Build the account infos struct using SDK
let account_infos = CreateCMintCpi {
mint_seed: accounts[2].clone(),
authority: accounts[3].clone(),
payer: accounts[4].clone(),
address_tree: accounts[11].clone(),
output_queue: accounts[10].clone(),
system_accounts,
cpi_context: None,
cpi_context_account: None,
params,
};
// Invoke with both PDAs signing
let mint_seed_seeds: &[&[u8]] = &[MINT_SIGNER_SEED, &[mint_seed_bump]];
let authority_seeds: &[&[u8]] = &[MINT_AUTHORITY_SEED, &[authority_bump]];
account_infos.invoke_signed(&[mint_seed_seeds, authority_seeds])?;
Ok(())
}