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 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 let candy_machine = if args.list { None } else { args.candy_machine };
70
71 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, 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}