sugar_cli/verify/
process.rs

1use 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    // loads the cache file (this needs to have been created by
36    // the upload command)
37    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        // nothing else to do, just tell that the cache file was not found (or empty)
48        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        // nothing else to do, there are no config lines in a candy machine
92        // with hidden settings
93        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        // sleeps for a about 1 second
102        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}