Contractless/src/bin/lookup_network_info.rs

146 lines
5.0 KiB
Rust

use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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::to_string_pretty;
const NETWORK_NAME_BYTES: usize = 7;
const NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX: usize = 40;
fn read_u32(response: &[u8], offset: &mut usize) -> Option<u32> {
let bytes: [u8; 4] = response.get(*offset..*offset + 4)?.try_into().ok()?;
*offset += 4;
Some(u32::from_le_bytes(bytes))
}
fn read_u64(response: &[u8], offset: &mut usize) -> Option<u64> {
let bytes: [u8; 8] = response.get(*offset..*offset + 8)?.try_into().ok()?;
*offset += 8;
Some(u64::from_le_bytes(bytes))
}
fn decode_network_info(response: &[u8]) -> Option<String> {
// Network-info responses are binary, with one variable-width field:
// the wallet prefix is whatever remains after the fixed fields.
if response.len() <= NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX {
return None;
}
let wallet_prefix_len = response.len() - NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX;
let mut offset = 0;
let version = *response.get(offset)?;
offset += 1;
// The network name is the fixed 7-byte value from network settings:
// "mainnet", "testnet", or "Invalid".
let network = String::from_utf8_lossy(response.get(offset..offset + NETWORK_NAME_BYTES)?)
.trim()
.to_string();
offset += NETWORK_NAME_BYTES;
let time = read_u32(response, &mut offset)?;
// Mainnet uses CLC and testnet uses CLTC, so the prefix length is
// inferred from the total payload size instead of hard-coded.
let wallet_prefix = String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?)
.trim()
.to_string();
offset += wallet_prefix_len;
let height = read_u32(response, &mut offset)?;
let next_block_difficulty = read_u64(response, &mut offset)?;
let total_block_transactions = read_u32(response, &mut offset)?;
let total_mempool_transactions = read_u32(response, &mut offset)?;
let largest_tx_fee = read_u64(response, &mut offset)?;
// Print JSON for the CLI user, but this is decoded from the binary
// RPC payload and no JSON crossed the TCP stream.
let output = json!({
"version": version,
"network": network,
"time": time,
"wallet_prefix": wallet_prefix,
"height": height,
"next_block_difficulty": next_block_difficulty,
"total_block_transactions": total_block_transactions,
"total_mempool_transactions": total_mempool_transactions,
"largest_tx_fee": largest_tx_fee,
});
to_string_pretty(&output).ok()
}
#[tokio::main]
async fn main() {
// Command 1 asks a peer for its current network-info snapshot.
let hashmap_key = generate_uid();
let rpc_command = 1;
// This lookup takes no arguments; the wallet key is only used for
// the authenticated handshake before the RPC command is sent.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./lookup_network_info");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try configured peers in order and stop at the first readable
// network-info response 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,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Prefer binary network-info decoding; if that fails,
// print a plain text error returned by the peer.
if let Some(output) = decode_network_info(&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");
}
}