solana_tokens/
commands.rs

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/// Allocation is a helper (mostly for tests), prefer using TypedAllocation instead when possible.
56#[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/// TypedAllocation is same as Allocation but contains typed fields.
64#[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
161/// Return true if the recipient and lockups are the same
162fn 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        // No stake args; a simple token transfer.
219        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        // Stake args provided, so create a recipient stake account.
228        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                // No source stake account, so create a recipient stake account directly.
235                None => {
236                    // Make the recipient both the new stake and withdraw authority
237                    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                // A sender stake account was provided, so create a recipient stake account by
258                // splitting the sender account.
259                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                    // Transfer some tokens to stake account to cover rent-exempt reserve.
267                    let mut instructions = vec![system_instruction::transfer(
268                        &sender_pubkey,
269                        new_stake_account_address,
270                        rent_exempt_reserve,
271                    )];
272
273                    // Split to stake account
274                    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                    // Make the recipient the new stake authority
282                    instructions.push(stake_instruction::authorize(
283                        new_stake_account_address,
284                        &stake_authority,
285                        &recipient,
286                        StakeAuthorize::Staker,
287                        None,
288                    ));
289
290                    // Make the recipient the new withdraw authority
291                    instructions.push(stake_instruction::authorize(
292                        new_stake_account_address,
293                        &withdraw_authority,
294                        &recipient,
295                        StakeAuthorize::Withdrawer,
296                        None,
297                    ));
298
299                    // Add lockup
300                    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            // Transfer some unlocked tokens to recipient, which they can use for transaction fees.
318            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(), // populated by a real blockhash for balance check and submission
396        );
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        // We only support SOL token in "require lockup" mode.
535        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                    // empty lockup date means no lockup, it's okay to have only some lockups specified
553                    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    // Start by finalizing any transactions from the previous run.
642    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 for about 1 slot
719        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
727// Update the finalized bit on any transactions that are now rooted
728// Return the lowest number of confirmations on the unfinalized transactions or None if all are finalized.
729fn 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()) // Filter out dry-run signatures
749        .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    // Now, run it again, and check there's no double-spend.
1041    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    // Now, run it again, and check there's no double-spend.
1156    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    // Now, run it again, and check there's no double-spend.
1288    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        // Empty file.
1465        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        // Missing 2nd column.
1473        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        // Missing 3rd column.
1484        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        // Bad pubkey (default).
1514        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        // Bad pubkey (with transfer amount).
1529        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        // Bad pubkey (with require lockup).
1544        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        // Bad pubkey (with raw amount).
1567        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        // Bad value in 2nd column (default).
1583        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        // Bad value in 2nd column (with require lockup).
1604        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        // Bad value in 2nd column (with raw amount).
1621        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()), // bad raw amount
1627            ],
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        // Bad value in 3rd column.
1635        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        // Ensure that we applied the transaction to the allocation with
1723        // a matching recipient address (to bob, not alice).
1724        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)); // Different recipient, no lockup
1764        assert!(!has_same_recipient(&alice_alloc, &alice_info_lockup0)); // One with no lockup, one locked up
1765        assert!(!has_same_recipient(
1766            &alice_alloc_lockup0,
1767            &alice_info_lockup1
1768        )); // Different lockups
1769        assert!(has_same_recipient(&alice_alloc, &alice_info)); // Same recipient, no lockups
1770        assert!(has_same_recipient(
1771            &alice_alloc_lockup0,
1772            &alice_info_lockup0
1773        )); // Same recipient, same lockups
1774    }
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); // Don't change the epoch
1826            assert_eq!(lockup_args.custodian, None); // Don't change the lockup authority
1827        } 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        // Fully funded payer
1883        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        // Unfunded payer
1892        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        // Payer funded enough for distribution only
1912        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        // Fully funded payers
1982        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        // Unfunded sender
1991        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        // Unfunded fee payer
2008        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        // Fully funded payer & stake account
2097        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        // Underfunded stake-account
2106        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        // Unfunded payer
2130        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        // Payer funded enough for distribution only
2150        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        // Fully funded payers
2227        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        // Unfunded sender
2236        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        // Unfunded fee payer
2253        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        // Queue db data
2286        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        // Check that data has not been dumped
2299        let read_db = db::open_db(&db_file, true).unwrap();
2300        assert!(db::read_transaction_infos(&read_db).is_empty());
2301
2302        // This is just dummy data; Args will not affect messages built
2303        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        // Exit false will not dump data
2325        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        // Empty allocations will not dump data
2341        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        // Any allocation should prompt data dump
2359        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        // Queue db data
2408        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        // Check that data has not been dumped
2421        let read_db = db::open_db(&db_file, true).unwrap();
2422        assert!(db::read_transaction_infos(&read_db).is_empty());
2423
2424        // This is just dummy data; Args will not affect messages
2425        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        // Exit false will not dump data
2444        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        // The method above will, however, write a record to the in-memory db
2457        // Grab that expected value to test successful dump
2458        let num_records = db::read_transaction_infos(&db).len();
2459
2460        // Empty messages/allocations will not dump data
2461        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        // Message/allocation should prompt data dump at start of loop
2467        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        // Next dump should write record written in last send_messages call
2500        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        // This is just dummy data; Args will not affect messages
2541        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        // Ensure data is always dumped after distribute_allocations
2556        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        // Queue unconfirmed transaction into db
2581        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        // Check that data has not been dumped
2594        let read_db = db::open_db(&db_file, true).unwrap();
2595        assert!(db::read_transaction_infos(&read_db).is_empty());
2596
2597        // Empty unconfirmed_transactions will not dump data
2598        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        // Exit false will not dump data
2614        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        // Exit true should dump data
2634        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        // Queue unconfirmed transaction into db
2674        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        // Ensure data is always dumped after update_finalized_transactions
2687        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}