1use {
2 crate::{
3 args::{
4 BalancesArgs, DistributeTokensArgs, SenderStakeArgs, StakeArgs, TransactionLogArgs,
5 },
6 db::{self, TransactionInfo},
7 spl_token::{build_spl_token_instructions, check_spl_token_balances, print_token_balances},
8 token_display::Token,
9 },
10 chrono::prelude::*,
11 console::style,
12 csv::{ReaderBuilder, Trim},
13 indexmap::IndexMap,
14 indicatif::{ProgressBar, ProgressStyle},
15 pickledb::PickleDb,
16 serde::{Deserialize, Serialize},
17 solana_account_decoder::parse_token::real_number_string,
18 solana_cli_output::display::build_balance_message,
19 solana_clock::Slot,
20 solana_commitment_config::CommitmentConfig,
21 solana_hash::Hash,
22 solana_instruction::Instruction,
23 solana_message::Message,
24 solana_native_token::sol_str_to_lamports,
25 solana_program_error::ProgramError,
26 solana_rpc_client::rpc_client::RpcClient,
27 solana_rpc_client_api::{
28 client_error::{Error as ClientError, Result as ClientResult},
29 config::RpcSendTransactionConfig,
30 request::{MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, MAX_MULTIPLE_ACCOUNTS},
31 },
32 solana_signature::Signature,
33 solana_signer::{unique_signers, Signer},
34 solana_stake_interface::{
35 instruction::{self as stake_instruction, LockupArgs},
36 state::{Authorized, Lockup, StakeAuthorize, StakeStateV2},
37 },
38 solana_system_interface::instruction as system_instruction,
39 solana_transaction::Transaction,
40 solana_transaction_status::TransactionStatus,
41 spl_associated_token_account_interface::address::get_associated_token_address,
42 std::{
43 cmp::{self},
44 io,
45 str::FromStr,
46 sync::{
47 atomic::{AtomicBool, Ordering},
48 Arc,
49 },
50 thread::sleep,
51 time::Duration,
52 },
53};
54
55#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
57pub struct Allocation {
58 pub recipient: String,
59 pub amount: u64,
60 pub lockup_date: String,
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
65pub struct TypedAllocation {
66 pub recipient: Pubkey,
67 pub amount: u64,
68 pub lockup_date: Option<DateTime<Utc>>,
69}
70
71#[derive(Debug, PartialEq, Eq)]
72pub enum FundingSource {
73 FeePayer,
74 SplTokenAccount,
75 StakeAccount,
76 SystemAccount,
77}
78
79pub struct FundingSources(Vec<FundingSource>);
80
81impl std::fmt::Debug for FundingSources {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 for (i, source) in self.0.iter().enumerate() {
84 if i > 0 {
85 write!(f, "/")?;
86 }
87 write!(f, "{source:?}")?;
88 }
89 Ok(())
90 }
91}
92
93impl PartialEq for FundingSources {
94 fn eq(&self, other: &Self) -> bool {
95 self.0 == other.0
96 }
97}
98
99impl From<Vec<FundingSource>> for FundingSources {
100 fn from(sources_vec: Vec<FundingSource>) -> Self {
101 Self(sources_vec)
102 }
103}
104
105type StakeExtras = Vec<(Keypair, Option<DateTime<Utc>>)>;
106
107#[allow(clippy::large_enum_variant)]
108#[derive(thiserror::Error, Debug)]
109pub enum Error {
110 #[error("I/O error")]
111 IoError(#[from] io::Error),
112 #[error("CSV file seems to be empty")]
113 CsvIsEmptyError,
114 #[error("CSV error")]
115 CsvError(#[from] csv::Error),
116 #[error("Bad input data for pubkey: {input}, error: {err}")]
117 BadInputPubkeyError {
118 input: String,
119 err: pubkey::ParsePubkeyError,
120 },
121 #[error("Bad input data for lockup date: {input}, error: {err}")]
122 BadInputLockupDate {
123 input: String,
124 err: chrono::ParseError,
125 },
126 #[error("PickleDb error")]
127 PickleDbError(#[from] pickledb::error::Error),
128 #[error("Transport error")]
129 ClientError(#[from] ClientError),
130 #[error("Missing lockup authority")]
131 MissingLockupAuthority,
132 #[error("Missing messages")]
133 MissingMessages,
134 #[error("Error estimating message fees")]
135 FeeEstimationError,
136 #[error("insufficient funds in {0:?}, requires {1}")]
137 InsufficientFunds(FundingSources, String),
138 #[error("Program error")]
139 ProgramError(#[from] ProgramError),
140 #[error("Exit signal received")]
141 ExitSignal,
142 #[error("Bad input data for SOL value: {input}")]
143 BadInputNumberError { input: String },
144}
145
146fn merge_allocations(allocations: &[TypedAllocation]) -> Vec<TypedAllocation> {
147 let mut allocation_map = IndexMap::new();
148 for allocation in allocations {
149 allocation_map
150 .entry(&allocation.recipient)
151 .or_insert(TypedAllocation {
152 recipient: allocation.recipient,
153 amount: 0,
154 lockup_date: None,
155 })
156 .amount += allocation.amount;
157 }
158 allocation_map.values().cloned().collect()
159}
160
161fn has_same_recipient(allocation: &TypedAllocation, transaction_info: &TransactionInfo) -> bool {
163 allocation.recipient == transaction_info.recipient
164 && allocation.lockup_date == transaction_info.lockup_date
165}
166
167fn apply_previous_transactions(
168 allocations: &mut Vec<TypedAllocation>,
169 transaction_infos: &[TransactionInfo],
170) {
171 for transaction_info in transaction_infos {
172 let mut amount = transaction_info.amount;
173 for allocation in allocations.iter_mut() {
174 if !has_same_recipient(allocation, transaction_info) {
175 continue;
176 }
177 if allocation.amount >= amount {
178 allocation.amount -= amount;
179 break;
180 } else {
181 amount -= allocation.amount;
182 allocation.amount = 0;
183 }
184 }
185 }
186 allocations.retain(|x| x.amount > 0);
187}
188
189fn transfer<S: Signer>(
190 client: &RpcClient,
191 lamports: u64,
192 sender_keypair: &S,
193 to_pubkey: &Pubkey,
194) -> ClientResult<Transaction> {
195 let create_instruction =
196 system_instruction::transfer(&sender_keypair.pubkey(), to_pubkey, lamports);
197 let message = Message::new(&[create_instruction], Some(&sender_keypair.pubkey()));
198 let recent_blockhash = client.get_latest_blockhash()?;
199 Ok(Transaction::new(
200 &[sender_keypair],
201 message,
202 recent_blockhash,
203 ))
204}
205
206fn distribution_instructions(
207 allocation: &TypedAllocation,
208 new_stake_account_address: &Pubkey,
209 args: &DistributeTokensArgs,
210 lockup_date: Option<DateTime<Utc>>,
211 do_create_associated_token_account: bool,
212) -> Vec<Instruction> {
213 if args.spl_token_args.is_some() {
214 return build_spl_token_instructions(allocation, args, do_create_associated_token_account);
215 }
216
217 match &args.stake_args {
218 None => {
220 let from = args.sender_keypair.pubkey();
221 let to = allocation.recipient;
222 let lamports = allocation.amount;
223 let instruction = system_instruction::transfer(&from, &to, lamports);
224 vec![instruction]
225 }
226
227 Some(stake_args) => {
229 let unlocked_sol = stake_args.unlocked_sol;
230 let sender_pubkey = args.sender_keypair.pubkey();
231 let recipient = allocation.recipient;
232
233 let mut instructions = match &stake_args.sender_stake_args {
234 None => {
236 let authorized = Authorized {
238 staker: recipient,
239 withdrawer: recipient,
240 };
241 let mut lockup = Lockup::default();
242 if let Some(lockup_date) = lockup_date {
243 lockup.unix_timestamp = lockup_date.timestamp();
244 }
245 if let Some(lockup_authority) = stake_args.lockup_authority {
246 lockup.custodian = lockup_authority;
247 }
248 stake_instruction::create_account(
249 &sender_pubkey,
250 new_stake_account_address,
251 &authorized,
252 &lockup,
253 allocation.amount - unlocked_sol,
254 )
255 }
256
257 Some(sender_stake_args) => {
260 let stake_authority = sender_stake_args.stake_authority.pubkey();
261 let withdraw_authority = sender_stake_args.withdraw_authority.pubkey();
262 let rent_exempt_reserve = sender_stake_args
263 .rent_exempt_reserve
264 .expect("SenderStakeArgs.rent_exempt_reserve should be populated");
265
266 let mut instructions = vec![system_instruction::transfer(
268 &sender_pubkey,
269 new_stake_account_address,
270 rent_exempt_reserve,
271 )];
272
273 instructions.append(&mut stake_instruction::split(
275 &sender_stake_args.stake_account_address,
276 &stake_authority,
277 allocation.amount - unlocked_sol - rent_exempt_reserve,
278 new_stake_account_address,
279 ));
280
281 instructions.push(stake_instruction::authorize(
283 new_stake_account_address,
284 &stake_authority,
285 &recipient,
286 StakeAuthorize::Staker,
287 None,
288 ));
289
290 instructions.push(stake_instruction::authorize(
292 new_stake_account_address,
293 &withdraw_authority,
294 &recipient,
295 StakeAuthorize::Withdrawer,
296 None,
297 ));
298
299 if let Some(lockup_date) = lockup_date {
301 let lockup = LockupArgs {
302 unix_timestamp: Some(lockup_date.timestamp()),
303 epoch: None,
304 custodian: None,
305 };
306 instructions.push(stake_instruction::set_lockup(
307 new_stake_account_address,
308 &lockup,
309 &stake_args.lockup_authority.unwrap(),
310 ));
311 }
312
313 instructions
314 }
315 };
316
317 instructions.push(system_instruction::transfer(
319 &sender_pubkey,
320 &recipient,
321 unlocked_sol,
322 ));
323
324 instructions
325 }
326 }
327}
328
329fn build_messages(
330 client: &RpcClient,
331 db: &mut PickleDb,
332 allocations: &[TypedAllocation],
333 args: &DistributeTokensArgs,
334 exit: Arc<AtomicBool>,
335 messages: &mut Vec<Message>,
336 stake_extras: &mut StakeExtras,
337 created_accounts: &mut u64,
338) -> Result<(), Error> {
339 let mut existing_associated_token_accounts = vec![];
340 if let Some(spl_token_args) = &args.spl_token_args {
341 let allocation_chunks = allocations.chunks(MAX_MULTIPLE_ACCOUNTS);
342 for allocation_chunk in allocation_chunks {
343 let associated_token_addresses = allocation_chunk
344 .iter()
345 .map(|x| {
346 let wallet_address = x.recipient;
347 get_associated_token_address(&wallet_address, &spl_token_args.mint)
348 })
349 .collect::<Vec<_>>();
350 let mut maybe_accounts = client.get_multiple_accounts(&associated_token_addresses)?;
351 existing_associated_token_accounts.append(&mut maybe_accounts);
352 }
353 }
354
355 for (i, allocation) in allocations.iter().enumerate() {
356 if exit.load(Ordering::SeqCst) {
357 db.dump()?;
358 return Err(Error::ExitSignal);
359 }
360 let new_stake_account_keypair = Keypair::new();
361 let lockup_date = allocation.lockup_date;
362
363 let do_create_associated_token_account = if let Some(spl_token_args) = &args.spl_token_args
364 {
365 let do_create_associated_token_account =
366 existing_associated_token_accounts[i].is_none();
367 if do_create_associated_token_account {
368 *created_accounts += 1;
369 }
370 println!(
371 "{:<44} {:>24}",
372 allocation.recipient,
373 real_number_string(allocation.amount, spl_token_args.decimals)
374 );
375 do_create_associated_token_account
376 } else {
377 println!(
378 "{:<44} {:>24.9}",
379 allocation.recipient,
380 build_balance_message(allocation.amount, false, false)
381 );
382 false
383 };
384 let instructions = distribution_instructions(
385 allocation,
386 &new_stake_account_keypair.pubkey(),
387 args,
388 lockup_date,
389 do_create_associated_token_account,
390 );
391 let fee_payer_pubkey = args.fee_payer.pubkey();
392 let message = Message::new_with_blockhash(
393 &instructions,
394 Some(&fee_payer_pubkey),
395 &Hash::default(), );
397 messages.push(message);
398 stake_extras.push((new_stake_account_keypair, lockup_date));
399 }
400 Ok(())
401}
402
403fn send_messages(
404 client: &RpcClient,
405 db: &mut PickleDb,
406 allocations: &[TypedAllocation],
407 args: &DistributeTokensArgs,
408 exit: Arc<AtomicBool>,
409 messages: Vec<Message>,
410 stake_extras: StakeExtras,
411) -> Result<(), Error> {
412 for ((allocation, message), (new_stake_account_keypair, lockup_date)) in
413 allocations.iter().zip(messages).zip(stake_extras)
414 {
415 if exit.load(Ordering::SeqCst) {
416 db.dump()?;
417 return Err(Error::ExitSignal);
418 }
419 let new_stake_account_address = new_stake_account_keypair.pubkey();
420
421 let mut signers = vec![&*args.fee_payer, &*args.sender_keypair];
422 if let Some(stake_args) = &args.stake_args {
423 signers.push(&new_stake_account_keypair);
424 if let Some(sender_stake_args) = &stake_args.sender_stake_args {
425 signers.push(&*sender_stake_args.stake_authority);
426 signers.push(&*sender_stake_args.withdraw_authority);
427 signers.push(&new_stake_account_keypair);
428 if allocation.lockup_date.is_some() {
429 if let Some(lockup_authority) = &sender_stake_args.lockup_authority {
430 signers.push(&**lockup_authority);
431 } else {
432 return Err(Error::MissingLockupAuthority);
433 }
434 }
435 }
436 }
437 let signers = unique_signers(signers);
438 let result: ClientResult<(Transaction, u64)> = {
439 if args.dry_run {
440 Ok((Transaction::new_unsigned(message), u64::MAX))
441 } else {
442 let (blockhash, last_valid_block_height) =
443 client.get_latest_blockhash_with_commitment(CommitmentConfig::default())?;
444 let transaction = Transaction::new(&signers, message, blockhash);
445 let config = RpcSendTransactionConfig {
446 skip_preflight: true,
447 ..RpcSendTransactionConfig::default()
448 };
449 client.send_transaction_with_config(&transaction, config)?;
450 Ok((transaction, last_valid_block_height))
451 }
452 };
453 match result {
454 Ok((transaction, last_valid_block_height)) => {
455 let new_stake_account_address_option =
456 args.stake_args.as_ref().map(|_| &new_stake_account_address);
457 db::set_transaction_info(
458 db,
459 &allocation.recipient,
460 allocation.amount,
461 &transaction,
462 new_stake_account_address_option,
463 false,
464 last_valid_block_height,
465 lockup_date,
466 )?;
467 }
468 Err(e) => {
469 eprintln!("Error sending tokens to {}: {}", allocation.recipient, e);
470 }
471 };
472 }
473 Ok(())
474}
475
476fn distribute_allocations(
477 client: &RpcClient,
478 db: &mut PickleDb,
479 allocations: &[TypedAllocation],
480 args: &DistributeTokensArgs,
481 exit: Arc<AtomicBool>,
482) -> Result<(), Error> {
483 let mut messages: Vec<Message> = vec![];
484 let mut stake_extras: StakeExtras = vec![];
485 let mut created_accounts = 0;
486
487 build_messages(
488 client,
489 db,
490 allocations,
491 args,
492 exit.clone(),
493 &mut messages,
494 &mut stake_extras,
495 &mut created_accounts,
496 )?;
497
498 if args.spl_token_args.is_some() {
499 check_spl_token_balances(&messages, allocations, client, args, created_accounts)?;
500 } else {
501 check_payer_balances(&messages, allocations, client, args)?;
502 }
503
504 send_messages(client, db, allocations, args, exit, messages, stake_extras)?;
505
506 db.dump()?;
507 Ok(())
508}
509
510fn read_allocations(
511 input_csv: &str,
512 transfer_amount: Option<u64>,
513 with_lockup: bool,
514 raw_amount: bool,
515) -> Result<Vec<TypedAllocation>, Error> {
516 let mut rdr = ReaderBuilder::new().trim(Trim::All).from_path(input_csv)?;
517 let allocations = if let Some(amount) = transfer_amount {
518 rdr.deserialize()
519 .map(|recipient| {
520 let recipient: String = recipient?;
521 let recipient =
522 Pubkey::from_str(&recipient).map_err(|err| Error::BadInputPubkeyError {
523 input: recipient,
524 err,
525 })?;
526 Ok(TypedAllocation {
527 recipient,
528 amount,
529 lockup_date: None,
530 })
531 })
532 .collect::<Result<Vec<TypedAllocation>, Error>>()?
533 } else if with_lockup {
534 rdr.deserialize()
536 .map(|recipient| {
537 let (recipient, amount, lockup_date): (String, String, String) = recipient?;
538 let recipient =
539 Pubkey::from_str(&recipient).map_err(|err| Error::BadInputPubkeyError {
540 input: recipient,
541 err,
542 })?;
543 let lockup_date = if !lockup_date.is_empty() {
544 let lockup_date = lockup_date.parse::<DateTime<Utc>>().map_err(|err| {
545 Error::BadInputLockupDate {
546 input: lockup_date,
547 err,
548 }
549 })?;
550 Some(lockup_date)
551 } else {
552 None
554 };
555 Ok(TypedAllocation {
556 recipient,
557 amount: sol_str_to_lamports(&amount)
558 .ok_or(Error::BadInputNumberError { input: amount })?,
559 lockup_date,
560 })
561 })
562 .collect::<Result<Vec<TypedAllocation>, Error>>()?
563 } else if raw_amount {
564 rdr.deserialize()
565 .map(|recipient| {
566 let (recipient, amount): (String, u64) = recipient?;
567 let recipient =
568 Pubkey::from_str(&recipient).map_err(|err| Error::BadInputPubkeyError {
569 input: recipient,
570 err,
571 })?;
572 Ok(TypedAllocation {
573 recipient,
574 amount,
575 lockup_date: None,
576 })
577 })
578 .collect::<Result<Vec<TypedAllocation>, Error>>()?
579 } else {
580 rdr.deserialize()
581 .map(|recipient| {
582 let (recipient, amount): (String, String) = recipient?;
583 let recipient =
584 Pubkey::from_str(&recipient).map_err(|err| Error::BadInputPubkeyError {
585 input: recipient,
586 err,
587 })?;
588 Ok(TypedAllocation {
589 recipient,
590 amount: sol_str_to_lamports(&amount)
591 .ok_or(Error::BadInputNumberError { input: amount })?,
592 lockup_date: None,
593 })
594 })
595 .collect::<Result<Vec<TypedAllocation>, Error>>()?
596 };
597 if allocations.is_empty() {
598 return Err(Error::CsvIsEmptyError);
599 }
600 Ok(allocations)
601}
602
603fn new_spinner_progress_bar() -> ProgressBar {
604 let progress_bar = ProgressBar::new(42);
605 progress_bar.set_style(
606 ProgressStyle::default_spinner()
607 .template("{spinner:.green} {wide_msg}")
608 .expect("ProgresStyle::template direct input to be correct"),
609 );
610 progress_bar.enable_steady_tick(Duration::from_millis(100));
611 progress_bar
612}
613
614pub fn process_allocations(
615 client: &RpcClient,
616 args: &DistributeTokensArgs,
617 exit: Arc<AtomicBool>,
618) -> Result<Option<usize>, Error> {
619 let with_lockup = args.stake_args.is_some();
620 let mut allocations: Vec<TypedAllocation> = read_allocations(
621 &args.input_csv,
622 args.transfer_amount,
623 with_lockup,
624 args.spl_token_args.is_some(),
625 )?;
626
627 let starting_total_tokens = allocations.iter().map(|x| x.amount).sum();
628 let starting_total_tokens = if let Some(spl_token_args) = &args.spl_token_args {
629 Token::spl_token(starting_total_tokens, spl_token_args.decimals)
630 } else {
631 Token::sol(starting_total_tokens)
632 };
633 println!(
634 "{} {}",
635 style("Total in input_csv:").bold(),
636 starting_total_tokens,
637 );
638
639 let mut db = db::open_db(&args.transaction_db, args.dry_run)?;
640
641 let confirmations = finalize_transactions(client, &mut db, args.dry_run, exit.clone())?;
643
644 let transaction_infos = db::read_transaction_infos(&db);
645 apply_previous_transactions(&mut allocations, &transaction_infos);
646
647 if allocations.is_empty() {
648 eprintln!("No work to do");
649 return Ok(confirmations);
650 }
651
652 let distributed_tokens = transaction_infos.iter().map(|x| x.amount).sum();
653 let undistributed_tokens = allocations.iter().map(|x| x.amount).sum();
654 let (distributed_tokens, undistributed_tokens) =
655 if let Some(spl_token_args) = &args.spl_token_args {
656 (
657 Token::spl_token(distributed_tokens, spl_token_args.decimals),
658 Token::spl_token(undistributed_tokens, spl_token_args.decimals),
659 )
660 } else {
661 (
662 Token::sol(distributed_tokens),
663 Token::sol(undistributed_tokens),
664 )
665 };
666 println!("{} {}", style("Distributed:").bold(), distributed_tokens,);
667 println!(
668 "{} {}",
669 style("Undistributed:").bold(),
670 undistributed_tokens,
671 );
672 println!(
673 "{} {}",
674 style("Total:").bold(),
675 distributed_tokens + undistributed_tokens,
676 );
677
678 println!(
679 "{}",
680 style(format!("{:<44} {:>24}", "Recipient", "Expected Balance",)).bold()
681 );
682
683 distribute_allocations(client, &mut db, &allocations, args, exit.clone())?;
684
685 let opt_confirmations = finalize_transactions(client, &mut db, args.dry_run, exit)?;
686
687 if !args.dry_run {
688 if let Some(output_path) = &args.output_path {
689 db::write_transaction_log(&db, &output_path)?;
690 }
691 }
692
693 Ok(opt_confirmations)
694}
695
696fn finalize_transactions(
697 client: &RpcClient,
698 db: &mut PickleDb,
699 dry_run: bool,
700 exit: Arc<AtomicBool>,
701) -> Result<Option<usize>, Error> {
702 if dry_run {
703 return Ok(None);
704 }
705
706 let mut opt_confirmations = update_finalized_transactions(client, db, exit.clone())?;
707
708 let progress_bar = new_spinner_progress_bar();
709
710 while opt_confirmations.is_some() {
711 if let Some(confirmations) = opt_confirmations {
712 progress_bar.set_message(format!(
713 "[{}/{}] Finalizing transactions",
714 confirmations, 32,
715 ));
716 }
717
718 sleep(Duration::from_millis(500));
720 let opt_conf = update_finalized_transactions(client, db, exit.clone())?;
721 opt_confirmations = opt_conf;
722 }
723
724 Ok(opt_confirmations)
725}
726
727fn update_finalized_transactions(
730 client: &RpcClient,
731 db: &mut PickleDb,
732 exit: Arc<AtomicBool>,
733) -> Result<Option<usize>, Error> {
734 let transaction_infos = db::read_transaction_infos(db);
735 let unconfirmed_transactions: Vec<_> = transaction_infos
736 .iter()
737 .filter_map(|info| {
738 if info.finalized_date.is_some() {
739 None
740 } else {
741 Some((&info.transaction, info.last_valid_block_height))
742 }
743 })
744 .collect();
745 let unconfirmed_signatures: Vec<_> = unconfirmed_transactions
746 .iter()
747 .map(|(tx, _slot)| tx.signatures[0])
748 .filter(|sig| *sig != Signature::default()) .collect();
750 let mut statuses = vec![];
751 for unconfirmed_signatures_chunk in
752 unconfirmed_signatures.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS - 1)
753 {
754 statuses.extend(
755 client
756 .get_signature_statuses(unconfirmed_signatures_chunk)?
757 .value
758 .into_iter(),
759 );
760 }
761
762 let mut confirmations = None;
763 log_transaction_confirmations(
764 client,
765 db,
766 exit,
767 unconfirmed_transactions,
768 statuses,
769 &mut confirmations,
770 )?;
771 db.dump()?;
772 Ok(confirmations)
773}
774
775fn log_transaction_confirmations(
776 client: &RpcClient,
777 db: &mut PickleDb,
778 exit: Arc<AtomicBool>,
779 unconfirmed_transactions: Vec<(&Transaction, Slot)>,
780 statuses: Vec<Option<TransactionStatus>>,
781 confirmations: &mut Option<usize>,
782) -> Result<(), Error> {
783 let finalized_block_height = client.get_block_height()?;
784 for ((transaction, last_valid_block_height), opt_transaction_status) in unconfirmed_transactions
785 .into_iter()
786 .zip(statuses.into_iter())
787 {
788 match db::update_finalized_transaction(
789 db,
790 &transaction.signatures[0],
791 opt_transaction_status,
792 last_valid_block_height,
793 finalized_block_height,
794 ) {
795 Ok(Some(confs)) => {
796 *confirmations = Some(cmp::min(confs, confirmations.unwrap_or(usize::MAX)));
797 }
798 result => {
799 result?;
800 }
801 }
802 if exit.load(Ordering::SeqCst) {
803 db.dump()?;
804 return Err(Error::ExitSignal);
805 }
806 }
807 Ok(())
808}
809
810pub fn get_fee_estimate_for_messages(
811 messages: &[Message],
812 client: &RpcClient,
813) -> Result<u64, Error> {
814 let mut message = messages.first().ok_or(Error::MissingMessages)?.clone();
815 let latest_blockhash = client.get_latest_blockhash()?;
816 message.recent_blockhash = latest_blockhash;
817 let fee = client.get_fee_for_message(&message)?;
818 let fee_estimate = fee
819 .checked_mul(messages.len() as u64)
820 .ok_or(Error::FeeEstimationError)?;
821 Ok(fee_estimate)
822}
823
824fn check_payer_balances(
825 messages: &[Message],
826 allocations: &[TypedAllocation],
827 client: &RpcClient,
828 args: &DistributeTokensArgs,
829) -> Result<(), Error> {
830 let mut undistributed_tokens: u64 = allocations.iter().map(|x| x.amount).sum();
831 let fees = get_fee_estimate_for_messages(messages, client)?;
832
833 let (distribution_source, unlocked_sol_source) = if let Some(stake_args) = &args.stake_args {
834 let total_unlocked_sol = allocations.len() as u64 * stake_args.unlocked_sol;
835 undistributed_tokens -= total_unlocked_sol;
836 let from_pubkey = if let Some(sender_stake_args) = &stake_args.sender_stake_args {
837 sender_stake_args.stake_account_address
838 } else {
839 args.sender_keypair.pubkey()
840 };
841 (
842 from_pubkey,
843 Some((args.sender_keypair.pubkey(), total_unlocked_sol)),
844 )
845 } else {
846 (args.sender_keypair.pubkey(), None)
847 };
848
849 let fee_payer_balance = client.get_balance(&args.fee_payer.pubkey())?;
850 if let Some((unlocked_sol_source, total_unlocked_sol)) = unlocked_sol_source {
851 let staker_balance = client.get_balance(&distribution_source)?;
852 if staker_balance < undistributed_tokens {
853 return Err(Error::InsufficientFunds(
854 vec![FundingSource::StakeAccount].into(),
855 build_balance_message(undistributed_tokens, false, false).to_string(),
856 ));
857 }
858 if args.fee_payer.pubkey() == unlocked_sol_source {
859 if fee_payer_balance < fees + total_unlocked_sol {
860 return Err(Error::InsufficientFunds(
861 vec![FundingSource::SystemAccount, FundingSource::FeePayer].into(),
862 build_balance_message(fees + total_unlocked_sol, false, false).to_string(),
863 ));
864 }
865 } else {
866 if fee_payer_balance < fees {
867 return Err(Error::InsufficientFunds(
868 vec![FundingSource::FeePayer].into(),
869 build_balance_message(fees, false, false).to_string(),
870 ));
871 }
872 let unlocked_sol_balance = client.get_balance(&unlocked_sol_source)?;
873 if unlocked_sol_balance < total_unlocked_sol {
874 return Err(Error::InsufficientFunds(
875 vec![FundingSource::SystemAccount].into(),
876 build_balance_message(total_unlocked_sol, false, false).to_string(),
877 ));
878 }
879 }
880 } else if args.fee_payer.pubkey() == distribution_source {
881 if fee_payer_balance < fees + undistributed_tokens {
882 return Err(Error::InsufficientFunds(
883 vec![FundingSource::SystemAccount, FundingSource::FeePayer].into(),
884 build_balance_message(fees + undistributed_tokens, false, false).to_string(),
885 ));
886 }
887 } else {
888 if fee_payer_balance < fees {
889 return Err(Error::InsufficientFunds(
890 vec![FundingSource::FeePayer].into(),
891 build_balance_message(fees, false, false).to_string(),
892 ));
893 }
894 let sender_balance = client.get_balance(&distribution_source)?;
895 if sender_balance < undistributed_tokens {
896 return Err(Error::InsufficientFunds(
897 vec![FundingSource::SystemAccount].into(),
898 build_balance_message(undistributed_tokens, false, false).to_string(),
899 ));
900 }
901 }
902 Ok(())
903}
904
905pub fn process_balances(
906 client: &RpcClient,
907 args: &BalancesArgs,
908 exit: Arc<AtomicBool>,
909) -> Result<(), Error> {
910 let allocations: Vec<TypedAllocation> =
911 read_allocations(&args.input_csv, None, false, args.spl_token_args.is_some())?;
912 let allocations = merge_allocations(&allocations);
913
914 let token = if let Some(spl_token_args) = &args.spl_token_args {
915 spl_token_args.mint.to_string()
916 } else {
917 "â—Ž".to_string()
918 };
919 println!("{} {}", style("Token:").bold(), token);
920
921 println!(
922 "{}",
923 style(format!(
924 "{:<44} {:>24} {:>24} {:>24}",
925 "Recipient", "Expected Balance", "Actual Balance", "Difference"
926 ))
927 .bold()
928 );
929
930 for allocation in &allocations {
931 if exit.load(Ordering::SeqCst) {
932 return Err(Error::ExitSignal);
933 }
934
935 if let Some(spl_token_args) = &args.spl_token_args {
936 print_token_balances(client, allocation, spl_token_args)?;
937 } else {
938 let address: Pubkey = allocation.recipient;
939 let expected = build_balance_message(allocation.amount, false, false);
940 let actual_amount = client.get_balance(&address).unwrap();
941 let actual = build_balance_message(actual_amount, false, false);
942 let diff = build_balance_message(actual_amount - allocation.amount, false, false);
943 println!(
944 "{:<44} {:>24.9} {:>24.9} {:>24.9}",
945 allocation.recipient, expected, actual, diff,
946 );
947 }
948 }
949
950 Ok(())
951}
952
953pub fn process_transaction_log(args: &TransactionLogArgs) -> Result<(), Error> {
954 let db = db::open_db(&args.transaction_db, true)?;
955 db::write_transaction_log(&db, &args.output_path)?;
956 Ok(())
957}
958
959use {
960 crate::db::check_output_file,
961 solana_keypair::Keypair,
962 solana_pubkey::{self as pubkey, Pubkey},
963 tempfile::{tempdir, NamedTempFile},
964};
965
966pub fn test_process_distribute_tokens_with_client(
967 client: &RpcClient,
968 sender_keypair: Keypair,
969 transfer_amount: Option<u64>,
970) {
971 let exit = Arc::new(AtomicBool::default());
972 let fee_payer = Keypair::new();
973 let transaction = transfer(
974 client,
975 sol_str_to_lamports("1.0").unwrap(),
976 &sender_keypair,
977 &fee_payer.pubkey(),
978 )
979 .unwrap();
980 client
981 .send_and_confirm_transaction_with_spinner(&transaction)
982 .unwrap();
983 assert_eq!(
984 client.get_balance(&fee_payer.pubkey()).unwrap(),
985 sol_str_to_lamports("1.0").unwrap(),
986 );
987
988 let expected_amount = if let Some(amount) = transfer_amount {
989 amount
990 } else {
991 sol_str_to_lamports("1000.0").unwrap()
992 };
993 let alice_pubkey = pubkey::new_rand();
994 let allocations_file = NamedTempFile::new().unwrap();
995 let input_csv = allocations_file.path().to_str().unwrap().to_string();
996 let mut wtr = csv::WriterBuilder::new().from_writer(allocations_file);
997 wtr.write_record(["recipient", "amount"]).unwrap();
998 wtr.write_record([
999 alice_pubkey.to_string(),
1000 build_balance_message(expected_amount, false, false).to_string(),
1001 ])
1002 .unwrap();
1003 wtr.flush().unwrap();
1004
1005 let dir = tempdir().unwrap();
1006 let transaction_db = dir
1007 .path()
1008 .join("transactions.db")
1009 .to_str()
1010 .unwrap()
1011 .to_string();
1012
1013 let output_file = NamedTempFile::new().unwrap();
1014 let output_path = output_file.path().to_str().unwrap().to_string();
1015
1016 let args = DistributeTokensArgs {
1017 sender_keypair: Box::new(sender_keypair),
1018 fee_payer: Box::new(fee_payer),
1019 dry_run: false,
1020 input_csv,
1021 transaction_db: transaction_db.clone(),
1022 output_path: Some(output_path.clone()),
1023 stake_args: None,
1024 spl_token_args: None,
1025 transfer_amount,
1026 };
1027 let confirmations = process_allocations(client, &args, exit.clone()).unwrap();
1028 assert_eq!(confirmations, None);
1029
1030 let transaction_infos =
1031 db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
1032 assert_eq!(transaction_infos.len(), 1);
1033 assert_eq!(transaction_infos[0].recipient, alice_pubkey);
1034 assert_eq!(transaction_infos[0].amount, expected_amount);
1035
1036 assert_eq!(client.get_balance(&alice_pubkey).unwrap(), expected_amount);
1037
1038 check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
1039
1040 process_allocations(client, &args, exit).unwrap();
1042 let transaction_infos =
1043 db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
1044 assert_eq!(transaction_infos.len(), 1);
1045 assert_eq!(transaction_infos[0].recipient, alice_pubkey);
1046 assert_eq!(transaction_infos[0].amount, expected_amount);
1047
1048 assert_eq!(client.get_balance(&alice_pubkey).unwrap(), expected_amount);
1049
1050 check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
1051}
1052
1053pub fn test_process_create_stake_with_client(client: &RpcClient, sender_keypair: Keypair) {
1054 let exit = Arc::new(AtomicBool::default());
1055 let fee_payer = Keypair::new();
1056 let transaction = transfer(
1057 client,
1058 sol_str_to_lamports("1.0").unwrap(),
1059 &sender_keypair,
1060 &fee_payer.pubkey(),
1061 )
1062 .unwrap();
1063 client
1064 .send_and_confirm_transaction_with_spinner(&transaction)
1065 .unwrap();
1066
1067 let stake_account_keypair = Keypair::new();
1068 let stake_account_address = stake_account_keypair.pubkey();
1069 let stake_authority = Keypair::new();
1070 let withdraw_authority = Keypair::new();
1071
1072 let authorized = Authorized {
1073 staker: stake_authority.pubkey(),
1074 withdrawer: withdraw_authority.pubkey(),
1075 };
1076 let lockup = Lockup::default();
1077 let instructions = stake_instruction::create_account(
1078 &sender_keypair.pubkey(),
1079 &stake_account_address,
1080 &authorized,
1081 &lockup,
1082 sol_str_to_lamports("3000.0").unwrap(),
1083 );
1084 let message = Message::new(&instructions, Some(&sender_keypair.pubkey()));
1085 let signers = [&sender_keypair, &stake_account_keypair];
1086 let blockhash = client.get_latest_blockhash().unwrap();
1087 let transaction = Transaction::new(&signers, message, blockhash);
1088 client
1089 .send_and_confirm_transaction_with_spinner(&transaction)
1090 .unwrap();
1091
1092 let expected_amount = sol_str_to_lamports("1000.0").unwrap();
1093 let alice_pubkey = pubkey::new_rand();
1094 let file = NamedTempFile::new().unwrap();
1095 let input_csv = file.path().to_str().unwrap().to_string();
1096 let mut wtr = csv::WriterBuilder::new().from_writer(file);
1097 wtr.write_record(["recipient", "amount", "lockup_date"])
1098 .unwrap();
1099 wtr.write_record([
1100 alice_pubkey.to_string(),
1101 build_balance_message(expected_amount, false, false).to_string(),
1102 "".to_string(),
1103 ])
1104 .unwrap();
1105 wtr.flush().unwrap();
1106
1107 let dir = tempdir().unwrap();
1108 let transaction_db = dir
1109 .path()
1110 .join("transactions.db")
1111 .to_str()
1112 .unwrap()
1113 .to_string();
1114
1115 let output_file = NamedTempFile::new().unwrap();
1116 let output_path = output_file.path().to_str().unwrap().to_string();
1117
1118 let stake_args = StakeArgs {
1119 lockup_authority: None,
1120 unlocked_sol: sol_str_to_lamports("1.0").unwrap(),
1121 sender_stake_args: None,
1122 };
1123 let args = DistributeTokensArgs {
1124 fee_payer: Box::new(fee_payer),
1125 dry_run: false,
1126 input_csv,
1127 transaction_db: transaction_db.clone(),
1128 output_path: Some(output_path.clone()),
1129 stake_args: Some(stake_args),
1130 spl_token_args: None,
1131 sender_keypair: Box::new(sender_keypair),
1132 transfer_amount: None,
1133 };
1134 let confirmations = process_allocations(client, &args, exit.clone()).unwrap();
1135 assert_eq!(confirmations, None);
1136
1137 let transaction_infos =
1138 db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
1139 assert_eq!(transaction_infos.len(), 1);
1140 assert_eq!(transaction_infos[0].recipient, alice_pubkey);
1141 assert_eq!(transaction_infos[0].amount, expected_amount);
1142
1143 assert_eq!(
1144 client.get_balance(&alice_pubkey).unwrap(),
1145 sol_str_to_lamports("1.0").unwrap(),
1146 );
1147 let new_stake_account_address = transaction_infos[0].new_stake_account_address.unwrap();
1148 assert_eq!(
1149 client.get_balance(&new_stake_account_address).unwrap(),
1150 expected_amount - sol_str_to_lamports("1.0").unwrap(),
1151 );
1152
1153 check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
1154
1155 process_allocations(client, &args, exit).unwrap();
1157 let transaction_infos =
1158 db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
1159 assert_eq!(transaction_infos.len(), 1);
1160 assert_eq!(transaction_infos[0].recipient, alice_pubkey);
1161 assert_eq!(transaction_infos[0].amount, expected_amount);
1162
1163 assert_eq!(
1164 client.get_balance(&alice_pubkey).unwrap(),
1165 sol_str_to_lamports("1.0").unwrap(),
1166 );
1167 assert_eq!(
1168 client.get_balance(&new_stake_account_address).unwrap(),
1169 expected_amount - sol_str_to_lamports("1.0").unwrap(),
1170 );
1171
1172 check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
1173}
1174
1175pub fn test_process_distribute_stake_with_client(client: &RpcClient, sender_keypair: Keypair) {
1176 let exit = Arc::new(AtomicBool::default());
1177 let fee_payer = Keypair::new();
1178 let transaction = transfer(
1179 client,
1180 sol_str_to_lamports("1.0").unwrap(),
1181 &sender_keypair,
1182 &fee_payer.pubkey(),
1183 )
1184 .unwrap();
1185 client
1186 .send_and_confirm_transaction_with_spinner(&transaction)
1187 .unwrap();
1188
1189 let stake_account_keypair = Keypair::new();
1190 let stake_account_address = stake_account_keypair.pubkey();
1191 let stake_authority = Keypair::new();
1192 let withdraw_authority = Keypair::new();
1193
1194 let authorized = Authorized {
1195 staker: stake_authority.pubkey(),
1196 withdrawer: withdraw_authority.pubkey(),
1197 };
1198 let lockup = Lockup::default();
1199 let instructions = stake_instruction::create_account(
1200 &sender_keypair.pubkey(),
1201 &stake_account_address,
1202 &authorized,
1203 &lockup,
1204 sol_str_to_lamports("3000.0").unwrap(),
1205 );
1206 let message = Message::new(&instructions, Some(&sender_keypair.pubkey()));
1207 let signers = [&sender_keypair, &stake_account_keypair];
1208 let blockhash = client.get_latest_blockhash().unwrap();
1209 let transaction = Transaction::new(&signers, message, blockhash);
1210 client
1211 .send_and_confirm_transaction_with_spinner(&transaction)
1212 .unwrap();
1213
1214 let expected_amount = sol_str_to_lamports("1000.0").unwrap();
1215 let alice_pubkey = pubkey::new_rand();
1216 let file = NamedTempFile::new().unwrap();
1217 let input_csv = file.path().to_str().unwrap().to_string();
1218 let mut wtr = csv::WriterBuilder::new().from_writer(file);
1219 wtr.write_record(["recipient", "amount", "lockup_date"])
1220 .unwrap();
1221 wtr.write_record([
1222 alice_pubkey.to_string(),
1223 build_balance_message(expected_amount, false, false).to_string(),
1224 "".to_string(),
1225 ])
1226 .unwrap();
1227 wtr.flush().unwrap();
1228
1229 let dir = tempdir().unwrap();
1230 let transaction_db = dir
1231 .path()
1232 .join("transactions.db")
1233 .to_str()
1234 .unwrap()
1235 .to_string();
1236
1237 let output_file = NamedTempFile::new().unwrap();
1238 let output_path = output_file.path().to_str().unwrap().to_string();
1239
1240 let rent_exempt_reserve = client
1241 .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())
1242 .unwrap();
1243 let sender_stake_args = SenderStakeArgs {
1244 stake_account_address,
1245 stake_authority: Box::new(stake_authority),
1246 withdraw_authority: Box::new(withdraw_authority),
1247 lockup_authority: None,
1248 rent_exempt_reserve: Some(rent_exempt_reserve),
1249 };
1250 let stake_args = StakeArgs {
1251 unlocked_sol: sol_str_to_lamports("1.0").unwrap(),
1252 lockup_authority: None,
1253 sender_stake_args: Some(sender_stake_args),
1254 };
1255 let args = DistributeTokensArgs {
1256 fee_payer: Box::new(fee_payer),
1257 dry_run: false,
1258 input_csv,
1259 transaction_db: transaction_db.clone(),
1260 output_path: Some(output_path.clone()),
1261 stake_args: Some(stake_args),
1262 spl_token_args: None,
1263 sender_keypair: Box::new(sender_keypair),
1264 transfer_amount: None,
1265 };
1266 let confirmations = process_allocations(client, &args, exit.clone()).unwrap();
1267 assert_eq!(confirmations, None);
1268
1269 let transaction_infos =
1270 db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
1271 assert_eq!(transaction_infos.len(), 1);
1272 assert_eq!(transaction_infos[0].recipient, alice_pubkey);
1273 assert_eq!(transaction_infos[0].amount, expected_amount);
1274
1275 assert_eq!(
1276 client.get_balance(&alice_pubkey).unwrap(),
1277 sol_str_to_lamports("1.0").unwrap(),
1278 );
1279 let new_stake_account_address = transaction_infos[0].new_stake_account_address.unwrap();
1280 assert_eq!(
1281 client.get_balance(&new_stake_account_address).unwrap(),
1282 expected_amount - sol_str_to_lamports("1.0").unwrap(),
1283 );
1284
1285 check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
1286
1287 process_allocations(client, &args, exit).unwrap();
1289 let transaction_infos =
1290 db::read_transaction_infos(&db::open_db(&transaction_db, true).unwrap());
1291 assert_eq!(transaction_infos.len(), 1);
1292 assert_eq!(transaction_infos[0].recipient, alice_pubkey);
1293 assert_eq!(transaction_infos[0].amount, expected_amount);
1294
1295 assert_eq!(
1296 client.get_balance(&alice_pubkey).unwrap(),
1297 sol_str_to_lamports("1.0").unwrap(),
1298 );
1299 assert_eq!(
1300 client.get_balance(&new_stake_account_address).unwrap(),
1301 expected_amount - sol_str_to_lamports("1.0").unwrap(),
1302 );
1303
1304 check_output_file(&output_path, &db::open_db(&transaction_db, true).unwrap());
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use {
1310 super::*,
1311 solana_instruction::AccountMeta,
1312 solana_keypair::{read_keypair_file, write_keypair_file},
1313 solana_native_token::LAMPORTS_PER_SOL,
1314 solana_signer::Signer,
1315 solana_stake_interface::instruction::StakeInstruction,
1316 solana_streamer::socket::SocketAddrSpace,
1317 solana_test_validator::TestValidator,
1318 solana_transaction_status::TransactionConfirmationStatus,
1319 std::slice,
1320 };
1321
1322 fn one_signer_message(client: &RpcClient) -> Message {
1323 Message::new_with_blockhash(
1324 &[Instruction::new_with_bytes(
1325 Pubkey::new_unique(),
1326 &[],
1327 vec![AccountMeta::new(Pubkey::default(), true)],
1328 )],
1329 None,
1330 &client.get_latest_blockhash().unwrap(),
1331 )
1332 }
1333
1334 #[test]
1335 fn test_process_token_allocations() {
1336 let alice = Keypair::new();
1337 let test_validator = simple_test_validator_no_fees(alice.pubkey());
1338 let url = test_validator.rpc_url();
1339
1340 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
1341 test_process_distribute_tokens_with_client(&client, alice, None);
1342 }
1343
1344 #[test]
1345 fn test_process_transfer_amount_allocations() {
1346 let alice = Keypair::new();
1347 let test_validator = simple_test_validator_no_fees(alice.pubkey());
1348 let url = test_validator.rpc_url();
1349
1350 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
1351 test_process_distribute_tokens_with_client(&client, alice, sol_str_to_lamports("1.5"));
1352 }
1353
1354 fn simple_test_validator_no_fees(pubkey: Pubkey) -> TestValidator {
1355 TestValidator::with_no_fees(pubkey, None, SocketAddrSpace::Unspecified)
1356 }
1357
1358 #[test]
1359 fn test_create_stake_allocations() {
1360 let alice = Keypair::new();
1361 let test_validator = simple_test_validator_no_fees(alice.pubkey());
1362 let url = test_validator.rpc_url();
1363
1364 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
1365 test_process_create_stake_with_client(&client, alice);
1366 }
1367
1368 #[test]
1369 fn test_process_stake_allocations() {
1370 let alice = Keypair::new();
1371 let test_validator = simple_test_validator_no_fees(alice.pubkey());
1372 let url = test_validator.rpc_url();
1373
1374 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
1375 test_process_distribute_stake_with_client(&client, alice);
1376 }
1377
1378 #[test]
1379 fn test_read_allocations() {
1380 let alice_pubkey = pubkey::new_rand();
1381 let allocation = TypedAllocation {
1382 recipient: alice_pubkey,
1383 amount: 42,
1384 lockup_date: None,
1385 };
1386 let file = NamedTempFile::new().unwrap();
1387 let input_csv = file.path().to_str().unwrap().to_string();
1388 let mut wtr = csv::WriterBuilder::new().from_writer(file);
1389 wtr.serialize((
1390 "recipient".to_string(),
1391 "amount".to_string(),
1392 "require_lockup".to_string(),
1393 ))
1394 .unwrap();
1395 wtr.serialize((
1396 allocation.recipient.to_string(),
1397 allocation.amount,
1398 allocation.lockup_date,
1399 ))
1400 .unwrap();
1401 wtr.flush().unwrap();
1402
1403 assert_eq!(
1404 read_allocations(&input_csv, None, false, true).unwrap(),
1405 vec![allocation]
1406 );
1407
1408 let allocation_sol = TypedAllocation {
1409 recipient: alice_pubkey,
1410 amount: sol_str_to_lamports("42.0").unwrap(),
1411 lockup_date: None,
1412 };
1413
1414 assert_eq!(
1415 read_allocations(&input_csv, None, true, true).unwrap(),
1416 vec![allocation_sol.clone()]
1417 );
1418 assert_eq!(
1419 read_allocations(&input_csv, None, false, false).unwrap(),
1420 vec![allocation_sol.clone()]
1421 );
1422 assert_eq!(
1423 read_allocations(&input_csv, None, true, false).unwrap(),
1424 vec![allocation_sol]
1425 );
1426 }
1427
1428 #[test]
1429 fn test_read_allocations_no_lockup() {
1430 let pubkey0 = pubkey::new_rand();
1431 let pubkey1 = pubkey::new_rand();
1432 let file = NamedTempFile::new().unwrap();
1433 let input_csv = file.path().to_str().unwrap().to_string();
1434 let mut wtr = csv::WriterBuilder::new().from_writer(file);
1435 wtr.serialize(("recipient".to_string(), "amount".to_string()))
1436 .unwrap();
1437 wtr.serialize((&pubkey0.to_string(), 42.0)).unwrap();
1438 wtr.serialize((&pubkey1.to_string(), 43.0)).unwrap();
1439 wtr.flush().unwrap();
1440
1441 let expected_allocations = vec![
1442 TypedAllocation {
1443 recipient: pubkey0,
1444 amount: sol_str_to_lamports("42.0").unwrap(),
1445 lockup_date: None,
1446 },
1447 TypedAllocation {
1448 recipient: pubkey1,
1449 amount: sol_str_to_lamports("43.0").unwrap(),
1450 lockup_date: None,
1451 },
1452 ];
1453 assert_eq!(
1454 read_allocations(&input_csv, None, false, false).unwrap(),
1455 expected_allocations
1456 );
1457 }
1458
1459 #[test]
1460 fn test_read_allocations_malformed() {
1461 let pubkey0 = pubkey::new_rand();
1462 let pubkey1 = pubkey::new_rand();
1463
1464 let file = NamedTempFile::new().unwrap();
1466 let mut wtr = csv::WriterBuilder::new().from_writer(&file);
1467 wtr.flush().unwrap();
1468 let input_csv = file.path().to_str().unwrap().to_string();
1469 let got = read_allocations(&input_csv, None, false, false);
1470 assert!(matches!(got, Err(Error::CsvIsEmptyError)));
1471
1472 let file = NamedTempFile::new().unwrap();
1474 let mut wtr = csv::WriterBuilder::new().from_writer(&file);
1475 wtr.serialize("recipient".to_string()).unwrap();
1476 wtr.serialize(pubkey0.to_string()).unwrap();
1477 wtr.serialize(pubkey1.to_string()).unwrap();
1478 wtr.flush().unwrap();
1479 let input_csv = file.path().to_str().unwrap().to_string();
1480 let got = read_allocations(&input_csv, None, false, false);
1481 assert!(matches!(got, Err(Error::CsvError(..))));
1482
1483 let file = NamedTempFile::new().unwrap();
1485 let mut wtr = csv::WriterBuilder::new().from_writer(&file);
1486 wtr.serialize(("recipient".to_string(), "amount".to_string()))
1487 .unwrap();
1488 wtr.serialize((pubkey0.to_string(), "42.0".to_string()))
1489 .unwrap();
1490 wtr.serialize((pubkey1.to_string(), "43.0".to_string()))
1491 .unwrap();
1492 wtr.flush().unwrap();
1493 let input_csv = file.path().to_str().unwrap().to_string();
1494 let got = read_allocations(&input_csv, None, true, false);
1495 assert!(matches!(got, Err(Error::CsvError(..))));
1496
1497 let generate_csv_file = |header: (String, String, String),
1498 data: Vec<(String, String, String)>,
1499 file: &NamedTempFile| {
1500 let mut wtr = csv::WriterBuilder::new().from_writer(file);
1501 wtr.serialize(header).unwrap();
1502 wtr.serialize(&data[0]).unwrap();
1503 wtr.serialize(&data[1]).unwrap();
1504 wtr.flush().unwrap();
1505 };
1506
1507 let default_header = (
1508 "recipient".to_string(),
1509 "amount".to_string(),
1510 "require_lockup".to_string(),
1511 );
1512
1513 let file = NamedTempFile::new().unwrap();
1515 generate_csv_file(
1516 default_header.clone(),
1517 vec![
1518 (pubkey0.to_string(), "42.0".to_string(), "".to_string()),
1519 ("bad pubkey".to_string(), "43.0".to_string(), "".to_string()),
1520 ],
1521 &file,
1522 );
1523 let input_csv = file.path().to_str().unwrap().to_string();
1524 let got_err = read_allocations(&input_csv, None, false, false).unwrap_err();
1525 assert!(
1526 matches!(got_err, Error::BadInputPubkeyError { input, .. } if input == *"bad pubkey")
1527 );
1528 let file = NamedTempFile::new().unwrap();
1530 generate_csv_file(
1531 default_header.clone(),
1532 vec![
1533 (pubkey0.to_string(), "42.0".to_string(), "".to_string()),
1534 ("bad pubkey".to_string(), "43.0".to_string(), "".to_string()),
1535 ],
1536 &file,
1537 );
1538 let input_csv = file.path().to_str().unwrap().to_string();
1539 let got_err = read_allocations(&input_csv, Some(123), false, false).unwrap_err();
1540 assert!(
1541 matches!(got_err, Error::BadInputPubkeyError { input, .. } if input == *"bad pubkey")
1542 );
1543 let file = NamedTempFile::new().unwrap();
1545 generate_csv_file(
1546 default_header.clone(),
1547 vec![
1548 (
1549 pubkey0.to_string(),
1550 "42.0".to_string(),
1551 "2021-02-07T00:00:00Z".to_string(),
1552 ),
1553 (
1554 "bad pubkey".to_string(),
1555 "43.0".to_string(),
1556 "2021-02-07T00:00:00Z".to_string(),
1557 ),
1558 ],
1559 &file,
1560 );
1561 let input_csv = file.path().to_str().unwrap().to_string();
1562 let got_err = read_allocations(&input_csv, None, true, false).unwrap_err();
1563 assert!(
1564 matches!(got_err, Error::BadInputPubkeyError { input, .. } if input == *"bad pubkey")
1565 );
1566 let file = NamedTempFile::new().unwrap();
1568 generate_csv_file(
1569 default_header.clone(),
1570 vec![
1571 (pubkey0.to_string(), "42".to_string(), "".to_string()),
1572 ("bad pubkey".to_string(), "43".to_string(), "".to_string()),
1573 ],
1574 &file,
1575 );
1576 let input_csv = file.path().to_str().unwrap().to_string();
1577 let got_err = read_allocations(&input_csv, None, false, true).unwrap_err();
1578 assert!(
1579 matches!(got_err, Error::BadInputPubkeyError { input, .. } if input == *"bad pubkey")
1580 );
1581
1582 let file = NamedTempFile::new().unwrap();
1584 generate_csv_file(
1585 default_header.clone(),
1586 vec![
1587 (
1588 pubkey0.to_string(),
1589 "bad amount".to_string(),
1590 "".to_string(),
1591 ),
1592 (
1593 pubkey1.to_string(),
1594 "43.0".to_string().to_string(),
1595 "".to_string(),
1596 ),
1597 ],
1598 &file,
1599 );
1600 let input_csv = file.path().to_str().unwrap().to_string();
1601 let got = read_allocations(&input_csv, None, false, false);
1602 assert!(matches!(got, Err(Error::BadInputNumberError { .. })));
1603 let file = NamedTempFile::new().unwrap();
1605 generate_csv_file(
1606 default_header.clone(),
1607 vec![
1608 (
1609 pubkey0.to_string(),
1610 "bad amount".to_string(),
1611 "".to_string(),
1612 ),
1613 (pubkey1.to_string(), "43.0".to_string(), "".to_string()),
1614 ],
1615 &file,
1616 );
1617 let input_csv = file.path().to_str().unwrap().to_string();
1618 let got = read_allocations(&input_csv, None, true, false);
1619 assert!(matches!(got, Err(Error::BadInputNumberError { .. })));
1620 let file = NamedTempFile::new().unwrap();
1622 generate_csv_file(
1623 default_header.clone(),
1624 vec![
1625 (pubkey0.to_string(), "42".to_string(), "".to_string()),
1626 (pubkey1.to_string(), "43.0".to_string(), "".to_string()), ],
1628 &file,
1629 );
1630 let input_csv = file.path().to_str().unwrap().to_string();
1631 let got = read_allocations(&input_csv, None, false, true);
1632 assert!(matches!(got, Err(Error::CsvError(..))));
1633
1634 let file = NamedTempFile::new().unwrap();
1636 generate_csv_file(
1637 default_header.clone(),
1638 vec![
1639 (
1640 pubkey0.to_string(),
1641 "42.0".to_string(),
1642 "2021-01-07T00:00:00Z".to_string(),
1643 ),
1644 (
1645 pubkey1.to_string(),
1646 "43.0".to_string(),
1647 "bad lockup date".to_string(),
1648 ),
1649 ],
1650 &file,
1651 );
1652 let input_csv = file.path().to_str().unwrap().to_string();
1653 let got_err = read_allocations(&input_csv, None, true, false).unwrap_err();
1654 assert!(
1655 matches!(got_err, Error::BadInputLockupDate { input, .. } if input == *"bad lockup date")
1656 );
1657 }
1658
1659 #[test]
1660 fn test_read_allocations_transfer_amount() {
1661 let pubkey0 = pubkey::new_rand();
1662 let pubkey1 = pubkey::new_rand();
1663 let pubkey2 = pubkey::new_rand();
1664 let file = NamedTempFile::new().unwrap();
1665 let input_csv = file.path().to_str().unwrap().to_string();
1666 let mut wtr = csv::WriterBuilder::new().from_writer(file);
1667 wtr.serialize("recipient".to_string()).unwrap();
1668 wtr.serialize(pubkey0.to_string()).unwrap();
1669 wtr.serialize(pubkey1.to_string()).unwrap();
1670 wtr.serialize(pubkey2.to_string()).unwrap();
1671 wtr.flush().unwrap();
1672
1673 let amount = sol_str_to_lamports("1.5").unwrap();
1674
1675 let expected_allocations = vec![
1676 TypedAllocation {
1677 recipient: pubkey0,
1678 amount,
1679 lockup_date: None,
1680 },
1681 TypedAllocation {
1682 recipient: pubkey1,
1683 amount,
1684 lockup_date: None,
1685 },
1686 TypedAllocation {
1687 recipient: pubkey2,
1688 amount,
1689 lockup_date: None,
1690 },
1691 ];
1692 assert_eq!(
1693 read_allocations(&input_csv, Some(amount), false, false).unwrap(),
1694 expected_allocations
1695 );
1696 }
1697
1698 #[test]
1699 fn test_apply_previous_transactions() {
1700 let alice = pubkey::new_rand();
1701 let bob = pubkey::new_rand();
1702 let mut allocations = vec![
1703 TypedAllocation {
1704 recipient: alice,
1705 amount: sol_str_to_lamports("1.0").unwrap(),
1706 lockup_date: None,
1707 },
1708 TypedAllocation {
1709 recipient: bob,
1710 amount: sol_str_to_lamports("1.0").unwrap(),
1711 lockup_date: None,
1712 },
1713 ];
1714 let transaction_infos = vec![TransactionInfo {
1715 recipient: bob,
1716 amount: sol_str_to_lamports("1.0").unwrap(),
1717 ..TransactionInfo::default()
1718 }];
1719 apply_previous_transactions(&mut allocations, &transaction_infos);
1720 assert_eq!(allocations.len(), 1);
1721
1722 assert_eq!(allocations[0].recipient, alice);
1725 }
1726
1727 #[test]
1728 fn test_has_same_recipient() {
1729 let alice_pubkey = pubkey::new_rand();
1730 let bob_pubkey = pubkey::new_rand();
1731 let lockup0 = "2021-01-07T00:00:00Z".to_string();
1732 let lockup1 = "9999-12-31T23:59:59Z".to_string();
1733 let alice_alloc = TypedAllocation {
1734 recipient: alice_pubkey,
1735 amount: sol_str_to_lamports("1.0").unwrap(),
1736 lockup_date: None,
1737 };
1738 let alice_alloc_lockup0 = TypedAllocation {
1739 recipient: alice_pubkey,
1740 amount: sol_str_to_lamports("1.0").unwrap(),
1741 lockup_date: lockup0.parse().ok(),
1742 };
1743 let alice_info = TransactionInfo {
1744 recipient: alice_pubkey,
1745 lockup_date: None,
1746 ..TransactionInfo::default()
1747 };
1748 let alice_info_lockup0 = TransactionInfo {
1749 recipient: alice_pubkey,
1750 lockup_date: lockup0.parse().ok(),
1751 ..TransactionInfo::default()
1752 };
1753 let alice_info_lockup1 = TransactionInfo {
1754 recipient: alice_pubkey,
1755 lockup_date: lockup1.parse().ok(),
1756 ..TransactionInfo::default()
1757 };
1758 let bob_info = TransactionInfo {
1759 recipient: bob_pubkey,
1760 lockup_date: None,
1761 ..TransactionInfo::default()
1762 };
1763 assert!(!has_same_recipient(&alice_alloc, &bob_info)); assert!(!has_same_recipient(&alice_alloc, &alice_info_lockup0)); assert!(!has_same_recipient(
1766 &alice_alloc_lockup0,
1767 &alice_info_lockup1
1768 )); assert!(has_same_recipient(&alice_alloc, &alice_info)); assert!(has_same_recipient(
1771 &alice_alloc_lockup0,
1772 &alice_info_lockup0
1773 )); }
1775
1776 const SET_LOCKUP_INDEX: usize = 6;
1777
1778 #[test]
1779 fn test_set_split_stake_lockup() {
1780 let lockup_date_str = "2021-01-07T00:00:00Z";
1781 let allocation = TypedAllocation {
1782 recipient: Pubkey::default(),
1783 amount: sol_str_to_lamports("1.002282880").unwrap(),
1784 lockup_date: lockup_date_str.parse().ok(),
1785 };
1786 let stake_account_address = pubkey::new_rand();
1787 let new_stake_account_address = pubkey::new_rand();
1788 let lockup_authority = Keypair::new();
1789 let lockup_authority_address = lockup_authority.pubkey();
1790 let sender_stake_args = SenderStakeArgs {
1791 stake_account_address,
1792 stake_authority: Box::new(Keypair::new()),
1793 withdraw_authority: Box::new(Keypair::new()),
1794 lockup_authority: Some(Box::new(lockup_authority)),
1795 rent_exempt_reserve: Some(2_282_880),
1796 };
1797 let stake_args = StakeArgs {
1798 lockup_authority: Some(lockup_authority_address),
1799 unlocked_sol: sol_str_to_lamports("1.0").unwrap(),
1800 sender_stake_args: Some(sender_stake_args),
1801 };
1802 let args = DistributeTokensArgs {
1803 fee_payer: Box::new(Keypair::new()),
1804 dry_run: false,
1805 input_csv: "".to_string(),
1806 transaction_db: "".to_string(),
1807 output_path: None,
1808 stake_args: Some(stake_args),
1809 spl_token_args: None,
1810 sender_keypair: Box::new(Keypair::new()),
1811 transfer_amount: None,
1812 };
1813 let lockup_date = lockup_date_str.parse().unwrap();
1814 let instructions = distribution_instructions(
1815 &allocation,
1816 &new_stake_account_address,
1817 &args,
1818 Some(lockup_date),
1819 false,
1820 );
1821 let lockup_instruction =
1822 bincode::deserialize(&instructions[SET_LOCKUP_INDEX].data).unwrap();
1823 if let StakeInstruction::SetLockup(lockup_args) = lockup_instruction {
1824 assert_eq!(lockup_args.unix_timestamp, Some(lockup_date.timestamp()));
1825 assert_eq!(lockup_args.epoch, None); assert_eq!(lockup_args.custodian, None); } else {
1828 panic!("expected SetLockup instruction");
1829 }
1830 }
1831
1832 fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String {
1833 use std::env;
1834 let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
1835
1836 format!("{out_dir}/tmp/{name}-{pubkey}")
1837 }
1838
1839 fn initialize_check_payer_balances_inputs(
1840 allocation_amount: u64,
1841 sender_keypair_file: &str,
1842 fee_payer: &str,
1843 stake_args: Option<StakeArgs>,
1844 ) -> (Vec<TypedAllocation>, DistributeTokensArgs) {
1845 let recipient = pubkey::new_rand();
1846 let allocations = vec![TypedAllocation {
1847 recipient,
1848 amount: allocation_amount,
1849 lockup_date: None,
1850 }];
1851 let args = DistributeTokensArgs {
1852 sender_keypair: Box::new(read_keypair_file(sender_keypair_file).unwrap()),
1853 fee_payer: Box::new(read_keypair_file(fee_payer).unwrap()),
1854 dry_run: false,
1855 input_csv: "".to_string(),
1856 transaction_db: "".to_string(),
1857 output_path: None,
1858 stake_args,
1859 spl_token_args: None,
1860 transfer_amount: None,
1861 };
1862 (allocations, args)
1863 }
1864
1865 #[test]
1866 fn test_check_payer_balances_distribute_tokens_single_payer() {
1867 let alice = Keypair::new();
1868 let test_validator = simple_test_validator(alice.pubkey());
1869 let url = test_validator.rpc_url();
1870
1871 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
1872 let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey());
1873 write_keypair_file(&alice, &sender_keypair_file).unwrap();
1874
1875 let fees = client
1876 .get_fee_for_message(&one_signer_message(&client))
1877 .unwrap();
1878 let fees_in_sol = fees as f64 / LAMPORTS_PER_SOL as f64;
1879
1880 let allocation_amount = 1000.0;
1881
1882 let (allocations, mut args) = initialize_check_payer_balances_inputs(
1884 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
1885 &sender_keypair_file,
1886 &sender_keypair_file,
1887 None,
1888 );
1889 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args).unwrap();
1890
1891 let unfunded_payer = Keypair::new();
1893 let unfunded_payer_keypair_file = tmp_file_path("keypair_file", &unfunded_payer.pubkey());
1894 write_keypair_file(&unfunded_payer, &unfunded_payer_keypair_file).unwrap();
1895 args.sender_keypair = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
1896 args.fee_payer = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
1897
1898 let err_result =
1899 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
1900 .unwrap_err();
1901 if let Error::InsufficientFunds(sources, amount) = err_result {
1902 assert_eq!(
1903 sources,
1904 vec![FundingSource::SystemAccount, FundingSource::FeePayer].into()
1905 );
1906 assert_eq!(amount, (allocation_amount + fees_in_sol).to_string());
1907 } else {
1908 panic!("check_payer_balances should have errored");
1909 }
1910
1911 let partially_funded_payer = Keypair::new();
1913 let partially_funded_payer_keypair_file =
1914 tmp_file_path("keypair_file", &partially_funded_payer.pubkey());
1915 write_keypair_file(
1916 &partially_funded_payer,
1917 &partially_funded_payer_keypair_file,
1918 )
1919 .unwrap();
1920 let transaction = transfer(
1921 &client,
1922 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
1923 &alice,
1924 &partially_funded_payer.pubkey(),
1925 )
1926 .unwrap();
1927 client
1928 .send_and_confirm_transaction_with_spinner(&transaction)
1929 .unwrap();
1930
1931 args.sender_keypair =
1932 Box::new(read_keypair_file(&partially_funded_payer_keypair_file).unwrap());
1933 args.fee_payer = Box::new(read_keypair_file(&partially_funded_payer_keypair_file).unwrap());
1934 let err_result =
1935 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
1936 .unwrap_err();
1937 if let Error::InsufficientFunds(sources, amount) = err_result {
1938 assert_eq!(
1939 sources,
1940 vec![FundingSource::SystemAccount, FundingSource::FeePayer].into()
1941 );
1942 assert_eq!(amount, (allocation_amount + fees_in_sol).to_string());
1943 } else {
1944 panic!("check_payer_balances should have errored");
1945 }
1946 }
1947
1948 #[test]
1949 fn test_check_payer_balances_distribute_tokens_separate_payers() {
1950 agave_logger::setup();
1951 let alice = Keypair::new();
1952 let test_validator = simple_test_validator(alice.pubkey());
1953 let url = test_validator.rpc_url();
1954
1955 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
1956
1957 let fees = client
1958 .get_fee_for_message(&one_signer_message(&client))
1959 .unwrap();
1960 let fees_in_sol = fees as f64 / LAMPORTS_PER_SOL as f64;
1961
1962 let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey());
1963 write_keypair_file(&alice, &sender_keypair_file).unwrap();
1964
1965 let allocation_amount = 1000.0;
1966
1967 let funded_payer = Keypair::new();
1968 let funded_payer_keypair_file = tmp_file_path("keypair_file", &funded_payer.pubkey());
1969 write_keypair_file(&funded_payer, &funded_payer_keypair_file).unwrap();
1970 let transaction = transfer(
1971 &client,
1972 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
1973 &alice,
1974 &funded_payer.pubkey(),
1975 )
1976 .unwrap();
1977 client
1978 .send_and_confirm_transaction_with_spinner(&transaction)
1979 .unwrap();
1980
1981 let (allocations, mut args) = initialize_check_payer_balances_inputs(
1983 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
1984 &funded_payer_keypair_file,
1985 &sender_keypair_file,
1986 None,
1987 );
1988 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args).unwrap();
1989
1990 let unfunded_payer = Keypair::new();
1992 let unfunded_payer_keypair_file = tmp_file_path("keypair_file", &unfunded_payer.pubkey());
1993 write_keypair_file(&unfunded_payer, &unfunded_payer_keypair_file).unwrap();
1994 args.sender_keypair = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
1995 args.fee_payer = Box::new(read_keypair_file(&sender_keypair_file).unwrap());
1996
1997 let err_result =
1998 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
1999 .unwrap_err();
2000 if let Error::InsufficientFunds(sources, amount) = err_result {
2001 assert_eq!(sources, vec![FundingSource::SystemAccount].into());
2002 assert_eq!(amount, allocation_amount.to_string());
2003 } else {
2004 panic!("check_payer_balances should have errored");
2005 }
2006
2007 args.sender_keypair = Box::new(read_keypair_file(&sender_keypair_file).unwrap());
2009 args.fee_payer = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
2010
2011 let err_result =
2012 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
2013 .unwrap_err();
2014 if let Error::InsufficientFunds(sources, amount) = err_result {
2015 assert_eq!(sources, vec![FundingSource::FeePayer].into());
2016 assert_eq!(amount, fees_in_sol.to_string());
2017 } else {
2018 panic!("check_payer_balances should have errored");
2019 }
2020 }
2021
2022 fn initialize_stake_account(
2023 stake_account_amount: u64,
2024 unlocked_sol: u64,
2025 sender_keypair: &Keypair,
2026 client: &RpcClient,
2027 ) -> StakeArgs {
2028 let stake_account_keypair = Keypair::new();
2029 let stake_account_address = stake_account_keypair.pubkey();
2030 let stake_authority = Keypair::new();
2031 let withdraw_authority = Keypair::new();
2032
2033 let authorized = Authorized {
2034 staker: stake_authority.pubkey(),
2035 withdrawer: withdraw_authority.pubkey(),
2036 };
2037 let lockup = Lockup::default();
2038 let instructions = stake_instruction::create_account(
2039 &sender_keypair.pubkey(),
2040 &stake_account_address,
2041 &authorized,
2042 &lockup,
2043 stake_account_amount,
2044 );
2045 let message = Message::new(&instructions, Some(&sender_keypair.pubkey()));
2046 let signers = [sender_keypair, &stake_account_keypair];
2047 let blockhash = client.get_latest_blockhash().unwrap();
2048 let transaction = Transaction::new(&signers, message, blockhash);
2049 client
2050 .send_and_confirm_transaction_with_spinner(&transaction)
2051 .unwrap();
2052
2053 let sender_stake_args = SenderStakeArgs {
2054 stake_account_address,
2055 stake_authority: Box::new(stake_authority),
2056 withdraw_authority: Box::new(withdraw_authority),
2057 lockup_authority: None,
2058 rent_exempt_reserve: Some(2_282_880),
2059 };
2060
2061 StakeArgs {
2062 lockup_authority: None,
2063 unlocked_sol,
2064 sender_stake_args: Some(sender_stake_args),
2065 }
2066 }
2067
2068 fn simple_test_validator(alice: Pubkey) -> TestValidator {
2069 TestValidator::with_custom_fees(alice, 10_000, None, SocketAddrSpace::Unspecified)
2070 }
2071
2072 #[test]
2073 fn test_check_payer_balances_distribute_stakes_single_payer() {
2074 let alice = Keypair::new();
2075 let test_validator = simple_test_validator(alice.pubkey());
2076 let url = test_validator.rpc_url();
2077 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
2078
2079 let fees = client
2080 .get_fee_for_message(&one_signer_message(&client))
2081 .unwrap();
2082 let fees_in_sol = fees as f64 / LAMPORTS_PER_SOL as f64;
2083
2084 let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey());
2085 write_keypair_file(&alice, &sender_keypair_file).unwrap();
2086
2087 let allocation_amount = 1000.0;
2088 let unlocked_sol = 1.0;
2089 let stake_args = initialize_stake_account(
2090 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
2091 sol_str_to_lamports(&unlocked_sol.to_string()).unwrap(),
2092 &alice,
2093 &client,
2094 );
2095
2096 let (allocations, mut args) = initialize_check_payer_balances_inputs(
2098 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
2099 &sender_keypair_file,
2100 &sender_keypair_file,
2101 Some(stake_args),
2102 );
2103 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args).unwrap();
2104
2105 let expensive_allocation_amount = 5000.0;
2107 let expensive_allocations = vec![TypedAllocation {
2108 recipient: pubkey::new_rand(),
2109 amount: sol_str_to_lamports(&expensive_allocation_amount.to_string()).unwrap(),
2110 lockup_date: None,
2111 }];
2112 let err_result = check_payer_balances(
2113 &[one_signer_message(&client)],
2114 &expensive_allocations,
2115 &client,
2116 &args,
2117 )
2118 .unwrap_err();
2119 if let Error::InsufficientFunds(sources, amount) = err_result {
2120 assert_eq!(sources, vec![FundingSource::StakeAccount].into());
2121 assert_eq!(
2122 amount,
2123 (expensive_allocation_amount - unlocked_sol).to_string()
2124 );
2125 } else {
2126 panic!("check_payer_balances should have errored");
2127 }
2128
2129 let unfunded_payer = Keypair::new();
2131 let unfunded_payer_keypair_file = tmp_file_path("keypair_file", &unfunded_payer.pubkey());
2132 write_keypair_file(&unfunded_payer, &unfunded_payer_keypair_file).unwrap();
2133 args.sender_keypair = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
2134 args.fee_payer = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
2135
2136 let err_result =
2137 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
2138 .unwrap_err();
2139 if let Error::InsufficientFunds(sources, amount) = err_result {
2140 assert_eq!(
2141 sources,
2142 vec![FundingSource::SystemAccount, FundingSource::FeePayer].into()
2143 );
2144 assert_eq!(amount, (unlocked_sol + fees_in_sol).to_string());
2145 } else {
2146 panic!("check_payer_balances should have errored");
2147 }
2148
2149 let partially_funded_payer = Keypair::new();
2151 let partially_funded_payer_keypair_file =
2152 tmp_file_path("keypair_file", &partially_funded_payer.pubkey());
2153 write_keypair_file(
2154 &partially_funded_payer,
2155 &partially_funded_payer_keypair_file,
2156 )
2157 .unwrap();
2158 let transaction = transfer(
2159 &client,
2160 sol_str_to_lamports(&unlocked_sol.to_string()).unwrap(),
2161 &alice,
2162 &partially_funded_payer.pubkey(),
2163 )
2164 .unwrap();
2165 client
2166 .send_and_confirm_transaction_with_spinner(&transaction)
2167 .unwrap();
2168
2169 args.sender_keypair =
2170 Box::new(read_keypair_file(&partially_funded_payer_keypair_file).unwrap());
2171 args.fee_payer = Box::new(read_keypair_file(&partially_funded_payer_keypair_file).unwrap());
2172 let err_result =
2173 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
2174 .unwrap_err();
2175 if let Error::InsufficientFunds(sources, amount) = err_result {
2176 assert_eq!(
2177 sources,
2178 vec![FundingSource::SystemAccount, FundingSource::FeePayer].into()
2179 );
2180 assert_eq!(amount, (unlocked_sol + fees_in_sol).to_string());
2181 } else {
2182 panic!("check_payer_balances should have errored");
2183 }
2184 }
2185
2186 #[test]
2187 fn test_check_payer_balances_distribute_stakes_separate_payers() {
2188 agave_logger::setup();
2189 let alice = Keypair::new();
2190 let test_validator = simple_test_validator(alice.pubkey());
2191 let url = test_validator.rpc_url();
2192
2193 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
2194
2195 let fees = client
2196 .get_fee_for_message(&one_signer_message(&client))
2197 .unwrap();
2198 let fees_in_sol = fees as f64 / LAMPORTS_PER_SOL as f64;
2199
2200 let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey());
2201 write_keypair_file(&alice, &sender_keypair_file).unwrap();
2202
2203 let allocation_amount = 1000.0;
2204 let unlocked_sol = 1.0;
2205 let stake_args = initialize_stake_account(
2206 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
2207 sol_str_to_lamports(&unlocked_sol.to_string()).unwrap(),
2208 &alice,
2209 &client,
2210 );
2211
2212 let funded_payer = Keypair::new();
2213 let funded_payer_keypair_file = tmp_file_path("keypair_file", &funded_payer.pubkey());
2214 write_keypair_file(&funded_payer, &funded_payer_keypair_file).unwrap();
2215 let transaction = transfer(
2216 &client,
2217 sol_str_to_lamports(&unlocked_sol.to_string()).unwrap(),
2218 &alice,
2219 &funded_payer.pubkey(),
2220 )
2221 .unwrap();
2222 client
2223 .send_and_confirm_transaction_with_spinner(&transaction)
2224 .unwrap();
2225
2226 let (allocations, mut args) = initialize_check_payer_balances_inputs(
2228 sol_str_to_lamports(&allocation_amount.to_string()).unwrap(),
2229 &funded_payer_keypair_file,
2230 &sender_keypair_file,
2231 Some(stake_args),
2232 );
2233 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args).unwrap();
2234
2235 let unfunded_payer = Keypair::new();
2237 let unfunded_payer_keypair_file = tmp_file_path("keypair_file", &unfunded_payer.pubkey());
2238 write_keypair_file(&unfunded_payer, &unfunded_payer_keypair_file).unwrap();
2239 args.sender_keypair = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
2240 args.fee_payer = Box::new(read_keypair_file(&sender_keypair_file).unwrap());
2241
2242 let err_result =
2243 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
2244 .unwrap_err();
2245 if let Error::InsufficientFunds(sources, amount) = err_result {
2246 assert_eq!(sources, vec![FundingSource::SystemAccount].into());
2247 assert_eq!(amount, unlocked_sol.to_string());
2248 } else {
2249 panic!("check_payer_balances should have errored");
2250 }
2251
2252 args.sender_keypair = Box::new(read_keypair_file(&sender_keypair_file).unwrap());
2254 args.fee_payer = Box::new(read_keypair_file(&unfunded_payer_keypair_file).unwrap());
2255
2256 let err_result =
2257 check_payer_balances(&[one_signer_message(&client)], &allocations, &client, &args)
2258 .unwrap_err();
2259 if let Error::InsufficientFunds(sources, amount) = err_result {
2260 assert_eq!(sources, vec![FundingSource::FeePayer].into());
2261 assert_eq!(amount, fees_in_sol.to_string());
2262 } else {
2263 panic!("check_payer_balances should have errored");
2264 }
2265 }
2266
2267 #[test]
2268 fn test_build_messages_dump_db() {
2269 let client = RpcClient::new_mock("mock_client".to_string());
2270 let dir = tempdir().unwrap();
2271 let db_file = dir
2272 .path()
2273 .join("build_messages.db")
2274 .to_str()
2275 .unwrap()
2276 .to_string();
2277 let mut db = db::open_db(&db_file, false).unwrap();
2278
2279 let sender = Keypair::new();
2280 let recipient = Pubkey::new_unique();
2281 let amount = sol_str_to_lamports("1.0").unwrap();
2282 let last_valid_block_height = 222;
2283 let transaction = transfer(&client, amount, &sender, &recipient).unwrap();
2284
2285 db::set_transaction_info(
2287 &mut db,
2288 &recipient,
2289 amount,
2290 &transaction,
2291 None,
2292 false,
2293 last_valid_block_height,
2294 None,
2295 )
2296 .unwrap();
2297
2298 let read_db = db::open_db(&db_file, true).unwrap();
2300 assert!(db::read_transaction_infos(&read_db).is_empty());
2301
2302 let args = DistributeTokensArgs {
2304 sender_keypair: Box::new(Keypair::new()),
2305 fee_payer: Box::new(Keypair::new()),
2306 dry_run: true,
2307 input_csv: "".to_string(),
2308 transaction_db: "".to_string(),
2309 output_path: None,
2310 stake_args: None,
2311 spl_token_args: None,
2312 transfer_amount: None,
2313 };
2314 let allocation = TypedAllocation {
2315 recipient,
2316 amount: sol_str_to_lamports("1.0").unwrap(),
2317 lockup_date: None,
2318 };
2319
2320 let mut messages: Vec<Message> = vec![];
2321 let mut stake_extras: StakeExtras = vec![];
2322 let mut created_accounts = 0;
2323
2324 build_messages(
2326 &client,
2327 &mut db,
2328 slice::from_ref(&allocation),
2329 &args,
2330 Arc::new(AtomicBool::new(false)),
2331 &mut messages,
2332 &mut stake_extras,
2333 &mut created_accounts,
2334 )
2335 .unwrap();
2336 let read_db = db::open_db(&db_file, true).unwrap();
2337 assert!(db::read_transaction_infos(&read_db).is_empty());
2338 assert_eq!(messages.len(), 1);
2339
2340 let mut messages: Vec<Message> = vec![];
2342 let exit = Arc::new(AtomicBool::new(true));
2343 build_messages(
2344 &client,
2345 &mut db,
2346 &[],
2347 &args,
2348 exit.clone(),
2349 &mut messages,
2350 &mut stake_extras,
2351 &mut created_accounts,
2352 )
2353 .unwrap();
2354 let read_db = db::open_db(&db_file, true).unwrap();
2355 assert!(db::read_transaction_infos(&read_db).is_empty());
2356 assert!(messages.is_empty());
2357
2358 let mut messages: Vec<Message> = vec![];
2360 build_messages(
2361 &client,
2362 &mut db,
2363 &[allocation],
2364 &args,
2365 exit,
2366 &mut messages,
2367 &mut stake_extras,
2368 &mut created_accounts,
2369 )
2370 .unwrap_err();
2371 let read_db = db::open_db(&db_file, true).unwrap();
2372 let transaction_info = db::read_transaction_infos(&read_db);
2373 assert_eq!(transaction_info.len(), 1);
2374 assert_eq!(
2375 transaction_info[0],
2376 TransactionInfo {
2377 recipient,
2378 amount,
2379 new_stake_account_address: None,
2380 finalized_date: None,
2381 transaction,
2382 last_valid_block_height,
2383 lockup_date: None,
2384 }
2385 );
2386 assert_eq!(messages.len(), 0);
2387 }
2388
2389 #[test]
2390 fn test_send_messages_dump_db() {
2391 let client = RpcClient::new_mock("mock_client".to_string());
2392 let dir = tempdir().unwrap();
2393 let db_file = dir
2394 .path()
2395 .join("send_messages.db")
2396 .to_str()
2397 .unwrap()
2398 .to_string();
2399 let mut db = db::open_db(&db_file, false).unwrap();
2400
2401 let sender = Keypair::new();
2402 let recipient = Pubkey::new_unique();
2403 let amount = sol_str_to_lamports("1.0").unwrap();
2404 let last_valid_block_height = 222;
2405 let transaction = transfer(&client, amount, &sender, &recipient).unwrap();
2406
2407 db::set_transaction_info(
2409 &mut db,
2410 &recipient,
2411 amount,
2412 &transaction,
2413 None,
2414 false,
2415 last_valid_block_height,
2416 None,
2417 )
2418 .unwrap();
2419
2420 let read_db = db::open_db(&db_file, true).unwrap();
2422 assert!(db::read_transaction_infos(&read_db).is_empty());
2423
2424 let args = DistributeTokensArgs {
2426 sender_keypair: Box::new(Keypair::new()),
2427 fee_payer: Box::new(Keypair::new()),
2428 dry_run: true,
2429 input_csv: "".to_string(),
2430 transaction_db: "".to_string(),
2431 output_path: None,
2432 stake_args: None,
2433 spl_token_args: None,
2434 transfer_amount: None,
2435 };
2436 let allocation = TypedAllocation {
2437 recipient,
2438 amount: sol_str_to_lamports("1.0").unwrap(),
2439 lockup_date: None,
2440 };
2441 let message = transaction.message.clone();
2442
2443 send_messages(
2445 &client,
2446 &mut db,
2447 slice::from_ref(&allocation),
2448 &args,
2449 Arc::new(AtomicBool::new(false)),
2450 vec![message.clone()],
2451 vec![(Keypair::new(), None)],
2452 )
2453 .unwrap();
2454 let read_db = db::open_db(&db_file, true).unwrap();
2455 assert!(db::read_transaction_infos(&read_db).is_empty());
2456 let num_records = db::read_transaction_infos(&db).len();
2459
2460 let exit = Arc::new(AtomicBool::new(true));
2462 send_messages(&client, &mut db, &[], &args, exit.clone(), vec![], vec![]).unwrap();
2463 let read_db = db::open_db(&db_file, true).unwrap();
2464 assert!(db::read_transaction_infos(&read_db).is_empty());
2465
2466 send_messages(
2468 &client,
2469 &mut db,
2470 &[allocation],
2471 &args,
2472 exit,
2473 vec![message.clone()],
2474 vec![(Keypair::new(), None)],
2475 )
2476 .unwrap_err();
2477 let read_db = db::open_db(&db_file, true).unwrap();
2478 let transaction_info = db::read_transaction_infos(&read_db);
2479 assert_eq!(transaction_info.len(), num_records);
2480 assert!(transaction_info.contains(&TransactionInfo {
2481 recipient,
2482 amount,
2483 new_stake_account_address: None,
2484 finalized_date: None,
2485 transaction,
2486 last_valid_block_height,
2487 lockup_date: None,
2488 }));
2489 assert!(transaction_info.contains(&TransactionInfo {
2490 recipient,
2491 amount,
2492 new_stake_account_address: None,
2493 finalized_date: None,
2494 transaction: Transaction::new_unsigned(message),
2495 last_valid_block_height: u64::MAX,
2496 lockup_date: None,
2497 }));
2498
2499 let num_records = db::read_transaction_infos(&db).len();
2501 db.dump().unwrap();
2502 let read_db = db::open_db(&db_file, true).unwrap();
2503 let transaction_info = db::read_transaction_infos(&read_db);
2504 assert_eq!(transaction_info.len(), num_records);
2505 }
2506
2507 #[test]
2508 fn test_distribute_allocations_dump_db() {
2509 let sender_keypair = Keypair::new();
2510 let test_validator = simple_test_validator_no_fees(sender_keypair.pubkey());
2511 let url = test_validator.rpc_url();
2512 let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed());
2513
2514 let fee_payer = Keypair::new();
2515 let transaction = transfer(
2516 &client,
2517 sol_str_to_lamports("1.0").unwrap(),
2518 &sender_keypair,
2519 &fee_payer.pubkey(),
2520 )
2521 .unwrap();
2522 client
2523 .send_and_confirm_transaction_with_spinner(&transaction)
2524 .unwrap();
2525
2526 let dir = tempdir().unwrap();
2527 let db_file = dir
2528 .path()
2529 .join("dist_allocations.db")
2530 .to_str()
2531 .unwrap()
2532 .to_string();
2533 let mut db = db::open_db(&db_file, false).unwrap();
2534 let recipient = Pubkey::new_unique();
2535 let allocation = TypedAllocation {
2536 recipient,
2537 amount: sol_str_to_lamports("1.0").unwrap(),
2538 lockup_date: None,
2539 };
2540 let args = DistributeTokensArgs {
2542 sender_keypair: Box::new(sender_keypair),
2543 fee_payer: Box::new(fee_payer),
2544 dry_run: true,
2545 input_csv: "".to_string(),
2546 transaction_db: "".to_string(),
2547 output_path: None,
2548 stake_args: None,
2549 spl_token_args: None,
2550 transfer_amount: None,
2551 };
2552
2553 let exit = Arc::new(AtomicBool::new(false));
2554
2555 distribute_allocations(&client, &mut db, &[allocation], &args, exit).unwrap();
2557 let read_db = db::open_db(&db_file, true).unwrap();
2558 let transaction_info = db::read_transaction_infos(&read_db);
2559 assert_eq!(transaction_info.len(), 1);
2560 }
2561
2562 #[test]
2563 fn test_log_transaction_confirmations_dump_db() {
2564 let client = RpcClient::new_mock("mock_client".to_string());
2565 let dir = tempdir().unwrap();
2566 let db_file = dir
2567 .path()
2568 .join("log_transaction_confirmations.db")
2569 .to_str()
2570 .unwrap()
2571 .to_string();
2572 let mut db = db::open_db(&db_file, false).unwrap();
2573
2574 let sender = Keypair::new();
2575 let recipient = Pubkey::new_unique();
2576 let amount = sol_str_to_lamports("1.0").unwrap();
2577 let last_valid_block_height = 222;
2578 let transaction = transfer(&client, amount, &sender, &recipient).unwrap();
2579
2580 db::set_transaction_info(
2582 &mut db,
2583 &recipient,
2584 amount,
2585 &transaction,
2586 None,
2587 false,
2588 last_valid_block_height,
2589 None,
2590 )
2591 .unwrap();
2592
2593 let read_db = db::open_db(&db_file, true).unwrap();
2595 assert!(db::read_transaction_infos(&read_db).is_empty());
2596
2597 let mut confirmations = None;
2599 let exit = Arc::new(AtomicBool::new(true));
2600 log_transaction_confirmations(
2601 &client,
2602 &mut db,
2603 exit.clone(),
2604 vec![],
2605 vec![],
2606 &mut confirmations,
2607 )
2608 .unwrap();
2609 let read_db = db::open_db(&db_file, true).unwrap();
2610 assert!(db::read_transaction_infos(&read_db).is_empty());
2611 assert_eq!(confirmations, None);
2612
2613 log_transaction_confirmations(
2615 &client,
2616 &mut db,
2617 Arc::new(AtomicBool::new(false)),
2618 vec![(&transaction, 111)],
2619 vec![Some(TransactionStatus {
2620 slot: 40,
2621 confirmations: Some(15),
2622 status: Ok(()),
2623 err: None,
2624 confirmation_status: Some(TransactionConfirmationStatus::Finalized),
2625 })],
2626 &mut confirmations,
2627 )
2628 .unwrap();
2629 let read_db = db::open_db(&db_file, true).unwrap();
2630 assert!(db::read_transaction_infos(&read_db).is_empty());
2631 assert_eq!(confirmations, Some(15));
2632
2633 log_transaction_confirmations(
2635 &client,
2636 &mut db,
2637 exit,
2638 vec![(&transaction, 111)],
2639 vec![Some(TransactionStatus {
2640 slot: 55,
2641 confirmations: None,
2642 status: Ok(()),
2643 err: None,
2644 confirmation_status: Some(TransactionConfirmationStatus::Finalized),
2645 })],
2646 &mut confirmations,
2647 )
2648 .unwrap_err();
2649 let read_db = db::open_db(&db_file, true).unwrap();
2650 let transaction_info = db::read_transaction_infos(&read_db);
2651 assert_eq!(transaction_info.len(), 1);
2652 assert!(transaction_info[0].finalized_date.is_some());
2653 }
2654
2655 #[test]
2656 fn test_update_finalized_transactions_dump_db() {
2657 let client = RpcClient::new_mock("mock_client".to_string());
2658 let dir = tempdir().unwrap();
2659 let db_file = dir
2660 .path()
2661 .join("update_finalized_transactions.db")
2662 .to_str()
2663 .unwrap()
2664 .to_string();
2665 let mut db = db::open_db(&db_file, false).unwrap();
2666
2667 let sender = Keypair::new();
2668 let recipient = Pubkey::new_unique();
2669 let amount = sol_str_to_lamports("1.0").unwrap();
2670 let last_valid_block_height = 222;
2671 let transaction = transfer(&client, amount, &sender, &recipient).unwrap();
2672
2673 db::set_transaction_info(
2675 &mut db,
2676 &recipient,
2677 amount,
2678 &transaction,
2679 None,
2680 false,
2681 last_valid_block_height,
2682 None,
2683 )
2684 .unwrap();
2685
2686 let confs =
2688 update_finalized_transactions(&client, &mut db, Arc::new(AtomicBool::new(false)))
2689 .unwrap();
2690 let read_db = db::open_db(&db_file, true).unwrap();
2691 let transaction_info = db::read_transaction_infos(&read_db);
2692 assert_eq!(transaction_info.len(), 1);
2693 assert_eq!(confs, None);
2694 }
2695}