Contractless/src/bin/lookup_token.rs

173 lines
5.6 KiB
Rust

use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::Wallet;
const TOKEN_NAME_BYTES: usize = 15;
const HASH_BYTES: usize = 32;
const CREATOR_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH;
const TOKEN_COUNT_BYTES: usize = 8;
const TOKEN_SPREAD_BYTES: usize = 4;
const HARD_LIMIT_BYTES: usize = 1;
const ISSUE_COUNT_BYTES: usize = 4;
const TOKEN_LOOKUP_FIXED_BYTES: usize = TOKEN_NAME_BYTES
+ HASH_BYTES
+ CREATOR_BYTES
+ TOKEN_COUNT_BYTES
+ TOKEN_SPREAD_BYTES
+ HARD_LIMIT_BYTES
+ ISSUE_COUNT_BYTES;
#[tokio::main]
async fn main() {
// Command 35 asks a peer for token metadata by token name.
let hashmap_key = generate_uid();
let rpc_command = 35;
// The request encoder pads/truncates the token name to the fixed 15-byte field.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_token <token_name>");
return;
}
let token_name = args[1].clone();
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a parsable token record or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
token_name.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Successful token lookups are binary records with variable issue/burn hash lists.
if let Some(token_json) = decode_token_lookup(&response) {
println!("{token_json}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}
fn decode_token_lookup(response: &[u8]) -> Option<String> {
// The fixed portion must be present before reading variable-length issue/burn lists.
if response.len() < TOKEN_LOOKUP_FIXED_BYTES {
return None;
}
// Decode the fixed-width token fields in the same order the RPC command writes them.
let mut cursor = 0usize;
let token_name = binary_to_string(response[cursor..cursor + TOKEN_NAME_BYTES].to_vec())
.trim()
.to_string();
cursor += TOKEN_NAME_BYTES;
let contract = hex::encode(&response[cursor..cursor + HASH_BYTES]);
cursor += HASH_BYTES;
let creator = Wallet::bytes_to_short_address(&response[cursor..cursor + CREATOR_BYTES])?;
cursor += CREATOR_BYTES;
let token_count = u64::from_le_bytes(
response[cursor..cursor + TOKEN_COUNT_BYTES]
.try_into()
.ok()?,
);
cursor += TOKEN_COUNT_BYTES;
let token_spread = u32::from_le_bytes(
response[cursor..cursor + TOKEN_SPREAD_BYTES]
.try_into()
.ok()?,
);
cursor += TOKEN_SPREAD_BYTES;
let hard_limit = response[cursor];
cursor += HARD_LIMIT_BYTES;
let issue_count = u32::from_le_bytes(
response[cursor..cursor + ISSUE_COUNT_BYTES]
.try_into()
.ok()?,
);
cursor += ISSUE_COUNT_BYTES;
// After the issue count, each issue hash is 32 bytes.
let issue_bytes = issue_count as usize * 32;
if response.len() < cursor + issue_bytes + 4 {
return None;
}
let mut issued_hashes = Vec::with_capacity(issue_count as usize);
for chunk in response[cursor..cursor + issue_bytes].chunks(32) {
issued_hashes.push(hex::encode(chunk));
}
cursor += issue_bytes;
let burn_count = u32::from_le_bytes(response[cursor..cursor + 4].try_into().ok()?);
cursor += 4;
// The final section is a fixed number of 32-byte burn hashes.
let burn_bytes = burn_count as usize * 32;
if response.len() != cursor + burn_bytes {
return None;
}
let mut burned_hashes = Vec::with_capacity(burn_count as usize);
for chunk in response[cursor..cursor + burn_bytes].chunks(32) {
burned_hashes.push(hex::encode(chunk));
}
// Convert atomic token units and counters into user-facing JSON.
let display_token_count = token_count as f64 / 100_000_000.0;
let display_token_spread = format!("{token_spread} addresses");
let output = json!({
"token_name": token_name,
"genesis": contract,
"creator": creator,
"token_count": display_token_count,
"token_spread": display_token_spread,
"hard_limit": hard_limit == 1,
"issued_hashes": issued_hashes,
"burned_hashes": burned_hashes
});
serde_json::to_string_pretty(&output).ok()
}