use crate::args::{ Args, BalancesArgs, Command, DistributeTokensArgs, StakeArgs, TransactionLogArgs, }; use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{ input_validators::{is_valid_pubkey, is_valid_signer}, keypair::{pubkey_from_path, signer_from_path}, }; use solana_cli_config::CONFIG_FILE; use solana_remote_wallet::remote_wallet::maybe_wallet_manager; use std::error::Error; use std::ffi::OsString; use std::process::exit; fn get_matches<'a, I, T>(args: I) -> ArgMatches<'a> where I: IntoIterator, T: Into + Clone, { let default_config_file = CONFIG_FILE.as_ref().unwrap(); App::new("solana-tokens") .about("about") .version("version") .arg( Arg::with_name("config_file") .long("config") .takes_value(true) .value_name("FILEPATH") .default_value(default_config_file) .help("Config file"), ) .arg( Arg::with_name("url") .long("url") .global(true) .takes_value(true) .value_name("URL") .help("RPC entrypoint address. i.e. http://devnet.solana.com"), ) .subcommand( SubCommand::with_name("distribute-tokens") .about("Distribute tokens") .arg( Arg::with_name("campaign_name") .long("campaign-name") .takes_value(true) .value_name("NAME") .help("Campaign name for storing transaction data"), ) .arg( Arg::with_name("from_bids") .long("from-bids") .help("Input CSV contains bids in dollars, not allocations in SOL"), ) .arg( Arg::with_name("input_csv") .long("input-csv") .required(true) .takes_value(true) .value_name("FILE") .help("Input CSV file"), ) .arg( Arg::with_name("dollars_per_sol") .long("dollars-per-sol") .takes_value(true) .value_name("NUMBER") .help("Dollars per SOL, if input CSV contains bids"), ) .arg( Arg::with_name("dry_run") .long("dry-run") .help("Do not execute any transfers"), ) .arg( Arg::with_name("sender_keypair") .long("from") .required(true) .takes_value(true) .value_name("SENDING_KEYPAIR") .validator(is_valid_signer) .help("Keypair to fund accounts"), ) .arg( Arg::with_name("fee_payer") .long("fee-payer") .required(true) .takes_value(true) .value_name("KEYPAIR") .validator(is_valid_signer) .help("Fee payer"), ), ) .subcommand( SubCommand::with_name("distribute-stake") .about("Distribute stake accounts") .arg( Arg::with_name("campaign_name") .long("campaign-name") .takes_value(true) .value_name("NAME") .help("Campaign name for storing transaction data"), ) .arg( Arg::with_name("input_csv") .long("input-csv") .required(true) .takes_value(true) .value_name("FILE") .help("Allocations CSV file"), ) .arg( Arg::with_name("dry_run") .long("dry-run") .help("Do not execute any transfers"), ) .arg( Arg::with_name("sender_keypair") .long("from") .required(true) .takes_value(true) .value_name("SENDING_KEYPAIR") .validator(is_valid_signer) .help("Keypair to fund accounts"), ) .arg( Arg::with_name("stake_account_address") .required(true) .long("stake-account-address") .takes_value(true) .value_name("ACCOUNT_ADDRESS") .validator(is_valid_pubkey) .help("Stake Account Address"), ) .arg( Arg::with_name("sol_for_fees") .default_value("1.0") .long("sol-for-fees") .takes_value(true) .value_name("SOL_AMOUNT") .help("Amount of SOL to put in system account to pay for fees"), ) .arg( Arg::with_name("stake_authority") .long("stake-authority") .required(true) .takes_value(true) .value_name("KEYPAIR") .validator(is_valid_signer) .help("Stake Authority Keypair"), ) .arg( Arg::with_name("withdraw_authority") .long("withdraw-authority") .required(true) .takes_value(true) .value_name("KEYPAIR") .validator(is_valid_signer) .help("Withdraw Authority Keypair"), ) .arg( Arg::with_name("fee_payer") .long("fee-payer") .required(true) .takes_value(true) .value_name("KEYPAIR") .validator(is_valid_signer) .help("Fee payer"), ), ) .subcommand( SubCommand::with_name("balances") .about("Balance of each account") .arg( Arg::with_name("input_csv") .long("input-csv") .required(true) .takes_value(true) .value_name("FILE") .help("Bids CSV file"), ) .arg( Arg::with_name("from_bids") .long("from-bids") .help("Input CSV contains bids in dollars, not allocations in SOL"), ) .arg( Arg::with_name("dollars_per_sol") .long("dollars-per-sol") .takes_value(true) .value_name("NUMBER") .help("Dollars per SOL"), ), ) .subcommand( SubCommand::with_name("transaction-log") .about("Print the database to a CSV file") .arg( Arg::with_name("campaign_name") .long("campaign-name") .takes_value(true) .value_name("NAME") .help("Campaign name for storing transaction data"), ) .arg( Arg::with_name("output_path") .long("output-path") .required(true) .takes_value(true) .value_name("FILE") .help("Output file"), ), ) .get_matches_from(args) } fn create_db_path(campaign_name: Option) -> String { let (prefix, hyphen) = if let Some(name) = campaign_name { (name, "-") } else { ("".to_string(), "") }; let path = dirs::home_dir().unwrap(); let filename = format!("{}{}transactions.db", prefix, hyphen); path.join(".config") .join("solana-tokens") .join(filename) .to_str() .unwrap() .to_string() } fn parse_distribute_tokens_args( matches: &ArgMatches<'_>, ) -> Result> { let mut wallet_manager = maybe_wallet_manager()?; let signer_matches = ArgMatches::default(); // No default signer let sender_keypair_str = value_t_or_exit!(matches, "sender_keypair", String); let sender_keypair = signer_from_path( &signer_matches, &sender_keypair_str, "sender", &mut wallet_manager, )?; let fee_payer_str = value_t_or_exit!(matches, "fee_payer", String); let fee_payer = signer_from_path( &signer_matches, &fee_payer_str, "fee-payer", &mut wallet_manager, )?; Ok(DistributeTokensArgs { input_csv: value_t_or_exit!(matches, "input_csv", String), from_bids: matches.is_present("from_bids"), transaction_db: create_db_path(value_t!(matches, "campaign_name", String).ok()), dollars_per_sol: value_t!(matches, "dollars_per_sol", f64).ok(), dry_run: matches.is_present("dry_run"), sender_keypair, fee_payer, stake_args: None, }) } fn parse_distribute_stake_args( matches: &ArgMatches<'_>, ) -> Result> { let mut wallet_manager = maybe_wallet_manager()?; let signer_matches = ArgMatches::default(); // No default signer let sender_keypair_str = value_t_or_exit!(matches, "sender_keypair", String); let sender_keypair = signer_from_path( &signer_matches, &sender_keypair_str, "sender", &mut wallet_manager, )?; let fee_payer_str = value_t_or_exit!(matches, "fee_payer", String); let fee_payer = signer_from_path( &signer_matches, &fee_payer_str, "fee-payer", &mut wallet_manager, )?; let stake_account_address_str = value_t_or_exit!(matches, "stake_account_address", String); let stake_account_address = pubkey_from_path( &signer_matches, &stake_account_address_str, "stake account address", &mut wallet_manager, )?; let stake_authority_str = value_t_or_exit!(matches, "stake_authority", String); let stake_authority = signer_from_path( &signer_matches, &stake_authority_str, "stake authority", &mut wallet_manager, )?; let withdraw_authority_str = value_t_or_exit!(matches, "withdraw_authority", String); let withdraw_authority = signer_from_path( &signer_matches, &withdraw_authority_str, "withdraw authority", &mut wallet_manager, )?; let stake_args = StakeArgs { stake_account_address, sol_for_fees: value_t_or_exit!(matches, "sol_for_fees", f64), stake_authority, withdraw_authority, }; Ok(DistributeTokensArgs { input_csv: value_t_or_exit!(matches, "input_csv", String), from_bids: false, transaction_db: create_db_path(value_t!(matches, "campaign_name", String).ok()), dollars_per_sol: None, dry_run: matches.is_present("dry_run"), sender_keypair, fee_payer, stake_args: Some(stake_args), }) } fn parse_balances_args(matches: &ArgMatches<'_>) -> BalancesArgs { BalancesArgs { input_csv: value_t_or_exit!(matches, "input_csv", String), from_bids: matches.is_present("from_bids"), dollars_per_sol: value_t!(matches, "dollars_per_sol", f64).ok(), } } fn parse_transaction_log_args(matches: &ArgMatches<'_>) -> TransactionLogArgs { TransactionLogArgs { transaction_db: create_db_path(value_t!(matches, "campaign_name", String).ok()), output_path: value_t_or_exit!(matches, "output_path", String), } } pub fn parse_args(args: I) -> Result> where I: IntoIterator, T: Into + Clone, { let matches = get_matches(args); let config_file = matches.value_of("config_file").unwrap().to_string(); let url = matches.value_of("url").map(|x| x.to_string()); let command = match matches.subcommand() { ("distribute-tokens", Some(matches)) => { Command::DistributeTokens(parse_distribute_tokens_args(matches)?) } ("distribute-stake", Some(matches)) => { Command::DistributeTokens(parse_distribute_stake_args(matches)?) } ("balances", Some(matches)) => Command::Balances(parse_balances_args(matches)), ("transaction-log", Some(matches)) => { Command::TransactionLog(parse_transaction_log_args(matches)) } _ => { eprintln!("{}", matches.usage()); exit(1); } }; let args = Args { config_file, url, command, }; Ok(args) }