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 a48cc073cf)

# Conflicts:
#	tokens/src/commands.rs
#	tokens/tests/commands.rs

* Fix build

Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
Co-authored-by: Tyera Eulberg <tyera@solana.com>
This commit is contained in:
mergify[bot]
2020-09-16 06:21:40 +00:00
committed by GitHub
parent c77fe54629
commit 2316846c53
5 changed files with 125 additions and 10 deletions

View File

@ -82,6 +82,32 @@ Recipient Expected Balance (◎)
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 42 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 <KEYPAIR> --input-csv <RECIPIENTS_CSV> --fee-payer <KEYPAIR>
```
Example output:
```text
Recipient Expected Balance (◎)
6Vo87BaDhp4v4GHwVDhw5huhxVF8CyxSXYtkUwVHbbPv 10
7aHDubg5FBYj1SgmyBgU3ZJdtfuqYCQsJQK2pTR5JUqr 10
CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT 10
```
## Distribute stake accounts ## Distribute stake accounts
Distributing tokens via stake accounts works similarly to how tokens are distributed. The Distributing tokens via stake accounts works similarly to how tokens are distributed. The

View File

@ -3,7 +3,8 @@ use crate::args::{
}; };
use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand};
use solana_clap_utils::{ 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}, keypair::{pubkey_from_path, signer_from_path},
}; };
use solana_cli_config::CONFIG_FILE; use solana_cli_config::CONFIG_FILE;
@ -60,6 +61,14 @@ where
.value_name("FILE") .value_name("FILE")
.help("Input CSV 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(
Arg::with_name("dry_run") Arg::with_name("dry_run")
.long("dry-run") .long("dry-run")
@ -255,6 +264,7 @@ fn parse_distribute_tokens_args(
sender_keypair, sender_keypair,
fee_payer, fee_payer,
stake_args: None, stake_args: None,
transfer_amount: value_of(matches, "transfer_amount"),
}) })
} }
@ -330,6 +340,7 @@ fn parse_distribute_stake_args(
sender_keypair, sender_keypair,
fee_payer, fee_payer,
stake_args: Some(stake_args), stake_args: Some(stake_args),
transfer_amount: None,
}) })
} }

View File

@ -8,6 +8,7 @@ pub struct DistributeTokensArgs {
pub sender_keypair: Box<dyn Signer>, pub sender_keypair: Box<dyn Signer>,
pub fee_payer: Box<dyn Signer>, pub fee_payer: Box<dyn Signer>,
pub stake_args: Option<StakeArgs>, pub stake_args: Option<StakeArgs>,
pub transfer_amount: Option<f64>,
} }
pub struct StakeArgs { pub struct StakeArgs {

View File

@ -246,9 +246,25 @@ fn distribute_allocations(
Ok(()) Ok(())
} }
fn read_allocations(input_csv: &str) -> io::Result<Vec<Allocation>> { fn read_allocations(input_csv: &str, transfer_amount: Option<f64>) -> io::Result<Vec<Allocation>> {
let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?; 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<String> = 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 { fn new_spinner_progress_bar() -> ProgressBar {
@ -263,7 +279,7 @@ pub fn process_allocations(
client: &ThinClient, client: &ThinClient,
args: &DistributeTokensArgs, args: &DistributeTokensArgs,
) -> Result<Option<usize>, Error> { ) -> Result<Option<usize>, Error> {
let mut allocations: Vec<Allocation> = read_allocations(&args.input_csv)?; let mut allocations: Vec<Allocation> = read_allocations(&args.input_csv, args.transfer_amount)?;
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum(); let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
println!( println!(
@ -433,7 +449,7 @@ fn check_payer_balances(
} }
pub fn process_balances(client: &ThinClient, args: &BalancesArgs) -> Result<(), csv::Error> { pub fn process_balances(client: &ThinClient, args: &BalancesArgs) -> Result<(), csv::Error> {
let allocations: Vec<Allocation> = read_allocations(&args.input_csv)?; let allocations: Vec<Allocation> = read_allocations(&args.input_csv, None)?;
let allocations = merge_allocations(&allocations); let allocations = merge_allocations(&allocations);
println!( println!(
@ -470,7 +486,11 @@ pub fn process_transaction_log(args: &TransactionLogArgs) -> Result<(), Error> {
use crate::db::check_output_file; use crate::db::check_output_file;
use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use tempfile::{tempdir, NamedTempFile}; use tempfile::{tempdir, NamedTempFile};
pub fn test_process_distribute_tokens_with_client<C: Client>(client: C, sender_keypair: Keypair) { pub fn test_process_distribute_tokens_with_client<C: Client>(
client: C,
sender_keypair: Keypair,
transfer_amount: Option<f64>,
) {
let thin_client = ThinClient::new(client, false); let thin_client = ThinClient::new(client, false);
let fee_payer = Keypair::new(); let fee_payer = Keypair::new();
let (transaction, _last_valid_slot) = thin_client let (transaction, _last_valid_slot) = thin_client
@ -483,7 +503,11 @@ pub fn test_process_distribute_tokens_with_client<C: Client>(client: C, sender_k
let alice_pubkey = Pubkey::new_rand(); let alice_pubkey = Pubkey::new_rand();
let allocation = Allocation { let allocation = Allocation {
recipient: alice_pubkey.to_string(), recipient: alice_pubkey.to_string(),
amount: 1000.0, amount: if let Some(amount) = transfer_amount {
amount
} else {
1000.0
},
lockup_date: "".to_string(), lockup_date: "".to_string(),
}; };
let allocations_file = NamedTempFile::new().unwrap(); let allocations_file = NamedTempFile::new().unwrap();
@ -511,6 +535,7 @@ pub fn test_process_distribute_tokens_with_client<C: Client>(client: C, sender_k
transaction_db: transaction_db.clone(), transaction_db: transaction_db.clone(),
output_path: Some(output_path.clone()), output_path: Some(output_path.clone()),
stake_args: None, stake_args: None,
transfer_amount,
}; };
let confirmations = process_allocations(&thin_client, &args).unwrap(); let confirmations = process_allocations(&thin_client, &args).unwrap();
assert_eq!(confirmations, None); assert_eq!(confirmations, None);
@ -623,6 +648,7 @@ pub fn test_process_distribute_stake_with_client<C: Client>(client: C, sender_ke
output_path: Some(output_path.clone()), output_path: Some(output_path.clone()),
stake_args: Some(stake_args), stake_args: Some(stake_args),
sender_keypair: Box::new(sender_keypair), sender_keypair: Box::new(sender_keypair),
transfer_amount: None,
}; };
let confirmations = process_allocations(&thin_client, &args).unwrap(); let confirmations = process_allocations(&thin_client, &args).unwrap();
assert_eq!(confirmations, None); 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 (genesis_config, sender_keypair) = create_genesis_config(sol_to_lamports(9_000_000.0));
let bank = Bank::new(&genesis_config); let bank = Bank::new(&genesis_config);
let bank_client = BankClient::new(bank); 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] #[test]
@ -710,7 +744,49 @@ mod tests {
wtr.serialize(&allocation).unwrap(); wtr.serialize(&allocation).unwrap();
wtr.flush().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] #[test]
@ -819,6 +895,7 @@ mod tests {
output_path: None, output_path: None,
stake_args: Some(stake_args), stake_args: Some(stake_args),
sender_keypair: Box::new(Keypair::new()), sender_keypair: Box::new(Keypair::new()),
transfer_amount: None,
}; };
let lockup_date = lockup_date_str.parse().unwrap(); let lockup_date = lockup_date_str.parse().unwrap();
let instructions = distribution_instructions( let instructions = distribution_instructions(

View File

@ -11,7 +11,7 @@ fn test_process_distribute_with_rpc_client() {
..TestValidatorOptions::default() ..TestValidatorOptions::default()
}); });
let rpc_client = RpcClient::new_socket(validator.leader_data.rpc); 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(); validator.server.close().unwrap();
remove_dir_all(validator.ledger_path).unwrap(); remove_dir_all(validator.ledger_path).unwrap();