use crate::cli::TxnCommands;
use crate::commands::debug::run_txn_debug;
use crate::commands::state_proof::make_state_proof as make_state_proof_util;
use crate::config::Config;
use crate::crypto;
use crate::error::CliError;
use crate::output;
use crate::utils::format_vm_error;
use base64::Engine;
use base64::engine::general_purpose;
use std::time::Duration;
use thru_base::rpc_types::ProofType;
use thru_base::tn_tools::{KeyPair, Pubkey};
use thru_base::txn_lib::{TnPubkey, Transaction};
use thru_client::{Client, ClientBuilder};
pub async fn handle_txn_command(
config: &Config,
subcommand: TxnCommands,
json_format: bool,
) -> Result<(), CliError> {
match subcommand {
TxnCommands::Sign {
program,
instruction_data,
fee_payer,
fee,
compute_units,
state_units,
memory_units,
expiry_after,
readwrite_accounts,
readonly_accounts,
} => {
sign_transaction(
config,
&program,
&instruction_data,
fee_payer.as_deref(),
fee,
compute_units,
state_units,
memory_units,
expiry_after,
&readwrite_accounts,
&readonly_accounts,
json_format,
)
.await
}
TxnCommands::Execute {
program,
instruction_data,
fee_payer,
fee,
compute_units,
state_units,
memory_units,
expiry_after,
timeout,
readwrite_accounts,
readonly_accounts,
} => {
execute_transaction(
config,
&program,
&instruction_data,
fee_payer.as_deref(),
fee,
compute_units,
state_units,
memory_units,
expiry_after,
timeout,
&readwrite_accounts,
&readonly_accounts,
json_format,
)
.await
}
TxnCommands::MakeStateProof {
proof_type,
account,
slot,
} => make_state_proof(config, &proof_type, &account, slot, json_format).await,
TxnCommands::Get {
signature,
retry_count,
} => get_transaction(config, &signature, json_format, retry_count).await,
TxnCommands::Debug {
signature,
include_state_before,
include_state_after,
include_account_data,
output_trace,
inline_trace,
include_memory_dump,
} => {
run_txn_debug(
config,
&signature,
include_state_before,
include_state_after,
include_account_data,
include_memory_dump,
output_trace.as_deref(),
inline_trace,
json_format,
)
.await
}
TxnCommands::Sort { pubkeys } => sort_pubkeys(&pubkeys, json_format),
}
}
async fn sign_transaction(
config: &Config,
program: &str,
instruction_data: &str,
fee_payer: Option<&str>,
fee: u64,
compute_units: u32,
state_units: u16,
memory_units: u16,
expiry_after: u32,
readwrite_accounts: &[String],
readonly_accounts: &[String],
json_format: bool,
) -> Result<(), CliError> {
let instruction_bytes = hex::decode(instruction_data)
.map_err(|e| CliError::Validation(format!("Invalid hex instruction data: {}", e)))?;
let transaction = build_and_sign_transaction(
config,
program,
&instruction_bytes,
fee_payer,
fee,
compute_units,
state_units,
memory_units,
expiry_after,
readwrite_accounts,
readonly_accounts,
)
.await?;
let transaction_bytes = transaction.to_wire();
let base64_transaction = general_purpose::STANDARD.encode(&transaction_bytes);
if json_format {
let response = serde_json::json!({
"transaction_sign": {
"status": "success",
"base64_transaction": base64_transaction,
"size_bytes": transaction_bytes.len()
}
});
output::print_output(response, true);
} else {
println!("{}", base64_transaction);
}
Ok(())
}
async fn execute_transaction(
config: &Config,
program: &str,
instruction_data: &str,
fee_payer: Option<&str>,
fee: u64,
compute_units: u32,
state_units: u16,
memory_units: u16,
expiry_after: u32,
timeout: u64,
readwrite_accounts: &[String],
readonly_accounts: &[String],
json_format: bool,
) -> Result<(), CliError> {
let instruction_bytes = hex::decode(instruction_data)
.map_err(|e| CliError::Validation(format!("Invalid hex instruction data: {}", e)))?;
let client = create_rpc_client(config)?;
let timeout_duration = Duration::from_secs(timeout);
const TN_ERR_NONCE_TOO_LOW: i32 = -511;
const MAX_NONCE_RETRIES: u32 = 3;
let mut transaction_details_opt = None;
for attempt in 0..MAX_NONCE_RETRIES {
if attempt > 0 {
tokio::time::sleep(Duration::from_millis(500)).await;
}
let transaction = build_and_sign_transaction(
config,
program,
&instruction_bytes,
fee_payer,
fee,
compute_units,
state_units,
memory_units,
expiry_after,
readwrite_accounts,
readonly_accounts,
)
.await?;
let transaction_bytes = transaction.to_wire();
let details = client
.execute_transaction(&transaction_bytes, timeout_duration)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to execute transaction: {:?}", e))
})?;
let is_nonce_err = details.execution_result as i32 == TN_ERR_NONCE_TOO_LOW;
transaction_details_opt = Some(details);
if !is_nonce_err {
break;
}
}
let transaction_details = transaction_details_opt.unwrap();
let has_failure =
transaction_details.execution_result != 0 || transaction_details.vm_error != 0;
if has_failure {
let vm_error_label = format_vm_error(transaction_details.vm_error);
let vm_error_display = if transaction_details.vm_error != 0 {
format!("{} ({})", transaction_details.vm_error, vm_error_label)
} else {
"0".to_string()
};
let user_error_label = if transaction_details.user_error_code != 0 {
format!("0x{:X}", transaction_details.user_error_code)
} else {
"0x0".to_string()
};
let message = format!(
"Transaction failed (execution_result={} (hex 0x{:X}), vm_error={}, user_error={})",
transaction_details.execution_result,
transaction_details.execution_result,
vm_error_display,
user_error_label
);
if !json_format {
output::print_error(&message);
}
return Err(CliError::TransactionFailed {
message,
execution_result: transaction_details.execution_result,
vm_error: transaction_details.vm_error,
vm_error_label,
user_error_code: transaction_details.user_error_code,
user_error_label,
signature: transaction_details.signature.as_str().to_string(),
});
}
if json_format {
let mut events_json = Vec::new();
for event in &transaction_details.events {
let mut event_json = serde_json::Map::new();
event_json.insert(
"call_idx".to_string(),
serde_json::Value::Number(serde_json::Number::from(event.call_idx as u64)),
);
event_json.insert(
"program_idx".to_string(),
serde_json::Value::Number(serde_json::Number::from(event.program_idx as u64)),
);
if let Some(event_id) = &event.event_id {
event_json.insert(
"event_id".to_string(),
serde_json::Value::String(event_id.clone()),
);
}
if let Some(program) = &event.program {
event_json.insert(
"program".to_string(),
serde_json::Value::String(program.as_str().to_string()),
);
}
if event.data.len() > 8 {
let event_type_bytes = &event.data[0..8];
let event_type = u64::from_le_bytes([
event_type_bytes[0],
event_type_bytes[1],
event_type_bytes[2],
event_type_bytes[3],
event_type_bytes[4],
event_type_bytes[5],
event_type_bytes[6],
event_type_bytes[7],
]);
event_json.insert(
"event_type".to_string(),
serde_json::Value::Number(serde_json::Number::from(event_type)),
);
let remaining_data = &event.data[8..];
if !remaining_data.is_empty() {
let parsed_data = parse_event_data_for_json(remaining_data);
event_json.insert("data".to_string(), parsed_data);
}
} else if !event.data.is_empty() {
event_json.insert(
"data_hex".to_string(),
serde_json::Value::String(hex::encode(&event.data)),
);
}
events_json.push(serde_json::Value::Object(event_json));
}
let response = serde_json::json!({
"transaction_execute": {
"status": "success",
"signature": transaction_details.signature.as_str(),
"slot": transaction_details.slot,
"compute_units_consumed": transaction_details.compute_units_consumed,
"state_units_consumed": transaction_details.state_units_consumed,
"execution_result": transaction_details.execution_result,
"vm_error": transaction_details.vm_error,
"vm_error_name": format_vm_error(transaction_details.vm_error),
"user_error_code": transaction_details.user_error_code,
"events_count": transaction_details.events_cnt,
"events_size": transaction_details.events_sz,
"pages_used": transaction_details.pages_used,
"readwrite_accounts": transaction_details.rw_accounts.iter().map(|pk| pk.as_str().to_string()).collect::<Vec<_>>(),
"readonly_accounts": transaction_details.ro_accounts.iter().map(|pk| pk.as_str().to_string()).collect::<Vec<_>>(),
"events": events_json
}
});
output::print_output(response, true);
} else {
output::print_success(&format!("Transaction executed successfully"));
println!("Signature: {}", transaction_details.signature.as_str());
println!("Slot: {}", transaction_details.slot);
println!(
"Compute Units Consumed: {}",
transaction_details.compute_units_consumed
);
println!(
"State Units Consumed: {}",
transaction_details.state_units_consumed
);
println!("Execution Result: {}", transaction_details.execution_result);
println!(
"VM Error: {}",
format_vm_error(transaction_details.vm_error)
);
println!("User Error Code: {}", transaction_details.user_error_code);
println!("Events Count: {}", transaction_details.events_cnt);
println!("Events Size: {}", transaction_details.events_sz);
println!("Pages Used: {}", transaction_details.pages_used);
if !transaction_details.rw_accounts.is_empty() {
println!("\nRead-write Accounts:");
for account in &transaction_details.rw_accounts {
println!(" {}", account.as_str());
}
}
if !transaction_details.ro_accounts.is_empty() {
println!("\nRead-only Accounts:");
for account in &transaction_details.ro_accounts {
println!(" {}", account.as_str());
}
}
if transaction_details.events_cnt > 0 {
println!("\nEvents:");
for (i, event) in transaction_details.events.iter().enumerate() {
println!(
" Event {}: call_idx={}, program_idx={}",
i + 1,
event.call_idx,
event.program_idx
);
if let Some(event_id) = &event.event_id {
println!(" Event ID: {}", event_id);
}
if let Some(program) = &event.program {
println!(" Program: {}", program.as_str());
}
if event.data.len() > 8 {
let event_type_bytes = &event.data[0..8];
let event_type = u64::from_le_bytes([
event_type_bytes[0],
event_type_bytes[1],
event_type_bytes[2],
event_type_bytes[3],
event_type_bytes[4],
event_type_bytes[5],
event_type_bytes[6],
event_type_bytes[7],
]);
println!(" Event type: {}", event_type);
let remaining_data = &event.data[8..];
if !remaining_data.is_empty() {
display_event_data(remaining_data);
}
} else if !event.data.is_empty() {
println!(" Data (hex): {}", hex::encode(&event.data));
}
}
}
}
Ok(())
}
async fn build_and_sign_transaction(
config: &Config,
program: &str,
instruction_data: &[u8],
fee_payer: Option<&str>,
fee: u64,
compute_units: u32,
state_units: u16,
memory_units: u16,
expiry_after: u32,
readwrite_accounts: &[String],
readonly_accounts: &[String],
) -> Result<Transaction, CliError> {
let fee_payer_key = fee_payer.unwrap_or("default");
let fee_payer_private_key = config.keys.get_key(fee_payer_key)?;
let fee_payer_keypair = KeyPair::from_hex_private_key(fee_payer_key, fee_payer_private_key)
.map_err(|e| CliError::Crypto(format!("Failed to create fee payer keypair: {}", e)))?;
let program_pubkey = resolve_program_public_key(config, program)?;
let client = create_rpc_client(config)?;
let account_info = client
.get_account_info(&fee_payer_keypair.address_string, None, None)
.await
.map_err(|e| CliError::NonceManagement(format!("Failed to get account info: {}", e)))?;
let nonce = if let Some(account) = account_info {
account.nonce
} else {
return Err(CliError::AccountNotFound(format!(
"Fee payer account {} not found",
fee_payer_keypair.address_string.as_str()
)));
};
let block_height = client
.get_block_height()
.await
.map_err(|e| CliError::NonceManagement(format!("Failed to get block height: {}", e)))?;
let rw_accounts: Result<Vec<TnPubkey>, CliError> = readwrite_accounts
.iter()
.map(|addr| crate::utils::validate_address_or_hex(addr))
.collect();
let rw_accounts = validate_and_process_accounts(rw_accounts?)?;
let r_accounts: Result<Vec<TnPubkey>, CliError> = readonly_accounts
.iter()
.map(|addr| crate::utils::validate_address_or_hex(addr))
.collect();
let r_accounts = validate_and_process_accounts(r_accounts?)?;
let mut transaction =
Transaction::new(fee_payer_keypair.public_key, program_pubkey, fee, nonce)
.with_instructions(instruction_data.to_vec())
.with_compute_units(compute_units)
.with_state_units(state_units)
.with_memory_units(memory_units)
.with_expiry_after(expiry_after)
.with_start_slot(block_height.finalized_height);
if !rw_accounts.is_empty() {
transaction = transaction.with_rw_accounts(rw_accounts);
}
if !r_accounts.is_empty() {
transaction = transaction.with_r_accounts(r_accounts);
}
transaction
.sign(&fee_payer_keypair.private_key)
.map_err(|e| CliError::Crypto(format!("Failed to sign transaction: {}", e)))?;
Ok(transaction)
}
fn resolve_program_public_key(config: &Config, program: &str) -> Result<TnPubkey, CliError> {
if program.starts_with("ta") && program.len() == 46 {
let pubkey = Pubkey::new(program.to_string())
.map_err(|e| CliError::Validation(format!("Invalid program public key: {}", e)))?;
return pubkey.to_bytes().map_err(|e| {
CliError::Validation(format!("Failed to decode program public key: {}", e))
});
}
if let Ok(private_key_hex) = config.keys.get_key(program) {
let keypair = KeyPair::from_hex_private_key(program, private_key_hex).map_err(|e| {
CliError::Validation(format!("Failed to resolve program key from config: {}", e))
})?;
return Ok(keypair.public_key);
}
if program.len() == 64 {
let program_bytes = crypto::hex_to_bytes(program).map_err(|e| {
CliError::Validation(format!("Failed to decode hex program key: {}", e))
})?;
if program_bytes.len() == 32 {
let mut pubkey = [0u8; 32];
pubkey.copy_from_slice(&program_bytes);
return Ok(pubkey);
}
}
Err(CliError::Validation(format!(
"Invalid program identifier: {}. Must be a ta... address, key name from config, or 64-character hex public key",
program
)))
}
fn display_event_data(data: &[u8]) {
let trimmed_data = data
.iter()
.rposition(|&b| b != 0)
.map(|pos| &data[..=pos])
.unwrap_or(&[]);
if trimmed_data.is_empty() {
println!(" Data: (empty)");
return;
}
if let Ok(string_data) = std::str::from_utf8(trimmed_data) {
let is_printable = string_data
.chars()
.all(|c| c.is_ascii_graphic() || c == ' ' || c == '\t' || c == '\n' || c == '\r');
if is_printable && !string_data.is_empty() {
println!(" Data (string): \"{}\"", string_data);
return;
}
}
println!(" Data (hex): {}", hex::encode(trimmed_data));
}
fn parse_event_data_for_json(data: &[u8]) -> serde_json::Value {
let trimmed_data = data
.iter()
.rposition(|&b| b != 0)
.map(|pos| &data[..=pos])
.unwrap_or(&[]);
if trimmed_data.is_empty() {
return serde_json::json!({
"type": "empty"
});
}
if let Ok(string_data) = std::str::from_utf8(trimmed_data) {
let is_printable = string_data
.chars()
.all(|c| c.is_ascii_graphic() || c == ' ' || c == '\t' || c == '\n' || c == '\r');
if is_printable && !string_data.is_empty() {
return serde_json::json!({
"type": "string",
"value": string_data
});
}
}
serde_json::json!({
"type": "hex",
"value": hex::encode(trimmed_data)
})
}
async fn make_state_proof(
config: &Config,
proof_type: &str,
account: &str,
slot: Option<u64>,
json_format: bool,
) -> Result<(), CliError> {
let parsed_proof_type = match proof_type.to_lowercase().as_str() {
"creating" => ProofType::Creating,
"updating" => ProofType::Updating,
"existing" => ProofType::Existing,
_ => {
return Err(CliError::Validation(format!(
"Invalid proof type: {}. Must be one of: creating, updating, existing",
proof_type
)));
}
};
let account_pubkey = if account.starts_with("ta") && account.len() == 46 {
Pubkey::new(account.to_string())
.map_err(|e| CliError::Validation(format!("Invalid account public key: {}", e)))?
} else if let Ok(private_key_hex) = config.keys.get_key(account) {
let keypair = KeyPair::from_hex_private_key(account, private_key_hex).map_err(|e| {
CliError::Validation(format!("Failed to resolve account key from config: {}", e))
})?;
keypair.address_string
} else if account.len() == 64 {
let account_bytes = crypto::hex_to_bytes(account).map_err(|e| {
CliError::Validation(format!("Failed to decode hex account key: {}", e))
})?;
if account_bytes.len() == 32 {
let mut pubkey_bytes = [0u8; 32];
pubkey_bytes.copy_from_slice(&account_bytes);
Pubkey::from_bytes(&pubkey_bytes)
} else {
return Err(CliError::Validation(format!(
"Hex account key must be exactly 32 bytes (64 hex characters), got {} bytes",
account_bytes.len()
)));
}
} else {
return Err(CliError::Validation(format!(
"Invalid account identifier: {}. Must be a ta... address, key name from config, or 64-character hex public key",
account
)));
};
let client = create_rpc_client(config)?;
let proof_data =
make_state_proof_util(&client, &account_pubkey, parsed_proof_type, slot).await?;
let base64_proof = general_purpose::STANDARD.encode(&proof_data);
let hex_proof = hex::encode(&proof_data);
if json_format {
let response = serde_json::json!({
"makeStateProof": {
"status": "success",
"account": account_pubkey.to_string(),
"proof_type": proof_type,
"slot": slot,
"proof_data": base64_proof,
"proof_data_hex": hex_proof,
"proof_size_bytes": proof_data.len()
}
});
output::print_output(response, true);
} else {
output::print_success("State proof created successfully");
println!("Account: {}", account_pubkey.to_string());
println!("Proof Type: {}", proof_type);
if let Some(slot_num) = slot {
println!("Slot: {}", slot_num);
}
println!("Proof Size: {} bytes", proof_data.len());
println!("Proof Data (base64): {}", base64_proof);
println!("Proof Data (hex): {}", hex_proof);
}
Ok(())
}
fn create_rpc_client(config: &Config) -> Result<Client, CliError> {
let rpc_url = config.get_grpc_url()?;
ClientBuilder::new()
.http_endpoint(rpc_url)
.timeout(Duration::from_secs(config.timeout_seconds))
.auth_token(config.auth_token.clone())
.build()
.map_err(|e| e.into())
}
fn validate_and_process_accounts(mut accounts: Vec<TnPubkey>) -> Result<Vec<TnPubkey>, CliError> {
if accounts.len() > 1024 {
return Err(CliError::Validation(format!(
"Too many accounts: {} (maximum 1024 allowed)",
accounts.len(),
)));
}
accounts.sort();
accounts.dedup();
Ok(accounts)
}
async fn get_transaction(
config: &Config,
signature_str: &str,
json_format: bool,
retry_count: u32,
) -> Result<(), CliError> {
let signature = if signature_str.starts_with("ts") && signature_str.len() == 90 {
thru_base::tn_tools::Signature::new(signature_str.to_string())
.map_err(|e| CliError::Validation(format!("Invalid signature: {}", e)))?
} else if signature_str.len() == 128 {
let sig_bytes = hex::decode(signature_str)
.map_err(|e| CliError::Validation(format!("Invalid hex signature: {}", e)))?;
if sig_bytes.len() != 64 {
return Err(CliError::Validation(format!(
"Hex signature must be exactly 64 bytes (128 hex characters), got {} bytes",
sig_bytes.len()
)));
}
let mut sig_array = [0u8; 64];
sig_array.copy_from_slice(&sig_bytes);
thru_base::tn_tools::Signature::from_bytes(&sig_array)
} else {
return Err(CliError::Validation(format!(
"Invalid signature: {}. Must be a ts... signature (90 characters) or 128-character hex string",
signature_str
)));
};
let timeout_duration = Duration::from_secs(config.timeout_seconds);
let client = create_rpc_client(config)?;
let transaction_details = client
.get_transaction(&signature, timeout_duration, retry_count)
.await
.map_err(|e| {
CliError::TransactionSubmission(format!("Failed to get transaction: {:?}", e))
})?;
match transaction_details {
None => {
if json_format {
let response = serde_json::json!({
"transaction_get": {
"status": "not_found",
"signature": signature.as_str()
}
});
output::print_output(response, true);
} else {
output::print_error(&format!("Transaction not found: {}", signature.as_str()));
}
Err(CliError::TransactionSubmission(format!(
"Transaction not found: {}",
signature.as_str()
)))
}
Some(details) => {
let has_failure = details.execution_result != 0 || details.vm_error != 0;
if json_format {
let mut events_json = Vec::new();
for event in &details.events {
let mut event_json = serde_json::Map::new();
event_json.insert(
"call_idx".to_string(),
serde_json::Value::Number(serde_json::Number::from(event.call_idx as u64)),
);
event_json.insert(
"program_idx".to_string(),
serde_json::Value::Number(serde_json::Number::from(
event.program_idx as u64,
)),
);
if let Some(event_id) = &event.event_id {
event_json.insert(
"event_id".to_string(),
serde_json::Value::String(event_id.clone()),
);
}
if let Some(program) = &event.program {
event_json.insert(
"program".to_string(),
serde_json::Value::String(program.as_str().to_string()),
);
}
if event.data.len() > 8 {
let event_type_bytes = &event.data[0..8];
let event_type = u64::from_le_bytes([
event_type_bytes[0],
event_type_bytes[1],
event_type_bytes[2],
event_type_bytes[3],
event_type_bytes[4],
event_type_bytes[5],
event_type_bytes[6],
event_type_bytes[7],
]);
event_json.insert(
"event_type".to_string(),
serde_json::Value::Number(serde_json::Number::from(event_type)),
);
let remaining_data = &event.data[8..];
if !remaining_data.is_empty() {
let parsed_data = parse_event_data_for_json(remaining_data);
event_json.insert("data".to_string(), parsed_data);
}
} else if !event.data.is_empty() {
event_json.insert(
"data_hex".to_string(),
serde_json::Value::String(hex::encode(&event.data)),
);
}
events_json.push(serde_json::Value::Object(event_json));
}
let instruction_data_hex = if let Some(body) = &details.body {
if details.instruction_data_size > 0
&& body.len() >= details.instruction_data_size as usize
{
Some(hex::encode(
&body[0..details.instruction_data_size as usize],
))
} else {
None
}
} else {
None
};
let response = serde_json::json!({
"transaction_get": {
"status": if has_failure { "failed" } else { "success" },
"signature": details.signature.as_str(),
"slot": details.slot,
"block_offset": details.block_offset,
"body": details.body.as_ref().map(|b| hex::encode(b)),
"instruction_data": instruction_data_hex,
"fee_payer_signature": details.fee_payer_signature.as_str(),
"version": details.version,
"flags": details.flags,
"readwrite_accounts_count": details.readwrite_accounts_count,
"readonly_accounts_count": details.readonly_accounts_count,
"instruction_data_size": details.instruction_data_size,
"requested_compute_units": details.requested_compute_units,
"requested_state_units": details.requested_state_units,
"requested_memory_units": details.requested_memory_units,
"expiry_after": details.expiry_after,
"fee": details.fee,
"nonce": details.nonce,
"start_slot": details.start_slot,
"fee_payer_pubkey": details.fee_payer_pubkey.as_str(),
"program_pubkey": details.program_pubkey.as_str(),
"compute_units_consumed": details.compute_units_consumed,
"memory_units_consumed": details.memory_units_consumed,
"state_units_consumed": details.state_units_consumed,
"execution_result": details.execution_result,
"vm_error": details.vm_error,
"vm_error_name": format_vm_error(details.vm_error),
"user_error_code": details.user_error_code,
"events_count": details.events_cnt,
"events_size": details.events_sz,
"pages_used": details.pages_used,
"readwrite_accounts": details.rw_accounts.iter().map(|pk| pk.as_str().to_string()).collect::<Vec<_>>(),
"readonly_accounts": details.ro_accounts.iter().map(|pk| pk.as_str().to_string()).collect::<Vec<_>>(),
"events": events_json
}
});
output::print_output(response, true);
} else {
if has_failure {
output::print_error("Transaction failed");
} else {
output::print_success("Transaction found");
}
println!("\n=== Transaction Metadata ===");
println!("Signature: {}", details.signature.as_str());
println!("Slot: {}", details.slot);
if let Some(block_offset) = details.block_offset {
println!("Block Offset: {}", block_offset);
}
if let Some(body) = &details.body {
println!("\n=== Transaction Body ===");
println!("Total Body Size: {} bytes", body.len());
println!("Full Body (hex): {}", hex::encode(body));
if details.instruction_data_size > 0
&& body.len() >= details.instruction_data_size as usize
{
let instruction_data = &body[0..details.instruction_data_size as usize];
println!("\nInstruction Data Size: {} bytes", instruction_data.len());
println!("Instruction Data (hex): {}", hex::encode(instruction_data));
if let Ok(ascii_str) = std::str::from_utf8(instruction_data) {
if ascii_str
.chars()
.all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
{
println!("Instruction Data (ASCII): {}", ascii_str);
}
}
}
}
println!("\n=== Transaction Header ===");
println!(
"Fee Payer Signature: {}",
details.fee_payer_signature.as_str()
);
println!("Version: {}", details.version);
println!("Flags: {}", details.flags);
println!(
"Read-write Accounts Count: {}",
details.readwrite_accounts_count
);
println!(
"Read-only Accounts Count: {}",
details.readonly_accounts_count
);
println!("Instruction Data Size: {}", details.instruction_data_size);
println!(
"Requested Compute Units: {}",
details.requested_compute_units
);
println!("Requested State Units: {}", details.requested_state_units);
println!("Requested Memory Units: {}", details.requested_memory_units);
println!("Expiry After: {}", details.expiry_after);
println!("Fee: {}", details.fee);
println!("Nonce: {}", details.nonce);
println!("Start Slot: {}", details.start_slot);
println!("Fee Payer Pubkey: {}", details.fee_payer_pubkey.as_str());
println!("Program Pubkey: {}", details.program_pubkey.as_str());
if !details.rw_accounts.is_empty() {
println!("\n=== Read-write Accounts ===");
for account in &details.rw_accounts {
println!(" {}", account.as_str());
}
}
if !details.ro_accounts.is_empty() {
println!("\n=== Read-only Accounts ===");
for account in &details.ro_accounts {
println!(" {}", account.as_str());
}
}
println!("\n=== Execution Result ===");
println!("Compute Units Consumed: {}", details.compute_units_consumed);
println!("Memory Units Consumed: {}", details.memory_units_consumed);
println!("State Units Consumed: {}", details.state_units_consumed);
println!("Execution Result: {}", details.execution_result);
println!("VM Error: {}", format_vm_error(details.vm_error));
println!("User Error Code: {}", details.user_error_code);
println!("Events Count: {}", details.events_cnt);
println!("Events Size: {}", details.events_sz);
println!("Pages Used: {}", details.pages_used);
if details.events_cnt > 0 {
println!("\nEvents:");
for (i, event) in details.events.iter().enumerate() {
println!(
" Event {}: call_idx={}, program_idx={}",
i + 1,
event.call_idx,
event.program_idx
);
if let Some(event_id) = &event.event_id {
println!(" Event ID: {}", event_id);
}
if let Some(program) = &event.program {
println!(" Program: {}", program.as_str());
}
if event.data.len() > 8 {
let event_type_bytes = &event.data[0..8];
let event_type = u64::from_le_bytes([
event_type_bytes[0],
event_type_bytes[1],
event_type_bytes[2],
event_type_bytes[3],
event_type_bytes[4],
event_type_bytes[5],
event_type_bytes[6],
event_type_bytes[7],
]);
println!(" Event type: {}", event_type);
let remaining_data = &event.data[8..];
if !remaining_data.is_empty() {
display_event_data(remaining_data);
}
} else if !event.data.is_empty() {
println!(" Data (hex): {}", hex::encode(&event.data));
}
}
}
}
Ok(())
}
}
}
fn sort_pubkeys(pubkeys: &[String], json_format: bool) -> Result<(), CliError> {
let mut parsed_pubkeys: Vec<([u8; 32], String)> = Vec::with_capacity(pubkeys.len());
for pubkey_str in pubkeys {
let bytes = crate::utils::validate_address_or_hex(pubkey_str)?;
let thrufmt = Pubkey::from_bytes(&bytes).to_string();
parsed_pubkeys.push((bytes, thrufmt));
}
parsed_pubkeys.sort_by(|a, b| a.0.cmp(&b.0));
if json_format {
let mut result_map = serde_json::Map::new();
for (index, (_, thrufmt)) in parsed_pubkeys.iter().enumerate() {
result_map.insert(thrufmt.clone(), serde_json::json!(index));
}
output::print_output(serde_json::Value::Object(result_map), true);
} else {
for (index, (_, thrufmt)) in parsed_pubkeys.iter().enumerate() {
println!("{}: {}", thrufmt, index);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::KeyManager;
fn create_test_config() -> Config {
let mut keys = KeyManager::new();
keys.add_key(
"test",
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
false,
)
.unwrap();
let mut config = Config::default();
config.rpc_base_url = "http://localhost:8080".to_string();
config.keys = keys;
config
}
#[test]
fn test_resolve_program_public_key_hex() {
let config = create_test_config();
let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = resolve_program_public_key(&config, hex_key);
assert!(result.is_ok());
}
#[test]
fn test_resolve_program_public_key_key_name() {
let config = create_test_config();
let result = resolve_program_public_key(&config, "test");
assert!(result.is_ok());
}
#[test]
fn test_resolve_program_public_key_invalid() {
let config = create_test_config();
let result = resolve_program_public_key(&config, "invalid");
assert!(result.is_err());
}
}