2026-05-24 17:56:57 +00:00
|
|
|
use crate::common::binary_conversions::ip_to_binary;
|
2026-05-26 06:24:57 +00:00
|
|
|
use crate::decode;
|
|
|
|
|
use crate::io;
|
2026-05-24 17:56:57 +00:00
|
|
|
use crate::records::memory::response_channels::Byte3;
|
|
|
|
|
use crate::rpc::command_maps::{
|
2026-06-12 16:27:28 +00:00
|
|
|
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_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY,
|
|
|
|
|
RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_HISTORY, 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, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS,
|
2026-05-24 17:56:57 +00:00
|
|
|
};
|
|
|
|
|
use crate::standalone_tools::transaction_creator::create_transaction_request;
|
2026-05-26 06:24:57 +00:00
|
|
|
use crate::timeout;
|
2026-05-24 17:56:57 +00:00
|
|
|
use crate::wallets::structures::Wallet;
|
|
|
|
|
use crate::Duration;
|
|
|
|
|
use crate::TcpStream;
|
2026-05-26 06:24:57 +00:00
|
|
|
use crate::{AsyncReadExt, AsyncWriteExt};
|
2026-05-24 17:56:57 +00:00
|
|
|
|
|
|
|
|
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 => {
|
2026-05-26 06:24:57 +00:00
|
|
|
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}"),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
2026-05-24 17:56:57 +00:00
|
|
|
bin_msg.extend_from_slice(&msg);
|
|
|
|
|
}
|
|
|
|
|
// Lookup a block by height.
|
|
|
|
|
9 => {
|
|
|
|
|
let command_number: u8 = RPC_BLOCK_BY_HEIGHT;
|
2026-05-26 06:24:57 +00:00
|
|
|
let block_number = command_input
|
|
|
|
|
.parse::<u32>()
|
|
|
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid block number"))?;
|
2026-05-24 17:56:57 +00:00
|
|
|
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 {
|
2026-05-26 06:24:57 +00:00
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
|
"Invalid IP address",
|
|
|
|
|
));
|
2026-05-24 17:56:57 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2026-05-26 06:24:57 +00:00
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
|
"Invalid IP address",
|
|
|
|
|
));
|
2026-05-24 17:56:57 +00:00
|
|
|
}
|
|
|
|
|
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",
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-05-26 06:24:57 +00:00
|
|
|
let series = series_text
|
|
|
|
|
.parse::<u32>()
|
|
|
|
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid NFT series"))?;
|
2026-05-24 17:56:57 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-06-04 16:18:55 +00:00
|
|
|
// Register a wallet using "short_address|public_key|signature".
|
2026-05-24 17:56:57 +00:00
|
|
|
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,
|
2026-06-04 16:18:55 +00:00
|
|
|
"Wallet registration input must be short|public_key|signature",
|
2026-05-24 17:56:57 +00:00
|
|
|
)
|
|
|
|
|
})?;
|
2026-06-04 16:18:55 +00:00
|
|
|
let (public_key, signature) = rest.split_once('|').ok_or_else(|| {
|
2026-05-24 17:56:57 +00:00
|
|
|
io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
2026-06-04 16:18:55 +00:00
|
|
|
"Wallet registration input must be short|public_key|signature",
|
2026-05-24 17:56:57 +00:00
|
|
|
)
|
|
|
|
|
})?;
|
2026-05-26 06:24:57 +00:00
|
|
|
let short_address_bytes =
|
|
|
|
|
Wallet::short_address_to_bytes(short_address).ok_or_else(|| {
|
|
|
|
|
io::Error::new(io::ErrorKind::InvalidInput, "Invalid short wallet address")
|
|
|
|
|
})?;
|
2026-06-04 16:18:55 +00:00
|
|
|
let public_key_bytes = decode(public_key).map_err(|err| {
|
|
|
|
|
io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
|
format!("Invalid wallet registration public key: {err}"),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
if public_key_bytes.len() != Wallet::PUBLIC_KEY_LENGTH {
|
2026-05-24 17:56:57 +00:00
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
2026-06-04 16:18:55 +00:00
|
|
|
"Invalid wallet registration public key",
|
2026-05-24 17:56:57 +00:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
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);
|
2026-06-04 16:18:55 +00:00
|
|
|
bin_msg.extend_from_slice(&public_key_bytes);
|
2026-05-24 17:56:57 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-06-04 15:06:51 +00:00
|
|
|
// Lookup canonical short owner address by registered vanity address.
|
2026-06-12 16:27:28 +00:00
|
|
|
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.
|
2026-06-04 15:06:51 +00:00
|
|
|
45 => {
|
|
|
|
|
let command_number: u8 = RPC_VANITY_OWNER_LOOKUP;
|
|
|
|
|
let address_bytes = Wallet::normalize_to_short_address(&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);
|
|
|
|
|
}
|
2026-06-12 16:27:28 +00:00
|
|
|
// Lookup newest confirmed transaction ids tied to an address.
|
|
|
|
|
46 => {
|
|
|
|
|
let command_number: u8 = RPC_LATEST_ADDRESS_HISTORY;
|
|
|
|
|
let (address, limit) = command_input.split_once('|').ok_or_else(|| {
|
|
|
|
|
io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
|
"Latest address history input must be address|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 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());
|
|
|
|
|
}
|
2026-05-24 17:56:57 +00:00
|
|
|
_ => {
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::InvalidInput,
|
|
|
|
|
format!("Unsupported standalone RPC command: {rpc_command}"),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(bin_msg)
|
|
|
|
|
}
|