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:
688
tokens/src/commands.rs
Normal file
688
tokens/src/commands.rs
Normal file
@@ -0,0 +1,688 @@
|
||||
use crate::args::{BalancesArgs, DistributeTokensArgs, StakeArgs, TransactionLogArgs};
|
||||
use crate::db::{self, TransactionInfo};
|
||||
use crate::thin_client::{Client, ThinClient};
|
||||
use console::style;
|
||||
use csv::{ReaderBuilder, Trim};
|
||||
use indexmap::IndexMap;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use itertools::Itertools;
|
||||
use pickledb::PickleDb;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::{
|
||||
message::Message,
|
||||
native_token::{lamports_to_sol, sol_to_lamports},
|
||||
signature::{Signature, Signer},
|
||||
system_instruction,
|
||||
transport::TransportError,
|
||||
};
|
||||
use solana_stake_program::{
|
||||
stake_instruction,
|
||||
stake_state::{Authorized, Lockup, StakeAuthorize},
|
||||
};
|
||||
use std::{
|
||||
cmp::{self},
|
||||
io,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Bid {
|
||||
accepted_amount_dollars: f64,
|
||||
primary_address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct Allocation {
|
||||
recipient: String,
|
||||
amount: f64,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("CSV error")]
|
||||
CsvError(#[from] csv::Error),
|
||||
#[error("PickleDb error")]
|
||||
PickleDbError(#[from] pickledb::error::Error),
|
||||
#[error("Transport error")]
|
||||
TransportError(#[from] TransportError),
|
||||
#[error("Signature not found")]
|
||||
SignatureNotFound,
|
||||
}
|
||||
|
||||
fn unique_signers(signers: Vec<&dyn Signer>) -> Vec<&dyn Signer> {
|
||||
signers.into_iter().unique_by(|s| s.pubkey()).collect_vec()
|
||||
}
|
||||
|
||||
fn merge_allocations(allocations: &[Allocation]) -> Vec<Allocation> {
|
||||
let mut allocation_map = IndexMap::new();
|
||||
for allocation in allocations {
|
||||
allocation_map
|
||||
.entry(&allocation.recipient)
|
||||
.or_insert(Allocation {
|
||||
recipient: allocation.recipient.clone(),
|
||||
amount: 0.0,
|
||||
})
|
||||
.amount += allocation.amount;
|
||||
}
|
||||
allocation_map.values().cloned().collect()
|
||||
}
|
||||
|
||||
fn apply_previous_transactions(
|
||||
allocations: &mut Vec<Allocation>,
|
||||
transaction_infos: &[TransactionInfo],
|
||||
) {
|
||||
for transaction_info in transaction_infos {
|
||||
let mut amount = transaction_info.amount;
|
||||
for allocation in allocations.iter_mut() {
|
||||
if allocation.recipient != transaction_info.recipient.to_string() {
|
||||
continue;
|
||||
}
|
||||
if allocation.amount >= amount {
|
||||
allocation.amount -= amount;
|
||||
break;
|
||||
} else {
|
||||
amount -= allocation.amount;
|
||||
allocation.amount = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn distribute_tokens<T: Client>(
|
||||
client: &ThinClient<T>,
|
||||
db: &mut PickleDb,
|
||||
allocations: &[Allocation],
|
||||
args: &DistributeTokensArgs<Pubkey, Box<dyn Signer>>,
|
||||
) -> Result<(), Error> {
|
||||
for allocation in allocations {
|
||||
let new_stake_account_keypair = Keypair::new();
|
||||
let new_stake_account_address = new_stake_account_keypair.pubkey();
|
||||
|
||||
let mut signers = vec![&*args.fee_payer, &*args.sender_keypair];
|
||||
if let Some(stake_args) = &args.stake_args {
|
||||
signers.push(&*stake_args.stake_authority);
|
||||
signers.push(&*stake_args.withdraw_authority);
|
||||
signers.push(&new_stake_account_keypair);
|
||||
}
|
||||
let signers = unique_signers(signers);
|
||||
|
||||
println!("{:<44} {:>24.9}", allocation.recipient, allocation.amount);
|
||||
let instructions = if let Some(stake_args) = &args.stake_args {
|
||||
let sol_for_fees = stake_args.sol_for_fees;
|
||||
let sender_pubkey = args.sender_keypair.pubkey();
|
||||
let stake_authority = stake_args.stake_authority.pubkey();
|
||||
let withdraw_authority = stake_args.withdraw_authority.pubkey();
|
||||
|
||||
let mut instructions = stake_instruction::split(
|
||||
&stake_args.stake_account_address,
|
||||
&stake_authority,
|
||||
sol_to_lamports(allocation.amount - sol_for_fees),
|
||||
&new_stake_account_address,
|
||||
);
|
||||
|
||||
let recipient = allocation.recipient.parse().unwrap();
|
||||
|
||||
// Make the recipient the new stake authority
|
||||
instructions.push(stake_instruction::authorize(
|
||||
&new_stake_account_address,
|
||||
&stake_authority,
|
||||
&recipient,
|
||||
StakeAuthorize::Staker,
|
||||
));
|
||||
|
||||
// Make the recipient the new withdraw authority
|
||||
instructions.push(stake_instruction::authorize(
|
||||
&new_stake_account_address,
|
||||
&withdraw_authority,
|
||||
&recipient,
|
||||
StakeAuthorize::Withdrawer,
|
||||
));
|
||||
|
||||
instructions.push(system_instruction::transfer(
|
||||
&sender_pubkey,
|
||||
&recipient,
|
||||
sol_to_lamports(sol_for_fees),
|
||||
));
|
||||
|
||||
instructions
|
||||
} else {
|
||||
let from = args.sender_keypair.pubkey();
|
||||
let to = allocation.recipient.parse().unwrap();
|
||||
let lamports = sol_to_lamports(allocation.amount);
|
||||
let instruction = system_instruction::transfer(&from, &to, lamports);
|
||||
vec![instruction]
|
||||
};
|
||||
|
||||
let fee_payer_pubkey = args.fee_payer.pubkey();
|
||||
let message = Message::new_with_payer(&instructions, Some(&fee_payer_pubkey));
|
||||
match client.send_message(message, &signers) {
|
||||
Ok(transaction) => {
|
||||
db::set_transaction_info(
|
||||
db,
|
||||
&allocation.recipient.parse().unwrap(),
|
||||
allocation.amount,
|
||||
&transaction,
|
||||
Some(&new_stake_account_address),
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error sending tokens to {}: {}", allocation.recipient, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
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 new_spinner_progress_bar() -> ProgressBar {
|
||||
let progress_bar = ProgressBar::new(42);
|
||||
progress_bar
|
||||
.set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}"));
|
||||
progress_bar.enable_steady_tick(100);
|
||||
progress_bar
|
||||
}
|
||||
|
||||
pub fn process_distribute_tokens<T: Client>(
|
||||
client: &ThinClient<T>,
|
||||
args: &DistributeTokensArgs<Pubkey, Box<dyn Signer>>,
|
||||
) -> Result<Option<usize>, Error> {
|
||||
let mut allocations: Vec<Allocation> =
|
||||
read_allocations(&args.input_csv, args.from_bids, args.dollars_per_sol);
|
||||
|
||||
let starting_total_tokens: f64 = allocations.iter().map(|x| x.amount).sum();
|
||||
println!(
|
||||
"{} ◎{}",
|
||||
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)?;
|
||||
|
||||
// Start by finalizing any transactions from the previous run.
|
||||
let confirmations = finalize_transactions(client, &mut db)?;
|
||||
|
||||
let transaction_infos = db::read_transaction_infos(&db);
|
||||
apply_previous_transactions(&mut allocations, &transaction_infos);
|
||||
|
||||
if allocations.is_empty() {
|
||||
eprintln!("No work to do");
|
||||
return Ok(confirmations);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
"{:<44} {:>24}",
|
||||
"Recipient", "Expected Balance (◎)"
|
||||
))
|
||||
.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_tokens(client, &mut db, &allocations, args)?;
|
||||
|
||||
let opt_confirmations = finalize_transactions(client, &mut db)?;
|
||||
Ok(opt_confirmations)
|
||||
}
|
||||
|
||||
fn finalize_transactions<T: Client>(
|
||||
client: &ThinClient<T>,
|
||||
db: &mut PickleDb,
|
||||
) -> Result<Option<usize>, Error> {
|
||||
let mut opt_confirmations = update_finalized_transactions(client, db)?;
|
||||
|
||||
let progress_bar = new_spinner_progress_bar();
|
||||
|
||||
while opt_confirmations.is_some() {
|
||||
if let Some(confirmations) = opt_confirmations {
|
||||
progress_bar.set_message(&format!(
|
||||
"[{}/{}] Finalizing transactions",
|
||||
confirmations, 32,
|
||||
));
|
||||
}
|
||||
|
||||
// Sleep for about 1 slot
|
||||
sleep(Duration::from_millis(500));
|
||||
let opt_conf = update_finalized_transactions(client, db)?;
|
||||
opt_confirmations = opt_conf;
|
||||
}
|
||||
|
||||
Ok(opt_confirmations)
|
||||
}
|
||||
|
||||
// Update the finalized bit on any transactions that are now rooted
|
||||
// Return the lowest number of confirmations on the unfinalized transactions or None if all are finalized.
|
||||
fn update_finalized_transactions<T: Client>(
|
||||
client: &ThinClient<T>,
|
||||
db: &mut PickleDb,
|
||||
) -> Result<Option<usize>, Error> {
|
||||
let transaction_infos = db::read_transaction_infos(db);
|
||||
let unconfirmed_transactions: Vec<_> = transaction_infos
|
||||
.iter()
|
||||
.filter_map(|info| {
|
||||
if info.finalized_date.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(&info.transaction)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let unconfirmed_signatures = unconfirmed_transactions
|
||||
.iter()
|
||||
.map(|tx| tx.signatures[0])
|
||||
.filter(|sig| *sig != Signature::default()) // Filter out dry-run signatures
|
||||
.collect_vec();
|
||||
let transaction_statuses = client.get_signature_statuses(&unconfirmed_signatures)?;
|
||||
let recent_blockhashes = client.get_recent_blockhashes()?;
|
||||
|
||||
let mut confirmations = None;
|
||||
for (transaction, opt_transaction_status) in unconfirmed_transactions
|
||||
.into_iter()
|
||||
.zip(transaction_statuses.into_iter())
|
||||
{
|
||||
match db::update_finalized_transaction(
|
||||
db,
|
||||
&transaction.signatures[0],
|
||||
opt_transaction_status,
|
||||
&transaction.message.recent_blockhash,
|
||||
&recent_blockhashes,
|
||||
) {
|
||||
Ok(Some(confs)) => {
|
||||
confirmations = Some(cmp::min(confs, confirmations.unwrap_or(usize::MAX)));
|
||||
}
|
||||
result => {
|
||||
result?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(confirmations)
|
||||
}
|
||||
|
||||
pub fn process_balances<T: Client>(
|
||||
client: &ThinClient<T>,
|
||||
args: &BalancesArgs,
|
||||
) -> Result<(), csv::Error> {
|
||||
let allocations: Vec<Allocation> =
|
||||
read_allocations(&args.input_csv, args.from_bids, args.dollars_per_sol);
|
||||
let allocations = merge_allocations(&allocations);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
"{:<44} {:>24} {:>24} {:>24}",
|
||||
"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
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_transaction_log(args: &TransactionLogArgs) -> Result<(), Error> {
|
||||
let db = db::open_db(&args.transaction_db, true)?;
|
||||
db::write_transaction_log(&db, &args.output_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use tempfile::{tempdir, NamedTempFile};
|
||||
pub fn test_process_distribute_tokens_with_client<C: Client>(client: C, sender_keypair: Keypair) {
|
||||
let thin_client = ThinClient::new(client, false);
|
||||
let fee_payer = Keypair::new();
|
||||
let transaction = thin_client
|
||||
.transfer(sol_to_lamports(1.0), &sender_keypair, &fee_payer.pubkey())
|
||||
.unwrap();
|
||||
thin_client
|
||||
.poll_for_confirmation(&transaction.signatures[0])
|
||||
.unwrap();
|
||||
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let allocation = Allocation {
|
||||
recipient: alice_pubkey.to_string(),
|
||||
amount: 1000.0,
|
||||
};
|
||||
let allocations_file = NamedTempFile::new().unwrap();
|
||||
let input_csv = allocations_file.path().to_str().unwrap().to_string();
|
||||
let mut wtr = csv::WriterBuilder::new().from_writer(allocations_file);
|
||||
wtr.serialize(&allocation).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let transaction_db = dir
|
||||
.path()
|
||||
.join("transactions.db")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let args: DistributeTokensArgs<Pubkey, Box<dyn Signer>> = 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,
|
||||
stake_args: None,
|
||||
};
|
||||
let confirmations = process_distribute_tokens(&thin_client, &args).unwrap();
|
||||
assert_eq!(confirmations, None);
|
||||
|
||||
let transaction_infos =
|
||||
db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
|
||||
assert_eq!(transaction_infos.len(), 1);
|
||||
assert_eq!(transaction_infos[0].recipient, alice_pubkey);
|
||||
let expected_amount = sol_to_lamports(allocation.amount);
|
||||
assert_eq!(
|
||||
sol_to_lamports(transaction_infos[0].amount),
|
||||
expected_amount
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
thin_client.get_balance(&alice_pubkey).unwrap(),
|
||||
expected_amount,
|
||||
);
|
||||
|
||||
// Now, run it again, and check there's no double-spend.
|
||||
process_distribute_tokens(&thin_client, &args).unwrap();
|
||||
let transaction_infos =
|
||||
db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
|
||||
assert_eq!(transaction_infos.len(), 1);
|
||||
assert_eq!(transaction_infos[0].recipient, alice_pubkey);
|
||||
let expected_amount = sol_to_lamports(allocation.amount);
|
||||
assert_eq!(
|
||||
sol_to_lamports(transaction_infos[0].amount),
|
||||
expected_amount
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
thin_client.get_balance(&alice_pubkey).unwrap(),
|
||||
expected_amount,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn test_process_distribute_stake_with_client<C: Client>(client: C, sender_keypair: Keypair) {
|
||||
let thin_client = ThinClient::new(client, false);
|
||||
let fee_payer = Keypair::new();
|
||||
let transaction = thin_client
|
||||
.transfer(sol_to_lamports(1.0), &sender_keypair, &fee_payer.pubkey())
|
||||
.unwrap();
|
||||
thin_client
|
||||
.poll_for_confirmation(&transaction.signatures[0])
|
||||
.unwrap();
|
||||
|
||||
let stake_account_keypair = Keypair::new();
|
||||
let stake_account_address = stake_account_keypair.pubkey();
|
||||
let stake_authority = Keypair::new();
|
||||
let withdraw_authority = Keypair::new();
|
||||
|
||||
let authorized = Authorized {
|
||||
staker: stake_authority.pubkey(),
|
||||
withdrawer: withdraw_authority.pubkey(),
|
||||
};
|
||||
let lockup = Lockup::default();
|
||||
let instructions = stake_instruction::create_account(
|
||||
&sender_keypair.pubkey(),
|
||||
&stake_account_address,
|
||||
&authorized,
|
||||
&lockup,
|
||||
sol_to_lamports(3000.0),
|
||||
);
|
||||
let message = Message::new(&instructions);
|
||||
let signers = [&sender_keypair, &stake_account_keypair];
|
||||
thin_client.send_message(message, &signers).unwrap();
|
||||
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let allocation = Allocation {
|
||||
recipient: alice_pubkey.to_string(),
|
||||
amount: 1000.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(&allocation).unwrap();
|
||||
wtr.flush().unwrap();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let transaction_db = dir
|
||||
.path()
|
||||
.join("transactions.db")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let stake_args: StakeArgs<Pubkey, Box<dyn Signer>> = StakeArgs {
|
||||
stake_account_address,
|
||||
stake_authority: Box::new(stake_authority),
|
||||
withdraw_authority: Box::new(withdraw_authority),
|
||||
sol_for_fees: 1.0,
|
||||
};
|
||||
let args: DistributeTokensArgs<Pubkey, Box<dyn Signer>> = DistributeTokensArgs {
|
||||
fee_payer: Box::new(fee_payer),
|
||||
dry_run: false,
|
||||
input_csv,
|
||||
transaction_db: transaction_db.clone(),
|
||||
stake_args: Some(stake_args),
|
||||
from_bids: false,
|
||||
sender_keypair: Box::new(sender_keypair),
|
||||
dollars_per_sol: None,
|
||||
};
|
||||
let confirmations = process_distribute_tokens(&thin_client, &args).unwrap();
|
||||
assert_eq!(confirmations, None);
|
||||
|
||||
let transaction_infos =
|
||||
db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
|
||||
assert_eq!(transaction_infos.len(), 1);
|
||||
assert_eq!(transaction_infos[0].recipient, alice_pubkey);
|
||||
let expected_amount = sol_to_lamports(allocation.amount);
|
||||
assert_eq!(
|
||||
sol_to_lamports(transaction_infos[0].amount),
|
||||
expected_amount
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
thin_client.get_balance(&alice_pubkey).unwrap(),
|
||||
sol_to_lamports(1.0),
|
||||
);
|
||||
let new_stake_account_address = transaction_infos[0].new_stake_account_address.unwrap();
|
||||
assert_eq!(
|
||||
thin_client.get_balance(&new_stake_account_address).unwrap(),
|
||||
expected_amount - sol_to_lamports(1.0),
|
||||
);
|
||||
|
||||
// Now, run it again, and check there's no double-spend.
|
||||
process_distribute_tokens(&thin_client, &args).unwrap();
|
||||
let transaction_infos =
|
||||
db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
|
||||
assert_eq!(transaction_infos.len(), 1);
|
||||
assert_eq!(transaction_infos[0].recipient, alice_pubkey);
|
||||
let expected_amount = sol_to_lamports(allocation.amount);
|
||||
assert_eq!(
|
||||
sol_to_lamports(transaction_infos[0].amount),
|
||||
expected_amount
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
thin_client.get_balance(&alice_pubkey).unwrap(),
|
||||
sol_to_lamports(1.0),
|
||||
);
|
||||
assert_eq!(
|
||||
thin_client.get_balance(&new_stake_account_address).unwrap(),
|
||||
expected_amount - sol_to_lamports(1.0),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_runtime::{bank::Bank, bank_client::BankClient};
|
||||
use solana_sdk::{genesis_config::create_genesis_config, transaction::Transaction};
|
||||
|
||||
#[test]
|
||||
fn test_process_distribute_tokens() {
|
||||
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]
|
||||
fn test_process_distribute_stake() {
|
||||
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_stake_with_client(bank_client, sender_keypair);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_allocations() {
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let allocation = Allocation {
|
||||
recipient: alice_pubkey.to_string(),
|
||||
amount: 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(&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,
|
||||
};
|
||||
assert_eq!(
|
||||
read_allocations(&input_csv, true, Some(0.5)),
|
||||
vec![allocation]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_previous_transactions() {
|
||||
let alice = Pubkey::new_rand();
|
||||
let bob = Pubkey::new_rand();
|
||||
let mut allocations = vec![
|
||||
Allocation {
|
||||
recipient: alice.to_string(),
|
||||
amount: 1.0,
|
||||
},
|
||||
Allocation {
|
||||
recipient: bob.to_string(),
|
||||
amount: 1.0,
|
||||
},
|
||||
];
|
||||
let transaction_infos = vec![TransactionInfo {
|
||||
recipient: bob,
|
||||
amount: 1.0,
|
||||
new_stake_account_address: None,
|
||||
finalized_date: None,
|
||||
transaction: Transaction::new_unsigned_instructions(&[]),
|
||||
}];
|
||||
apply_previous_transactions(&mut allocations, &transaction_infos);
|
||||
assert_eq!(allocations.len(), 1);
|
||||
|
||||
// Ensure that we applied the transaction to the allocation with
|
||||
// a matching recipient address (to bob, not alice).
|
||||
assert_eq!(allocations[0].recipient, alice.to_string());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user