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 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 return Err(CacheError::CacheFileNotFound(args.cache).into());
60 }
61
62 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 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 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, 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(), };
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 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 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 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 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}