From 2316846c53c7146ecb58e0b0af7997882247e68a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 06:21:40 +0000 Subject: [PATCH] solana-tokens: Add capability to perform the same transfer to a batch of recipients (bp #12259) (#12266) * solana-tokens: Add capability to perform the same transfer to a batch of recipients (#12259) * Add transfer-amount argument, use simplified input-csv * Add transfer-amount to readme (cherry picked from commit a48cc073cfc6cc9a7b18cc52ac8a842d3874e8e2) # Conflicts: # tokens/src/commands.rs # tokens/tests/commands.rs * Fix build Co-authored-by: Tyera Eulberg Co-authored-by: Tyera Eulberg --- tokens/README.md | 26 +++++++++++ tokens/src/arg_parser.rs | 13 +++++- tokens/src/args.rs | 1 + tokens/src/commands.rs | 93 ++++++++++++++++++++++++++++++++++++---- tokens/tests/commands.rs | 2 +- 5 files changed, 125 insertions(+), 10 deletions(-) diff --git a/tokens/README.md b/tokens/README.md index 47c2222178..bb4853f1ed 100644 --- a/tokens/README.md +++ b/tokens/README.md @@ -82,6 +82,32 @@ Recipient Expected Balance (◎) 7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 42 ``` +## Distribute tokens: transfer-amount + +This tool also makes it straightforward to transfer the same amount of tokens to a simple list of recipients. Just add the `--transfer-amount` arg to specify the amount: + +Example recipients.csv: + +```text +recipient +6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv +7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr +CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT +``` + +```bash +solana-tokens distribute-tokens --transfer-amount 10 --from --input-csv --fee-payer +``` + +Example output: + +```text +Recipient Expected Balance (◎) +6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 10 +7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 10 +CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 10 +``` + ## Distribute stake accounts Distributing tokens via stake accounts works similarly to how tokens are distributed. The diff --git a/tokens/src/arg_parser.rs b/tokens/src/arg_parser.rs index 907e6c94d1..cbf9922346 100644 --- a/tokens/src/arg_parser.rs +++ b/tokens/src/arg_parser.rs @@ -3,7 +3,8 @@ use crate::args::{ }; use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{ - input_validators::{is_valid_pubkey, is_valid_signer}, + input_parsers::value_of, + input_validators::{is_amount, is_valid_pubkey, is_valid_signer}, keypair::{pubkey_from_path, signer_from_path}, }; use solana_cli_config::CONFIG_FILE; @@ -60,6 +61,14 @@ where .value_name("FILE") .help("Input CSV file"), ) + .arg( + Arg::with_name("transfer_amount") + .long("transfer-amount") + .takes_value(true) + .value_name("AMOUNT") + .validator(is_amount) + .help("The amount to send to each recipient, in SOL"), + ) .arg( Arg::with_name("dry_run") .long("dry-run") @@ -255,6 +264,7 @@ fn parse_distribute_tokens_args( sender_keypair, fee_payer, stake_args: None, + transfer_amount: value_of(matches, "transfer_amount"), }) } @@ -330,6 +340,7 @@ fn parse_distribute_stake_args( sender_keypair, fee_payer, stake_args: Some(stake_args), + transfer_amount: None, }) } diff --git a/tokens/src/args.rs b/tokens/src/args.rs index b40d9de79c..8c28f99044 100644 --- a/tokens/src/args.rs +++ b/tokens/src/args.rs @@ -8,6 +8,7 @@ pub struct DistributeTokensArgs { pub sender_keypair: Box, pub fee_payer: Box, pub stake_args: Option, + pub transfer_amount: Option, } pub struct StakeArgs { diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index 98567aaab9..95e32c94d7 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -246,9 +246,25 @@ fn distribute_allocations( Ok(()) } -fn read_allocations(input_csv: &str) -> io::Result> { +fn read_allocations(input_csv: &str, transfer_amount: Option) -> io::Result> { let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?; - Ok(rdr.deserialize().map(|entry| entry.unwrap()).collect()) + let allocations = if let Some(amount) = transfer_amount { + let recipients: Vec = rdr + .deserialize() + .map(|recipient| recipient.unwrap()) + .collect(); + recipients + .into_iter() + .map(|recipient| Allocation { + recipient, + amount, + lockup_date: "".to_string(), + }) + .collect() + } else { + rdr.deserialize().map(|entry| entry.unwrap()).collect() + }; + Ok(allocations) } fn new_spinner_progress_bar() -> ProgressBar { @@ -263,7 +279,7 @@ pub fn process_allocations( client: &ThinClient, args: &DistributeTokensArgs, ) -> Result, Error> { - let mut allocations: Vec = read_allocations(&args.input_csv)?; + let mut allocations: Vec = read_allocations(&args.input_csv, args.transfer_amount)?; let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum(); println!( @@ -433,7 +449,7 @@ fn check_payer_balances( } pub fn process_balances(client: &ThinClient, args: &BalancesArgs) -> Result<(), csv::Error> { - let allocations: Vec = read_allocations(&args.input_csv)?; + let allocations: Vec = read_allocations(&args.input_csv, None)?; let allocations = merge_allocations(&allocations); println!( @@ -470,7 +486,11 @@ pub fn process_transaction_log(args: &TransactionLogArgs) -> Result<(), Error> { use crate::db::check_output_file; use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use tempfile::{tempdir, NamedTempFile}; -pub fn test_process_distribute_tokens_with_client(client: C, sender_keypair: Keypair) { +pub fn test_process_distribute_tokens_with_client( + client: C, + sender_keypair: Keypair, + transfer_amount: Option, +) { let thin_client = ThinClient::new(client, false); let fee_payer = Keypair::new(); let (transaction, _last_valid_slot) = thin_client @@ -483,7 +503,11 @@ pub fn test_process_distribute_tokens_with_client(client: C, sender_k let alice_pubkey = Pubkey::new_rand(); let allocation = Allocation { recipient: alice_pubkey.to_string(), - amount: 1000.0, + amount: if let Some(amount) = transfer_amount { + amount + } else { + 1000.0 + }, lockup_date: "".to_string(), }; let allocations_file = NamedTempFile::new().unwrap(); @@ -511,6 +535,7 @@ pub fn test_process_distribute_tokens_with_client(client: C, sender_k transaction_db: transaction_db.clone(), output_path: Some(output_path.clone()), stake_args: None, + transfer_amount, }; let confirmations = process_allocations(&thin_client, &args).unwrap(); assert_eq!(confirmations, None); @@ -623,6 +648,7 @@ pub fn test_process_distribute_stake_with_client(client: C, sender_ke output_path: Some(output_path.clone()), stake_args: Some(stake_args), sender_keypair: Box::new(sender_keypair), + transfer_amount: None, }; let confirmations = process_allocations(&thin_client, &args).unwrap(); assert_eq!(confirmations, None); @@ -685,7 +711,15 @@ mod tests { let (genesis_config, sender_keypair) = create_genesis_config(sol_to_lamports(9_000_000.0)); let bank = Bank::new(&genesis_config); let bank_client = BankClient::new(bank); - test_process_distribute_tokens_with_client(bank_client, sender_keypair); + test_process_distribute_tokens_with_client(bank_client, sender_keypair, None); + } + + #[test] + fn test_process_transfer_amount_allocations() { + let (genesis_config, sender_keypair) = create_genesis_config(sol_to_lamports(9_000_000.0)); + let bank = Bank::new(&genesis_config); + let bank_client = BankClient::new(bank); + test_process_distribute_tokens_with_client(bank_client, sender_keypair, Some(1.5)); } #[test] @@ -710,7 +744,49 @@ mod tests { wtr.serialize(&allocation).unwrap(); wtr.flush().unwrap(); - assert_eq!(read_allocations(&input_csv).unwrap(), vec![allocation]); + assert_eq!( + read_allocations(&input_csv, None).unwrap(), + vec![allocation] + ); + } + + #[test] + fn test_read_allocations_transfer_amount() { + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let pubkey2 = Pubkey::new_rand(); + let file = NamedTempFile::new().unwrap(); + let input_csv = file.path().to_str().unwrap().to_string(); + let mut wtr = csv::WriterBuilder::new().from_writer(file); + wtr.serialize("recipient".to_string()).unwrap(); + wtr.serialize(&pubkey0.to_string()).unwrap(); + wtr.serialize(&pubkey1.to_string()).unwrap(); + wtr.serialize(&pubkey2.to_string()).unwrap(); + wtr.flush().unwrap(); + + let amount = 1.5; + + let expected_allocations = vec![ + Allocation { + recipient: pubkey0.to_string(), + amount, + lockup_date: "".to_string(), + }, + Allocation { + recipient: pubkey1.to_string(), + amount, + lockup_date: "".to_string(), + }, + Allocation { + recipient: pubkey2.to_string(), + amount, + lockup_date: "".to_string(), + }, + ]; + assert_eq!( + read_allocations(&input_csv, Some(amount)).unwrap(), + expected_allocations + ); } #[test] @@ -819,6 +895,7 @@ mod tests { output_path: None, stake_args: Some(stake_args), sender_keypair: Box::new(Keypair::new()), + transfer_amount: None, }; let lockup_date = lockup_date_str.parse().unwrap(); let instructions = distribution_instructions( diff --git a/tokens/tests/commands.rs b/tokens/tests/commands.rs index 3bdab39c44..c58302f0a7 100644 --- a/tokens/tests/commands.rs +++ b/tokens/tests/commands.rs @@ -11,7 +11,7 @@ fn test_process_distribute_with_rpc_client() { ..TestValidatorOptions::default() }); let rpc_client = RpcClient::new_socket(validator.leader_data.rpc); - test_process_distribute_tokens_with_client(rpc_client, validator.alice); + test_process_distribute_tokens_with_client(rpc_client, validator.alice, None); validator.server.close().unwrap(); remove_dir_all(validator.ledger_path).unwrap();