sugar_cli/withdraw/
process.rs

1use std::{rc::Rc, str::FromStr};
2
3pub use anchor_client::{
4    solana_sdk::{
5        commitment_config::{CommitmentConfig, CommitmentLevel},
6        native_token::LAMPORTS_PER_MLN,
7        pubkey::Pubkey,
8        signature::{Keypair, Signature, Signer},
9        system_instruction, system_program, sysvar,
10        transaction::Transaction,
11    },
12    Client, Program,
13};
14use console::{style, Style};
15use dialoguer::{theme::ColorfulTheme, Confirm};
16use miraland_account_decoder::UiAccountEncoding;
17use miraland_client::{
18    rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
19    rpc_filter::{Memcmp, RpcFilterType},
20};
21use mpl_candy_machine_core::{accounts as nft_accounts, instruction as nft_instruction};
22
23use crate::{
24    candy_machine::CANDY_MACHINE_ID,
25    common::*,
26    parse::parse_sugar_errors,
27    setup::{setup_client, sugar_setup},
28    utils::*,
29};
30
31pub struct WithdrawArgs {
32    pub candy_machine: Option<String>,
33    pub keypair: Option<String>,
34    pub rpc_url: Option<String>,
35    pub list: bool,
36}
37
38#[derive(Debug)]
39struct WithdrawError {
40    candy_machine: String,
41    error_message: String,
42}
43
44pub fn process_withdraw(args: WithdrawArgs) -> Result<()> {
45    // (1) Setting up connection
46
47    println!(
48        "{} {}Initializing connection",
49        style("[1/2]").bold().dim(),
50        COMPUTER_EMOJI
51    );
52
53    let pb = spinner_with_style();
54    pb.set_message("Connecting...");
55
56    let (program, payer) = setup_withdraw(args.keypair, args.rpc_url)?;
57
58    pb.finish_with_message("Connected");
59
60    println!(
61        "\n{} {}{} funds",
62        style("[2/2]").bold().dim(),
63        WITHDRAW_EMOJI,
64        if args.list { "Listing" } else { "Retrieving" }
65    );
66
67    // the --list flag takes precedence; even if a candy machine id is passed
68    // as an argument, we will list the candy machines (no draining happens)
69    let candy_machine = if args.list { None } else { args.candy_machine };
70
71    // (2) Retrieving data for listing/draining
72
73    match &candy_machine {
74        Some(candy_machine) => {
75            let candy_machine = Pubkey::from_str(candy_machine)?;
76
77            let pb = spinner_with_style();
78            pb.set_message("Draining candy machine...");
79
80            do_withdraw(Rc::new(program), candy_machine, payer)?;
81
82            pb.finish_with_message("Done");
83        }
84        None => {
85            let config = RpcProgramAccountsConfig {
86                filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
87                    8, // key
88                    payer.as_ref(),
89                ))]),
90                account_config: RpcAccountInfoConfig {
91                    encoding: Some(UiAccountEncoding::Base64),
92                    data_slice: None,
93                    commitment: Some(CommitmentConfig {
94                        commitment: CommitmentLevel::Confirmed,
95                    }),
96                    min_context_slot: None,
97                },
98                with_context: None,
99            };
100
101            let pb = spinner_with_style();
102            pb.set_message("Looking up candy machines...");
103
104            let program = Rc::new(program);
105            let accounts = program
106                .rpc()
107                .get_program_accounts_with_config(&program.id(), config)?;
108
109            pb.finish_and_clear();
110
111            let mut total = 0.0f64;
112
113            accounts.iter().for_each(|account| {
114                let (_pubkey, account) = account;
115                total += account.lamports as f64;
116            });
117
118            println!(
119                "\nFound {} candy machines, total amount: ◎ {}",
120                accounts.len(),
121                total / LAMPORTS_PER_MLN as f64
122            );
123
124            if !accounts.is_empty() {
125                if args.list {
126                    println!("\n{:48} Balance", "Candy Machine ID");
127                    println!("{:-<61}", "-");
128
129                    for (pubkey, account) in accounts {
130                        println!(
131                            "{:48} {:>12.8}",
132                            pubkey.to_string(),
133                            account.lamports as f64 / LAMPORTS_PER_MLN as f64
134                        );
135                    }
136                } else {
137                    let warning = format!(
138                        "\n\
139                        +-----------------------------------------------------+\n\
140                        | {} WARNING: This will drain ALL your Candy Machines |\n\
141                        +-----------------------------------------------------+",
142                        WARNING_EMOJI
143                    );
144
145                    println!("{}\n", style(warning).bold().yellow());
146
147                    let theme = ColorfulTheme {
148                        success_prefix: style("✔".to_string()).yellow().force_styling(true),
149                        values_style: Style::new().yellow(),
150                        ..get_dialoguer_theme()
151                    };
152
153                    if !Confirm::with_theme(&theme)
154                        .with_prompt("Do you want to continue?")
155                        .interact()?
156                    {
157                        return Err(anyhow!("Withdraw aborted"));
158                    }
159
160                    let pb = progress_bar_with_style(accounts.len() as u64);
161                    let mut not_drained = 0;
162                    let mut error_messages = Vec::new();
163
164                    accounts.iter().for_each(|account| {
165                        let (candy_machine, _account) = account;
166                        do_withdraw(program.clone(), *candy_machine, payer).unwrap_or_else(|e| {
167                            not_drained += 1;
168                            error!("Error: {}", e);
169                            let error_message = parse_sugar_errors(&e.to_string());
170                            error_messages.push(WithdrawError {
171                                candy_machine: candy_machine.to_string(),
172                                error_message,
173                            });
174                        });
175                        pb.inc(1);
176                    });
177
178                    pb.finish();
179
180                    if not_drained > 0 {
181                        println!(
182                            "{}",
183                            style(format!("Could not drain {} candy machine(s)", not_drained))
184                                .red()
185                                .bold()
186                                .dim()
187                        );
188                        println!("{}", style("Errors:").red().bold().dim());
189                        for error in error_messages {
190                            println!(
191                                "{} {}\n{} {}",
192                                style("Candy Machine:").bold().dim(),
193                                style(error.candy_machine).bold().red(),
194                                style("Error:").bold().dim(),
195                                style(error.error_message).bold().red()
196                            );
197                        }
198                    }
199                }
200            }
201        }
202    }
203
204    Ok(())
205}
206
207fn setup_withdraw(keypair: Option<String>, rpc_url: Option<String>) -> Result<(Program, Pubkey)> {
208    let sugar_config = sugar_setup(keypair, rpc_url)?;
209    let client = setup_client(&sugar_config)?;
210    let program = client.program(CANDY_MACHINE_ID);
211    let payer = program.payer();
212
213    Ok((program, payer))
214}
215
216fn do_withdraw(program: Rc<Program>, candy_machine: Pubkey, payer: Pubkey) -> Result<()> {
217    program
218        .request()
219        .accounts(nft_accounts::Withdraw {
220            candy_machine,
221            authority: payer,
222        })
223        .args(nft_instruction::Withdraw {})
224        .send()?;
225
226    Ok(())
227}