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::encode; 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; use blockchain::{Map, Value}; const NFT_NAME_BYTES: usize = 15; const SERIES_BYTES: usize = 4; const GENESIS_HASH_BYTES: usize = 32; const CREATOR_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH; const IPFS_BYTES: usize = 100; const HOLDER_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH; const HISTORY_COUNT_BYTES: usize = 4; const HISTORY_ENTRY_SIZE: usize = 32 + 4 + 1 + 1 + (2 * Wallet::SHORT_ADDRESS_BYTES_LENGTH) + 15 + 4 + 8; const NFT_LOOKUP_FIXED_BYTES: usize = NFT_NAME_BYTES + SERIES_BYTES + GENESIS_HASH_BYTES + CREATOR_BYTES + IPFS_BYTES + HOLDER_BYTES + HISTORY_COUNT_BYTES; fn action_name(action: u8) -> &'static str { // History action bytes are stored compactly on the wire and expanded for display. match action { 1 => "create_nft", 2 => "transfer", 3 => "swap", 4 => "loan_locked", 5 => "loan_payment", 6 => "collateral_claimed", 7 => "loan_issued", _ => "unknown", } } fn decode_wallet(bytes: &[u8]) -> String { // Empty wallet fields are encoded as all zeroes; otherwise decode a short address. if bytes.iter().all(|b| *b == 0) { String::new() } else { Wallet::bytes_to_short_address(bytes).unwrap_or_default() } } fn decode_history_entry(entry: &[u8]) -> Option { // Each history row has a fixed binary size so bad lengths mean an invalid response. if entry.len() != HISTORY_ENTRY_SIZE { return None; } // Decode the history fields in the same order the RPC command writes them. let txid = encode(&entry[0..32]); let block = u32::from_le_bytes(entry[32..36].try_into().ok()?); let txtype = entry[36]; let action = entry[37]; let from = decode_wallet(&entry[38..60]); let to = decode_wallet(&entry[60..82]); let received_asset = binary_to_string(entry[82..97].to_vec()).trim().to_string(); let received_series = u32::from_le_bytes(entry[97..101].try_into().ok()?); let received_value_raw = u64::from_le_bytes(entry[101..109].try_into().ok()?); let received_value = received_value_raw as f64 / 100_000_000.0; // Omit empty optional fields so the CLI output stays readable. let mut obj = Map::new(); obj.insert("txid".to_string(), json!(txid)); obj.insert("block".to_string(), json!(block)); obj.insert("txtype".to_string(), json!(txtype)); obj.insert("action".to_string(), json!(action_name(action))); if !from.is_empty() { obj.insert("from".to_string(), json!(from)); } if !to.is_empty() { obj.insert("to".to_string(), json!(to)); } if !received_asset.is_empty() { obj.insert("received_asset".to_string(), json!(received_asset)); if received_series > 0 { obj.insert("received_series".to_string(), json!(received_series)); } obj.insert("received_value".to_string(), json!(received_value)); } Some(Value::Object(obj)) } fn decode_nft_lookup(response: &[u8]) -> Option { // The fixed NFT fields must exist before any history entries can be decoded. if response.len() < NFT_LOOKUP_FIXED_BYTES { return None; } // Decode the fixed-width NFT metadata fields in wire order. let mut cursor = 0usize; let nft_name = binary_to_string(response[cursor..cursor + NFT_NAME_BYTES].to_vec()) .trim() .to_string(); cursor += NFT_NAME_BYTES; let series = u32::from_le_bytes(response[cursor..cursor + SERIES_BYTES].try_into().ok()?); cursor += SERIES_BYTES; let genesis = encode(&response[cursor..cursor + GENESIS_HASH_BYTES]); cursor += GENESIS_HASH_BYTES; let creator = Wallet::bytes_to_short_address(&response[cursor..cursor + CREATOR_BYTES])?; cursor += CREATOR_BYTES; let ipfs_cid = binary_to_string(response[cursor..cursor + IPFS_BYTES].to_vec()) .trim() .to_string(); cursor += IPFS_BYTES; let current_holder = decode_wallet(&response[cursor..cursor + HOLDER_BYTES]); cursor += HOLDER_BYTES; let history_count = u32::from_le_bytes( response[cursor..cursor + HISTORY_COUNT_BYTES] .try_into() .ok()?, ); cursor += HISTORY_COUNT_BYTES; // Any remaining bytes are fixed-size history entries. let mut history = Vec::new(); let mut offset = cursor; while offset + HISTORY_ENTRY_SIZE <= response.len() { if let Some(entry) = decode_history_entry(&response[offset..offset + HISTORY_ENTRY_SIZE]) { history.push(entry); } offset += HISTORY_ENTRY_SIZE; } // Convert the binary record into user-facing JSON. let mut output = Map::new(); output.insert("nft_name".to_string(), json!(nft_name)); output.insert("series".to_string(), json!(series)); if series == 0 { output.insert("ipfs".to_string(), json!(format!("ipfs://{ipfs_cid}"))); } else { output.insert( "ipfs".to_string(), json!(format!("ipfs://{ipfs_cid}/{series}.json")), ); } if series > 0 { output.insert( "asset_name".to_string(), json!(format!("{nft_name}_{series}")), ); } output.insert("genesis".to_string(), json!(genesis)); output.insert("creator".to_string(), json!(creator)); output.insert("current_holder".to_string(), json!(current_holder)); output.insert("history_count".to_string(), json!(history_count)); output.insert("history".to_string(), Value::Array(history)); serde_json::to_string_pretty(&Value::Object(output)).ok() } #[tokio::main] async fn main() { // Command 36 asks a peer for NFT metadata by name and series/item number. let hashmap_key = generate_uid(); let rpc_command = 36; // Item number 0 means a 1/1 NFT; nonzero values identify a series item. let args: Vec = env::args().collect(); if args.len() != 3 { println!("Usage: ./lookup_nft "); println!("Use item_number 0 for a 1/1 NFT, or the specific item number for a series NFT."); return; } let nft_name = args[1].clone(); let item_number = args[2].clone(); let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; // sending_request expects command 36 lookup input as "name|series". let payload = format!("{nft_name}|{item_number}"); // Try each configured peer until one returns a parsable NFT 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, payload.clone(), rpc_command, handshake::HandshakeWallet::WalletKey(encryption_key.clone()), hashmap_key, ) .await; match result { Ok(response) => { // Successful NFT lookups are binary records; errors are returned as text. if let Some(output) = decode_nft_lookup(&response) { println!("{output}"); 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"); } }