sugar_cli/reveal/
process.rs

1use std::{
2    sync::{Arc, Mutex},
3    time::Duration,
4};
5
6use anchor_client::solana_sdk::account::Account;
7use anchor_lang::AnchorDeserialize;
8use console::style;
9use futures::future::join_all;
10use miraland_client::{client_error::ClientError, rpc_client::RpcClient};
11use mpl_token_metadata::{
12    instruction::update_metadata_accounts_v2,
13    state::{DataV2, Metadata},
14    ID as TOKEN_METADATA_PROGRAM_ID,
15};
16use serde::Serialize;
17use tokio::sync::Semaphore;
18
19use crate::{
20    cache::load_cache,
21    candy_machine::CANDY_MACHINE_ID,
22    common::*,
23    config::{get_config_data, Cluster},
24    pdas::{find_candy_machine_creator_pda, find_metadata_pda},
25    setup::get_rpc_url,
26    utils::*,
27};
28
29pub struct RevealArgs {
30    pub keypair: Option<String>,
31    pub rpc_url: Option<String>,
32    pub cache: String,
33    pub config: String,
34    pub timeout: Option<u64>,
35}
36
37#[derive(Clone, Debug)]
38pub struct MetadataUpdateValues {
39    pub metadata_pubkey: Pubkey,
40    pub metadata: Metadata,
41    pub new_uri: String,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45struct RevealTx {
46    metadata_pubkey: Pubkey,
47    result: RevealResult,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51enum RevealResult {
52    Success,
53    Failure(String),
54}
55
56// Timeout for the GPA call (in seconds).
57const DEFAULT_TIMEOUT: u64 = 300;
58
59pub async fn process_reveal(args: RevealArgs) -> Result<()> {
60    println!(
61        "{} {}Loading items from the cache",
62        style("[1/4]").bold().dim(),
63        LOOKING_GLASS_EMOJI
64    );
65
66    let spinner = spinner_with_style();
67    spinner.set_message("Connecting...");
68
69    let config = get_config_data(&args.config)?;
70
71    // If it's not a Hidden Settings mint, return an error.
72    let _hidden_settings = if let Some(hidden_settings) = config.hidden_settings {
73        hidden_settings
74    } else {
75        return Err(anyhow!("Candy machine is not a Hidden Settings mint."));
76    };
77
78    let cache = load_cache(&args.cache, false)?;
79    let sugar_config = sugar_setup(args.keypair, args.rpc_url.clone())?;
80    let anchor_client = setup_client(&sugar_config)?;
81    let program = anchor_client.program(CANDY_MACHINE_ID);
82
83    let candy_machine_id = match Pubkey::from_str(&cache.program.candy_machine) {
84        Ok(candy_machine_id) => candy_machine_id,
85        Err(_) => {
86            let error = anyhow!(
87                "Failed to parse candy machine id: {}",
88                &cache.program.candy_machine
89            );
90            error!("{:?}", error);
91            return Err(error);
92        }
93    };
94
95    spinner.finish_with_message("Done");
96
97    println!(
98        "\n{} {}Getting minted NFTs for candy machine {}",
99        style("[2/4]").bold().dim(),
100        LOOKING_GLASS_EMOJI,
101        candy_machine_id
102    );
103
104    let spinner = spinner_with_style();
105    spinner.set_message("Loading...");
106    let miraland_cluster: Cluster = get_cluster(program.rpc())?;
107    let rpc_url = get_rpc_url(args.rpc_url);
108
109    let miraland_cluster = if rpc_url.ends_with("8899") {
110        Cluster::Localnet
111    } else {
112        miraland_cluster
113    };
114
115    let metadata_pubkeys = match miraland_cluster {
116        Cluster::Mainnet | Cluster::Devnet | Cluster::Localnet => {
117            let client = RpcClient::new_with_timeout(
118                &rpc_url,
119                Duration::from_secs(if let Some(timeout) = args.timeout {
120                    timeout
121                } else {
122                    DEFAULT_TIMEOUT
123                }),
124            );
125            let (creator, _) = find_candy_machine_creator_pda(&candy_machine_id);
126            let creator = bs58::encode(creator).into_string();
127            get_cm_creator_metadata_accounts(&client, &creator, 0)?
128        }
129        _ => {
130            return Err(anyhow!(
131                "Cluster being used is unsupported for this command."
132            ))
133        }
134    };
135
136    if metadata_pubkeys.is_empty() {
137        spinner.finish_with_message(format!(
138            "{}{:?}",
139            style("No NFTs found on ").red().bold(),
140            style(miraland_cluster).red().bold()
141        ));
142        return Err(anyhow!(
143            "No minted NFTs found for candy machine {}",
144            candy_machine_id
145        ));
146    }
147
148    spinner.finish_with_message(format!(
149        "Found {:?} accounts",
150        metadata_pubkeys.len() as u64
151    ));
152
153    println!(
154        "\n{} {}Matching NFTs to cache values",
155        style("[3/4]").bold().dim(),
156        LOOKING_GLASS_EMOJI
157    );
158    let spinner = spinner_with_style();
159
160    let mut futures = Vec::new();
161    let client = RpcClient::new(&rpc_url);
162    let client = Arc::new(client);
163
164    // Get all metadata accounts.
165    metadata_pubkeys.as_slice().chunks(100).for_each(|chunk| {
166        let client = client.clone();
167        futures.push(async move { async_get_multiple_accounts(client, chunk).await });
168    });
169    let results = join_all(futures).await;
170    let mut accounts = Vec::new();
171
172    for result in results {
173        let res = result.unwrap();
174        accounts.extend(res);
175    }
176
177    let metadata: Vec<Metadata> = accounts
178        .into_iter()
179        .map(|a| a.unwrap().data)
180        .map(|d| Metadata::deserialize(&mut d.as_slice()).unwrap())
181        .collect();
182
183    // Convert cache to make keys match NFT numbers.
184    let nft_lookup: HashMap<String, &CacheItem> = cache
185        .items
186        .iter()
187        .filter(|(k, _)| *k != "-1") // skip collection index
188        .map(|(k, item)| (increment_key(k), item))
189        .collect();
190
191    spinner.finish_with_message("Done");
192
193    let mut update_values = Vec::new();
194
195    println!(
196        "\n{} {}Updating NFT URIs from cache values",
197        style("[4/4]").bold().dim(),
198        UPLOAD_EMOJI
199    );
200
201    let pattern = regex::Regex::new(r"#([0-9]+)").expect("Failed to create regex pattern.");
202
203    let spinner = spinner_with_style();
204    spinner.set_message("Setting up transactions...");
205    for m in metadata {
206        let name = m.data.name.trim_matches(char::from(0)).to_string();
207        let capture = pattern
208            .captures(&name)
209            .map(|c| c[0].to_string())
210            .ok_or_else(|| anyhow!("No captures found for {name}"))?;
211        let num = capture
212            .split('#')
213            .nth(1)
214            .ok_or_else(|| anyhow!("No NFT number found for name: {name}"))?;
215
216        let metadata_pubkey = find_metadata_pda(&m.mint);
217        let new_uri = nft_lookup
218            .get(num)
219            .ok_or_else(|| anyhow!("No URI found for number: {num}"))?
220            .metadata_link
221            .clone();
222        update_values.push(MetadataUpdateValues {
223            metadata_pubkey,
224            metadata: m,
225            new_uri,
226        });
227    }
228    spinner.finish_and_clear();
229
230    let keypair = Arc::new(sugar_config.keypair);
231    let sem = Arc::new(Semaphore::new(1000));
232    let reveal_results = Arc::new(Mutex::new(Vec::new()));
233    let mut tx_tasks = Vec::new();
234
235    let pb = progress_bar_with_style(metadata_pubkeys.len() as u64);
236    pb.set_message("Updating NFTs... ");
237
238    for item in update_values {
239        let permit = Arc::clone(&sem).acquire_owned().await.unwrap();
240        let client = client.clone();
241        let keypair = keypair.clone();
242        let reveal_results = reveal_results.clone();
243        let pb = pb.clone();
244
245        tx_tasks.push(tokio::spawn(async move {
246            // Move permit into the closure so it is dropped when the task is dropped.
247            let _permit = permit;
248            let metadata_pubkey = item.metadata_pubkey;
249            let mut tx = RevealTx {
250                metadata_pubkey,
251                result: RevealResult::Success,
252            };
253
254            match update_metadata_value(client, keypair, item).await {
255                Ok(_) => reveal_results.lock().unwrap().push(tx),
256                Err(e) => {
257                    tx.result = RevealResult::Failure(e.to_string());
258                    reveal_results.lock().unwrap().push(tx);
259                }
260            }
261
262            pb.inc(1);
263        }));
264    }
265
266    for task in tx_tasks {
267        task.await.unwrap();
268    }
269    pb.finish();
270
271    let results = reveal_results.lock().unwrap();
272
273    let errors: Vec<&RevealTx> = results
274        .iter()
275        .filter(|r| matches!(r.result, RevealResult::Failure(_)))
276        .collect();
277
278    if !errors.is_empty() {
279        println!(
280            "{}Some reveals failed. See the reveal cache file for details. Re-run the command.",
281            WARNING_EMOJI
282        );
283        let f = File::create("sugar-reveal-cache.json")
284            .map_err(|e| anyhow!("Failed to create sugar reveal cache file: {e}"))?;
285        serde_json::to_writer_pretty(f, &errors).unwrap();
286    } else {
287        println!("\n{}Reveal complete!", CONFETTI_EMOJI);
288    }
289
290    Ok(())
291}
292
293async fn async_get_multiple_accounts(
294    client: Arc<RpcClient>,
295    pubkeys: &[Pubkey],
296) -> Result<Vec<Option<Account>>, ClientError> {
297    client.get_multiple_accounts(pubkeys)
298}
299
300async fn update_metadata_value(
301    client: Arc<RpcClient>,
302    update_authority: Arc<Keypair>,
303    value: MetadataUpdateValues,
304) -> Result<(), ClientError> {
305    let mut data = value.metadata.data;
306    if data.uri.trim_matches(char::from(0)) != value.new_uri.trim_matches(char::from(0)) {
307        data.uri = value.new_uri;
308
309        let data_v2 = DataV2 {
310            name: data.name,
311            symbol: data.symbol,
312            uri: data.uri,
313            seller_fee_basis_points: data.seller_fee_basis_points,
314            creators: data.creators,
315            collection: value.metadata.collection,
316            uses: value.metadata.uses,
317        };
318
319        let ix = update_metadata_accounts_v2(
320            TOKEN_METADATA_PROGRAM_ID,
321            value.metadata_pubkey,
322            update_authority.pubkey(),
323            None,
324            Some(data_v2),
325            None,
326            None,
327        );
328
329        let recent_blockhash = client.get_latest_blockhash()?;
330        let tx = Transaction::new_signed_with_payer(
331            &[ix],
332            Some(&update_authority.pubkey()),
333            &[&*update_authority],
334            recent_blockhash,
335        );
336
337        client.send_and_confirm_transaction(&tx)?;
338    }
339
340    Ok(())
341}
342
343fn increment_key(key: &str) -> String {
344    (key.parse::<u32>()
345        .expect("Key parsing out of bounds for u32.")
346        + 1)
347    .to_string()
348}