Contractless/src/standalone_tools/connections/sending_request.rs

683 lines
29 KiB
Rust

use crate::common::binary_conversions::ip_to_binary;
use crate::decode;
use crate::io;
use crate::records::memory::response_channels::Byte3;
use crate::rpc::command_maps::{
MAX_RPC_REPLY_BYTES, RPC_ADDRESS_HISTORY, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH,
RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_COLLATERAL_STATUS,
RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS,
RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY, 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_CATALOG, RPC_TOKEN_DETAILS,
RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID,
RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
RPC_WALLET_REGISTRATION_STATUS, RPC_WALLET_REGISTRY_SYNC,
};
use crate::standalone_tools::transaction_creator::create_transaction_request;
use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input;
use crate::timeout;
use crate::wallets::structures::Wallet;
use crate::Duration;
use crate::TcpStream;
use crate::{AsyncReadExt, AsyncWriteExt};
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)
}
// Validate a signature over a UTF-8 message for a registered wallet.
25 => {
let command_number: u8 = RPC_VALIDATE_MESSAGE;
let mut parts = command_input.splitn(3, '|');
let message = parts.next().unwrap_or_default();
let address = parts.next().unwrap_or_default();
let signature = parts.next().unwrap_or_default();
let message_bytes = message.as_bytes();
let message_size = u16::try_from(message_bytes.len()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Signed message is too long")
})?;
let address_bytes = Wallet::normalize_to_short_address(address)
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid signer 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(&message_size.to_le_bytes());
bin_msg.extend_from_slice(message_bytes);
bin_msg.extend_from_slice(&address_bytes);
bin_msg.extend_from_slice(&signature_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|public_key|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|public_key|signature",
)
})?;
let (public_key, signature) = rest.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Wallet registration input must be short|public_key|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 public_key_bytes = decode(public_key).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid wallet registration public key: {err}"),
)
})?;
if !Wallet::has_valid_public_key_bytes(&public_key_bytes) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid wallet registration public key",
));
}
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(&public_key_bytes);
bin_msg.extend_from_slice(&signature_bytes);
}
// Request the deterministic wallet registry snapshot.
39 => {
let command_number: u8 = RPC_WALLET_REGISTRY_SYNC;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
// 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);
}
// Lookup canonical short owner address by registered vanity address.
43 => {
let command_number: u8 = RPC_WALLET_REGISTRATION_STATUS;
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 canonical short owner address by registered vanity address.
44 => {
let command_number: u8 = RPC_ADDRESS_HISTORY;
let (address, rest) = command_input.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Address history input must be address|skip|limit",
)
})?;
let (skip, limit) = rest.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Address history input must be address|skip|limit",
)
})?;
let address_bytes = Wallet::normalize_to_short_address(address)
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
let skip = skip
.parse::<u32>()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid history skip"))?;
let limit = limit.parse::<u32>().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid history limit")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes);
bin_msg.extend_from_slice(&skip.to_le_bytes());
bin_msg.extend_from_slice(&limit.to_le_bytes());
}
// Lookup canonical short owner address by registered vanity address.
45 => {
let command_number: u8 = RPC_VANITY_OWNER_LOOKUP;
let address_bytes = canonical_vanity_address_input(&command_input)
.and_then(|vanity| Wallet::vanity_address_to_bytes(&vanity))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid vanity 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 newest transaction records tied to an address.
46 => {
let command_number: u8 = RPC_LATEST_ADDRESS_TRANSACTIONS;
let (address, limit) = command_input.split_once('|').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Latest address transactions input must be address|limit",
)
})?;
let address_bytes = Wallet::normalize_to_short_address(address)
.and_then(|s| Wallet::short_address_to_bytes(&s))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
let limit = limit.parse::<u32>().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid history limit")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes);
bin_msg.extend_from_slice(&limit.to_le_bytes());
}
// Lookup marketing campaign summary and paged campaign records.
47 => {
let command_number: u8 = RPC_MARKETING_CAMPAIGN_HISTORY;
let parts = command_input.split('|').collect::<Vec<_>>();
if parts.len() != 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Marketing campaign history input must be advertiser|campaign|skip|limit",
));
}
let address_bytes = Wallet::normalize_to_short_address(parts[0])
.and_then(|s| Wallet::short_address_to_bytes(&s))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid advertiser address")
})?;
let campaign = parts[1]
.parse::<u64>()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid campaign id"))?;
let skip = parts[2].parse::<u32>().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid campaign history skip")
})?;
let limit = parts[3].parse::<u32>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid campaign history limit",
)
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&address_bytes);
bin_msg.extend_from_slice(&campaign.to_le_bytes());
bin_msg.extend_from_slice(&skip.to_le_bytes());
bin_msg.extend_from_slice(&limit.to_le_bytes());
}
48 => {
let command_number: u8 = RPC_TOKEN_CATALOG;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
}
49 => {
let command_number: u8 = RPC_COLLATERAL_STATUS;
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);
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported standalone RPC command: {rpc_command}"),
));
}
}
Ok(bin_msg)
}
#[cfg(test)]
mod tests {
use super::build_request_bytes;
use crate::rpc::command_maps::RPC_VALIDATE_MESSAGE;
use crate::wallets::structures::Wallet;
#[tokio::test]
async fn validate_message_request_uses_expected_wire_layout() {
let address = format!("{}.cltc", "00".repeat(20));
let signature = "00".repeat(Wallet::SIGNATURE_LENGTH);
let uid = [1, 2, 3];
let bytes = build_request_bytes(
format!("message|{address}|{signature}"),
RPC_VALIDATE_MESSAGE as usize,
uid,
)
.await
.unwrap();
assert_eq!(bytes[0], RPC_VALIDATE_MESSAGE);
assert_eq!(&bytes[1..4], &uid);
assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 7);
assert_eq!(&bytes[6..13], b"message");
assert_eq!(bytes.len(), 1 + 3 + 2 + 7 + 22 + Wallet::SIGNATURE_LENGTH);
}
}