sugar_cli/deploy/
process.rs

1use std::{
2    collections::HashSet,
3    fmt::Write as _,
4    str::FromStr,
5    sync::{
6        atomic::{AtomicBool, Ordering},
7        Arc,
8    },
9};
10
11use anchor_client::solana_sdk::{
12    pubkey::Pubkey,
13    signature::{Keypair, Signer},
14};
15use anyhow::Result;
16use console::style;
17use mpl_token_metadata::state::{Metadata, TokenMetadataAccount};
18
19use crate::{
20    cache::*,
21    candy_machine::{get_candy_machine_state, CANDY_MACHINE_ID},
22    common::*,
23    config::parser::get_config_data,
24    deploy::{
25        create_candy_machine_data, create_collection, errors::*, generate_config_lines,
26        initialize_candy_machine, upload_config_lines,
27    },
28    hash::hash_and_update,
29    pdas::find_metadata_pda,
30    setup::{setup_client, sugar_setup},
31    update::{process_update, UpdateArgs},
32    utils::*,
33    validate::parser::{check_name, check_seller_fee_basis_points, check_symbol, check_url},
34};
35
36pub struct DeployArgs {
37    pub config: String,
38    pub cache: String,
39    pub keypair: Option<String>,
40    pub rpc_url: Option<String>,
41    pub interrupted: Arc<AtomicBool>,
42    pub collection_mint: Option<String>,
43}
44
45pub async fn process_deploy(args: DeployArgs) -> Result<()> {
46    // loads the cache file (this needs to have been created by
47    // the upload command)
48    let mut cache = load_cache(&args.cache, false)?;
49
50    if cache.items.is_empty() {
51        println!(
52            "{}",
53            style("No cache items found - run 'upload' to create the cache file first.")
54                .red()
55                .bold()
56        );
57
58        // nothing else to do, just tell that the cache file was not found (or empty)
59        return Err(CacheError::CacheFileNotFound(args.cache).into());
60    }
61
62    // checks that all metadata information are present and have the
63    // correct length
64
65    for (index, item) in &cache.items.0 {
66        if item.name.is_empty() {
67            return Err(DeployError::MissingName(index.to_string()).into());
68        } else {
69            check_name(&item.name)?;
70        }
71
72        if item.metadata_link.is_empty() {
73            return Err(DeployError::MissingMetadataLink(index.to_string()).into());
74        } else {
75            check_url(&item.metadata_link)?;
76        }
77    }
78
79    let sugar_config = Arc::new(sugar_setup(args.keypair.clone(), args.rpc_url.clone())?);
80    let client = setup_client(&sugar_config)?;
81    let mut config_data = get_config_data(&args.config)?;
82
83    let candy_machine_address = cache.program.candy_machine.clone();
84
85    // checks the candy machine data
86
87    let num_items = config_data.number;
88    let hidden = config_data.hidden_settings.is_some();
89    let collection_in_cache = cache.items.get("-1").is_some();
90
91    let cache_items_sans_collection = (cache.items.len() - collection_in_cache as usize) as u64;
92
93    if num_items != cache_items_sans_collection {
94        return Err(anyhow!(
95            "Number of items ({}) do not match cache items ({}). 
96            Item number in the config should only include asset files, not the collection file.",
97            num_items,
98            cache_items_sans_collection
99        ));
100    } else {
101        check_symbol(&config_data.symbol)?;
102        check_seller_fee_basis_points(config_data.seller_fee_basis_points)?;
103    }
104
105    let total_steps = 2 + if candy_machine_address.is_empty() {
106        collection_in_cache as u8
107    } else {
108        0
109    } - (hidden as u8);
110
111    let candy_pubkey = if candy_machine_address.is_empty() {
112        let candy_keypair = Keypair::new();
113        let candy_pubkey = candy_keypair.pubkey();
114
115        // collection_item could be missing when args.collection_mint is provided
116        let collection_item = if let Some(collection_item) = cache.items.get_mut("-1") {
117            Some(collection_item)
118        } else {
119            match args.collection_mint {
120                Some(_) => None, // existing collection provided
121                None => return Err(anyhow!("Missing collection item in cache")),
122            }
123        };
124
125        println!(
126            "\n{} {}Creating collection NFT for candy machine",
127            style(format!("[1/{}]", total_steps)).bold().dim(),
128            COLLECTION_EMOJI
129        );
130
131        let collection_minted = match collection_item {
132            Some(item) => item.on_chain,
133            None => args.collection_mint.is_some(), // the provided collection must have been minted
134        };
135        let collection_str = args
136            .collection_mint
137            .clone()
138            .unwrap_or_else(|| cache.program.collection_mint.clone());
139
140        let collection_mint = if collection_minted {
141            println!("\nCollection mint already deployed.");
142            Pubkey::from_str(&collection_str)?
143        } else {
144            let pb = spinner_with_style();
145            pb.set_message("Creating NFT...");
146
147            let (_, collection_mint) =
148                create_collection(&client, candy_pubkey, &mut cache, &config_data)?;
149
150            pb.finish_and_clear();
151            println!(
152                "{} {}",
153                style("Collection mint ID:").bold(),
154                collection_mint
155            );
156
157            collection_mint
158        };
159
160        println!(
161            "{} {}Creating candy machine",
162            style(format!("\n[2/{}]", total_steps)).bold().dim(),
163            CANDY_EMOJI
164        );
165        info!("Candy machine address is empty, creating new candy machine...");
166
167        let spinner = spinner_with_style();
168        spinner.set_message("Creating candy machine...");
169
170        let candy_data = create_candy_machine_data(&client, &config_data, &cache)?;
171        let program = client.program(CANDY_MACHINE_ID);
172
173        // all good, let's create the candy machine
174
175        let collection_metadata = find_metadata_pda(&collection_mint);
176        let data = program.rpc().get_account_data(&collection_metadata)?;
177        let metadata = Metadata::safe_deserialize(data.as_slice())?;
178
179        let sig = initialize_candy_machine(
180            &config_data,
181            &candy_keypair,
182            candy_data,
183            collection_mint,
184            metadata.update_authority,
185            program,
186        )?;
187        info!("Candy machine initialized with sig: {}", sig);
188        info!(
189            "Candy machine created with address: {}",
190            &candy_pubkey.to_string()
191        );
192
193        cache.program = CacheProgram::new_from_cm(&candy_pubkey);
194        cache.program.collection_mint = collection_mint.to_string();
195        cache.sync_file()?;
196
197        spinner.finish_and_clear();
198
199        candy_pubkey
200    } else {
201        println!(
202            "{} {}Loading candy machine",
203            style(format!("[1/{}]", total_steps)).bold().dim(),
204            CANDY_EMOJI
205        );
206
207        let candy_pubkey = match Pubkey::from_str(&candy_machine_address) {
208            Ok(pubkey) => pubkey,
209            Err(_err) => {
210                error!(
211                    "Invalid candy machine address in cache file: {}!",
212                    candy_machine_address
213                );
214                return Err(CacheError::InvalidCandyMachineAddress(
215                    candy_machine_address.to_string(),
216                )
217                .into());
218            }
219        };
220
221        if get_candy_machine_state(&Arc::clone(&sugar_config), &candy_pubkey).is_err() {
222            println!(
223                "\n{} Candy machine {} not found on-chain",
224                WARNING_EMOJI, candy_machine_address
225            );
226            println!(
227                "\nThis can happen if you are trying to re-deploy a candy machine from \
228                    a previously used cache file. If this is the case, re-run the deploy command \
229                    with the option '--new'.",
230            );
231
232            return Err(anyhow!(
233                "Candy machine from cache does't exist on chain: {}",
234                candy_machine_address
235            ));
236        }
237
238        candy_pubkey
239    };
240
241    println!("{} {}", style("Candy machine ID:").bold(), candy_pubkey);
242
243    // Hidden Settings check needs to be the last action in this command, so we can
244    // update the hash with the final cache state.
245    if !hidden {
246        let step_num = 2 + if candy_machine_address.is_empty() {
247            collection_in_cache as u8
248        } else {
249            0
250        };
251        println!(
252            "\n{} {}Writing config lines",
253            style(format!("[{}/{}]", step_num, total_steps))
254                .bold()
255                .dim(),
256            PAPER_EMOJI
257        );
258
259        let cndy_state = get_candy_machine_state(&sugar_config, &candy_pubkey)?;
260        let cndy_data = cndy_state.data;
261
262        let config_lines = generate_config_lines(num_items, &cache.items, &cndy_data)?;
263
264        if config_lines.is_empty() {
265            println!("\nAll config lines deployed.");
266        } else {
267            // clear the interruption handler value ahead of the upload
268            args.interrupted.store(false, Ordering::SeqCst);
269
270            let errors = upload_config_lines(
271                Arc::clone(&sugar_config),
272                candy_pubkey,
273                &mut cache,
274                config_lines,
275                args.interrupted,
276            )
277            .await?;
278
279            if !errors.is_empty() {
280                let mut message = String::new();
281                write!(
282                    message,
283                    "Failed to deploy all config lines, {0} error(s) occurred:",
284                    errors.len()
285                )?;
286
287                let mut unique = HashSet::new();
288
289                for err in errors {
290                    unique.insert(err.to_string());
291                }
292
293                for u in unique {
294                    message.push_str(&style("\n=> ").dim().to_string());
295                    message.push_str(&u);
296                }
297
298                return Err(DeployError::AddConfigLineFailed(message).into());
299            }
300        }
301    } else {
302        // If hidden settings are enabled, update the hash value with the new cache file.
303        println!("\nCandy machine with hidden settings deployed.");
304        let hidden_settings = config_data.hidden_settings.as_ref().unwrap().clone();
305
306        println!(
307            "\nHidden settings hash: {}",
308            hash_and_update(hidden_settings, &args.config, &mut config_data, &args.cache,)?
309        );
310
311        println!("\nUpdating candy machine state with new hash value:\n");
312        let update_args = UpdateArgs {
313            keypair: args.keypair,
314            rpc_url: args.rpc_url,
315            cache: args.cache,
316            new_authority: None,
317            config: args.config,
318            candy_machine: Some(candy_pubkey.to_string()),
319        };
320
321        process_update(update_args)?;
322    }
323
324    Ok(())
325}