Add solana-tokens (#10011)
* Initial commit * Execute transfers * Refactor for testing * Cleanup readme * Rewrite * Cleanup * Cleanup * Cleanup client * Use a Null Client to move prints closer to where messages are sent * Upgrade Solana * Move core functionality into its own module * Handle transaction errors * Merge allocations * Fixes * Cleanup readme * Fix markdown * Add example input * Add integration test - currently fails * Add integration test * Add metrics * Use RpcClient in dry-run, just don't send messages * More metrics * Fix dry run with no keys * Only require one approval if fee-payer is the sender keypair * Fix bugs * Don't create the transaction log if nothing to put into it; otherwise the next innvocation won't add the header * Apply previous transactions to allocations with matching recipients * Bail out of any account already has a balance * Polish * Add new 'balances' command * 9 decimal places * Add missing file * Better dry-run; keypair options now optional * Change field name from 'bid' to 'accepted' Also, tolerate precision change from 2 decimal places to 4 * Write to transaction log immediately * Rename allocations_csv to bids_csv So that we can bypass bids_csv with an allocations CSV file * Upgrade Solana * Remove faucet from integration test * Cleaner integration test Won't work until this lands and is released: https://github.com/solana-labs/solana/pull/9717 * Update README * Add TravicCI script to build and test (#1) * Add distribute-stake command (#2) * Distribute -> DistributeTokens (#3) * Cache cargo deps (#4) * Add docs (#5) * Switch to latest Solana 1.1 release (#7) * distribute -> distribute-tokens (#9) * Switch from CSV to a pickledb database (#8) * Switch from CSV to a pickledb database * Allow PickleDb errors to bubble up * Dedup * Hoist db * Add finalized field to TransactionInfo * Don't allow RPC client to resign transactions * Remove dead code * Use transport::Result * Record unconfirmed transaction * Fix: separate stake account per allocation * Catch transport errors * Panic if we attempt to replay a transaction that hasn't been finalized * Attempt to fix CI PickleDb isn't calling flush() or close() after writing to files. No issue on MacOS, but looks racy in CI. * Revert "Attempt to fix CI" This reverts commit 1632394f636c54402b3578120e8817dd1660e19b. * Poll for signature before returning * Add --sol-for-fees option for stake distributions * Add --allocations-csv option (#14) * Add allocations-csv option * Add tests or GTFO * Apply review feedback * apply feedback * Add read_allocations function * Update arg_parser.rs * Fix balances command (#17) * Fix balances command * Fix readme * Add --force to transfer to non-empty accounts (#18) * Add --no-wait (#16) * Add ThinClient methods to implement --no-wait * Plumb --no-wait through No tests yet * Check transaction status on startup * Easier to test * Wait until transaction is finalized before checking if it failed with an error It's possible that a minority fork thinks it failed. * Add unit tests * Remove dead code and rustfmt * Don't flush database to file if doing a dry-run * Continue when transactions not yet finalized (#20) If those transactions are dropped, the next run will execute them. * Return the number of confirmations (#21) * Add read_allocations() unit-test (#22) Delete the copy-pasted top-level test. Fixes #19 * Add a CSV printer (#23) * Remove all the copypasta (#24) * Move resolve_distribute_stake_args into its own function * Add stake args to token args * Unify option names * Move Command::DistributeStake into DistributeTokens * Remove process_distribute_stake * Only unique signers * Use sender keypair to fund new fee-payer accounts * Unify distribute_tokens and distribute_stake * Rename print-database command to transaction-log (#25) * Send all transactions as quickly as possible, then wait (#26) * Send all transactions as quickly as possible, then wait * Exit when finalized or blockhashes have expired * Don't need blockhash in the CSV output * Better types CSV library was choking on Pubkey as a type. PickleDb doesn't have that problem. * Resend if blockhash has not expired * Attempt to fix CI * Move log to stderr * Add constructor, tuck away client (#30) * Add constructor, tuck away client * Fix unwrap() caught by CI * Fix optional option flagged as required * Bunch of cleanup (#31) * Remove untested --no-wait feature * Make --transactions-db an option, not an arg So that in the future, we can make it optional * Remove more untested features Too many false positives in that santity check. Use --dry-run instead. * Add dry-run mode to ThinClient * Cleaner dry-run * Make key parameters required Just don't use them in --dry-run * Add option to write the transaction log --dry-run doesn't write to the database. Use this option if you want a copy of the transaction log before the final run. * Revert --transaction-log addition Implement #27 first * Fix CI * Update readme * Fix CI in copypasta * Sort transaction log by finalized date (#33) * Make --transaction-db option implicit (#34) * Move db functionality into its own module (#35) * Move db functionality into its own module * Rename tokens module to commands * Version bump * Upgrade Solana * Add solana-tokens to build * Remove Cargo.lock * Remove vscode file * Remove TravisCI build script * Install solana-tokens Co-authored-by: Dan Albert <dan@solana.com>
This commit is contained in:
305
tokens/src/arg_parser.rs
Normal file
305
tokens/src/arg_parser.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
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};
|
||||
use solana_cli_config::CONFIG_FILE;
|
||||
use std::ffi::OsString;
|
||||
use std::process::exit;
|
||||
|
||||
fn get_matches<'a, I, T>(args: I) -> ArgMatches<'a>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + 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>) -> 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<'_>) -> DistributeTokensArgs<String, String> {
|
||||
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: value_t_or_exit!(matches, "sender_keypair", String),
|
||||
fee_payer: value_t_or_exit!(matches, "fee_payer", String),
|
||||
stake_args: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_distribute_stake_args(matches: &ArgMatches<'_>) -> DistributeTokensArgs<String, String> {
|
||||
let stake_args = StakeArgs {
|
||||
stake_account_address: value_t_or_exit!(matches, "stake_account_address", String),
|
||||
sol_for_fees: value_t_or_exit!(matches, "sol_for_fees", f64),
|
||||
stake_authority: value_t_or_exit!(matches, "stake_authority", String),
|
||||
withdraw_authority: value_t_or_exit!(matches, "withdraw_authority", String),
|
||||
};
|
||||
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: value_t_or_exit!(matches, "sender_keypair", String),
|
||||
fee_payer: value_t_or_exit!(matches, "fee_payer", String),
|
||||
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: value_t_or_exit!(matches, "transaction_db", String),
|
||||
output_path: value_t_or_exit!(matches, "output_path", String),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_args<I, T>(args: I) -> Args<String, String>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + 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);
|
||||
}
|
||||
};
|
||||
Args {
|
||||
config_file,
|
||||
url,
|
||||
command,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user