1use std::{str::FromStr, sync::Arc};
2
3use anchor_client::solana_sdk::{
4 compute_budget::ComputeBudgetInstruction,
5 pubkey::Pubkey,
6 signature::{Keypair, Signature, Signer},
7 system_program, sysvar,
8};
9use anyhow::Result;
10use console::style;
11use miraland_client::rpc_response::Response;
12use mpl_candy_machine_core::{
13 accounts as nft_accounts, instruction as nft_instruction, AccountVersion, CandyMachine,
14};
15use mpl_token_metadata::{
16 instruction::MetadataDelegateRole,
17 pda::{
18 find_collection_authority_account, find_metadata_delegate_record_account,
19 find_token_record_account,
20 },
21 state::{Metadata, TokenMetadataAccount},
22};
23use spl_associated_token_account::get_associated_token_address;
24use spl_token::ID as TOKEN_PROGRAM_ID;
25use tokio::sync::Semaphore;
26
27use crate::{
28 cache::load_cache,
29 candy_machine::{CANDY_MACHINE_ID, *},
30 common::*,
31 config::{Cluster, SugarConfig},
32 pdas::*,
33 utils::*,
34};
35
36pub struct MintArgs {
37 pub keypair: Option<String>,
38 pub rpc_url: Option<String>,
39 pub cache: String,
40 pub number: Option<u64>,
41 pub receiver: Option<String>,
42 pub candy_machine: Option<String>,
43}
44
45pub async fn process_mint(args: MintArgs) -> Result<()> {
46 let sugar_config = sugar_setup(args.keypair, args.rpc_url)?;
47 let client = setup_client(&sugar_config)?;
48 let program = client.program(CANDY_MACHINE_ID);
49
50 let candy_machine_id = match args.candy_machine {
53 Some(candy_machine_id) => candy_machine_id,
54 None => {
55 let cache = load_cache(&args.cache, false)?;
56 cache.program.candy_machine
57 }
58 };
59
60 let candy_pubkey = match Pubkey::from_str(&candy_machine_id) {
61 Ok(candy_pubkey) => candy_pubkey,
62 Err(_) => {
63 let error = anyhow!("Failed to parse candy machine id: {}", candy_machine_id);
64 error!("{:?}", error);
65 return Err(error);
66 }
67 };
68
69 println!(
70 "{} {}Loading candy machine",
71 style("[1/2]").bold().dim(),
72 LOOKING_GLASS_EMOJI
73 );
74 println!("{} {}", style("Candy machine ID:").bold(), candy_machine_id);
75
76 let pb = spinner_with_style();
77 pb.set_message("Connecting...");
78
79 let candy_machine_state = Arc::new(get_candy_machine_state(&sugar_config, &candy_pubkey)?);
80 let (_, collection_metadata) =
81 get_metadata_pda(&candy_machine_state.collection_mint, &program)?;
82 let collection_update_authority = collection_metadata.update_authority;
83
84 pb.finish_with_message("Done");
85
86 println!(
87 "\n{} {}Minting from candy machine",
88 style("[2/2]").bold().dim(),
89 CANDY_EMOJI
90 );
91
92 let receiver_pubkey = match args.receiver {
93 Some(receiver_id) => Pubkey::from_str(&receiver_id)
94 .map_err(|_| anyhow!("Failed to parse receiver pubkey: {}", receiver_id))?,
95 None => sugar_config.keypair.pubkey(),
96 };
97 println!("\nMinting to {}", &receiver_pubkey);
98
99 let number = args.number.unwrap_or(1);
100 let available = candy_machine_state.data.items_available - candy_machine_state.items_redeemed;
101
102 if number > available || number == 0 {
103 let error = anyhow!("{} item(s) available, requested {}", available, number);
104 error!("{:?}", error);
105 return Err(error);
106 }
107
108 info!("Minting NFT from candy machine: {}", &candy_machine_id);
109 info!("Candy machine program id: {:?}", CANDY_MACHINE_ID);
110
111 if number == 1 {
112 let pb = spinner_with_style();
113 pb.set_message(format!(
114 "{} item(s) remaining",
115 candy_machine_state.data.items_available - candy_machine_state.items_redeemed
116 ));
117 let config = Arc::new(sugar_config);
118
119 let result = match mint(
120 Arc::clone(&config),
121 candy_pubkey,
122 Arc::clone(&candy_machine_state),
123 collection_update_authority,
124 receiver_pubkey,
125 )
126 .await
127 {
128 Ok(signature) => {
129 println!("Signature: {}", signature);
130 format!("{}", style("Mint success").bold())
131 }
132 Err(err) => {
133 pb.abandon_with_message(format!("{}", style("Mint failed ").red().bold()));
134 error!("{:?}", err);
135 return Err(err);
136 }
137 };
138
139 pb.finish_with_message(result);
140 } else {
141 let pb = progress_bar_with_style(number);
142
143 let mut tasks = Vec::new();
144 let semaphore = Arc::new(Semaphore::new(10));
145 let config = Arc::new(sugar_config);
146
147 for _i in 0..number {
148 let config = config.clone();
149 let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap();
150 let candy_machine_state = candy_machine_state.clone();
151 let pb = pb.clone();
152
153 tasks.push(tokio::spawn(async move {
155 let _permit = permit;
156 let res = mint(
157 config,
158 candy_pubkey,
159 candy_machine_state,
160 collection_update_authority,
161 receiver_pubkey,
162 )
163 .await;
164 pb.inc(1);
165 res
166 }));
167 }
168
169 let mut error_count = 0;
170
171 for task in tasks {
173 let res = task.await.unwrap();
174 if let Err(e) = res {
175 error_count += 1;
176 error!("{:?}, continuing. . .", e);
177 }
178 }
179
180 if error_count > 0 {
181 pb.abandon_with_message(format!(
182 "{} {} items failed.",
183 style("Some of the items failed to mint.").red().bold(),
184 error_count
185 ));
186 return Err(anyhow!(
187 "{} {}/{} {}",
188 style("Minted").red().bold(),
189 number - error_count,
190 number,
191 style("of the items").red().bold()
192 ));
193 }
194 pb.finish();
195 }
196
197 Ok(())
198}
199
200pub async fn mint(
201 config: Arc<SugarConfig>,
202 candy_machine_id: Pubkey,
203 candy_machine_state: Arc<CandyMachine>,
204 collection_update_authority: Pubkey,
205 receiver: Pubkey,
206) -> Result<Signature> {
207 let client = setup_client(&config)?;
208 let program = client.program(CANDY_MACHINE_ID);
209 let payer = program.payer();
210
211 if candy_machine_state.mint_authority != payer {
212 return Err(anyhow!(
213 "Payer is not the Candy Machine mint authority, mint disallowed."
214 ));
215 }
216
217 let nft_mint = Keypair::new();
218 let metaplex_program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID)?;
219 let token = get_associated_token_address(&receiver, &nft_mint.pubkey());
221
222 let collection_mint = candy_machine_state.collection_mint;
223
224 let (authority_pda, _) = find_candy_machine_creator_pda(&candy_machine_id);
225
226 let (token_record, collection_delegate_record) =
227 if matches!(candy_machine_state.version, AccountVersion::V1) {
228 (
229 None,
230 find_collection_authority_account(&collection_mint, &authority_pda).0,
231 )
232 } else {
233 (
234 Some(find_token_record_account(&nft_mint.pubkey(), &token).0),
235 find_metadata_delegate_record_account(
236 &collection_mint,
237 MetadataDelegateRole::Collection,
238 &collection_update_authority,
239 &authority_pda,
240 )
241 .0,
242 )
243 };
244
245 let collection_metadata = find_metadata_pda(&collection_mint);
246
247 let data = program.rpc().get_account_data(&collection_metadata)?;
248 let metadata = Metadata::safe_deserialize(data.as_slice())?;
249
250 let metadata_pda = find_metadata_pda(&nft_mint.pubkey());
251 let master_edition_pda = find_master_edition_pda(&nft_mint.pubkey());
252
253 let mint_ix = program
254 .request()
255 .accounts(nft_accounts::MintV2 {
256 candy_machine: candy_machine_id,
257 authority_pda,
258 payer,
259 nft_owner: receiver,
260 token: Some(token),
261 token_record,
262 mint_authority: payer,
263 nft_metadata: metadata_pda,
264 nft_mint: nft_mint.pubkey(),
265 nft_master_edition: master_edition_pda,
266 nft_mint_authority: payer,
267 collection_mint: candy_machine_state.collection_mint,
268 collection_metadata: find_metadata_pda(&candy_machine_state.collection_mint),
269 collection_master_edition: find_master_edition_pda(
270 &candy_machine_state.collection_mint,
271 ),
272 collection_delegate_record,
273 collection_update_authority: metadata.update_authority,
274 token_metadata_program: metaplex_program_id,
275 spl_token_program: TOKEN_PROGRAM_ID,
276 spl_ata_program: Some(spl_associated_token_account::ID),
277 system_program: system_program::id(),
278 sysvar_instructions: sysvar::instructions::ID,
279 recent_slothashes: sysvar::slot_hashes::ID,
280 authorization_rules_program: None,
281 authorization_rules: None,
282 })
283 .args(nft_instruction::MintV2 {});
284
285 let mut mint_ix = mint_ix.instructions()?;
286
287 for account_meta in &mut mint_ix[0].accounts {
288 if account_meta.pubkey == nft_mint.pubkey() {
289 account_meta.is_signer = true;
290 account_meta.is_writable = true;
291 }
292 }
293
294 let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(COMPUTE_UNITS);
296
297 let builder = program
298 .request()
299 .instruction(compute_ix)
300 .instruction(mint_ix[0].clone())
301 .signer(&nft_mint);
302
303 let sig = builder.send()?;
304
305 if let Err(_) | Ok(Response { value: None, .. }) = program
306 .rpc()
307 .get_account_with_commitment(&metadata_pda, CommitmentConfig::processed())
308 {
309 let cluster_param = match get_cluster(program.rpc()).unwrap_or(Cluster::Mainnet) {
310 Cluster::Devnet => "?devnet",
311 _ => "",
312 };
313 return Err(anyhow!(
314 "Minting most likely failed with a bot tax. Check the transaction link for more details: https://explorer.miraland.top/tx/{}{}",
315 sig.to_string(),
316 cluster_param,
317 ));
318 }
319
320 info!("Minted! TxId: {}", sig);
321
322 Ok(sig)
323}