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, 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, io::Error> { let mut bin_msg: Vec = 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::() .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::().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::() .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::() .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid history skip"))?; let limit = limit.parse::().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::().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::>(); 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::() .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid campaign id"))?; let skip = parts[2].parse::().map_err(|_| { io::Error::new(io::ErrorKind::InvalidInput, "Invalid campaign history skip") })?; let limit = parts[3].parse::().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); } }