Distribute spl tokens (#13559)
* Add helpers to covert between sdk types * Add distribute-spl-tokens to args and arg-parsing * Build spl-token transfer-checked instructions * Check spl-token balances properly * Add display handling to support spl-token * Small refactor to allow failures in allocation iter * Use Associated Token Account for spl-token distributions * Add spl token support to balances command * Update readme * Add spl-token tests * Rename spl-tokens file * Move a couple more things out of commands * Stop requiring lockup_date heading for non-stake distributions * Use epsilon for allocation retention
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
use crate::args::{BalancesArgs, DistributeTokensArgs, StakeArgs, TransactionLogArgs};
|
||||
use crate::db::{self, TransactionInfo};
|
||||
use crate::{
|
||||
args::{BalancesArgs, DistributeTokensArgs, StakeArgs, TransactionLogArgs},
|
||||
db::{self, TransactionInfo},
|
||||
spl_token::*,
|
||||
token_display::Token,
|
||||
};
|
||||
use chrono::prelude::*;
|
||||
use console::style;
|
||||
use csv::{ReaderBuilder, Trim};
|
||||
@@ -7,6 +11,7 @@ use indexmap::IndexMap;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use pickledb::PickleDb;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_account_decoder::parse_token::{pubkey_from_spl_token_v2_0, spl_token_v2_0_pubkey};
|
||||
use solana_client::{
|
||||
client_error::{ClientError, Result as ClientResult},
|
||||
rpc_client::RpcClient,
|
||||
@@ -25,6 +30,8 @@ use solana_stake_program::{
|
||||
stake_instruction::{self, LockupArgs},
|
||||
stake_state::{Authorized, Lockup, StakeAuthorize},
|
||||
};
|
||||
use spl_associated_token_account_v1_0::get_associated_token_address;
|
||||
use spl_token_v2_0::solana_program::program_error::ProgramError;
|
||||
use std::{
|
||||
cmp::{self},
|
||||
io,
|
||||
@@ -33,15 +40,16 @@ use std::{
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct Allocation {
|
||||
recipient: String,
|
||||
amount: f64,
|
||||
lockup_date: String,
|
||||
pub struct Allocation {
|
||||
pub recipient: String,
|
||||
pub amount: f64,
|
||||
pub lockup_date: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum FundingSource {
|
||||
FeePayer,
|
||||
SplTokenAccount,
|
||||
StakeAccount,
|
||||
SystemAccount,
|
||||
}
|
||||
@@ -86,6 +94,8 @@ pub enum Error {
|
||||
MissingLockupAuthority,
|
||||
#[error("insufficient funds in {0:?}, requires {1} SOL")]
|
||||
InsufficientFunds(FundingSources, f64),
|
||||
#[error("Program error")]
|
||||
ProgramError(#[from] ProgramError),
|
||||
}
|
||||
|
||||
fn merge_allocations(allocations: &[Allocation]) -> Vec<Allocation> {
|
||||
@@ -128,7 +138,7 @@ fn apply_previous_transactions(
|
||||
}
|
||||
}
|
||||
}
|
||||
allocations.retain(|x| x.amount > 0.5);
|
||||
allocations.retain(|x| x.amount > f64::EPSILON);
|
||||
}
|
||||
|
||||
fn transfer<S: Signer>(
|
||||
@@ -153,8 +163,9 @@ fn distribution_instructions(
|
||||
new_stake_account_address: &Pubkey,
|
||||
args: &DistributeTokensArgs,
|
||||
lockup_date: Option<DateTime<Utc>>,
|
||||
do_create_associated_token_account: bool,
|
||||
) -> Vec<Instruction> {
|
||||
if args.stake_args.is_none() {
|
||||
if args.stake_args.is_none() && args.spl_token_args.is_none() {
|
||||
let from = args.sender_keypair.pubkey();
|
||||
let to = allocation.recipient.parse().unwrap();
|
||||
let lamports = sol_to_lamports(allocation.amount);
|
||||
@@ -162,6 +173,10 @@ fn distribution_instructions(
|
||||
return vec![instruction];
|
||||
}
|
||||
|
||||
if args.spl_token_args.is_some() {
|
||||
return build_spl_token_instructions(allocation, args, do_create_associated_token_account);
|
||||
}
|
||||
|
||||
let stake_args = args.stake_args.as_ref().unwrap();
|
||||
let unlocked_sol = stake_args.unlocked_sol;
|
||||
let sender_pubkey = args.sender_keypair.pubkey();
|
||||
@@ -228,34 +243,65 @@ fn distribute_allocations(
|
||||
args: &DistributeTokensArgs,
|
||||
) -> Result<(), Error> {
|
||||
type StakeExtras = Vec<(Keypair, Option<DateTime<Utc>>)>;
|
||||
let (messages, stake_extras): (Vec<Message>, StakeExtras) = allocations
|
||||
.iter()
|
||||
.map(|allocation| {
|
||||
let new_stake_account_keypair = Keypair::new();
|
||||
let lockup_date = if allocation.lockup_date == "" {
|
||||
None
|
||||
} else {
|
||||
Some(allocation.lockup_date.parse::<DateTime<Utc>>().unwrap())
|
||||
};
|
||||
let mut messages: Vec<Message> = vec![];
|
||||
let mut stake_extras: StakeExtras = vec![];
|
||||
let mut created_accounts = 0;
|
||||
for allocation in allocations.iter() {
|
||||
let new_stake_account_keypair = Keypair::new();
|
||||
let lockup_date = if allocation.lockup_date == "" {
|
||||
None
|
||||
} else {
|
||||
Some(allocation.lockup_date.parse::<DateTime<Utc>>().unwrap())
|
||||
};
|
||||
|
||||
println!("{:<44} {:>24.9}", allocation.recipient, allocation.amount);
|
||||
let instructions = distribution_instructions(
|
||||
allocation,
|
||||
&new_stake_account_keypair.pubkey(),
|
||||
args,
|
||||
lockup_date,
|
||||
let (decimals, do_create_associated_token_account) = if let Some(spl_token_args) =
|
||||
&args.spl_token_args
|
||||
{
|
||||
let wallet_address = allocation.recipient.parse().unwrap();
|
||||
let associated_token_address = get_associated_token_address(
|
||||
&wallet_address,
|
||||
&spl_token_v2_0_pubkey(&spl_token_args.mint),
|
||||
);
|
||||
let fee_payer_pubkey = args.fee_payer.pubkey();
|
||||
let message = Message::new(&instructions, Some(&fee_payer_pubkey));
|
||||
(message, (new_stake_account_keypair, lockup_date))
|
||||
})
|
||||
.unzip();
|
||||
let do_create_associated_token_account = client
|
||||
.get_multiple_accounts(&[pubkey_from_spl_token_v2_0(&associated_token_address)])?
|
||||
[0]
|
||||
.is_none();
|
||||
if do_create_associated_token_account {
|
||||
created_accounts += 1;
|
||||
}
|
||||
(
|
||||
spl_token_args.decimals as usize,
|
||||
do_create_associated_token_account,
|
||||
)
|
||||
} else {
|
||||
(9, false)
|
||||
};
|
||||
println!(
|
||||
"{:<44} {:>24.2$}",
|
||||
allocation.recipient, allocation.amount, decimals
|
||||
);
|
||||
let instructions = distribution_instructions(
|
||||
allocation,
|
||||
&new_stake_account_keypair.pubkey(),
|
||||
args,
|
||||
lockup_date,
|
||||
do_create_associated_token_account,
|
||||
);
|
||||
let fee_payer_pubkey = args.fee_payer.pubkey();
|
||||
let message = Message::new(&instructions, Some(&fee_payer_pubkey));
|
||||
messages.push(message);
|
||||
stake_extras.push((new_stake_account_keypair, lockup_date));
|
||||
}
|
||||
|
||||
let num_signatures = messages
|
||||
.iter()
|
||||
.map(|message| message.header.num_required_signatures as usize)
|
||||
.sum();
|
||||
check_payer_balances(num_signatures, allocations, client, args)?;
|
||||
if args.spl_token_args.is_some() {
|
||||
check_spl_token_balances(num_signatures, allocations, client, args, created_accounts)?;
|
||||
} else {
|
||||
check_payer_balances(num_signatures, allocations, client, args)?;
|
||||
}
|
||||
|
||||
for ((allocation, message), (new_stake_account_keypair, lockup_date)) in
|
||||
allocations.iter().zip(messages).zip(stake_extras)
|
||||
@@ -313,7 +359,11 @@ fn distribute_allocations(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_allocations(input_csv: &str, transfer_amount: Option<f64>) -> io::Result<Vec<Allocation>> {
|
||||
fn read_allocations(
|
||||
input_csv: &str,
|
||||
transfer_amount: Option<f64>,
|
||||
require_lockup_heading: bool,
|
||||
) -> io::Result<Vec<Allocation>> {
|
||||
let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?;
|
||||
let allocations = if let Some(amount) = transfer_amount {
|
||||
let recipients: Vec<String> = rdr
|
||||
@@ -328,8 +378,21 @@ fn read_allocations(input_csv: &str, transfer_amount: Option<f64>) -> io::Result
|
||||
lockup_date: "".to_string(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
} else if require_lockup_heading {
|
||||
rdr.deserialize().map(|entry| entry.unwrap()).collect()
|
||||
} else {
|
||||
let recipients: Vec<(String, f64)> = rdr
|
||||
.deserialize()
|
||||
.map(|recipient| recipient.unwrap())
|
||||
.collect();
|
||||
recipients
|
||||
.into_iter()
|
||||
.map(|(recipient, amount)| Allocation {
|
||||
recipient,
|
||||
amount,
|
||||
lockup_date: "".to_string(),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Ok(allocations)
|
||||
}
|
||||
@@ -346,11 +409,17 @@ pub fn process_allocations(
|
||||
client: &RpcClient,
|
||||
args: &DistributeTokensArgs,
|
||||
) -> Result<Option<usize>, Error> {
|
||||
let mut allocations: Vec<Allocation> = read_allocations(&args.input_csv, args.transfer_amount)?;
|
||||
let require_lockup_heading = args.stake_args.is_some();
|
||||
let mut allocations: Vec<Allocation> = read_allocations(
|
||||
&args.input_csv,
|
||||
args.transfer_amount,
|
||||
require_lockup_heading,
|
||||
)?;
|
||||
let is_sol = args.spl_token_args.is_none();
|
||||
|
||||
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||
let starting_total_tokens = Token::from(allocations.iter().map(|x| x.amount).sum(), is_sol);
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
"{} {}",
|
||||
style("Total in input_csv:").bold(),
|
||||
starting_total_tokens,
|
||||
);
|
||||
@@ -368,27 +437,23 @@ pub fn process_allocations(
|
||||
return Ok(confirmations);
|
||||
}
|
||||
|
||||
let distributed_tokens: f64 = transaction_infos.iter().map(|x| x.amount).sum();
|
||||
let undistributed_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||
println!("{} ◎{}", style("Distributed:").bold(), distributed_tokens,);
|
||||
let distributed_tokens = Token::from(transaction_infos.iter().map(|x| x.amount).sum(), is_sol);
|
||||
let undistributed_tokens = Token::from(allocations.iter().map(|x| x.amount).sum(), is_sol);
|
||||
println!("{} {}", style("Distributed:").bold(), distributed_tokens,);
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
"{} {}",
|
||||
style("Undistributed:").bold(),
|
||||
undistributed_tokens,
|
||||
);
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
"{} {}",
|
||||
style("Total:").bold(),
|
||||
distributed_tokens + undistributed_tokens,
|
||||
);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
"{:<44} {:>24}",
|
||||
"Recipient", "Expected Balance (◎)"
|
||||
))
|
||||
.bold()
|
||||
style(format!("{:<44} {:>24}", "Recipient", "Expected Balance",)).bold()
|
||||
);
|
||||
|
||||
distribute_allocations(client, &mut db, &allocations, args)?;
|
||||
@@ -572,30 +637,41 @@ fn check_payer_balances(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_balances(client: &RpcClient, args: &BalancesArgs) -> Result<(), csv::Error> {
|
||||
let allocations: Vec<Allocation> = read_allocations(&args.input_csv, None)?;
|
||||
pub fn process_balances(client: &RpcClient, args: &BalancesArgs) -> Result<(), Error> {
|
||||
let allocations: Vec<Allocation> = read_allocations(&args.input_csv, None, false)?;
|
||||
let allocations = merge_allocations(&allocations);
|
||||
|
||||
let token = if let Some(spl_token_args) = &args.spl_token_args {
|
||||
spl_token_args.mint.to_string()
|
||||
} else {
|
||||
"◎".to_string()
|
||||
};
|
||||
println!("{} {}", style("Token:").bold(), token);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
"{:<44} {:>24} {:>24} {:>24}",
|
||||
"Recipient", "Expected Balance (◎)", "Actual Balance (◎)", "Difference (◎)"
|
||||
"Recipient", "Expected Balance", "Actual Balance", "Difference"
|
||||
))
|
||||
.bold()
|
||||
);
|
||||
|
||||
for allocation in &allocations {
|
||||
let address = allocation.recipient.parse().unwrap();
|
||||
let expected = lamports_to_sol(sol_to_lamports(allocation.amount));
|
||||
let actual = lamports_to_sol(client.get_balance(&address).unwrap());
|
||||
println!(
|
||||
"{:<44} {:>24.9} {:>24.9} {:>24.9}",
|
||||
allocation.recipient,
|
||||
expected,
|
||||
actual,
|
||||
actual - expected
|
||||
);
|
||||
if let Some(spl_token_args) = &args.spl_token_args {
|
||||
print_token_balances(client, allocation, spl_token_args)?;
|
||||
} else {
|
||||
let address: Pubkey = allocation.recipient.parse().unwrap();
|
||||
let expected = lamports_to_sol(sol_to_lamports(allocation.amount));
|
||||
let actual = lamports_to_sol(client.get_balance(&address).unwrap());
|
||||
println!(
|
||||
"{:<44} {:>24.9} {:>24.9} {:>24.9}",
|
||||
allocation.recipient,
|
||||
expected,
|
||||
actual,
|
||||
actual - expected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -666,6 +742,7 @@ pub fn test_process_distribute_tokens_with_client(
|
||||
transaction_db: transaction_db.clone(),
|
||||
output_path: Some(output_path.clone()),
|
||||
stake_args: None,
|
||||
spl_token_args: None,
|
||||
transfer_amount,
|
||||
};
|
||||
let confirmations = process_allocations(client, &args).unwrap();
|
||||
@@ -777,6 +854,7 @@ pub fn test_process_distribute_stake_with_client(client: &RpcClient, sender_keyp
|
||||
transaction_db: transaction_db.clone(),
|
||||
output_path: Some(output_path.clone()),
|
||||
stake_args: Some(stake_args),
|
||||
spl_token_args: None,
|
||||
sender_keypair: Box::new(sender_keypair),
|
||||
transfer_amount: None,
|
||||
};
|
||||
@@ -910,11 +988,78 @@ mod tests {
|
||||
wtr.flush().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, None).unwrap(),
|
||||
read_allocations(&input_csv, None, false).unwrap(),
|
||||
vec![allocation.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, None, true).unwrap(),
|
||||
vec![allocation]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_allocations_no_lockup() {
|
||||
let pubkey0 = solana_sdk::pubkey::new_rand();
|
||||
let pubkey1 = solana_sdk::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(), "amount".to_string()))
|
||||
.unwrap();
|
||||
wtr.serialize((&pubkey0.to_string(), 42.0)).unwrap();
|
||||
wtr.serialize((&pubkey1.to_string(), 43.0)).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
let expected_allocations = vec![
|
||||
Allocation {
|
||||
recipient: pubkey0.to_string(),
|
||||
amount: 42.0,
|
||||
lockup_date: "".to_string(),
|
||||
},
|
||||
Allocation {
|
||||
recipient: pubkey1.to_string(),
|
||||
amount: 43.0,
|
||||
lockup_date: "".to_string(),
|
||||
},
|
||||
];
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, None, false).unwrap(),
|
||||
expected_allocations
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_read_allocations_malformed() {
|
||||
let pubkey0 = solana_sdk::pubkey::new_rand();
|
||||
let pubkey1 = solana_sdk::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(), "amount".to_string()))
|
||||
.unwrap();
|
||||
wtr.serialize((&pubkey0.to_string(), 42.0)).unwrap();
|
||||
wtr.serialize((&pubkey1.to_string(), 43.0)).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
let expected_allocations = vec![
|
||||
Allocation {
|
||||
recipient: pubkey0.to_string(),
|
||||
amount: 42.0,
|
||||
lockup_date: "".to_string(),
|
||||
},
|
||||
Allocation {
|
||||
recipient: pubkey1.to_string(),
|
||||
amount: 43.0,
|
||||
lockup_date: "".to_string(),
|
||||
},
|
||||
];
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, None, true).unwrap(),
|
||||
expected_allocations
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_allocations_transfer_amount() {
|
||||
let pubkey0 = solana_sdk::pubkey::new_rand();
|
||||
@@ -949,7 +1094,7 @@ mod tests {
|
||||
},
|
||||
];
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, Some(amount)).unwrap(),
|
||||
read_allocations(&input_csv, Some(amount), false).unwrap(),
|
||||
expected_allocations
|
||||
);
|
||||
}
|
||||
@@ -1059,6 +1204,7 @@ mod tests {
|
||||
transaction_db: "".to_string(),
|
||||
output_path: None,
|
||||
stake_args: Some(stake_args),
|
||||
spl_token_args: None,
|
||||
sender_keypair: Box::new(Keypair::new()),
|
||||
transfer_amount: None,
|
||||
};
|
||||
@@ -1068,6 +1214,7 @@ mod tests {
|
||||
&new_stake_account_address,
|
||||
&args,
|
||||
Some(lockup_date),
|
||||
false,
|
||||
);
|
||||
let lockup_instruction =
|
||||
bincode::deserialize(&instructions[SET_LOCKUP_INDEX].data).unwrap();
|
||||
@@ -1107,6 +1254,7 @@ mod tests {
|
||||
transaction_db: "".to_string(),
|
||||
output_path: None,
|
||||
stake_args,
|
||||
spl_token_args: None,
|
||||
transfer_amount: None,
|
||||
};
|
||||
(allocations, args)
|
||||
|
Reference in New Issue
Block a user