use std::fs::read;
use cached::once;
use clap::{Parser, Subcommand};
use serde::Deserialize;
use solana_client::rpc_client::RpcClient;
use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about = "Equa Network protocol CLI", long_about = None)]
pub struct Config {
#[arg(long, env, global = true)]
signer: Option<String>,
#[arg(long = "rpc", env, global = true)]
rpc_url: Option<String>,
#[arg(long, env, global = true)]
commitment: Option<CommitmentLevel>,
#[arg(long, env, global = true, default_value = "false")]
pub no_decoration: bool,
#[arg(long, env, global = true, default_value = "false")]
pub debug: bool,
#[arg(long, env, global = true, default_value = "false")]
pub skip_simulation: bool,
#[arg(long, env, global = true, default_value = "false")]
pub yes: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Command {
Show,
Program {
#[command(subcommand)]
command: ProgramCommand,
},
Permission {
#[command(subcommand)]
command: PermissionCommand,
},
Network {
#[command(subcommand)]
command: NetworkCommand,
},
Vault {
#[command(subcommand)]
command: VaultCommand,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum ProgramCommand {
Initialize,
Version,
}
#[derive(Subcommand, Debug, Clone)]
pub enum PermissionCommand {
Initialize {
#[arg(long)]
account: Pubkey,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum NetworkCommand {
Initialize {
#[arg(long)]
network_id: String,
},
Swap {
#[arg(long)]
network: Pubkey,
#[arg(long)]
input_mint: Pubkey,
#[arg(long)]
output_mint: Pubkey,
#[arg(long)]
amount: u64,
#[arg(long, default_value = "0")]
minimum_output_amount: u64,
#[arg(long, default_value = "false")]
allow_partial_fill: bool,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum VaultCommand {
Initialize {
#[arg(long)]
network: Pubkey,
#[arg(long)]
mint: Pubkey,
},
Deposit {
#[arg(long)]
network: Pubkey,
#[arg(long)]
mint: Pubkey,
#[arg(long)]
amount: u64,
},
Withdraw {
#[arg(long)]
network: Pubkey,
#[arg(long)]
mint: Pubkey,
#[arg(long)]
amount: u64,
#[arg(long)]
destination: Option<Pubkey>,
},
}
#[once]
fn solana_config() -> SolanaConfig {
let config_path = std::env::var("HOME").unwrap() + "/.config/solana/cli/config.yml";
let config = std::fs::read_to_string(config_path).unwrap();
let config: SolanaConfig = serde_yaml::from_str(&config).unwrap();
config
}
#[once]
fn solana_config_keypair() -> String {
let config = solana_config();
let keypair_file = std::fs::read(config.keypair_path).unwrap();
let keypair_bytes: Vec<u8> = serde_json::from_slice(&keypair_file).unwrap();
bs58::encode(keypair_bytes).into_string()
}
impl Config {
pub fn rpc_client(&self) -> RpcClient {
let config = solana_config();
let commitment = CommitmentConfig {
commitment: self.commitment.unwrap_or(config.commitment),
};
if let Some(rpc_url) = &self.rpc_url {
RpcClient::new_with_commitment(rpc_url.clone(), commitment)
} else {
RpcClient::new_with_commitment(config.json_rpc_url.clone(), commitment)
}
}
#[allow(dead_code)]
pub fn keypair(&self) -> Option<Keypair> {
if let Some(keypair) = &self.signer {
let bytes = read(keypair)
.ok()
.and_then(|x| serde_json::from_slice::<Vec<u8>>(&x).ok())
.map(|x| bs58::encode(x).into_string())
.unwrap_or(keypair.clone());
if bytes.len() > 44 {
Some(Keypair::from_base58_string(&bytes))
} else {
None
}
} else {
let bytes = solana_config_keypair();
Some(Keypair::from_base58_string(&bytes))
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct SolanaConfig {
json_rpc_url: String,
keypair_path: String,
commitment: CommitmentLevel,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111";
#[test]
fn parses_version() {
let config = Config::try_parse_from(["equa", "program", "version"]).unwrap();
assert!(matches!(
config.command,
Command::Program {
command: ProgramCommand::Version,
}
));
}
#[test]
fn parses_program_initialize() {
let config = Config::try_parse_from(["equa", "program", "initialize"]).unwrap();
assert!(matches!(
config.command,
Command::Program {
command: ProgramCommand::Initialize,
}
));
}
#[test]
fn parses_show() {
let config = Config::try_parse_from(["equa", "show"]).unwrap();
assert!(matches!(config.command, Command::Show));
}
#[test]
fn parses_network_initialize() {
let config =
Config::try_parse_from(["equa", "network", "initialize", "--network-id", "7"]).unwrap();
match config.command {
Command::Network {
command: NetworkCommand::Initialize { network_id },
} => assert_eq!(network_id, "7"),
other => panic!("expected Network::Initialize, got {other:?}"),
}
}
#[test]
fn parses_network_swap() {
let config = Config::try_parse_from([
"equa",
"network",
"swap",
"--network",
SYSTEM_PROGRAM,
"--input-mint",
SYSTEM_PROGRAM,
"--output-mint",
SYSTEM_PROGRAM,
"--amount",
"100",
])
.unwrap();
match config.command {
Command::Network {
command:
NetworkCommand::Swap {
amount,
minimum_output_amount,
allow_partial_fill,
..
},
} => {
assert_eq!(amount, 100);
assert_eq!(minimum_output_amount, 0);
assert!(!allow_partial_fill);
}
other => panic!("expected Network::Swap, got {other:?}"),
}
}
#[test]
fn parses_vault_initialize() {
let config = Config::try_parse_from([
"equa",
"vault",
"initialize",
"--network",
SYSTEM_PROGRAM,
"--mint",
SYSTEM_PROGRAM,
])
.unwrap();
match config.command {
Command::Vault {
command: VaultCommand::Initialize { network, mint },
} => {
assert_eq!(network.to_string(), SYSTEM_PROGRAM);
assert_eq!(mint.to_string(), SYSTEM_PROGRAM);
}
other => panic!("expected Vault::Initialize, got {other:?}"),
}
}
#[test]
fn parses_vault_deposit() {
let config = Config::try_parse_from([
"equa",
"vault",
"deposit",
"--network",
SYSTEM_PROGRAM,
"--mint",
SYSTEM_PROGRAM,
"--amount",
"42",
])
.unwrap();
match config.command {
Command::Vault {
command: VaultCommand::Deposit { amount, .. },
} => assert_eq!(amount, 42),
other => panic!("expected Vault::Deposit, got {other:?}"),
}
}
#[test]
fn parses_global_flags() {
let config = Config::try_parse_from([
"equa",
"--rpc",
"http://127.0.0.1:8899",
"--debug",
"--yes",
"--skip-simulation",
"show",
])
.unwrap();
assert_eq!(config.rpc_url.as_deref(), Some("http://127.0.0.1:8899"));
assert!(config.debug);
assert!(config.yes);
assert!(config.skip_simulation);
assert!(matches!(config.command, Command::Show));
}
#[test]
fn rejects_unknown_subcommand() {
let err = Config::try_parse_from(["equa", "not-a-command"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidSubcommand);
}
}