use blockchain::common::cli_prompts::prompt_hidden_nonempty; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::from_str; use blockchain::read_to_string; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; use blockchain::tilde; use blockchain::Value; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; use rustyline::{history::DefaultHistory, CompletionType, Config, Editor}; use rustyline_derive::Completer; use rustyline_derive::Helper as RustyHelper; use rustyline_derive::Highlighter as RustyHighlighter; use rustyline_derive::Hinter as RustyHinter; use rustyline_derive::Validator as RustyValidator; fn format_balance(balance: u64) -> String { // Balance values are atomic units and display with 8 decimal places. let whole = balance / 100_000_000; let fractional = balance % 100_000_000; format!("{whole}.{fractional:08}") } fn extract_address(contents: &str) -> Result { // Address files may be plain text or saved wallet JSON. let trimmed = contents.trim(); if trimmed.is_empty() { return Err("Address file is empty".to_string()); } if trimmed.starts_with('{') { // Prefer short_address for balance lookups, but accept long_address too. let value: Value = from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?; let address = value .get("short_address") .and_then(|v| v.as_str()) .or_else(|| value.get("long_address").and_then(|v| v.as_str())) .ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?; return Ok(address.trim().to_string()); } Ok(trimmed.to_string()) } #[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)] struct PathHelper { #[rustyline(Completer)] completer: FilenameCompleter, } fn prompt_for_path(prompt: &str) -> Result { // Rustyline gives the interactive prompt filesystem completion. let config = Config::builder() .completion_type(CompletionType::List) .build(); let mut editor = Editor::::with_config(config).map_err(|e| e.to_string())?; editor.set_helper(Some(PathHelper { completer: FilenameCompleter::new(), })); match editor.readline(prompt) { Ok(line) => Ok(line.trim().to_string()), Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { Err("Input cancelled".to_string()) } Err(err) => Err(format!("Failed to read address file path: {err}")), } } #[tokio::main] async fn main() { // Command 23 asks a peer for all asset balances for one wallet address. let hashmap_key = generate_uid(); let rpc_command = 23; // Accept an address file path as an arg or prompt interactively. let args: Vec = env::args().collect(); if args.len() > 1 && args.len() != 2 { println!("Usage: ./address_balance_lookup "); return; } let address_file_path = if args.len() == 2 { args[1].clone() } else { match prompt_for_path( "Please enter the path to the file containing the wallet address: ", ) { Ok(path) => path, Err(err) => { eprintln!("{err}"); return; } } }; // Extract the lookup address from the selected file before opening a peer connection. let expanded_path = tilde(&address_file_path).to_string(); let address_contents = read_to_string(&expanded_path) .await .expect("Failed to read address file"); let wallet_address = match extract_address(&address_contents) { Ok(address) => address, Err(err) => { eprintln!("{err}"); return; } }; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; let json = wallet_address; // Try each configured peer until one returns a parsable balance response or text error. let connections = get_connections().await; let mut connected: bool = 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, json.clone(), rpc_command, handshake::HandshakeWallet::WalletKey(encryption_key.clone()), hashmap_key, ) .await; match result { Ok(response) => { if response.is_empty() { connected = true; println!("[]"); break; } // Balance rows are 15 bytes asset name, 4 bytes NFT series, and 8 bytes balance. if response.len() % 27 == 0 { for entry in response.chunks_exact(27) { let name_bytes = &entry[..15]; let nft_series = u32::from_le_bytes(entry[15..19].try_into().unwrap()); let balance = u64::from_le_bytes(entry[19..27].try_into().unwrap()); let asset_name = String::from_utf8_lossy(name_bytes).trim_end().to_string(); let formatted_balance = format_balance(balance); if nft_series > 0 { println!("{asset_name}:{nft_series} = {formatted_balance}"); } else { println!("{asset_name} = {formatted_balance}"); } } 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"); } }