Skip to content

Commit ae1c158

Browse files
authored
feat(cli): Remove solana cli dependency (#4099)
* feat: Address command to retrieve public key from wallet config * feat: Add Balance command * feat(cli): Added airdrop command * feat(cli): Add epoch and epoch info commands * refactor(cli): cargo fmt * feat(cli): Add support for transaction log streaming * feat(cli): improve WebSocket URL handling * feat(cli): add ShowAccount command to display account contents * feat(cli): implement keypair generation and management commands * feat(cli): enhance airdrop and balance commands with cluster and wallet configuration retrieval * feat(cli): introduce program management commands for deploying, upgrading, and managing Solana programs * feat(cli): add configuration management commands for retrieving and updating Anchor.toml settings * feat(cli): enhance log streaming with WebSocket subscriptions and improved error handling * docs(cli): clarify account struct deserialization format in Command enum * fix(cli): Fix workspace detection in various commands * refactor(cli): replace external Solana CLI calls with native program deployment and upgrade implementations * refactor(cli): update with_workspace function to return Result type for improved error handling
1 parent 15ad6d5 commit ae1c158

File tree

8 files changed

+4860
-340
lines changed

8 files changed

+4860
-340
lines changed

Cargo.lock

Lines changed: 1024 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ solana-instructions-sysvar = "3.0.0"
3030
solana-invoke = "0.5.0"
3131
solana-keypair = "3.0.0"
3232
solana-loader-v3-interface = "6.0.0"
33+
solana-message = "3.0.0"
3334
solana-msg = "3.0.0"
35+
solana-packet = "3.0.0"
3436
solana-program = "3.0.0"
3537
solana-program-entrypoint = "3.0.0"
3638
solana-program-error = "3.0.0"

cli/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ cargo_toml = "0.19.2"
2525
chrono = "0.4.19"
2626
clap = { version = "4.5.17", features = ["derive"] }
2727
clap_complete = "4.5.26"
28+
console = "0.15"
2829
dirs = "4.0"
30+
ed25519-dalek = "2"
2931
flate2 = "1.0.19"
3032
heck = "0.4.0"
3133
pathdiff = "0.2.0"
@@ -37,19 +39,28 @@ serde = { version = "1.0.130", features = ["derive"] }
3739
serde_json = "1.0"
3840
shellexpand = "2.1.0"
3941
solana-cli-config.workspace = true
42+
solana-client = "3.0.0"
4043
solana-clock.workspace = true
4144
solana-commitment-config.workspace = true
4245
solana-compute-budget-interface.workspace = true
4346
solana-faucet = { workspace = true, features = ["agave-unstable-api"] }
4447
solana-instruction.workspace = true
4548
solana-keypair.workspace = true
49+
solana-loader-v3-interface.workspace = true
50+
solana-message.workspace = true
51+
solana-packet.workspace = true
4652
solana-pubkey.workspace = true
53+
solana-sdk-ids.workspace = true
4754
solana-signature.workspace = true
4855
solana-signer.workspace = true
4956
solana-system-interface.workspace = true
5057
solana-transaction.workspace = true
5158
solana-rpc-client.workspace = true
59+
solana-rpc-client-api.workspace = true
60+
solana-pubsub-client.workspace = true
5261
syn = { version = "1.0.60", features = ["full", "extra-traits"] }
5362
tar = "0.4.35"
63+
tempfile = "3"
64+
tiny-bip39 = "2.0"
5465
toml = "0.7.6"
5566
walkdir = "2.3.2"

cli/src/account.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use anyhow::{anyhow, Result};
2+
use clap::Parser;
3+
use solana_commitment_config::CommitmentConfig;
4+
use solana_pubkey::Pubkey;
5+
use solana_rpc_client::rpc_client::RpcClient;
6+
use std::fs::File;
7+
use std::io::Write;
8+
use std::path::PathBuf;
9+
10+
use crate::config::{Config, ConfigOverride};
11+
12+
#[derive(Debug, Parser)]
13+
pub struct ShowAccountCommand {
14+
/// Account address to show
15+
pub account_address: Pubkey,
16+
/// Display balance in lamports instead of SOL
17+
#[clap(long)]
18+
pub lamports: bool,
19+
/// Write the account data to this file
20+
#[clap(short = 'o', long)]
21+
pub output_file: Option<PathBuf>,
22+
/// Return information in specified output format
23+
#[clap(long, value_parser = ["json", "json-compact"])]
24+
pub output: Option<String>,
25+
}
26+
27+
pub fn show_account(cfg_override: &ConfigOverride, cmd: ShowAccountCommand) -> Result<()> {
28+
let config = Config::discover(cfg_override)?;
29+
let url = match config {
30+
Some(ref cfg) => cfg.provider.cluster.url().to_string(),
31+
None => {
32+
// If not in workspace, use cluster override or default to localhost
33+
if let Some(ref cluster) = cfg_override.cluster {
34+
cluster.url().to_string()
35+
} else {
36+
"https://api.mainnet-beta.solana.com".to_string()
37+
}
38+
}
39+
};
40+
41+
let rpc_client = RpcClient::new_with_commitment(url, CommitmentConfig::confirmed());
42+
43+
// Fetch the account
44+
let account = rpc_client
45+
.get_account(&cmd.account_address)
46+
.map_err(|e| anyhow!("Unable to fetch account {}: {}", cmd.account_address, e))?;
47+
48+
// Handle JSON output
49+
if let Some(format) = cmd.output {
50+
use base64::{engine::general_purpose::STANDARD, Engine};
51+
52+
let json_output = serde_json::json!({
53+
"pubkey": cmd.account_address.to_string(),
54+
"account": {
55+
"lamports": account.lamports,
56+
"owner": account.owner.to_string(),
57+
"executable": account.executable,
58+
"rentEpoch": account.rent_epoch,
59+
"data": STANDARD.encode(&account.data),
60+
}
61+
});
62+
63+
let output_str = match format.as_str() {
64+
"json" => serde_json::to_string_pretty(&json_output)?,
65+
"json-compact" => serde_json::to_string(&json_output)?,
66+
_ => unreachable!(),
67+
};
68+
69+
if let Some(output_file) = cmd.output_file {
70+
let mut file = File::create(&output_file)?;
71+
file.write_all(output_str.as_bytes())?;
72+
println!("Wrote account to {}", output_file.display());
73+
} else {
74+
println!("{}", output_str);
75+
}
76+
77+
return Ok(());
78+
}
79+
80+
// Text output
81+
println!("Public Key: {}", cmd.account_address);
82+
83+
if cmd.lamports {
84+
println!("Balance: {} lamports", account.lamports);
85+
} else {
86+
println!("Balance: {} SOL", account.lamports as f64 / 1_000_000_000.0);
87+
}
88+
89+
println!("Owner: {}", account.owner);
90+
println!("Executable: {}", account.executable);
91+
println!("Rent Epoch: {}", account.rent_epoch);
92+
93+
// Display account data
94+
let data_len = account.data.len();
95+
println!("Length: {} (0x{:x}) bytes", data_len, data_len);
96+
97+
if !account.data.is_empty() {
98+
// Write to output file if specified
99+
if let Some(output_file) = cmd.output_file {
100+
let mut file = File::create(&output_file)?;
101+
file.write_all(&account.data)?;
102+
println!("Wrote account data to {}", output_file.display());
103+
}
104+
105+
// Display hex dump
106+
print_hex_dump(&account.data);
107+
}
108+
109+
Ok(())
110+
}
111+
112+
fn print_hex_dump(data: &[u8]) {
113+
const BYTES_PER_LINE: usize = 16;
114+
115+
for (i, chunk) in data.chunks(BYTES_PER_LINE).enumerate() {
116+
let offset = i * BYTES_PER_LINE;
117+
118+
// Print offset
119+
print!("{:04x}: ", offset);
120+
121+
// Print hex values
122+
for (j, byte) in chunk.iter().enumerate() {
123+
if j > 0 && j % 4 == 0 {
124+
print!(" ");
125+
}
126+
print!("{:02x} ", byte);
127+
}
128+
129+
// Pad if this is the last line and it's not complete
130+
if chunk.len() < BYTES_PER_LINE {
131+
for j in chunk.len()..BYTES_PER_LINE {
132+
if j > 0 && j % 4 == 0 {
133+
print!(" ");
134+
}
135+
print!(" ");
136+
}
137+
}
138+
139+
print!(" ");
140+
141+
// Print ASCII representation
142+
for byte in chunk {
143+
let c = *byte as char;
144+
if c.is_ascii_graphic() || c == ' ' {
145+
print!("{}", c);
146+
} else {
147+
print!(".");
148+
}
149+
}
150+
151+
println!();
152+
}
153+
}

cli/src/config.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use serde::ser::SerializeMap;
1111
use serde::{Deserialize, Deserializer, Serialize, Serializer};
1212
use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE};
1313
use solana_clock::Slot;
14+
use solana_commitment_config::CommitmentLevel;
1415
use solana_keypair::Keypair;
1516
use solana_pubkey::Pubkey;
1617
use solana_signer::Signer;
@@ -27,6 +28,32 @@ use std::str::FromStr;
2728
use std::{fmt, io};
2829
use walkdir::WalkDir;
2930

31+
/// Wrapper around CommitmentLevel to support case-insensitive parsing
32+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33+
pub struct CaseInsensitiveCommitmentLevel(pub CommitmentLevel);
34+
35+
impl FromStr for CaseInsensitiveCommitmentLevel {
36+
type Err = String;
37+
38+
fn from_str(s: &str) -> Result<Self, Self::Err> {
39+
// Convert to lowercase for case-insensitive matching
40+
let lowercase = s.to_lowercase();
41+
let commitment = CommitmentLevel::from_str(&lowercase).map_err(|_| {
42+
format!(
43+
"Invalid commitment level '{}'. Valid values are: processed, confirmed, finalized",
44+
s
45+
)
46+
})?;
47+
Ok(CaseInsensitiveCommitmentLevel(commitment))
48+
}
49+
}
50+
51+
impl From<CaseInsensitiveCommitmentLevel> for CommitmentLevel {
52+
fn from(val: CaseInsensitiveCommitmentLevel) -> Self {
53+
val.0
54+
}
55+
}
56+
3057
pub trait Merge: Sized {
3158
fn merge(&mut self, _other: Self) {}
3259
}
@@ -39,6 +66,9 @@ pub struct ConfigOverride {
3966
/// Wallet override.
4067
#[clap(global = true, long = "provider.wallet")]
4168
pub wallet: Option<WalletPath>,
69+
/// Commitment override (valid values: processed, confirmed, finalized).
70+
#[clap(global = true, long = "commitment")]
71+
pub commitment: Option<CaseInsensitiveCommitmentLevel>,
4272
}
4373

4474
#[derive(Debug)]

0 commit comments

Comments
 (0)