sugar_cli/mint/
process.rs

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    // the candy machine id specified takes precedence over the one from the cache
51
52    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            // Start tasks
154            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        // Resolve tasks
172        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    // derive associated token account
220    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    // need to increase the number of compute units
295    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}