Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0834511
feat: Address command to retrieve public key from wallet config
swaroop-osec Nov 27, 2025
4751e11
feat: Add Balance command
swaroop-osec Nov 27, 2025
938a70e
feat(cli): Added airdrop command
swaroop-osec Nov 27, 2025
8d85c80
feat(cli): Add epoch and epoch info commands
swaroop-osec Nov 27, 2025
7969a4f
refactor(cli): cargo fmt
swaroop-osec Nov 27, 2025
7f4b64a
feat(cli): Add support for transaction log streaming
swaroop-osec Nov 27, 2025
6d57b29
feat(cli): improve WebSocket URL handling
swaroop-osec Nov 27, 2025
3f06011
feat(cli): add ShowAccount command to display account contents
swaroop-osec Nov 27, 2025
c439b4c
feat(cli): implement keypair generation and management commands
swaroop-osec Nov 27, 2025
cc442ff
feat(cli): enhance airdrop and balance commands with cluster and wall…
swaroop-osec Nov 27, 2025
2ad4cdd
feat(cli): introduce program management commands for deploying, upgra…
swaroop-osec Nov 28, 2025
e319da7
feat(cli): add configuration management commands for retrieving and u…
swaroop-osec Nov 28, 2025
bf7e06b
feat(cli): enhance log streaming with WebSocket subscriptions and imp…
swaroop-osec Nov 28, 2025
1a92773
docs(cli): clarify account struct deserialization format in Command enum
swaroop-osec Nov 28, 2025
424ce58
fix(cli): Fix workspace detection in various commands
swaroop-osec Nov 28, 2025
4a43007
refactor(cli): replace external Solana CLI calls with native program …
swaroop-osec Nov 28, 2025
c081ab4
refactor(cli): update with_workspace function to return Result type f…
swaroop-osec Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,066 changes: 1,024 additions & 42 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ solana-instructions-sysvar = "3.0.0"
solana-invoke = "0.5.0"
solana-keypair = "3.0.0"
solana-loader-v3-interface = "6.0.0"
solana-message = "3.0.0"
solana-msg = "3.0.0"
solana-packet = "3.0.0"
solana-program = "3.0.0"
solana-program-entrypoint = "3.0.0"
solana-program-error = "3.0.0"
Expand Down
11 changes: 11 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ cargo_toml = "0.19.2"
chrono = "0.4.19"
clap = { version = "4.5.17", features = ["derive"] }
clap_complete = "4.5.26"
console = "0.15"
dirs = "4.0"
ed25519-dalek = "2"
flate2 = "1.0.19"
heck = "0.4.0"
pathdiff = "0.2.0"
Expand All @@ -37,19 +39,28 @@ serde = { version = "1.0.130", features = ["derive"] }
serde_json = "1.0"
shellexpand = "2.1.0"
solana-cli-config.workspace = true
solana-client = "3.0.0"
solana-clock.workspace = true
solana-commitment-config.workspace = true
solana-compute-budget-interface.workspace = true
solana-faucet = { workspace = true, features = ["agave-unstable-api"] }
solana-instruction.workspace = true
solana-keypair.workspace = true
solana-loader-v3-interface.workspace = true
solana-message.workspace = true
solana-packet.workspace = true
solana-pubkey.workspace = true
solana-sdk-ids.workspace = true
solana-signature.workspace = true
solana-signer.workspace = true
solana-system-interface.workspace = true
solana-transaction.workspace = true
solana-rpc-client.workspace = true
solana-rpc-client-api.workspace = true
solana-pubsub-client.workspace = true
syn = { version = "1.0.60", features = ["full", "extra-traits"] }
tar = "0.4.35"
tempfile = "3"
tiny-bip39 = "2.0"
toml = "0.7.6"
walkdir = "2.3.2"
153 changes: 153 additions & 0 deletions cli/src/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use anyhow::{anyhow, Result};
use clap::Parser;
use solana_commitment_config::CommitmentConfig;
use solana_pubkey::Pubkey;
use solana_rpc_client::rpc_client::RpcClient;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

use crate::config::{Config, ConfigOverride};

#[derive(Debug, Parser)]
pub struct ShowAccountCommand {
/// Account address to show
pub account_address: Pubkey,
/// Display balance in lamports instead of SOL
#[clap(long)]
pub lamports: bool,
/// Write the account data to this file
#[clap(short = 'o', long)]
pub output_file: Option<PathBuf>,
/// Return information in specified output format
#[clap(long, value_parser = ["json", "json-compact"])]
pub output: Option<String>,
}

pub fn show_account(cfg_override: &ConfigOverride, cmd: ShowAccountCommand) -> Result<()> {
let config = Config::discover(cfg_override)?;
let url = match config {
Some(ref cfg) => cfg.provider.cluster.url().to_string(),
None => {
// If not in workspace, use cluster override or default to localhost
if let Some(ref cluster) = cfg_override.cluster {
cluster.url().to_string()
} else {
"https://api.mainnet-beta.solana.com".to_string()
}
}
};

let rpc_client = RpcClient::new_with_commitment(url, CommitmentConfig::confirmed());

// Fetch the account
let account = rpc_client
.get_account(&cmd.account_address)
.map_err(|e| anyhow!("Unable to fetch account {}: {}", cmd.account_address, e))?;

// Handle JSON output
if let Some(format) = cmd.output {
use base64::{engine::general_purpose::STANDARD, Engine};

let json_output = serde_json::json!({
"pubkey": cmd.account_address.to_string(),
"account": {
"lamports": account.lamports,
"owner": account.owner.to_string(),
"executable": account.executable,
"rentEpoch": account.rent_epoch,
"data": STANDARD.encode(&account.data),
}
});

let output_str = match format.as_str() {
"json" => serde_json::to_string_pretty(&json_output)?,
"json-compact" => serde_json::to_string(&json_output)?,
_ => unreachable!(),
};

if let Some(output_file) = cmd.output_file {
let mut file = File::create(&output_file)?;
file.write_all(output_str.as_bytes())?;
println!("Wrote account to {}", output_file.display());
} else {
println!("{}", output_str);
}

return Ok(());
}

// Text output
println!("Public Key: {}", cmd.account_address);

if cmd.lamports {
println!("Balance: {} lamports", account.lamports);
} else {
println!("Balance: {} SOL", account.lamports as f64 / 1_000_000_000.0);
}

println!("Owner: {}", account.owner);
println!("Executable: {}", account.executable);
println!("Rent Epoch: {}", account.rent_epoch);

// Display account data
let data_len = account.data.len();
println!("Length: {} (0x{:x}) bytes", data_len, data_len);

if !account.data.is_empty() {
// Write to output file if specified
if let Some(output_file) = cmd.output_file {
let mut file = File::create(&output_file)?;
file.write_all(&account.data)?;
println!("Wrote account data to {}", output_file.display());
}

// Display hex dump
print_hex_dump(&account.data);
}

Ok(())
}

fn print_hex_dump(data: &[u8]) {
const BYTES_PER_LINE: usize = 16;

for (i, chunk) in data.chunks(BYTES_PER_LINE).enumerate() {
let offset = i * BYTES_PER_LINE;

// Print offset
print!("{:04x}: ", offset);

// Print hex values
for (j, byte) in chunk.iter().enumerate() {
if j > 0 && j % 4 == 0 {
print!(" ");
}
print!("{:02x} ", byte);
}

// Pad if this is the last line and it's not complete
if chunk.len() < BYTES_PER_LINE {
for j in chunk.len()..BYTES_PER_LINE {
if j > 0 && j % 4 == 0 {
print!(" ");
}
print!(" ");
}
}

print!(" ");

// Print ASCII representation
for byte in chunk {
let c = *byte as char;
if c.is_ascii_graphic() || c == ' ' {
print!("{}", c);
} else {
print!(".");
}
}

println!();
}
}
30 changes: 30 additions & 0 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE};
use solana_clock::Slot;
use solana_commitment_config::CommitmentLevel;
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
Expand All @@ -27,6 +28,32 @@ use std::str::FromStr;
use std::{fmt, io};
use walkdir::WalkDir;

/// Wrapper around CommitmentLevel to support case-insensitive parsing
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CaseInsensitiveCommitmentLevel(pub CommitmentLevel);

impl FromStr for CaseInsensitiveCommitmentLevel {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// Convert to lowercase for case-insensitive matching
let lowercase = s.to_lowercase();
let commitment = CommitmentLevel::from_str(&lowercase).map_err(|_| {
format!(
"Invalid commitment level '{}'. Valid values are: processed, confirmed, finalized",
s
)
})?;
Ok(CaseInsensitiveCommitmentLevel(commitment))
}
}

impl From<CaseInsensitiveCommitmentLevel> for CommitmentLevel {
fn from(val: CaseInsensitiveCommitmentLevel) -> Self {
val.0
}
}

pub trait Merge: Sized {
fn merge(&mut self, _other: Self) {}
}
Expand All @@ -39,6 +66,9 @@ pub struct ConfigOverride {
/// Wallet override.
#[clap(global = true, long = "provider.wallet")]
pub wallet: Option<WalletPath>,
/// Commitment override (valid values: processed, confirmed, finalized).
#[clap(global = true, long = "commitment")]
pub commitment: Option<CaseInsensitiveCommitmentLevel>,
}

#[derive(Debug)]
Expand Down
Loading