Contractless/src/rpc/commands/token_lookup.rs

298 lines
9.8 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use crate::blocks::token::CreateTokenTransaction;
use crate::common::binary_conversions::binary_to_string;
2026-05-26 06:24:57 +00:00
use crate::fs;
2026-05-24 17:56:57 +00:00
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};
2026-05-26 06:24:57 +00:00
use crate::wallets::structures::Wallet;
2026-05-24 17:56:57 +00:00
use crate::PathBuf;
2026-05-26 06:24:57 +00:00
use crate::{decode, encode};
2026-05-24 17:56:57 +00:00
fn parse_token_supply(value: &[u8]) -> Option<u64> {
// 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::<u64>().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<String> {
// 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,
};
2026-05-26 06:24:57 +00:00
let RpcResponse::Binary(tx_bytes) =
request_transaction_by_txid(db, txid_bytes.to_vec()).await;
2026-05-24 17:56:57 +00:00
// 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<String> {
// 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()),
};
2026-05-26 06:24:57 +00:00
let RpcResponse::Binary(tx_bytes) =
request_transaction_by_txid(db, decode(&origin_hash).unwrap_or_default()).await;
2026-05-24 17:56:57 +00:00
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)
}