Contractless/src/standalone_tools/connections/sending_request.rs

460 lines
19 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use crate::common::binary_conversions::ip_to_binary;
use crate::records::memory::response_channels::Byte3;
use crate::rpc::command_maps::{
RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH, RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT,
RPC_BLOCK_IP, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE,
MAX_RPC_REPLY_BYTES,
RPC_LOAN_CONTRACT, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE,
RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET,
RPC_TIME, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX,
RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VANITY_LOOKUP,
};
use crate::standalone_tools::transaction_creator::create_transaction_request;
use crate::wallets::structures::Wallet;
use crate::{AsyncReadExt, AsyncWriteExt};
use crate::decode;
use crate::Duration;
use crate::io;
use crate::TcpStream;
use crate::timeout;
pub async fn request(
stream: &mut TcpStream,
command_input: String,
rpc_command: usize,
hashmap_key: Byte3,
) -> Result<Vec<u8>, io::Error> {
// Build the command-specific wire bytes before writing anything to the stream.
let bin_msg = build_request_bytes(command_input, rpc_command, hashmap_key).await?;
// Send the standalone RPC request across the already-verified handshake stream.
stream.write_all(&bin_msg).await?;
stream.flush().await?;
// The first 4 reply bytes are the response UID/header. Standalone tools only need
// the payload, so this reads and discards those bytes before reading the message.
let mut discard_buffer = [0u8; 4];
timeout(
Duration::from_secs(10),
stream.read_exact(&mut discard_buffer),
)
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"Timed out waiting for RPC reply header",
)
})??;
// The next 4 bytes tell us how large the actual reply payload is.
let mut length_buffer = [0u8; 4];
timeout(
Duration::from_secs(10),
stream.read_exact(&mut length_buffer),
)
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"Timed out waiting for RPC reply length",
)
})??;
let length = u32::from_le_bytes(length_buffer) as usize;
if length > MAX_RPC_REPLY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("RPC reply payload too large: len={length} max={MAX_RPC_REPLY_BYTES}"),
));
}
// Read and return only the command payload so each standalone tool can decode
// the response according to the command it sent.
let mut message_buffer = vec![0u8; length];
timeout(
Duration::from_secs(10),
stream.read_exact(&mut message_buffer),
)
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"Timed out waiting for RPC reply payload",
)
})??;
Ok(message_buffer)
}
async fn build_request_bytes(
command_input: String,
rpc_command: usize,
hashmap_key: Byte3,
) -> Result<Vec<u8>, io::Error> {
let mut bin_msg: Vec<u8> = Vec::new();
match rpc_command {
// Network-info request.
1 => {
let command_number: u8 = RPC_NETWORK_INFO;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Network height request.
2 => {
let command_number: u8 = RPC_BLOCK_HEIGHT;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Node UTC timestamp request.
4 => {
let command_number: u8 = RPC_TIME;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Difficulty request.
5 => {
let command_number: u8 = RPC_DIFFICULTY;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Broadcast a signed transaction built by the standalone transaction creator.
8 => {
let msg = create_transaction_request(command_input, hashmap_key).await.map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Failed to build broadcast request: {err}"),
)
})?;
bin_msg.extend_from_slice(&msg);
}
// Lookup a block by height.
9 => {
let command_number: u8 = RPC_BLOCK_BY_HEIGHT;
let block_number = command_input.parse::<u32>().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid block number")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&block_number.to_le_bytes());
}
// Lookup a block by 32-byte hash.
10 => {
let command_number: u8 = RPC_BLOCK_BY_HASH;
let block_hash = decode(&command_input).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid block hash: {err}"),
)
})?;
if block_hash.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Block hash must decode to 32 bytes",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&block_hash);
}
// Lookup a torrent by block height.
12 => {
let command_number: u8 = RPC_TORRENT_BY_HEIGHT;
let block_number = command_input.parse::<u32>().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid torrent block number")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&block_number.to_le_bytes());
}
// Total transaction count request.
13 => {
let command_number: u8 = RPC_LARGEST_TX_FEE;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Lookup one unprocessed mempool transaction by signature.
14 => {
let command_number: u8 = RPC_MEMPOOL_TX_BY_SIGNATURE;
let signature = decode(&command_input).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid signature: {err}"),
)
})?;
if signature.len() != Wallet::SIGNATURE_LENGTH {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Signature has the wrong byte length",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&signature);
}
// Mempool transaction count request.
15 => {
let command_number: u8 = RPC_MEMPOOL_TX_COUNT;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Lookup unprocessed mempool transactions tied to an address.
16 => {
let command_number: u8 = RPC_MEMPOOL_TX_BY_ADDRESS;
let address_bytes = Wallet::normalize_to_short_address(&command_input)
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes);
}
// Lookup a transaction by 32-byte transaction id.
17 => {
let command_number: u8 = RPC_TRANSACTION_BY_TXID;
let txid = decode(&command_input).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid transaction id: {err}"),
)
})?;
if txid.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Transaction id must decode to 32 bytes",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&txid);
}
// Largest transaction-fee request.
18 => {
let command_number: u8 = RPC_TOTAL_CONFIRMED_TX;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Lookup a wallet balance by long, short, or vanity address.
23 => {
let command_number: u8 = RPC_ADDRESS_TOTAL_BALANCE;
let address_bytes = Wallet::normalize_to_short_address(&command_input)
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes)
}
// Server-owner block IP request.
26 => {
let command_number: u8 = RPC_BLOCK_IP;
let (ip, signature) = command_input.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Server owner IP command input must be ip|signature",
)
})?;
let ip_bytes = ip_to_binary(ip);
if ip_bytes.len() != 16 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid IP address"));
}
let signature_bytes = decode(signature).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid signature: {err}"),
)
})?;
if signature_bytes.len() != Wallet::SIGNATURE_LENGTH {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Signature has the wrong byte length",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&ip_bytes);
bin_msg.extend_from_slice(&signature_bytes);
}
// Server-owner unblock IP request.
27 => {
let command_number: u8 = RPC_UNBLOCK_IP;
let (ip, signature) = command_input.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Server owner IP command input must be ip|signature",
)
})?;
let ip_bytes = ip_to_binary(ip);
if ip_bytes.len() != 16 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid IP address"));
}
let signature_bytes = decode(signature).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid signature: {err}"),
)
})?;
if signature_bytes.len() != Wallet::SIGNATURE_LENGTH {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Signature has the wrong byte length",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&ip_bytes);
bin_msg.extend_from_slice(&signature_bytes);
}
// Token-list request.
31 => {
let command_number: u8 = RPC_TOKEN_LIST;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// NFT-list request.
32 => {
let command_number: u8 = RPC_NFT_LIST;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// Lookup a loan contract by 32-byte contract hash.
33 => {
let command_number: u8 = RPC_LOAN_CONTRACT;
let contract_bytes = decode(&command_input).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid contract hash: {err}"),
)
})?;
if contract_bytes.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Contract hash must decode to 32 bytes",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&contract_bytes);
}
// Lookup token metadata by fixed-width ticker/name.
35 => {
let command_number: u8 = RPC_TOKEN_DETAILS;
if command_input.trim().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Token name cannot be empty",
));
}
let mut token_bytes = command_input.into_bytes();
token_bytes.truncate(15);
if token_bytes.len() < 15 {
token_bytes.resize(15, b' ');
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&token_bytes);
}
// Lookup NFT metadata by "name|series".
36 => {
let command_number: u8 = RPC_NFT_DETAILS;
let (name, series_text) = command_input.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"NFT lookup input must be name|series",
)
})?;
if name.trim().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"NFT name cannot be empty",
));
}
let series = series_text.parse::<u32>().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid NFT series")
})?;
let mut nft_name_bytes = name.as_bytes().to_vec();
nft_name_bytes.truncate(15);
if nft_name_bytes.len() < 15 {
nft_name_bytes.resize(15, b' ');
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&nft_name_bytes);
bin_msg.extend_from_slice(&series.to_le_bytes());
}
// Lookup transactions by long, short, or vanity address.
37 => {
let command_number: u8 = RPC_CONTRACT_BY_ADDRESS;
let address_bytes = Wallet::normalize_to_short_address(&command_input)
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes);
}
// Register a wallet using "short_address|long_address|signature".
38 => {
let command_number: u8 = RPC_REGISTER_WALLET;
let (short_address, rest) = command_input.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Wallet registration input must be short|long|signature",
)
})?;
let (long_address, signature) = rest.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Wallet registration input must be short|long|signature",
)
})?;
let short_address_bytes = Wallet::short_address_to_bytes(short_address).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid short wallet address")
})?;
let long_address_bytes = Wallet::long_address_to_bytes(long_address.to_string());
if long_address_bytes.len() != Wallet::ADDRESS_BYTES_LENGTH {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid long wallet address",
));
}
let signature_bytes = decode(signature).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid wallet registration signature: {err}"),
)
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&short_address_bytes);
bin_msg.extend_from_slice(&long_address_bytes);
bin_msg.extend_from_slice(&signature_bytes);
}
// Lookup remote balance by long, short, or vanity address.
40 => {
let command_number: u8 = RPC_VANITY_LOOKUP;
let address_bytes = Wallet::normalize_to_short_address(&command_input)
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes);
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported standalone RPC command: {rpc_command}"),
));
}
}
Ok(bin_msg)
}