use crate::blocks::token::CreateTokenTransaction; use crate::common::binary_conversions::binary_to_string; use crate::fs; use crate::records::balance_sheet::pathing::{balance_asset_segments, balance_root_path}; use crate::records::balance_sheet::tokens_to_lower::strip_spaces_and_lowercase; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; use crate::sled::{Db, Tree}; use crate::wallets::structures::Wallet; use crate::PathBuf; use crate::{decode, encode}; fn parse_token_supply(value: &[u8]) -> Option { // Token supply may be stored either as raw bytes or as a decimal string. if value.len() == 8 { let mut buffer = [0u8; 8]; buffer.copy_from_slice(value); return Some(u64::from_le_bytes(buffer)); } let value_str = binary_to_string(value.to_vec()); value_str.trim().parse::().ok() } fn count_wallets_holding(normalized_token: &str) -> u64 { // Spread is computed by scanning address balance files for non-zero // holdings of the requested token. let mut count = 0_u64; let root = balance_root_path(); let addresses = match fs::read_dir(&root) { Ok(entries) => entries, Err(_) => return 0, }; for entry in addresses.flatten() { let entry_path = entry.path(); if !entry_path.is_dir() { continue; } // Balance files are sharded under hashed asset path segments, so // reconstruct the expected token balance path for each address. let mut balance_path: PathBuf = entry_path.clone(); for segment in balance_asset_segments(normalized_token) { balance_path.push(segment); } balance_path.push("wallet.bal"); let bytes = match fs::read(&balance_path) { Ok(bytes) => bytes, Err(_) => continue, }; if bytes.len() < 8 { continue; } let mut buffer = [0u8; 8]; buffer.copy_from_slice(&bytes[..8]); if u64::from_le_bytes(buffer) > 0 { count += 1; } } count } async fn find_origin_hash( db: &Db, origins_tree: &Tree, matched_ticker: &str, normalized_query: &str, ) -> Option { // Prefer the dedicated token-origins tree, but fall back to scanning // saved txids if the origin mapping is missing. if let Ok(Some(bytes)) = origins_tree.get(matched_ticker.as_bytes()) { return Some(binary_to_string(bytes.to_vec())); } let txid_tree = db.open_tree("txid").ok()?; for entry in txid_tree.iter() { let (txid_bytes, _) = match entry { Ok(pair) => pair, Err(_) => continue, }; let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await; // The fallback only cares about create-token transactions because // those define the origin hash for a token. if tx_bytes.is_empty() || tx_bytes[0] != 3 { continue; } let contract = match CreateTokenTransaction::from_bytes(tx_bytes[0], &tx_bytes[1..]).await { Ok(tx) => tx, Err(_) => continue, }; if strip_spaces_and_lowercase(&contract.unsigned_create_token.ticker) == normalized_query { return Some(encode(&txid_bytes)); } } None } fn find_matching_ticker( tokens_tree: &Tree, origins_tree: &Tree, normalized_query: &str, ) -> Option { // Prefer live token rows, but fall back to the origin registry so // fully burned tokens can still be looked up historically. for entry in tokens_tree.iter() { let (key, _) = match entry { Ok(pair) => pair, Err(_) => continue, }; let stored_ticker = binary_to_string(key.to_vec()); if strip_spaces_and_lowercase(&stored_ticker) == normalized_query { return Some(stored_ticker); } } for entry in origins_tree.iter() { let (key, _) = match entry { Ok(pair) => pair, Err(_) => continue, }; let stored_ticker = binary_to_string(key.to_vec()); if strip_spaces_and_lowercase(&stored_ticker) == normalized_query { return Some(stored_ticker); } } None } async fn collect_token_history( db: &Db, ticker: &str, origin_hash_bytes: &[u8], ) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) { // Token lookup expands saved token history into separate issue and // burn txid lists so the current supply can be audited from history. let history_tree = match db.open_tree("token_history") { Ok(tree) => tree, Err(_) => return (Vec::new(), Vec::new()), }; let mut issued_hashes = Vec::new(); let mut burned_hashes = Vec::new(); let Some(history_bytes) = history_tree .get(ticker.as_bytes()) .ok() .flatten() .map(|bytes| bytes.to_vec()) else { return (issued_hashes, burned_hashes); }; for chunk in history_bytes.chunks(32) { if chunk.len() != 32 || chunk == origin_hash_bytes { continue; } let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, chunk.to_vec()).await; if tx_bytes.is_empty() { continue; } let mut hash = [0u8; 32]; hash.copy_from_slice(chunk); // Token history stores txids only; inspect each transaction type // to divide the history into issue and burn lists. match tx_bytes[0] { 10 => burned_hashes.push(hash), 11 => issued_hashes.push(hash), _ => {} } } (issued_hashes, burned_hashes) } pub async fn lookup_token_details(db: &Db, token_name: String) -> RpcResponse { // Build the token-details response from token supply, origin txid, // creator wallet, holder spread, and related issuance and burn txids. let normalized_query = strip_spaces_and_lowercase(&token_name); if normalized_query.is_empty() { return RpcResponse::Binary(b"error: Token name required".to_vec()); } let tokens_tree = db.open_tree("tokens").unwrap(); let origins_tree = db.open_tree("token_origins").unwrap(); let limits_tree = db.open_tree("token_limits").unwrap(); let matched_ticker = match find_matching_ticker(&tokens_tree, &origins_tree, &normalized_query) { Some(ticker) => ticker, None => return RpcResponse::Binary(b"error: Token not found".to_vec()), }; // Current supply comes from the live token registry, but fully // burned tokens still resolve historically through the origin tree. let token_count = tokens_tree .get(matched_ticker.as_bytes()) .ok() .flatten() .and_then(|value| parse_token_supply(value.as_ref())); let origin_hash = match find_origin_hash(db, &origins_tree, &matched_ticker, &normalized_query).await { Some(hash) => hash, None => return RpcResponse::Binary(b"error: Token origin not found".to_vec()), }; let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, decode(&origin_hash).unwrap_or_default()).await; if tx_bytes.is_empty() { return RpcResponse::Binary(b"error: Token contract transaction not found".to_vec()); } let txtype = tx_bytes[0]; let body = &tx_bytes[1..]; let contract = match CreateTokenTransaction::from_bytes(txtype, body).await { Ok(tx) => tx, Err(_) => { return RpcResponse::Binary(b"error: Failed to parse token contract".to_vec()); } }; let spread = count_wallets_holding(&normalized_query); let token_count = token_count.unwrap_or(0); let contract_hash_bytes = match decode(&origin_hash) { Ok(bytes) if bytes.len() == 32 => bytes, _ => return RpcResponse::Binary(b"error: Failed to decode token origin".to_vec()), }; let creator_bytes = match Wallet::short_address_to_bytes(&contract.unsigned_create_token.creator) { Some(bytes) => bytes, None => return RpcResponse::Binary(b"error: Failed to encode token creator".to_vec()), }; let ticker_bytes = contract.unsigned_create_token.ticker.as_bytes().to_vec(); if ticker_bytes.len() != 15 || creator_bytes.len() != Wallet::SHORT_ADDRESS_BYTES_LENGTH { return RpcResponse::Binary(b"error: Invalid token lookup response size".to_vec()); } let hard_limit = limits_tree .get(matched_ticker.as_bytes()) .ok() .flatten() .and_then(|bytes| bytes.first().copied()) .unwrap_or(contract.unsigned_create_token.hard_limit); let (issued_hashes, burned_hashes) = collect_token_history(db, &matched_ticker, &contract_hash_bytes).await; // Response layout: ticker, origin hash, creator, live supply, holder // spread, hard-limit flag, issue-count/list, burn-count/list. let mut response = Vec::with_capacity( 15 + 32 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + 4 + 1 + 4 + (issued_hashes.len() * 32) + 4 + (burned_hashes.len() * 32), ); response.extend_from_slice(&ticker_bytes); response.extend_from_slice(&contract_hash_bytes); response.extend_from_slice(&creator_bytes); response.extend_from_slice(&token_count.to_le_bytes()); response.extend_from_slice(&(spread as u32).to_le_bytes()); response.push(hard_limit); response.extend_from_slice(&(issued_hashes.len() as u32).to_le_bytes()); for hash in issued_hashes { response.extend_from_slice(&hash); } response.extend_from_slice(&(burned_hashes.len() as u32).to_le_bytes()); for hash in burned_hashes { response.extend_from_slice(&hash); } RpcResponse::Binary(response) } pub async fn token_catalog(db: &Db) -> RpcResponse { let tokens_tree = match db.open_tree("tokens") { Ok(tree) => tree, Err(err) => { return RpcResponse::Binary( format!("error: Failed to open tokens tree: {err}").into_bytes(), ) } }; let origins_tree = match db.open_tree("token_origins") { Ok(tree) => tree, Err(err) => { return RpcResponse::Binary( format!("error: Failed to open token origins tree: {err}").into_bytes(), ) } }; let limits_tree = match db.open_tree("token_limits") { Ok(tree) => tree, Err(err) => { return RpcResponse::Binary( format!("error: Failed to open token limits tree: {err}").into_bytes(), ) } }; let mut rows = Vec::new(); for entry in origins_tree.iter() { let (ticker_key, origin_value) = match entry { Ok(pair) => pair, Err(_) => continue, }; let ticker = binary_to_string(ticker_key.to_vec()); let normalized_ticker = strip_spaces_and_lowercase(&ticker); if normalized_ticker.is_empty() { continue; } let origin_hash = binary_to_string(origin_value.to_vec()); let origin_hash_bytes = match decode(&origin_hash) { Ok(bytes) if bytes.len() == 32 => bytes, _ => continue, }; let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, origin_hash_bytes.clone()).await; if tx_bytes.is_empty() || tx_bytes[0] != 3 { continue; } let contract = match CreateTokenTransaction::from_bytes(tx_bytes[0], &tx_bytes[1..]).await { Ok(tx) => tx, Err(_) => continue, }; let creator_bytes = match Wallet::short_address_to_bytes(&contract.unsigned_create_token.creator) { Some(bytes) => bytes, None => continue, }; let current_supply = tokens_tree .get(ticker_key.as_ref()) .ok() .flatten() .and_then(|value| parse_token_supply(value.as_ref())) .unwrap_or(0); let hard_limit = limits_tree .get(ticker_key.as_ref()) .ok() .flatten() .and_then(|bytes| bytes.first().copied()) .unwrap_or(contract.unsigned_create_token.hard_limit); let (_, burned_hashes) = collect_token_history(db, &ticker, &origin_hash_bytes).await; let mut total_burned = 0_u64; for hash in burned_hashes { let RpcResponse::Binary(burn_bytes) = request_transaction_by_txid(db, hash.to_vec()).await; if burn_bytes.is_empty() || burn_bytes[0] != 10 { continue; } let Ok(burn) = crate::blocks::burn::BurnTransaction::from_bytes(burn_bytes[0], &burn_bytes[1..]) .await else { continue; }; if burn.unsigned_burn.nft_series == 0 && strip_spaces_and_lowercase(&burn.unsigned_burn.coin) == normalized_ticker { total_burned = total_burned.saturating_add(burn.unsigned_burn.value); } } let mut padded_ticker = ticker.as_bytes().to_vec(); if padded_ticker.len() < 15 { padded_ticker.resize(15, b' '); } if padded_ticker.len() != 15 || creator_bytes.len() != Wallet::SHORT_ADDRESS_BYTES_LENGTH || origin_hash_bytes.len() != 32 { continue; } rows.push(( padded_ticker, origin_hash_bytes, creator_bytes, contract.unsigned_create_token.time, contract.unsigned_create_token.number, current_supply, total_burned, count_wallets_holding(&normalized_ticker) as u32, hard_limit, )); } rows.sort_by(|left, right| { binary_to_string(left.0.clone()) .to_ascii_lowercase() .cmp(&binary_to_string(right.0.clone()).to_ascii_lowercase()) }); let mut response = Vec::with_capacity(4 + rows.len() * 102); response.extend_from_slice(&(rows.len() as u32).to_le_bytes()); for ( ticker, origin_hash, creator, created_timestamp, initial_supply, current_supply, total_burned, holder_count, hard_limit, ) in rows { response.extend_from_slice(&ticker); response.extend_from_slice(&origin_hash); response.extend_from_slice(&creator); response.extend_from_slice(&created_timestamp.to_le_bytes()); response.extend_from_slice(&initial_supply.to_le_bytes()); response.extend_from_slice(¤t_supply.to_le_bytes()); response.extend_from_slice(&total_burned.to_le_bytes()); response.extend_from_slice(&holder_count.to_le_bytes()); response.push(hard_limit); } RpcResponse::Binary(response) }