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
56const 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 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 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 let nft_lookup: HashMap<String, &CacheItem> = cache
185 .items
186 .iter()
187 .filter(|(k, _)| *k != "-1") .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 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}