sugar_cli/freeze/
thaw.rs

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
59// Default timeout for 300 seconds (5 minutes).
60const 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    // candy guard id specified takes precedence over the one from the cache
70    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    // candy machine id specified takes precedence over the one from the cache
79    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    // destination address specified takes precedence over the one from the cache
105    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    // sanity check: loads the PDA
121    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        // only thaw frozen accounts.
189        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    // Thaw all frozen NFTs.
221    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    // should use existing cache or not?
249    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    // create a cache of the mint list
286    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    // padding
293    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            // Only thaw frozen accounts.
350            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    // padding
386    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}