1use std::time::Duration;
2
3use mpl_candy_guard::{
4 accounts::Route as RouteAccount, guards::FreezeInstruction, instruction::Route,
5 instructions::RouteArgs, state::GuardType,
6};
7
8use super::*;
9
10pub struct ThawArgs {
11 pub keypair: Option<String>,
12 pub rpc_url: Option<String>,
13 pub cache: String,
14 pub config: String,
15 pub all: bool,
16 pub nft_mint: Option<String>,
17 pub candy_guard: Option<String>,
18 pub candy_machine: Option<String>,
19 pub destination: Option<String>,
20 pub label: Option<String>,
21 pub use_cache: bool,
22 pub timeout: Option<u64>,
23}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
26struct FailedThaw {
27 nft: ThawNft,
28 error: String,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
32struct ThawNft {
33 #[serde(serialize_with = "serialize_pubkey")]
34 mint: Pubkey,
35 #[serde(serialize_with = "serialize_pubkey")]
36 owner: Pubkey,
37 #[serde(serialize_with = "serialize_pubkey")]
38 token_account: Pubkey,
39}
40
41fn serialize_pubkey<S>(p: &Pubkey, serializer: S) -> Result<S::Ok, S::Error>
42where
43 S: Serializer,
44{
45 p.to_string().serialize(serializer)
46}
47
48#[derive(Debug, Deserialize)]
49pub struct JRpcResponse {
50 value: Vec<TokenAccount>,
51}
52
53#[derive(Debug, Deserialize)]
54struct TokenAccount {
55 address: String,
56 amount: String,
57}
58
59const DEFAULT_TIMEOUT: u64 = 300;
61
62pub async fn process_thaw(args: ThawArgs) -> Result<()> {
63 let sugar_config = sugar_setup(args.keypair.clone(), args.rpc_url.clone())?;
64 let client = setup_client(&sugar_config)?;
65 let program = client.program(mpl_candy_guard::ID);
66 let rpc_url = get_rpc_url(args.rpc_url.clone());
67 let rpc_client = RpcClient::new(&rpc_url);
68
69 let candy_guard_id = match args.candy_guard {
71 Some(ref candy_guard_id) => candy_guard_id.to_owned(),
72 None => {
73 let cache = load_cache(&args.cache, false)?;
74 cache.program.candy_guard
75 }
76 };
77
78 let candy_machine_id = match args.candy_machine {
80 Some(ref candy_machine_id) => candy_machine_id.to_owned(),
81 None => {
82 let cache = load_cache(&args.cache, false)?;
83 cache.program.candy_machine
84 }
85 };
86
87 let candy_guard = Pubkey::from_str(&candy_guard_id)
88 .map_err(|_| anyhow!("Failed to parse candy guard id: {}", &candy_guard_id))?;
89
90 let candy_machine = Pubkey::from_str(&candy_machine_id)
91 .map_err(|_| anyhow!("Failed to parse candy machine id: {}", &candy_guard_id))?;
92
93 let total_steps = if args.all { 4 } else { 2 };
94
95 println!(
96 "{} {}Loading freeze escrow information",
97 style(format!("[1/{}]", total_steps)).bold().dim(),
98 LOOKING_GLASS_EMOJI
99 );
100
101 let pb = spinner_with_style();
102 pb.set_message("Connecting...");
103
104 let destination_address = match args.destination {
106 Some(ref destination_address) => Pubkey::from_str(destination_address).map_err(|_| {
107 anyhow!(
108 "Failed to parse destination address: {}",
109 &destination_address
110 )
111 })?,
112 None => get_destination(
113 &program,
114 &candy_guard,
115 get_config_data(&args.config)?,
116 &args.label,
117 )?,
118 };
119
120 let (freeze_escrow, _) = find_freeze_pda(&candy_guard, &candy_machine, &destination_address);
122 let account_data = program
123 .rpc()
124 .get_account_data(&freeze_escrow)
125 .map_err(|_| anyhow!("Could not load freeze escrow"))?;
126
127 if account_data.is_empty() {
128 return Err(anyhow!("Freeze escrow account not found"));
129 }
130
131 pb.finish_with_message("Done");
132
133 if !args.all {
134 println!(
135 "\n{} {}Thawing NFT",
136 style(format!("[2/{}]", total_steps)).bold().dim(),
137 MONEY_BAG_EMOJI
138 );
139
140 let nft_mint = if let Some(nft_mint) = &args.nft_mint {
141 nft_mint.to_owned()
142 } else {
143 return Err(anyhow!("NFT mint is required if thawing a single NFT"));
144 };
145
146 let nft_mint_pubkey = Pubkey::from_str(&nft_mint)
147 .map_err(|_| anyhow!("Failed to parse nft mint id: {}", &nft_mint))?;
148
149 let config = Arc::new(sugar_config);
150
151 let request = RpcRequest::Custom {
152 method: "getTokenLargestAccounts",
153 };
154 let params = json!([nft_mint, { "commitment": "confirmed" }]);
155 let result: JRpcResponse = rpc_client.send(request, params).unwrap();
156
157 let token_accounts: Vec<TokenAccount> = result
158 .value
159 .into_iter()
160 .filter(|account| account.amount.parse::<u64>().unwrap() == 1)
161 .collect();
162
163 if token_accounts.len() > 1 {
164 return Err(anyhow!(
165 "Mint account {} had more than one token account with 1 token",
166 nft_mint
167 ));
168 }
169
170 if token_accounts.is_empty() {
171 return Err(anyhow!(
172 "Mint account {} had zero token accounts with 1 token",
173 nft_mint
174 ));
175 }
176
177 let token_account = Pubkey::from_str(&token_accounts[0].address).unwrap();
178
179 let account = program
180 .rpc()
181 .get_account_with_commitment(&token_account, CommitmentConfig::confirmed())
182 .unwrap()
183 .value
184 .unwrap();
185 let account_data = SplAccount::unpack(&account.data).unwrap();
186 let owner = account_data.owner;
187
188 if !account_data.is_frozen() {
190 println!("\n NFT is already thawed.");
191 return Ok(());
192 }
193
194 let nft = ThawNft {
195 mint: nft_mint_pubkey,
196 owner,
197 token_account,
198 };
199
200 let pb = spinner_with_style();
201 pb.set_message("Sending thaw transaction...");
202
203 let signature = thaw_nft(
204 config,
205 &candy_guard,
206 &candy_machine,
207 &destination_address,
208 &nft,
209 &args.label,
210 )?;
211
212 pb.finish_with_message(format!(
213 "{} {}",
214 style("Thaw NFT signature:").bold(),
215 signature
216 ));
217 return Ok(());
218 }
219
220 println!(
222 "\n{} {}Getting minted NFTs for candy guard {}",
223 style(format!("[2/{}]", total_steps)).bold().dim(),
224 LOOKING_GLASS_EMOJI,
225 candy_guard_id
226 );
227
228 let pb = spinner_with_style();
229 pb.set_message("Searching...");
230
231 let miraland_cluster: Cluster = get_cluster(program.rpc())?;
232 let rpc_url = get_rpc_url(args.rpc_url);
233 let client = RpcClient::new_with_timeout(
234 &rpc_url,
235 Duration::from_secs(if let Some(timeout) = args.timeout {
236 timeout
237 } else {
238 DEFAULT_TIMEOUT
239 }),
240 );
241
242 let miraland_cluster = if rpc_url.ends_with("8899") {
243 Cluster::Localnet
244 } else {
245 miraland_cluster
246 };
247
248 let mint_pubkeys: Vec<Pubkey> =
250 if args.use_cache && Path::exists(Path::new("mint_pubkeys_cache.json")) {
251 let mint_pubkeys_cache = File::open("mint_pubkeys_cache.json")?;
252 let cache: Vec<String> = serde_json::from_reader(mint_pubkeys_cache)?;
253 cache
254 .iter()
255 .map(|x| {
256 Pubkey::from_str(x)
257 .map_err(|_| anyhow!("Invalid pubkey found: {}", x))
258 .unwrap()
259 })
260 .collect()
261 } else {
262 match miraland_cluster {
263 Cluster::Devnet | Cluster::Localnet | Cluster::Mainnet => {
264 let (creator, _) = find_candy_machine_creator_pda(&candy_machine);
265 let creator = bs58::encode(creator).into_string();
266 get_cm_creator_mint_accounts(&client, &creator, 0)?
267 }
268 _ => {
269 return Err(anyhow!(
270 "Cluster being used is unsupported for this command."
271 ))
272 }
273 }
274 };
275
276 if mint_pubkeys.is_empty() {
277 pb.finish_with_message(format!("{}", style("No NFTs found.").green().bold()));
278 return Err(anyhow!(format!(
279 "No NFTs found for candy machine id {candy_guard_id}.",
280 )));
281 } else {
282 pb.finish_with_message(format!("Found {:?} accounts", mint_pubkeys.len() as u64));
283 }
284
285 if args.use_cache {
287 let mint_pubkeys_cache = File::create("mint_pubkeys_cache.json")?;
288 let mint_list: Vec<String> = mint_pubkeys.iter().map(|x| x.to_string()).collect();
289 serde_json::to_writer_pretty(mint_pubkeys_cache, &mint_list)?;
290 }
291
292 println!();
294
295 let pb = progress_bar_with_style(mint_pubkeys.len() as u64);
296 pb.set_message("Getting NFT information....");
297
298 let semaphore = Arc::new(Semaphore::new(100));
299 let client = Arc::new(client);
300
301 let mut tasks = Vec::new();
302 let mut thaw_tasks = Vec::new();
303 let errors = Arc::new(Mutex::new(Vec::new()));
304 let thaw_errors = Arc::new(Mutex::new(Vec::new()));
305 let thaw_nfts = Arc::new(Mutex::new(Vec::new()));
306 let failed_thaws = Arc::new(Mutex::new(Vec::new()));
307
308 let mint_pubkeys_len = mint_pubkeys.len();
309
310 for mint in mint_pubkeys {
311 let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap();
312 let client = client.clone();
313 let pb = pb.clone();
314 let errors = errors.clone();
315 let thaw_nfts = thaw_nfts.clone();
316
317 tasks.push(tokio::spawn(async move {
318 let _permit = permit;
319
320 let request = RpcRequest::Custom {
321 method: "getTokenLargestAccounts",
322 };
323 let params = json!([mint.to_string(), { "commitment": "confirmed" }]);
324 let result: JRpcResponse = client.send(request, params).unwrap();
325
326 let token_accounts: Vec<TokenAccount> = result
327 .value
328 .into_iter()
329 .filter(|account| account.amount.parse::<u64>().unwrap() == 1)
330 .collect();
331
332 if token_accounts.len() != 1 {
333 errors.lock().unwrap().push(anyhow!(
334 "Mint account {} had more than one token account with 1 token",
335 mint
336 ));
337 return;
338 }
339
340 let token_account = Pubkey::from_str(&token_accounts[0].address).unwrap();
341 let account = client
342 .get_account_with_commitment(&token_account, CommitmentConfig::confirmed())
343 .unwrap()
344 .value
345 .unwrap();
346 let account_data = SplAccount::unpack(&account.data).unwrap();
347 let owner = account_data.owner;
348
349 if account_data.is_frozen() {
351 thaw_nfts.lock().unwrap().push(ThawNft {
352 mint,
353 token_account,
354 owner,
355 });
356
357 pb.inc(1);
358 }
359 }));
360 }
361
362 for task in tasks {
363 task.await
364 .map_err(|err| errors.lock().unwrap().push(anyhow!(err)))
365 .ok();
366 }
367
368 if !errors.lock().unwrap().is_empty() {
369 println!(
370 "{} {}/{} {}",
371 style("Found :").bold(),
372 errors.lock().unwrap().len(),
373 mint_pubkeys_len,
374 style("NFT information").bold()
375 );
376 }
377
378 pb.finish_with_message(format!(
379 "{}",
380 style("Finished fetching NFT information ").green().bold()
381 ));
382
383 let config = Arc::new(sugar_config);
384
385 println!();
387
388 let nfts = thaw_nfts.lock().unwrap().clone();
389 let thaw_pb = progress_bar_with_style(nfts.len() as u64);
390 thaw_pb.set_message("Thawing NFTs....");
391
392 for nft in nfts.into_iter() {
393 let permit = Arc::clone(&semaphore).acquire_owned().await.unwrap();
394 let thaw_pb = thaw_pb.clone();
395 let failed_thaws = failed_thaws.clone();
396
397 let config = config.clone();
398 let label = args.label.to_owned();
399
400 thaw_tasks.push(tokio::spawn(async move {
401 let _permit = permit;
402
403 let _signature = thaw_nft(
404 config,
405 &candy_guard,
406 &candy_machine,
407 &destination_address,
408 &nft,
409 &label,
410 )
411 .map_err(|e| {
412 failed_thaws.lock().unwrap().push(FailedThaw {
413 nft: nft.clone(),
414 error: e.to_string(),
415 });
416 });
417
418 thaw_pb.inc(1);
419 }));
420 }
421
422 for task in thaw_tasks {
423 match task.await {
424 Ok(_) => {}
425 Err(err) => thaw_errors.lock().unwrap().push(anyhow!(err)),
426 }
427 }
428
429 if !thaw_errors.lock().unwrap().is_empty() || !failed_thaws.lock().unwrap().is_empty() {
430 thaw_pb.abandon_with_message(format!(
431 "{}",
432 style("Failed to Thaw all NFTs ").red().bold()
433 ));
434 let failed_thaws = Arc::try_unwrap(failed_thaws).unwrap().into_inner().unwrap();
435
436 let failed_thaws_cache = File::create("failed_thaws.json")?;
437 serde_json::to_writer(failed_thaws_cache, &failed_thaws)?;
438
439 return Err(anyhow!("Not all NFTs were thawed.".to_string()));
440 } else {
441 thaw_pb.finish_with_message(format!(
442 "{}",
443 style("All NFTs thawed successfully ").green().bold()
444 ));
445 }
446
447 let remaining_nfts = Arc::try_unwrap(failed_thaws).unwrap().into_inner().unwrap();
448
449 if !remaining_nfts.is_empty() {
450 let remaining_items_cache = File::create("remaining_thaw_items_cache.json")?;
451 serde_json::to_writer_pretty(remaining_items_cache, &remaining_nfts)?;
452 }
453
454 Ok(())
455}
456
457fn thaw_nft(
458 config: Arc<SugarConfig>,
459 candy_guard_id: &Pubkey,
460 candy_machine_id: &Pubkey,
461 destination: &Pubkey,
462 nft: &ThawNft,
463 label: &Option<String>,
464) -> Result<Signature> {
465 let client = setup_client(&config)?;
466 let program = client.program(mpl_candy_guard::ID);
467
468 let mut remaining_accounts = Vec::with_capacity(7);
469 let (freeze_pda, _) = find_freeze_pda(candy_guard_id, candy_machine_id, destination);
470 remaining_accounts.push(AccountMeta {
471 pubkey: freeze_pda,
472 is_signer: false,
473 is_writable: true,
474 });
475 remaining_accounts.push(AccountMeta {
476 pubkey: nft.mint,
477 is_signer: false,
478 is_writable: false,
479 });
480 remaining_accounts.push(AccountMeta {
481 pubkey: nft.owner,
482 is_signer: false,
483 is_writable: false,
484 });
485 remaining_accounts.push(AccountMeta {
486 pubkey: get_associated_token_address(&nft.owner, &nft.mint),
487 is_signer: false,
488 is_writable: true,
489 });
490 remaining_accounts.push(AccountMeta {
491 pubkey: find_master_edition_pda(&nft.mint),
492 is_signer: false,
493 is_writable: false,
494 });
495 remaining_accounts.push(AccountMeta {
496 pubkey: spl_token::ID,
497 is_signer: false,
498 is_writable: false,
499 });
500 remaining_accounts.push(AccountMeta {
501 pubkey: Pubkey::from_str(METAPLEX_PROGRAM_ID)?,
502 is_signer: false,
503 is_writable: false,
504 });
505
506 let builder = program
507 .request()
508 .accounts(RouteAccount {
509 candy_guard: *candy_guard_id,
510 candy_machine: *candy_machine_id,
511 payer: program.payer(),
512 })
513 .accounts(remaining_accounts)
514 .args(Route {
515 args: RouteArgs {
516 data: vec![FreezeInstruction::Thaw as u8],
517 guard: GuardType::FreezeSolPayment,
518 },
519 label: label.to_owned(),
520 });
521 let sig = builder.send()?;
522
523 Ok(sig)
524}