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";
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
}
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> {
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::*;
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");
assert_eq!(parse_metadata_symbol(&data[..data.len() - 2]), None);
assert_eq!(parse_metadata_symbol(&data[..60]), None);
}
}