use std::fs;
use std::process;
pub fn cmd_version(_args: &[String]) {
println!("hopper {}", env!("CARGO_PKG_VERSION"));
println!("hopper-schema linked: {}", hopper_schema::MANIFEST_VERSION);
if let Some(sha) = option_env!("HOPPER_GIT_SHA") {
println!("git sha: {sha}");
}
if let Some(t) = option_env!("TARGET") {
println!("target : {t}");
}
}
pub fn cmd_completions(args: &[String]) {
let Some(shell) = args.first() else {
print_completions_usage();
process::exit(1);
};
match shell.as_str() {
"bash" => print!("{}", BASH_COMPLETION),
"zsh" => print!("{}", ZSH_COMPLETION),
"fish" => print!("{}", FISH_COMPLETION),
other => {
eprintln!("unsupported shell: {other}");
print_completions_usage();
process::exit(1);
}
}
}
fn print_completions_usage() {
eprintln!("Usage: hopper completions <bash | zsh | fish>");
eprintln!();
eprintln!("Emit a shell-completion script. Save it to your rc file or sourcing dir:");
eprintln!(" bash -> hopper completions bash > /etc/bash_completion.d/hopper");
eprintln!(" zsh -> hopper completions zsh > \"${{fpath[1]}}/_hopper\"");
eprintln!(" fish -> hopper completions fish > ~/.config/fish/completions/hopper.fish");
}
#[allow(dead_code)]
const TOP_LEVEL: &[&str] = &[
"schema",
"compile",
"inspect",
"explain",
"client",
"profile",
"fetch",
"init",
"build",
"test",
"deploy",
"dump",
"verify",
"keys",
"config",
"lint",
"expand",
"tx",
"manager",
"doctor",
"completions",
"version",
"help",
];
const BASH_COMPLETION: &str = r#"_hopper() {
local cur prev words cword
_init_completion || return
if [ "$cword" -eq 1 ]; then
COMPREPLY=($(compgen -W "schema compile inspect explain client profile fetch init build test deploy dump verify keys config lint expand tx manager doctor completions version help" -- "$cur"))
return
fi
case "${words[1]}" in
keys) COMPREPLY=($(compgen -W "new list print pda" -- "$cur")) ;;
config) COMPREPLY=($(compgen -W "get set list reset path" -- "$cur")) ;;
tx) COMPREPLY=($(compgen -W "explain simulate submit" -- "$cur")) ;;
manager) COMPREPLY=($(compgen -W "fetch summary identify decode instruction layouts policies events fingerprints compat receipt explain diff simulate invoke crank accounts interactive" -- "$cur")) ;;
profile) COMPREPLY=($(compgen -W "bench elf" -- "$cur")) ;;
schema) COMPREPLY=($(compgen -W "export validate diff" -- "$cur")) ;;
completions) COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur")) ;;
esac
}
complete -F _hopper hopper
"#;
const ZSH_COMPLETION: &str = r#"#compdef hopper
_hopper() {
local -a commands
commands=(
'schema:manifest export/validate/diff'
'compile:emit lowered Rust'
'inspect:decode account bytes'
'explain:narrate account/receipt/compat'
'client:generate TypeScript/Kotlin/Python clients'
'profile:bench or ELF flamegraph'
'fetch:pull on-chain manifest'
'init:scaffold a Hopper project'
'build:compile (optionally --watch)'
'test:run tests (optionally --watch)'
'deploy:deploy an SBF artifact'
'dump:disassemble .so'
'verify:ABI fingerprint check'
'keys:key + PDA helpers'
'config:global config store'
'lint:account-relationship checker'
'expand:macro expansion'
'tx:on-chain transaction helpers'
'manager:on-chain introspection + invoke + crank'
'doctor:environment sanity check'
'completions:emit shell completions'
'version:print CLI version info'
'help:print top-level usage'
)
_describe -t commands 'hopper command' commands
}
_hopper
"#;
const FISH_COMPLETION: &str = r#"complete -c hopper -f
complete -c hopper -n '__fish_use_subcommand' -a 'schema' -d 'manifest export/validate/diff'
complete -c hopper -n '__fish_use_subcommand' -a 'compile' -d 'emit lowered Rust'
complete -c hopper -n '__fish_use_subcommand' -a 'inspect' -d 'decode account bytes'
complete -c hopper -n '__fish_use_subcommand' -a 'explain' -d 'narrate account/receipt/compat'
complete -c hopper -n '__fish_use_subcommand' -a 'client' -d 'generate TS/KT/PY clients'
complete -c hopper -n '__fish_use_subcommand' -a 'profile' -d 'bench or ELF flamegraph'
complete -c hopper -n '__fish_use_subcommand' -a 'fetch' -d 'pull on-chain manifest'
complete -c hopper -n '__fish_use_subcommand' -a 'init' -d 'scaffold a Hopper project'
complete -c hopper -n '__fish_use_subcommand' -a 'build' -d 'compile (optionally --watch)'
complete -c hopper -n '__fish_use_subcommand' -a 'test' -d 'run tests (optionally --watch)'
complete -c hopper -n '__fish_use_subcommand' -a 'deploy' -d 'deploy an SBF artifact'
complete -c hopper -n '__fish_use_subcommand' -a 'dump' -d 'disassemble .so'
complete -c hopper -n '__fish_use_subcommand' -a 'verify' -d 'ABI fingerprint check'
complete -c hopper -n '__fish_use_subcommand' -a 'keys' -d 'key + PDA helpers'
complete -c hopper -n '__fish_use_subcommand' -a 'config' -d 'global config store'
complete -c hopper -n '__fish_use_subcommand' -a 'lint' -d 'account-relationship checker'
complete -c hopper -n '__fish_use_subcommand' -a 'expand' -d 'macro expansion'
complete -c hopper -n '__fish_use_subcommand' -a 'tx' -d 'on-chain transaction helpers'
complete -c hopper -n '__fish_use_subcommand' -a 'manager' -d 'on-chain introspection + invoke + crank'
complete -c hopper -n '__fish_use_subcommand' -a 'doctor' -d 'environment sanity check'
complete -c hopper -n '__fish_use_subcommand' -a 'completions' -d 'emit shell completions'
complete -c hopper -n '__fish_use_subcommand' -a 'version' -d 'print CLI version info'
"#;
pub fn cmd_tx_simulate(args: &[String]) {
tx_simulate_or_submit(args, false);
}
pub fn cmd_tx_submit(args: &[String]) {
tx_simulate_or_submit(args, true);
}
fn tx_simulate_or_submit(args: &[String], send: bool) {
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_commitment_config::CommitmentConfig;
use solana_transaction::Transaction;
if args.is_empty() || matches!(args[0].as_str(), "--help" | "-h") {
let verb = if send { "submit" } else { "simulate" };
eprintln!("Usage: hopper tx {verb} <tx-base64> [--rpc <url>]");
return;
}
let tx_b64 = &args[0];
let mut rpc: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--rpc" => {
i += 1;
rpc = args.get(i).cloned();
}
other => {
eprintln!("unknown flag: {other}");
process::exit(1);
}
}
i += 1;
}
let rpc_url = rpc.unwrap_or_else(|| crate::rpc::resolve_rpc_url(None));
let bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, tx_b64) {
Ok(b) => b,
Err(e) => {
eprintln!("base64 decode: {e}");
process::exit(1);
}
};
let tx: Transaction = match bincode::deserialize(&bytes) {
Ok(t) => t,
Err(e) => {
eprintln!("bincode decode (transaction): {e}");
process::exit(1);
}
};
let client = RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed());
if send {
match client.send_and_confirm_transaction(&tx) {
Ok(sig) => {
println!("signature: {sig}");
println!("status : confirmed");
}
Err(e) => {
eprintln!("send_and_confirm: {e}");
process::exit(1);
}
}
} else {
let cfg = RpcSimulateTransactionConfig {
sig_verify: false,
replace_recent_blockhash: true,
commitment: Some(CommitmentConfig::processed()),
..Default::default()
};
match client.simulate_transaction_with_config(&tx, cfg) {
Ok(res) => {
if let Some(err) = res.value.err {
println!("simulation failed: {err:?}");
} else {
println!("simulation: ok");
}
if let Some(cu) = res.value.units_consumed {
println!("units : {cu}");
}
if let Some(logs) = res.value.logs {
println!("logs:");
for log in logs {
println!(" {log}");
}
}
}
Err(e) => {
eprintln!("simulate: {e}");
process::exit(1);
}
}
}
}
pub fn cmd_manager_accounts_read(args: &[String]) {
if args.is_empty() || matches!(args[0].as_str(), "--help" | "-h") {
eprintln!("Usage: hopper manager accounts read <pubkey> [--rpc <url>] [--manifest <path> | --program-id <id>]");
return;
}
let pubkey = &args[0];
let mut rpc: Option<String> = None;
let mut manifest_path: Option<String> = None;
let mut program_id: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--rpc" => {
i += 1;
rpc = args.get(i).cloned();
}
"--manifest" => {
i += 1;
manifest_path = args.get(i).cloned();
}
"--program-id" => {
i += 1;
program_id = args.get(i).cloned();
}
other => {
eprintln!("unknown flag: {other}");
process::exit(1);
}
}
i += 1;
}
let rpc_url = rpc.unwrap_or_else(|| crate::rpc::resolve_rpc_url(None));
let manifest_json = match manifest_path {
Some(p) => match fs::read_to_string(&p) {
Ok(s) => s,
Err(e) => {
eprintln!("read {p}: {e}");
process::exit(1);
}
},
None => match program_id.as_deref() {
Some(pid) => match super::manager_invoke::try_fetch_manifest(&rpc_url, pid) {
Ok(s) => s,
Err(e) => {
eprintln!("fetch manifest: {e}");
process::exit(1);
}
},
None => {
eprintln!("supply either --manifest <path> or --program-id <id>");
process::exit(1);
}
},
};
let info = match crate::rpc::get_account_info(&rpc_url, pubkey) {
Ok(Some(info)) => info,
Ok(None) => {
eprintln!("account {pubkey} does not exist");
process::exit(1);
}
Err(e) => {
eprintln!("get_account_info: {e}");
process::exit(1);
}
};
println!("pubkey : {pubkey}");
println!("owner : {}", info.owner);
println!("lamports : {}", info.lamports);
println!("data len : {} bytes", info.data.len());
if info.data.is_empty() {
return;
}
let disc = info.data[0];
println!("disc : 0x{:02x}", disc);
match layout_name_by_disc(&manifest_json, disc) {
Some(name) => println!("layout : {name}"),
None => println!("layout : (no match in manifest for disc 0x{:02x})", disc),
}
let (version, layout_id) = if info.data.len() >= 16 {
let ver = info.data[1];
let mut id = [0u8; 8];
id.copy_from_slice(&info.data[8..16]);
(Some(ver), Some(id))
} else {
(None, None)
};
if let Some(v) = version {
println!("version : {v}");
}
if let Some(id) = layout_id {
println!(
"layout_id: {}",
id.iter().map(|b| format!("{:02x}", b)).collect::<String>()
);
}
}
fn layout_name_by_disc(manifest_json: &str, disc: u8) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(manifest_json).ok()?;
let layouts = v.get("layouts")?.as_array()?;
for l in layouts {
let d = l.get("disc").and_then(|x| x.as_u64())? as u8;
if d == disc {
return l.get("name").and_then(|x| x.as_str()).map(String::from);
}
}
None
}