Improve solana-tokens UX (#12253)
* Fix computed banks port * Readme incorrect * Return error if csv cannot be read * Move column headers over columns * Add dry-run check for sender/fee-payer balances * Use clap requires method for paired args * Write transaction-log anytime outfile is specified * Replace campaign-name with required db-path * Remove bids * Exclude new_stake_account_address from logs for non-stake distributions * Fix readme
This commit is contained in:
@@ -54,6 +54,12 @@ pub enum Error {
|
||||
TransportError(#[from] TransportError),
|
||||
#[error("Missing lockup authority")]
|
||||
MissingLockupAuthority,
|
||||
#[error("insufficient funds for fee ({0} SOL)")]
|
||||
InsufficientFundsForFees(f64),
|
||||
#[error("insufficient funds for distribution ({0} SOL)")]
|
||||
InsufficientFundsForDistribution(f64),
|
||||
#[error("insufficient funds for distribution ({0} SOL) and fee ({1} SOL)")]
|
||||
InsufficientFundsForDistributionAndFees(f64, f64),
|
||||
}
|
||||
|
||||
fn merge_allocations(allocations: &[Allocation]) -> Vec<Allocation> {
|
||||
@@ -99,14 +105,6 @@ fn apply_previous_transactions(
|
||||
allocations.retain(|x| x.amount > 0.5);
|
||||
}
|
||||
|
||||
fn create_allocation(bid: &Bid, dollars_per_sol: f64) -> Allocation {
|
||||
Allocation {
|
||||
recipient: bid.primary_address.clone(),
|
||||
amount: bid.accepted_amount_dollars / dollars_per_sol,
|
||||
lockup_date: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer<S: Signer>(
|
||||
client: &mut BanksClient,
|
||||
lamports: u64,
|
||||
@@ -203,6 +201,7 @@ async fn distribute_allocations(
|
||||
allocations: &[Allocation],
|
||||
args: &DistributeTokensArgs,
|
||||
) -> Result<(), Error> {
|
||||
let mut num_signatures = 0;
|
||||
for allocation in allocations {
|
||||
let new_stake_account_keypair = Keypair::new();
|
||||
let new_stake_account_address = new_stake_account_keypair.pubkey();
|
||||
@@ -221,6 +220,7 @@ async fn distribute_allocations(
|
||||
}
|
||||
}
|
||||
let signers = unique_signers(signers);
|
||||
num_signatures += signers.len();
|
||||
|
||||
let lockup_date = if allocation.lockup_date == "" {
|
||||
None
|
||||
@@ -250,7 +250,7 @@ async fn distribute_allocations(
|
||||
&allocation.recipient.parse().unwrap(),
|
||||
allocation.amount,
|
||||
&transaction,
|
||||
Some(&new_stake_account_address),
|
||||
args.stake_args.as_ref().map(|_| &new_stake_account_address),
|
||||
false,
|
||||
last_valid_slot,
|
||||
lockup_date,
|
||||
@@ -261,26 +261,22 @@ async fn distribute_allocations(
|
||||
}
|
||||
};
|
||||
}
|
||||
if args.dry_run {
|
||||
let undistributed_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||
check_payer_balances(
|
||||
num_signatures,
|
||||
sol_to_lamports(undistributed_tokens),
|
||||
client,
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_allocations(
|
||||
input_csv: &str,
|
||||
from_bids: bool,
|
||||
dollars_per_sol: Option<f64>,
|
||||
) -> Vec<Allocation> {
|
||||
let rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv);
|
||||
if from_bids {
|
||||
let bids: Vec<Bid> = rdr.unwrap().deserialize().map(|bid| bid.unwrap()).collect();
|
||||
bids.into_iter()
|
||||
.map(|bid| create_allocation(&bid, dollars_per_sol.unwrap()))
|
||||
.collect()
|
||||
} else {
|
||||
rdr.unwrap()
|
||||
.deserialize()
|
||||
.map(|entry| entry.unwrap())
|
||||
.collect()
|
||||
}
|
||||
fn read_allocations(input_csv: &str) -> io::Result<Vec<Allocation>> {
|
||||
let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?;
|
||||
Ok(rdr.deserialize().map(|entry| entry.unwrap()).collect())
|
||||
}
|
||||
|
||||
fn new_spinner_progress_bar() -> ProgressBar {
|
||||
@@ -295,8 +291,7 @@ pub async fn process_allocations(
|
||||
client: &mut BanksClient,
|
||||
args: &DistributeTokensArgs,
|
||||
) -> Result<Option<usize>, Error> {
|
||||
let mut allocations: Vec<Allocation> =
|
||||
read_allocations(&args.input_csv, args.from_bids, args.dollars_per_sol);
|
||||
let mut allocations: Vec<Allocation> = read_allocations(&args.input_csv)?;
|
||||
|
||||
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||
println!(
|
||||
@@ -304,13 +299,6 @@ pub async fn process_allocations(
|
||||
style("Total in input_csv:").bold(),
|
||||
starting_total_tokens,
|
||||
);
|
||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
||||
println!(
|
||||
"{} ${}",
|
||||
style("Total in input_csv:").bold(),
|
||||
starting_total_tokens * dollars_per_sol,
|
||||
);
|
||||
}
|
||||
|
||||
let mut db = db::open_db(&args.transaction_db, args.dry_run)?;
|
||||
|
||||
@@ -325,6 +313,20 @@ pub async 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,);
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
style("Undistributed:").bold(),
|
||||
undistributed_tokens,
|
||||
);
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
style("Total:").bold(),
|
||||
distributed_tokens + undistributed_tokens,
|
||||
);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
@@ -334,44 +336,16 @@ pub async fn process_allocations(
|
||||
.bold()
|
||||
);
|
||||
|
||||
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,);
|
||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
||||
println!(
|
||||
"{} ${}",
|
||||
style("Distributed:").bold(),
|
||||
distributed_tokens * dollars_per_sol,
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
style("Undistributed:").bold(),
|
||||
undistributed_tokens,
|
||||
);
|
||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
||||
println!(
|
||||
"{} ${}",
|
||||
style("Undistributed:").bold(),
|
||||
undistributed_tokens * dollars_per_sol,
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
style("Total:").bold(),
|
||||
distributed_tokens + undistributed_tokens,
|
||||
);
|
||||
if let Some(dollars_per_sol) = args.dollars_per_sol {
|
||||
println!(
|
||||
"{} ${}",
|
||||
style("Total:").bold(),
|
||||
(distributed_tokens + undistributed_tokens) * dollars_per_sol,
|
||||
);
|
||||
}
|
||||
|
||||
distribute_allocations(client, &mut db, &allocations, args).await?;
|
||||
|
||||
let opt_confirmations = finalize_transactions(client, &mut db, args.dry_run).await?;
|
||||
|
||||
if !args.dry_run {
|
||||
if let Some(output_path) = &args.output_path {
|
||||
db::write_transaction_log(&db, &output_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(opt_confirmations)
|
||||
}
|
||||
|
||||
@@ -455,12 +429,45 @@ async fn update_finalized_transactions(
|
||||
Ok(confirmations)
|
||||
}
|
||||
|
||||
async fn check_payer_balances(
|
||||
num_signatures: usize,
|
||||
allocation_lamports: u64,
|
||||
client: &mut BanksClient,
|
||||
args: &DistributeTokensArgs,
|
||||
) -> Result<(), Error> {
|
||||
let (fee_calculator, _blockhash, _last_valid_slot) = client.get_fees().await?;
|
||||
let fees = fee_calculator
|
||||
.lamports_per_signature
|
||||
.checked_mul(num_signatures as u64)
|
||||
.unwrap();
|
||||
if args.fee_payer.pubkey() == args.sender_keypair.pubkey() {
|
||||
let balance = client.get_balance(args.fee_payer.pubkey()).await?;
|
||||
if balance < fees + allocation_lamports {
|
||||
return Err(Error::InsufficientFundsForDistributionAndFees(
|
||||
lamports_to_sol(allocation_lamports),
|
||||
lamports_to_sol(fees),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let fee_payer_balance = client.get_balance(args.fee_payer.pubkey()).await?;
|
||||
if fee_payer_balance < fees {
|
||||
return Err(Error::InsufficientFundsForFees(lamports_to_sol(fees)));
|
||||
}
|
||||
let sender_balance = client.get_balance(args.sender_keypair.pubkey()).await?;
|
||||
if sender_balance < allocation_lamports {
|
||||
return Err(Error::InsufficientFundsForDistribution(lamports_to_sol(
|
||||
allocation_lamports,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn process_balances(
|
||||
client: &mut BanksClient,
|
||||
args: &BalancesArgs,
|
||||
) -> Result<(), csv::Error> {
|
||||
let allocations: Vec<Allocation> =
|
||||
read_allocations(&args.input_csv, args.from_bids, args.dollars_per_sol);
|
||||
let allocations: Vec<Allocation> = read_allocations(&args.input_csv)?;
|
||||
let allocations = merge_allocations(&allocations);
|
||||
|
||||
println!(
|
||||
@@ -494,6 +501,7 @@ pub fn process_transaction_log(args: &TransactionLogArgs) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use crate::db::check_output_file;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use tempfile::{tempdir, NamedTempFile};
|
||||
pub async fn test_process_distribute_tokens_with_client(
|
||||
@@ -541,14 +549,16 @@ pub async fn test_process_distribute_tokens_with_client(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let output_file = NamedTempFile::new().unwrap();
|
||||
let output_path = output_file.path().to_str().unwrap().to_string();
|
||||
|
||||
let args = DistributeTokensArgs {
|
||||
sender_keypair: Box::new(sender_keypair),
|
||||
fee_payer: Box::new(fee_payer),
|
||||
dry_run: false,
|
||||
input_csv,
|
||||
from_bids: false,
|
||||
transaction_db: transaction_db.clone(),
|
||||
dollars_per_sol: None,
|
||||
output_path: Some(output_path.clone()),
|
||||
stake_args: None,
|
||||
};
|
||||
let confirmations = process_allocations(client, &args).await.unwrap();
|
||||
@@ -569,6 +579,8 @@ pub async fn test_process_distribute_tokens_with_client(
|
||||
expected_amount,
|
||||
);
|
||||
|
||||
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||
|
||||
// Now, run it again, and check there's no double-spend.
|
||||
process_allocations(client, &args).await.unwrap();
|
||||
let transaction_infos =
|
||||
@@ -585,6 +597,8 @@ pub async fn test_process_distribute_tokens_with_client(
|
||||
client.get_balance(alice_pubkey).await.unwrap(),
|
||||
expected_amount,
|
||||
);
|
||||
|
||||
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||
}
|
||||
|
||||
pub async fn test_process_distribute_stake_with_client(
|
||||
@@ -651,6 +665,9 @@ pub async fn test_process_distribute_stake_with_client(
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let output_file = NamedTempFile::new().unwrap();
|
||||
let output_path = output_file.path().to_str().unwrap().to_string();
|
||||
|
||||
let stake_args = StakeArgs {
|
||||
stake_account_address,
|
||||
stake_authority: Box::new(stake_authority),
|
||||
@@ -663,10 +680,9 @@ pub async fn test_process_distribute_stake_with_client(
|
||||
dry_run: false,
|
||||
input_csv,
|
||||
transaction_db: transaction_db.clone(),
|
||||
output_path: Some(output_path.clone()),
|
||||
stake_args: Some(stake_args),
|
||||
from_bids: false,
|
||||
sender_keypair: Box::new(sender_keypair),
|
||||
dollars_per_sol: None,
|
||||
};
|
||||
let confirmations = process_allocations(client, &args).await.unwrap();
|
||||
assert_eq!(confirmations, None);
|
||||
@@ -691,6 +707,8 @@ pub async fn test_process_distribute_stake_with_client(
|
||||
expected_amount - sol_to_lamports(1.0),
|
||||
);
|
||||
|
||||
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||
|
||||
// Now, run it again, and check there's no double-spend.
|
||||
process_allocations(client, &args).await.unwrap();
|
||||
let transaction_infos =
|
||||
@@ -711,6 +729,8 @@ pub async fn test_process_distribute_stake_with_client(
|
||||
client.get_balance(new_stake_account_address).await.unwrap(),
|
||||
expected_amount - sol_to_lamports(1.0),
|
||||
);
|
||||
|
||||
check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -760,31 +780,7 @@ mod tests {
|
||||
wtr.serialize(&allocation).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
assert_eq!(read_allocations(&input_csv, false, None), vec![allocation]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_allocations_from_bids() {
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let bid = Bid {
|
||||
primary_address: alice_pubkey.to_string(),
|
||||
accepted_amount_dollars: 42.0,
|
||||
};
|
||||
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(&bid).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
let allocation = Allocation {
|
||||
recipient: bid.primary_address,
|
||||
amount: 84.0,
|
||||
lockup_date: "".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, true, Some(0.5)),
|
||||
vec![allocation]
|
||||
);
|
||||
assert_eq!(read_allocations(&input_csv).unwrap(), vec![allocation]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -890,10 +886,9 @@ mod tests {
|
||||
dry_run: false,
|
||||
input_csv: "".to_string(),
|
||||
transaction_db: "".to_string(),
|
||||
output_path: None,
|
||||
stake_args: Some(stake_args),
|
||||
from_bids: false,
|
||||
sender_keypair: Box::new(Keypair::new()),
|
||||
dollars_per_sol: None,
|
||||
};
|
||||
let lockup_date = lockup_date_str.parse().unwrap();
|
||||
let instructions = distribution_instructions(
|
||||
|
Reference in New Issue
Block a user