sim-cli 0.7.0

CLI tool for running and comparing Solana simulator backtests
Documentation
//! Best-effort token-symbol resolution via on-chain Metaplex metadata.
//! Failures degrade to the generic `TOK` ticker; nothing here is load-bearing.

use std::collections::{BTreeSet, HashMap};

use futures::{StreamExt, stream};
use solana_address::Address;
use solana_client::nonblocking::rpc_client::RpcClient;

use super::model::PnlDiff;

const METADATA_PROGRAM: &str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
const RPC_URL: &str = "https://api.mainnet-beta.solana.com";
const WSOL_MINT: &str = "So11111111111111111111111111111111111111112";
/// In-flight lookup cap; conservative to stay polite to the public RPC.
const SYMBOL_LOOKUP_CONCURRENCY: usize = 8;

pub(super) async fn resolve_token_symbols(pnl_diffs: &[PnlDiff]) -> HashMap<String, String> {
    let mints: BTreeSet<&str> = pnl_diffs
        .iter()
        .flat_map(|d| d.tokens.iter().map(|t| t.mint.as_str()))
        .collect();

    if mints.is_empty() {
        return HashMap::new();
    }

    let Ok(program_id) = METADATA_PROGRAM.parse::<Address>() else {
        return HashMap::new();
    };
    let rpc = RpcClient::new(RPC_URL.to_string());
    let rpc = &rpc;

    stream::iter(mints)
        .map(|mint| async move {
            lookup_symbol(rpc, program_id, mint)
                .await
                .map(|symbol| (mint.to_string(), symbol))
        })
        .buffer_unordered(SYMBOL_LOOKUP_CONCURRENCY)
        .filter_map(|resolved| async move { resolved })
        .collect()
        .await
}

/// Fetch one mint's Metaplex metadata account and extract its symbol.
/// Any failure along the way (bad mint, RPC error, missing/empty symbol)
/// resolves to `None` and the mint keeps the generic ticker.
async fn lookup_symbol(rpc: &RpcClient, program_id: Address, mint: &str) -> Option<String> {
    let mint_addr = mint.parse::<Address>().ok()?;
    let (pda, _) = Address::find_program_address(
        &[b"metadata", program_id.as_ref(), mint_addr.as_ref()],
        &program_id,
    );
    let account = rpc.get_account(&pda).await.ok()?;
    parse_metadata_symbol(&account.data)
}

fn parse_metadata_symbol(data: &[u8]) -> Option<String> {
    // Layout: 1 (key) + 32 (update_authority) + 32 (mint) + (4+n) name + (4+m) symbol
    let mut off = 1 + 32 + 32;
    let name_len = u32::from_le_bytes(data.get(off..off + 4)?.try_into().ok()?) as usize;
    off += 4 + name_len;
    let sym_len = u32::from_le_bytes(data.get(off..off + 4)?.try_into().ok()?) as usize;
    off += 4;
    let symbol = std::str::from_utf8(data.get(off..off + sym_len)?)
        .ok()?
        .trim_end_matches('\0')
        .to_string();
    if symbol.is_empty() {
        return None;
    }
    Some(symbol)
}

pub(super) fn resolve_symbol<'a>(mint: &str, symbols: &'a HashMap<String, String>) -> &'a str {
    if mint == WSOL_MINT {
        "WSOL"
    } else {
        symbols.get(mint).map(String::as_str).unwrap_or("TOK")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Metaplex metadata layout: 1 (key) + 32 (update_authority) + 32 (mint)
    /// + (4+n) name + (4+m) symbol.
    fn metadata_bytes(name: &str, symbol: &str) -> Vec<u8> {
        let mut data = vec![0_u8; 65];
        data.extend((name.len() as u32).to_le_bytes());
        data.extend(name.as_bytes());
        data.extend((symbol.len() as u32).to_le_bytes());
        data.extend(symbol.as_bytes());
        data
    }

    #[test]
    fn parses_symbol_and_trims_padding() {
        let data = metadata_bytes("USD Coin", "USDC\0\0\0");
        assert_eq!(parse_metadata_symbol(&data).as_deref(), Some("USDC"));
    }

    #[test]
    fn empty_or_all_padding_symbol_is_none() {
        assert_eq!(parse_metadata_symbol(&metadata_bytes("Name", "")), None);
        assert_eq!(
            parse_metadata_symbol(&metadata_bytes("Name", "\0\0\0")),
            None
        );
    }

    #[test]
    fn truncated_buffer_is_none() {
        let data = metadata_bytes("Name", "USDC");
        // Cut into the symbol bytes: the declared length overruns the buffer.
        assert_eq!(parse_metadata_symbol(&data[..data.len() - 2]), None);
        // Cut before the name length prefix.
        assert_eq!(parse_metadata_symbol(&data[..60]), None);
    }
}