use std::collections::HashMap;
use std::process;
pub fn cmd_tx_explain(args: &[String]) {
if args.is_empty() || matches!(args[0].as_str(), "--help" | "-h") {
print_usage();
return;
}
let mut signature: Option<String> = None;
let mut rpc: Option<String> = None;
let mut show_raw_logs = false;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--rpc" => {
i += 1;
rpc = args.get(i).cloned();
}
"--raw-logs" => show_raw_logs = true,
other if !other.starts_with("--") && signature.is_none() => {
signature = Some(other.to_string());
}
other => {
eprintln!("unknown arg: {other}");
print_usage();
process::exit(1);
}
}
i += 1;
}
let signature = signature.unwrap_or_else(|| {
eprintln!("missing <signature> arg");
print_usage();
process::exit(1);
});
let rpc_url = rpc.unwrap_or_else(|| crate::rpc::resolve_rpc_url(None));
if let Err(e) = run_explain(&rpc_url, &signature, show_raw_logs) {
eprintln!("hopper tx explain failed: {e}");
process::exit(1);
}
}
fn print_usage() {
eprintln!("Usage: hopper tx explain <signature> [--rpc <url>] [--raw-logs]");
eprintln!();
eprintln!("Fetch a confirmed transaction by signature and decode every");
eprintln!("instruction against the target Hopper program's on-chain manifest.");
eprintln!();
eprintln!("Options:");
eprintln!(" --rpc <url> RPC endpoint (default from config / env)");
eprintln!(" --raw-logs Print the full Program-log stream verbatim");
}
fn run_explain(rpc_url: &str, signature: &str, show_raw_logs: bool) -> Result<(), String> {
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::RpcTransactionConfig;
use solana_commitment_config::CommitmentConfig;
use solana_signature::Signature;
use solana_transaction_status::UiTransactionEncoding;
let sig: Signature = signature
.parse()
.map_err(|e| format!("invalid base58 signature: {e}"))?;
let rpc = RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed());
let config = RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::JsonParsed),
commitment: Some(CommitmentConfig::confirmed()),
max_supported_transaction_version: Some(0),
};
let tx = rpc
.get_transaction_with_config(&sig, config)
.map_err(|e| format!("get_transaction: {e}"))?;
println!("-- hopper tx explain --");
println!("signature : {}", signature);
println!("slot : {}", tx.slot);
println!(
"block time: {}",
tx.block_time
.map(|t| t.to_string())
.unwrap_or_else(|| "-".into())
);
if let Some(meta) = tx.transaction.meta.as_ref() {
let status = if meta.err.is_none() {
"success"
} else {
"failed"
};
println!("status : {status}");
println!("fee : {} lamports", meta.fee);
if let solana_transaction_status::option_serializer::OptionSerializer::Some(cu) =
&meta.compute_units_consumed
{
println!("compute : {cu} CU");
}
if let Some(err) = &meta.err {
println!("error : {err:?}");
}
}
println!();
use solana_transaction_status::{
EncodedTransaction, UiInstruction, UiMessage, UiParsedInstruction,
};
let enc_tx = &tx.transaction.transaction;
let message: &UiMessage = match enc_tx {
EncodedTransaction::Json(parsed) => &parsed.message,
_ => {
println!("(transaction was not returned in JsonParsed shape; showing raw)");
return Ok(());
}
};
let instructions: Vec<UiInstruction> = match message {
UiMessage::Parsed(m) => m.instructions.clone(),
UiMessage::Raw(_) => {
println!("(message is raw-encoded; JsonParsed would yield richer output. try --rpc with a richer endpoint)");
return Ok(());
}
};
let mut manifest_cache: HashMap<String, Option<String>> = HashMap::new();
for (i, ix) in instructions.iter().enumerate() {
println!("[instruction {i}]");
match ix {
UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed)) => {
println!(" program: {} (parsed by RPC)", parsed.program);
println!(" kind : {}", parsed.parsed);
}
UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded(partial)) => {
let program_id = partial.program_id.clone();
explain_partial(
&program_id,
&partial.accounts,
&partial.data,
rpc_url,
&mut manifest_cache,
);
}
UiInstruction::Compiled(compiled) => {
println!(" program idx: {}", compiled.program_id_index);
println!(" accounts : {:?}", compiled.accounts);
println!(" data : {}", compiled.data);
}
}
}
if show_raw_logs {
if let Some(meta) = tx.transaction.meta.as_ref() {
if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) =
&meta.log_messages
{
println!();
println!("logs:");
for log in logs {
println!(" {log}");
}
}
}
}
Ok(())
}
fn explain_partial(
program_id: &str,
accounts: &[String],
data_b58: &str,
rpc_url: &str,
manifest_cache: &mut HashMap<String, Option<String>>,
) {
println!(" program : {program_id}");
let manifest = manifest_cache
.entry(program_id.to_string())
.or_insert_with(|| super::manager_invoke::try_fetch_manifest(rpc_url, program_id).ok());
let data_bytes = match bs58::decode(data_b58).into_vec() {
Ok(b) => b,
Err(e) => {
println!(" data : <base58 decode failed: {e}>");
return;
}
};
if data_bytes.is_empty() {
println!(" data : (empty)");
return;
}
let tag = data_bytes[0];
println!(" disc byte : 0x{:02x}", tag);
println!(" data len : {} bytes", data_bytes.len());
if let Some(manifest_json) = manifest {
match super::manager_invoke::lookup_instruction_by_tag(manifest_json, tag) {
Some(ix_line) => {
println!(" matched : {ix_line}");
}
None => {
println!(
" matched : (no Hopper instruction with disc 0x{:02x})",
tag
);
}
}
} else {
println!(" manifest : (no Hopper manifest on chain; skipping decode)");
}
println!(" accounts : {} slots", accounts.len());
for (i, a) in accounts.iter().enumerate() {
println!(" [{}] {}", i, a);
}
}