use colored::*;
use serde_json::{Value, json};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum OutputFormat {
Json,
Text,
}
pub fn print_output(data: Value, json_format: bool) {
if json_format {
println!(
"{}",
serde_json::to_string_pretty(&data).unwrap_or_else(|_| "{}".to_string())
);
} else {
print_human_readable(&data);
}
}
fn print_human_readable(data: &Value) {
match data {
Value::Object(map) => {
for (key, value) in map {
match key.as_str() {
"version" => print_version_info(value),
"health" => print_health_info(value),
"account_info" => print_account_info(value),
"balance" => print_balance_info(value),
"transfer" => print_transfer_info(value),
"program_upload" => print_program_upload_info(value),
"program_cleanup" => print_program_cleanup_info(value),
"keys" => print_keys_info(value),
"networks" => print_networks_info(value),
"account_create" => print_account_create_info(value),
"account_transactions" => print_account_transactions(value),
_ => println!("{}: {}", key.cyan(), format_value(value)),
}
}
}
_ => println!("{}", format_value(data)),
}
}
fn format_number(n: u64) -> String {
let s = n.to_string();
let mut out = String::new();
let chars = s.chars().rev().collect::<Vec<_>>();
for (i, c) in chars.iter().enumerate() {
if i > 0 && i % 3 == 0 {
out.push('_');
}
out.push(*c);
}
out.chars().rev().collect()
}
fn format_value(value: &Value) -> String {
format_value_ext(value, false)
}
fn format_value_ext(value: &Value, thousand_separator: bool) -> String {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => {
if thousand_separator {
format_number(n.as_u64().unwrap_or(0))
} else {
n.to_string()
}
}
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
Value::Array(arr) => format!(
"[{}]",
arr.iter().map(format_value).collect::<Vec<_>>().join(", ")
),
Value::Object(_) => {
serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
}
}
}
fn print_version_info(data: &Value) {
if let Value::Object(version_data) = data {
println!("{}", "Version Information".bold().green());
if let Some(thru_node) = version_data.get("thru-node") {
println!(" {}: {}", "Thru Node".cyan(), format_value(thru_node));
}
if let Some(thru_rpc) = version_data.get("thru-rpc") {
println!(" {}: {}", "Thru RPC".cyan(), format_value(thru_rpc));
}
}
}
fn print_health_info(data: &Value) {
match data {
Value::String(status) if status == "ok" => {
println!("{}", "Node is healthy".bold().green());
}
Value::Object(error_data) => {
println!("{}", "Node is unhealthy".bold().red());
if let Some(message) = error_data.get("message") {
println!(" {}: {}", "Reason".cyan(), format_value(message));
}
if let Some(data) = error_data.get("data") {
if let Value::Object(data_obj) = data {
if let Some(slots_behind) = data_obj.get("numSlotsBehind") {
println!(
" {}: {}",
"Slots Behind".cyan(),
format_value(slots_behind)
);
}
}
}
}
_ => println!("Health status: {}", format_value(data)),
}
}
fn print_account_info(data: &Value) {
if let Value::Object(account_data) = data {
println!("{}", "Account Information".bold().green());
if let Some(pubkey) = account_data.get("pubkey") {
println!(" {}: {}", "Public Key".cyan(), format_value(pubkey));
}
if let Some(balance) = account_data.get("balance") {
println!(
" {}: {}",
"Balance".cyan(),
format_value_ext(balance, true)
);
}
if let Some(owner) = account_data.get("owner") {
println!(" {}: {}", "Owner".cyan(), format_value(owner));
}
if let Some(data_size) = account_data.get("dataSize") {
println!(" {}: {}", "Data Size".cyan(), format_value(data_size));
}
if let Some(nonce) = account_data.get("nonce") {
println!(" {}: {}", "Nonce".cyan(), format_value(nonce));
}
if let Some(seq) = account_data.get("seq") {
println!(" {}: {}", "Seq".cyan(), format_value(seq));
}
if let Some(program) = account_data.get("program") {
let is_program = program.as_bool().unwrap_or(false);
println!(
" {}: {}",
"Is Program".cyan(),
if is_program {
"Yes".green()
} else {
"No".red()
}
);
}
if let Some(is_new) = account_data.get("isNew") {
let is_new = is_new.as_bool().unwrap_or(false);
println!(
" {}: {}",
"Is New".cyan(),
if is_new { "Yes".green() } else { "No".red() }
);
}
if let Some(is_ephemeral) = account_data.get("isEphemeral") {
let is_ephemeral = is_ephemeral.as_bool().unwrap_or(false);
println!(
" {}: {}",
"Is Ephemeral".cyan(),
if is_ephemeral {
"Yes".green()
} else {
"No".red()
}
);
}
if let Some(is_deleted) = account_data.get("isDeleted") {
let is_deleted = is_deleted.as_bool().unwrap_or(false);
println!(
" {}: {}",
"Is Deleted".cyan(),
if is_deleted {
"Yes".yellow()
} else {
"No".green()
}
);
}
if let Some(is_privileged) = account_data.get("isPrivileged") {
let is_privileged = is_privileged.as_bool().unwrap_or(false);
println!(
" {}: {}",
"Is Privileged".cyan(),
if is_privileged {
"Yes".green()
} else {
"No".red()
}
);
}
if let Some(slot) = account_data.get("slot") {
println!(" {}: {}", "Slot".cyan(), format_value(slot));
}
if let Some(timestamp) = account_data.get("blockTimestamp") {
let formatted_timestamp = if let Some(ts_str) = timestamp.as_str() {
if let Some((secs_str, _nanos_str)) = ts_str.split_once('.') {
if let Ok(secs) = secs_str.parse::<i64>() {
if let Some(dt) = chrono::DateTime::from_timestamp(secs, 0) {
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
} else {
format_value(timestamp)
}
} else {
format_value(timestamp)
}
} else {
format_value(timestamp)
}
} else {
format_value(timestamp)
};
println!(" {}: {}", "Block Timestamp".cyan(), formatted_timestamp);
}
if let Some(data_hex) = account_data.get("dataHex") {
let hex_str = format_value(data_hex);
if !hex_str.is_empty() {
println!(" {}: {}", "Data (Hex)".cyan(), hex_str.bright_yellow());
if let Some(start) = account_data.get("dataHexStart") {
println!(" {}: {}", " Data Start".cyan(), format_value(start));
}
if let Some(len) = account_data.get("dataHexLen") {
println!(" {}: {} bytes", " Data Length".cyan(), format_value(len));
}
}
}
}
}
fn print_account_create_info(data: &Value) {
if let Value::Object(account_create_data) = data {
println!("{}", "Account Creation".bold().green());
if let Some(key_name) = account_create_data.get("key_name") {
println!(" {}: {}", "Key Name".cyan(), format_value(key_name));
}
if let Some(public_key) = account_create_data.get("public_key") {
println!(" {}: {}", "Public Key".cyan(), format_value(public_key));
}
if let Some(signature) = account_create_data.get("signature") {
println!(" {}: {}", "Signature".cyan(), format_value(signature));
}
if let Some(status) = account_create_data.get("status") {
let status_str = format_value(status);
let colored_status = match status_str.as_str() {
"success" => status_str.green(),
"failed" => status_str.red(),
_ => status_str.normal(),
};
println!(" {}: {}", "Status".cyan(), colored_status);
}
}
}
fn print_account_transactions(data: &Value) {
if let Value::Object(tx_data) = data {
println!("{}", "Account Transactions".bold().green());
if let Some(account) = tx_data.get("account") {
println!(" {}: {}", "Account".cyan(), format_value(account));
}
match tx_data.get("signatures").and_then(|value| value.as_array()) {
Some(signatures) if signatures.is_empty() => {
println!(" {}", "No transactions found.".italic());
}
Some(signatures) => {
println!(" {}:", "Signatures".cyan());
for (idx, sig) in signatures.iter().enumerate() {
println!(" {:>2}. {}", idx + 1, format_value(sig));
}
}
None => {
println!(" {}", "No transactions found.".italic());
}
}
if let Some(Value::String(token)) = tx_data.get("nextPageToken") {
if !token.is_empty() {
println!(" {}: {}", "Next Page Token".cyan(), token);
}
}
}
}
fn print_balance_info(data: &Value) {
if let Value::Object(balance_data) = data {
if let Some(pubkey) = balance_data.get("pubkey") {
println!("{}: {}", "Account".cyan(), format_value(pubkey));
}
if let Some(balance) = balance_data.get("balance") {
println!(
"{}: {}",
"Balance".bold().green(),
format_value_ext(balance, true)
);
}
} else {
println!(
"{}: {}",
"Balance".bold().green(),
format_value_ext(data, true)
);
}
}
fn print_program_upload_info(data: &Value) {
if let Value::Object(upload_data) = data {
println!("{}", "Program Upload".bold().green());
if let Some(status) = upload_data.get("status") {
let status_str = format_value(status);
let colored_status = match status_str.as_str() {
"success" => status_str.green(),
"failed" => status_str.red(),
"in_progress" => status_str.yellow(),
_ => status_str.normal(),
};
println!(" {}: {}", "Status".cyan(), colored_status);
}
if let Some(transactions) = upload_data.get("total_transactions") {
println!(
" {}: {}",
"Total Transactions".cyan(),
format_value(transactions)
);
}
if let Some(completed) = upload_data.get("completed_transactions") {
println!(" {}: {}", "Completed".cyan(), format_value(completed));
}
if let Some(program_size) = upload_data.get("program_size") {
println!(
" {}: {} bytes",
"Program Size".cyan(),
format_value(program_size)
);
}
if let Some(meta_account) = upload_data.get("meta_account") {
println!(
" {}: {}",
"Meta Account".cyan(),
format_value(meta_account)
);
}
if let Some(buffer_account) = upload_data.get("buffer_account") {
println!(
" {}: {}",
"Buffer Account".cyan(),
format_value(buffer_account)
);
}
}
}
fn print_program_cleanup_info(data: &Value) {
if let Value::Object(cleanup_data) = data {
println!("{}", "Program Cleanup".bold().green());
if let Some(status) = cleanup_data.get("status") {
let status_str = format_value(status);
let colored_status = match status_str.as_str() {
"success" => status_str.green(),
"failed" => status_str.red(),
_ => status_str.normal(),
};
println!(" {}: {}", "Status".cyan(), colored_status);
}
if let Some(message) = cleanup_data.get("message") {
println!(" {}: {}", "Message".cyan(), format_value(message));
}
}
}
fn print_keys_info(data: &Value) {
if let Value::Object(keys_data) = data {
if let Some(list) = keys_data.get("list") {
if let Value::Array(key_names) = list {
println!("{}", "Available Keys".bold().green());
for key_name in key_names {
println!(" {}", format_value(key_name));
}
}
}
if let Some(operation) = keys_data.get("operation") {
let op_str = format_value(operation);
if let Some(name) = keys_data.get("name") {
let name_str = format_value(name);
match op_str.as_str() {
"add" => println!(
"{}: Key '{}' added to configuration",
"Success".bold().green(),
name_str
),
"get" => {
if let Some(value) = keys_data.get("value") {
println!("Key '{}': {}", name_str.cyan(), format_value(value));
}
}
"generate" => {
if let Some(value) = keys_data.get("value") {
println!(
"Generated key '{}': {}",
name_str.cyan(),
format_value(value)
);
println!(
"{}: Key '{}' added to configuration",
"Success".bold().green(),
name_str
);
}
}
"remove" => println!(
"{}: Key '{}' removed from configuration",
"Success".bold().green(),
name_str
),
_ => println!(
"{}: Operation '{}' on key '{}'",
"Info".bold().blue(),
op_str,
name_str
),
}
}
}
}
}
fn print_transfer_info(data: &Value) {
if let Value::Object(transfer_data) = data {
println!("{}", "Transfer Information".bold().green());
if let Some(src) = transfer_data.get("src") {
println!(" {}: {}", "Source".cyan(), format_value(src));
}
if let Some(dst) = transfer_data.get("dst") {
println!(" {}: {}", "Destination".cyan(), format_value(dst));
}
if let Some(value) = transfer_data.get("value") {
println!(" {}: {}", "Value".cyan(), format_value(value));
}
if let Some(signature) = transfer_data.get("signature") {
println!(" {}: {}", "Signature".cyan(), format_value(signature));
}
if let Some(status) = transfer_data.get("status") {
let status_str = format_value(status);
let colored_status = match status_str.as_str() {
"success" => status_str.green(),
"failed" => status_str.red(),
_ => status_str.normal(),
};
println!(" {}: {}", "Status".cyan(), colored_status);
}
}
}
fn print_networks_info(data: &Value) {
if let Value::Object(net_data) = data {
if let Some(list) = net_data.get("list") {
if let Value::Array(entries) = list {
if entries.is_empty() {
println!("{}", "No networks configured.".italic());
return;
}
let active_name = net_data
.get("active")
.or_else(|| net_data.get("default"))
.and_then(|v| v.as_str())
.unwrap_or("");
println!("{}", "Networks".bold().green());
for entry in entries {
if let Value::Object(e) = entry {
let name = e.get("name").and_then(|v| v.as_str()).unwrap_or("?");
let url = e.get("url").and_then(|v| v.as_str()).unwrap_or("?");
let has_auth = e
.get("has_auth_token")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let marker = if name == active_name {
" (active)".green().to_string()
} else {
String::new()
};
let auth_info = if has_auth { " [auth]" } else { "" };
println!(" {}{}: {}{}", name.cyan(), marker, url, auth_info);
}
}
}
}
if let Some(operation) = net_data.get("operation") {
let op_str = format_value(operation);
if let Some(name) = net_data.get("name") {
let name_str = format_value(name);
match op_str.as_str() {
"add" => {
if let Some(url) = net_data.get("url") {
println!(
"{}: Network '{}' added ({})",
"Success".bold().green(),
name_str,
format_value(url)
);
}
}
"use" => println!(
"{}: Active network set to '{}'",
"Success".bold().green(),
name_str
),
"set" => println!(
"{}: Network '{}' updated",
"Success".bold().green(),
name_str
),
"remove" => println!(
"{}: Network '{}' removed",
"Success".bold().green(),
name_str
),
_ => println!(
"{}: Operation '{}' on network '{}'",
"Info".bold().blue(),
op_str,
name_str
),
}
}
}
}
}
pub fn print_error(error: &str) {
eprintln!("{}: {}", "Error".bold().red(), error);
}
pub fn print_warning(warning: &str) {
eprintln!("{}: {}", "Warning".bold().yellow(), warning);
}
pub fn print_success(message: &str) {
println!("{}: {}", "Success".bold().green(), message);
}
pub fn print_info(message: &str) {
println!("{}: {}", "Info".bold().blue(), message);
}
pub fn create_version_response(thru_node: &str, thru_rpc: &str) -> Value {
json!({
"getversion": {
"status": "success",
"thru-node": thru_node,
"thru-rpc": thru_rpc
}
})
}
pub fn create_health_response(status: &str) -> Value {
json!({
"gethealth": {
"status": status
}
})
}
pub fn create_account_info_response(account_data: HashMap<String, Value>) -> Value {
json!({
"account_info": account_data
})
}
pub fn create_account_transactions_response(
account: &str,
signatures: Vec<String>,
next_page_token: Option<String>,
) -> Value {
let mut response = json!({
"account_transactions": {
"account": account,
"signatures": signatures,
}
});
if let Some(token) = next_page_token {
if let Some(obj) = response
.get_mut("account_transactions")
.and_then(|value| value.as_object_mut())
{
obj.insert("nextPageToken".to_string(), json!(token));
}
}
response
}
pub fn create_balance_response(pubkey: &str, balance: u64) -> Value {
json!({
"balance": {
"pubkey": pubkey,
"balance": balance
}
})
}
pub fn create_program_upload_response(
status: &str,
total_transactions: usize,
completed_transactions: usize,
program_size: usize,
meta_account: Option<&str>,
buffer_account: Option<&str>,
) -> Value {
let mut response = json!({
"program_upload": {
"status": status,
"total_transactions": total_transactions,
"completed_transactions": completed_transactions,
"program_size": program_size
}
});
if let Some(meta) = meta_account {
response["program_upload"]["meta_account"] = json!(meta);
}
if let Some(buffer) = buffer_account {
response["program_upload"]["buffer_account"] = json!(buffer);
}
response
}
pub fn create_program_cleanup_response(status: &str, message: &str) -> Value {
json!({
"program_cleanup": {
"status": status,
"message": message
}
})
}
pub fn create_keys_list_response(key_names: Vec<String>) -> Value {
json!({
"keys": {
"list": key_names
}
})
}
pub fn create_transfer_response(
src: &str,
dst: &str,
value: u64,
signature: &str,
status: &str,
) -> Value {
json!({
"transfer": {
"src": src,
"dst": dst,
"value": value,
"signature": signature,
"status": status
}
})
}
pub fn create_keys_operation_response(
operation: &str,
name: &str,
status: &str,
value: Option<&str>,
) -> Value {
let mut response = json!({
"keys": {
"operation": operation,
"name": name,
"status": status
}
});
if let Some(key_value) = value {
response["keys"]["value"] = json!(key_value);
}
response
}
pub fn create_account_create_response(
key_name: &str,
public_key: &str,
signature: &str,
status: &str,
) -> Value {
json!({
"account_create": {
"key_name": key_name,
"public_key": public_key,
"signature": signature,
"status": status
}
})
}
pub fn create_network_list_response(config: &crate::config::Config) -> Value {
let entries: Vec<Value> = config
.list_network_names()
.iter()
.filter_map(|name| {
config.networks.get(name).map(|net| {
json!({
"name": name,
"url": net.url,
"has_auth_token": net.auth_token.is_some(),
"is_active": config.default_network.as_deref() == Some(name.as_str()),
})
})
})
.collect();
let mut response = json!({
"networks": {
"list": entries,
}
});
if let Some(ref active) = config.default_network {
response["networks"]["active"] = json!(active);
}
response
}
pub fn create_network_operation_response(
operation: &str,
name: &str,
status: &str,
url: Option<&str>,
) -> Value {
let mut response = json!({
"networks": {
"operation": operation,
"name": name,
"status": status,
}
});
if let Some(u) = url {
response["networks"]["url"] = json!(u);
}
response
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, NetworkConfig};
#[test]
fn create_network_list_response_marks_active_network() {
let mut config = Config::default();
config.networks.insert(
"1".to_string(),
NetworkConfig {
url: "https://rpc-1.example.com".to_string(),
auth_token: None,
},
);
config.networks.insert(
"2".to_string(),
NetworkConfig {
url: "https://rpc-2.example.com".to_string(),
auth_token: Some("secret".to_string()),
},
);
config.default_network = Some("1".to_string());
let response = create_network_list_response(&config);
let networks = &response["networks"];
assert_eq!(networks["active"], "1");
assert_eq!(networks["list"][0]["name"], "1");
assert_eq!(networks["list"][0]["is_active"], true);
assert_eq!(networks["list"][1]["name"], "2");
assert_eq!(networks["list"][1]["is_active"], false);
assert_eq!(networks["list"][1]["has_auth_token"], true);
}
}