sugar_cli/verify/
process.rs1use std::{thread, time::Duration};
2
3use anchor_lang::AccountDeserialize;
4use borsh::BorshDeserialize;
5use console::style;
6use mpl_candy_machine_core::{constants::HIDDEN_SECTION, CandyMachine};
7use mpl_token_metadata::state::Metadata;
8
9use crate::{
10 cache::*,
11 candy_machine::CANDY_MACHINE_ID,
12 common::*,
13 config::Cluster,
14 constants::{CANDY_EMOJI, PAPER_EMOJI},
15 pdas::find_metadata_pda,
16 utils::*,
17 verify::VerifyError,
18};
19
20pub struct VerifyArgs {
21 pub keypair: Option<String>,
22 pub rpc_url: Option<String>,
23 pub cache: String,
24}
25
26#[derive(Debug)]
27pub struct OnChainItem {
28 pub name: String,
29 pub uri: String,
30}
31
32pub fn process_verify(args: VerifyArgs) -> Result<()> {
33 let sugar_config = sugar_setup(args.keypair, args.rpc_url)?;
34
35 let mut cache = load_cache(&args.cache, false)?;
38
39 if cache.items.is_empty() {
40 println!(
41 "{}",
42 style("No cache items found - run 'upload' to create the cache file first.")
43 .red()
44 .bold()
45 );
46
47 return Err(CacheError::CacheFileNotFound(args.cache).into());
49 }
50
51 println!(
52 "{} {}Loading candy machine",
53 style("[1/2]").bold().dim(),
54 CANDY_EMOJI
55 );
56
57 let pb = spinner_with_style();
58 pb.set_message("Connecting...");
59
60 let candy_machine_pubkey = match Pubkey::from_str(&cache.program.candy_machine) {
61 Ok(pubkey) => pubkey,
62 Err(_) => {
63 pb.finish_and_clear();
64 return Err(CacheError::InvalidCandyMachineAddress(
65 cache.program.candy_machine.clone(),
66 )
67 .into());
68 }
69 };
70
71 let client = setup_client(&sugar_config)?;
72 let program = client.program(CANDY_MACHINE_ID);
73
74 let data = match program.rpc().get_account_data(&candy_machine_pubkey) {
75 Ok(account_data) => account_data,
76 Err(err) => {
77 return Err(VerifyError::FailedToGetAccountData(err.to_string()).into());
78 }
79 };
80 let candy_machine: CandyMachine = CandyMachine::try_deserialize(&mut data.as_slice())?;
81
82 pb.finish_with_message("Completed");
83
84 println!(
85 "\n{} {}Verification",
86 style("[2/2]").bold().dim(),
87 PAPER_EMOJI
88 );
89
90 if candy_machine.data.hidden_settings.is_some() {
91 println!("\nHidden settings enabled. No config items to verify.");
94 } else if let Some(config_line_settings) = &candy_machine.data.config_line_settings {
95 let num_items = candy_machine.data.items_available;
96 let cache_items = &mut cache.items;
97 let mut errors = Vec::new();
98
99 println!("Verifying {} config line(s): (Ctrl+C to abort)", num_items);
100 let pb = progress_bar_with_style(num_items);
101 let step: u64 = if num_items > 0 {
103 1_000_000u64 / num_items
104 } else {
105 0
106 };
107
108 let line_size = candy_machine.data.get_config_line_size();
109 let name_length = config_line_settings.name_length as usize;
110 let uri_length = config_line_settings.uri_length as usize;
111
112 for i in 0..num_items {
113 let name_start = HIDDEN_SECTION + STRING_LEN_SIZE + line_size * (i as usize);
114 let name_end = name_start + name_length;
115
116 let uri_start = name_end;
117 let uri_end = uri_start + uri_length;
118
119 let name_error = format!("Failed to decode name for item {}", i);
120 let name = String::from_utf8(data[name_start..name_end].to_vec())
121 .expect(&name_error)
122 .trim_matches(char::from(0))
123 .to_string();
124
125 let uri_error = format!("Failed to decode uri for item {}", i);
126 let uri = String::from_utf8(data[uri_start..uri_end].to_vec())
127 .expect(&uri_error)
128 .trim_matches(char::from(0))
129 .to_string();
130
131 let on_chain_item = OnChainItem {
132 name: config_line_settings.prefix_name.to_string() + &name,
133 uri: config_line_settings.prefix_uri.to_string() + &uri,
134 };
135 let cache_item = cache_items
136 .get_mut(&i.to_string())
137 .expect("Failed to get item from config.");
138
139 if let Err(err) = items_match(cache_item, &on_chain_item) {
140 cache_item.on_chain = false;
141 errors.push((i.to_string(), err.to_string()));
142 }
143
144 pb.inc(1);
145 thread::sleep(Duration::from_micros(step));
146 }
147
148 if !errors.is_empty() {
149 pb.abandon_with_message(format!("{}", style("Verification failed ").red().bold()));
150 cache.sync_file()?;
151
152 let total = errors.len();
153 println!("\nInvalid items found: ");
154
155 for e in errors {
156 println!("- Item {}: {}", e.0, e.1);
157 }
158 println!("\nCache updated - re-run `deploy`.");
159 return Err(anyhow!("{} invalid item(s) found.", total));
160 } else {
161 pb.finish_with_message(format!(
162 "{}",
163 style("Config line verification successful ").green().bold()
164 ));
165 }
166 } else {
167 return Err(anyhow!(
168 "Could not determine candy machine config line settings"
169 ));
170 }
171
172 if candy_machine.items_redeemed > 0 {
173 println!(
174 "\nAn item has already been minted. Skipping candy machine collection verification..."
175 );
176 } else {
177 let collection_mint_cache = cache.program.collection_mint.clone();
178 let collection_needs_deploy = if let Some(collection_item) = cache.items.get("-1") {
179 !collection_item.on_chain
180 } else {
181 false
182 };
183 let collection_item = cache.items.get_mut("-1");
184
185 let collection_metadata = find_metadata_pda(&candy_machine.collection_mint);
186 let data = program.rpc().get_account_data(&collection_metadata)?;
187 let metadata: Metadata = BorshDeserialize::deserialize(&mut data.as_slice())?;
188
189 if metadata.mint.to_string() != collection_mint_cache {
190 println!("\nInvalid collection state found");
191 cache.program.collection_mint = metadata.mint.to_string();
192 if let Some(collection_item) = collection_item {
193 collection_item.on_chain = false;
194 }
195 cache.sync_file()?;
196 println!("Cache updated - re-run `deploy`.");
197 return Err(anyhow!(
198 "Collection mint in cache {} doesn't match on chain collection mint {}!",
199 collection_mint_cache,
200 metadata.mint.to_string()
201 ));
202 } else if collection_needs_deploy {
203 println!("\nInvalid collection state found - re-run `deploy`.");
204 return Err(CacheError::InvalidState.into());
205 }
206 }
207
208 let cluster = match get_cluster(program.rpc())? {
209 Cluster::Devnet => "devnet",
210 Cluster::Mainnet => "mainnet",
211 Cluster::Localnet => "localnet",
212 Cluster::Unknown => "",
213 };
214
215 if cluster.is_empty() {
216 println!("\nVerification successful. You're good to go!");
217 } else {
218 println!(
219 "\nVerification successful. You're good to go!\n\nSee your candy machine at:\n -> https://www.miraland.xyz/address/{}?cluster={}",
220 cache.program.candy_machine,
221 cluster
222 );
223 }
224 Ok(())
225}
226
227fn items_match(cache_item: &CacheItem, on_chain_item: &OnChainItem) -> Result<()> {
228 if cache_item.name != on_chain_item.name {
229 return Err(VerifyError::Mismatch(
230 "name".to_string(),
231 cache_item.name.clone(),
232 on_chain_item.name.clone(),
233 )
234 .into());
235 } else if cache_item.metadata_link != on_chain_item.uri {
236 return Err(VerifyError::Mismatch(
237 "uri".to_string(),
238 cache_item.metadata_link.clone(),
239 on_chain_item.uri.clone(),
240 )
241 .into());
242 }
243
244 Ok(())
245}