Skip to main content
EventDescription
New mintsRaw mint data
Mint updatesSupply changes, authority changes
TokenMetadataName, symbol, URI, additional_metadata
Find devnet examples here.

Setup

Light mints are Solana accounts owned by the Light Token Program. Subscribe to account updates to detect new mints and changes.
Cargo.toml
[dependencies]
helius-laserstream = "0.1"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
anyhow = "1"
dotenvy = "0.15"
bs58 = "0.5"
borsh = "0.10"

light-token-interface = "0.3.0"
light-compressed-account = { version = "0.9.0", features = ["std"] }

# Pin blake3 to avoid constant_time_eq edition2024 issue
blake3 = "=1.5.5"
use borsh::BorshDeserialize;
use futures::StreamExt;
use helius_laserstream::grpc::subscribe_request_filter_accounts_filter::Filter;
use helius_laserstream::grpc::subscribe_request_filter_accounts_filter_memcmp::Data;
use helius_laserstream::grpc::{
    SubscribeRequestFilterAccounts, SubscribeRequestFilterAccountsFilter,
    SubscribeRequestFilterAccountsFilterMemcmp,
};
use helius_laserstream::{subscribe, LaserstreamConfig};
use light_token_interface::state::{ExtensionStruct, Mint};

const LIGHT_TOKEN_PROGRAM_ID: &str = "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m";

/// Byte offset of account_type in the Mint account data.
/// BaseMint (82) + MintMetadata (67) + reserved (16) = 165
const ACCOUNT_TYPE_OFFSET: u64 = 165;
1

Connect to Laserstream

let config = LaserstreamConfig::new(
    "https://laserstream-mainnet-ewr.helius-rpc.com".to_string(),
    std::env::var("HELIUS_API_KEY")?,
);
// Subscribe to Solana accounts owned by the Light Token Program
// with account_type = 1 (Mint) at byte offset 165
let request = helius_laserstream::grpc::SubscribeRequest {
    accounts: [(
        "light_mints".to_string(),
        SubscribeRequestFilterAccounts {
            owner: vec![LIGHT_TOKEN_PROGRAM_ID.to_string()],
            filters: vec![SubscribeRequestFilterAccountsFilter {
                filter: Some(Filter::Memcmp(SubscribeRequestFilterAccountsFilterMemcmp {
                    offset: ACCOUNT_TYPE_OFFSET,
                    data: Some(Data::Bytes(vec![1])),
                })),
            }],
            nonempty_txn_signature: Some(true),
            ..Default::default()
        },
    )]
    .into(),
    ..Default::default()
};

let (stream, _handle) = subscribe(config, request);
tokio::pin!(stream);

while let Some(update) = stream.next().await {
    // Process account updates...
}
2

Deserialize mint accounts

if let Some(helius_laserstream::grpc::subscribe_update::UpdateOneof::Account(
    account_update,
)) = msg.update_oneof
{
    if let Some(account_info) = account_update.account {
        let pubkey = bs58::encode(&account_info.pubkey).into_string();
        let tx_sig = account_info
            .txn_signature
            .as_ref()
            .map(|s| bs58::encode(s).into_string())
            .unwrap_or_default();

        match Mint::deserialize(&mut account_info.data.as_slice()) {
            Ok(mint) => {
                println!("Mint: {}", pubkey);
                println!("  tx: {}", tx_sig);
                println!("  decimals: {}", mint.base.decimals);
                println!("  supply: {}", mint.base.supply);

                if let Some(authority) = &mint.base.mint_authority {
                    println!(
                        "  mint_authority: {}",
                        bs58::encode(authority.to_bytes()).into_string()
                    );
                }

                if let Some(authority) = &mint.base.freeze_authority {
                    println!(
                        "  freeze_authority: {}",
                        bs58::encode(authority.to_bytes()).into_string()
                    );
                }
            }
            Err(e) => {
                eprintln!("Failed to deserialize mint {}: {}", pubkey, e);
            }
        }
    }
}
3

Extract Token Metadata from mint extensions

fn extract_metadata(mint: &Mint) -> Option<(String, String, String)> {
    let extensions = mint.extensions.as_ref()?;

    for ext in extensions {
        if let ExtensionStruct::TokenMetadata(m) = ext {
            let name = String::from_utf8_lossy(&m.name).to_string();
            let symbol = String::from_utf8_lossy(&m.symbol).to_string();
            let uri = String::from_utf8_lossy(&m.uri).to_string();
            return Some((name, symbol, uri));
        }
    }
    None
}
if let Some((name, symbol, uri)) = extract_metadata(&mint) {
    println!("  Name: {}", name);
    println!("  Symbol: {}", symbol);
    println!("  URI: {}", uri);
}

Data Layouts

Mint

#[repr(C)]
pub struct Mint {
    pub base: BaseMint,
    pub metadata: MintMetadata,
    pub reserved: [u8; 16],
    pub account_type: u8,
    pub compression: CompressionInfo,
    pub extensions: Option<Vec<ExtensionStruct>>,
}

/// SPL-compatible base mint structure
#[repr(C)]
pub struct BaseMint {
    pub mint_authority: Option<Pubkey>,
    pub supply: u64,
    pub decimals: u8,
    pub is_initialized: bool,
    pub freeze_authority: Option<Pubkey>,
}

/// Light Protocol metadata for mints (67 bytes)
#[repr(C)]
pub struct MintMetadata {
    pub version: u8,
    pub mint_decompressed: bool,
    pub mint: Pubkey,
    pub mint_signer: [u8; 32],
    pub bump: u8,
}

TokenMetadata

#[repr(C)]
pub struct TokenMetadata {
    pub update_authority: Pubkey,  // [0u8; 32] = immutable
    pub mint: Pubkey,
    pub name: Vec<u8>,
    pub symbol: Vec<u8>,
    pub uri: Vec<u8>,
    pub additional_metadata: Vec<AdditionalMetadata>,
}

pub struct AdditionalMetadata {
    pub key: Vec<u8>,
    pub value: Vec<u8>,
}

Index Tokens

Toolkit to stream light-token accounts