sugar-cli 2.9.1

Command line tool for creating and managing Metaplex Candy Machines.
use anchor_client::solana_sdk::compute_budget::ComputeBudgetInstruction;
use mpl_candy_guard::{
    accounts::Route as RouteAccount, guards::FreezeInstruction, instruction::Route,
    instructions::RouteArgs, state::GuardType,
};

use super::*;

pub struct UnlockFundsArgs {
    pub keypair: Option<String>,
    pub rpc_url: Option<String>,
    pub cache: String,
    pub config: String,
    pub candy_guard: Option<String>,
    pub candy_machine: Option<String>,
    pub destination: Option<String>,
    pub label: Option<String>,
    pub token: bool,
    pub priority_fee: u64,
}

pub fn process_unlock_funds(args: UnlockFundsArgs) -> Result<()> {
    let sugar_config = sugar_setup(args.keypair.clone(), args.rpc_url.clone())?;
    let client = setup_client(&sugar_config)?;
    let program = client.program(mpl_candy_guard::ID);

    // candy guard id specified takes precedence over the one from the cache
    let candy_guard_id = match args.candy_guard {
        Some(ref candy_guard_id) => candy_guard_id.to_owned(),
        None => {
            let cache = load_cache(&args.cache, false)?;
            cache.program.candy_guard
        }
    };

    // candy machine id specified takes precedence over the one from the cache
    let candy_machine_id = match args.candy_machine {
        Some(ref candy_machine_id) => candy_machine_id.to_owned(),
        None => {
            let cache = load_cache(&args.cache, false)?;
            cache.program.candy_machine
        }
    };

    let candy_guard = Pubkey::from_str(&candy_guard_id)
        .map_err(|_| anyhow!("Failed to parse candy guard id: {}", &candy_guard_id))?;

    let candy_machine = Pubkey::from_str(&candy_machine_id)
        .map_err(|_| anyhow!("Failed to parse candy machine id: {}", &candy_guard_id))?;

    println!(
        "{} {}Loading freeze escrow information",
        style("[1/2]").bold().dim(),
        LOOKING_GLASS_EMOJI
    );

    let pb = spinner_with_style();
    pb.set_message("Connecting...");

    // destination address specified takes precedence over the one from the cache
    let (destination_address, freeze_guard) = match args.destination {
        Some(ref destination_address) => {
            let address = Pubkey::from_str(destination_address).map_err(|_| {
                anyhow!(
                    "Failed to parse destination address: {}",
                    &destination_address
                )
            })?;
            (
                address,
                if args.token {
                    GuardType::FreezeTokenPayment
                } else {
                    GuardType::FreezeSolPayment
                },
            )
        }
        None => {
            let (destination_address, freeze_guard) = get_destination(
                &program,
                &candy_guard,
                get_config_data(&args.config)?,
                &args.label,
            )?;
            (
                destination_address,
                if freeze_guard.is_some() {
                    GuardType::FreezeTokenPayment
                } else {
                    GuardType::FreezeSolPayment
                },
            )
        }
    };

    // sanity check: loads the PDA
    let (freeze_escrow, _) = find_freeze_pda(&candy_guard, &candy_machine, &destination_address);
    let account_data = program
        .rpc()
        .get_account_data(&freeze_escrow)
        .map_err(|_| anyhow!("Could not load freeze escrow"))?;

    if account_data.is_empty() {
        return Err(anyhow!("Freeze escrow account not found"));
    }

    pb.finish_with_message("Done");

    println!(
        "\n{} {}Unlocking treasury funds",
        style("[2/2]").bold().dim(),
        MONEY_BAG_EMOJI
    );

    let pb = spinner_with_style();
    pb.set_message("Sending unlock funds transaction...");

    let signature = unlock_funds(
        &program,
        &candy_guard,
        &candy_machine,
        &destination_address,
        &args.label,
        freeze_guard,
        &args.priority_fee,
    )?;

    pb.finish_with_message(format!(
        "{} {}",
        style("Unlock funds signature:").bold(),
        signature
    ));

    Ok(())
}

pub fn unlock_funds<C: Deref<Target = impl Signer> + Clone>(
    program: &Program<C>,
    candy_guard_id: &Pubkey,
    candy_machine_id: &Pubkey,
    destination: &Pubkey,
    label: &Option<String>,
    freeze_guard: GuardType,
    priority_fee: &u64,
) -> Result<Signature> {
    let mut remaining_accounts = Vec::with_capacity(4);
    let (freeze_pda, _) = find_freeze_pda(candy_guard_id, candy_machine_id, destination);
    remaining_accounts.push(AccountMeta {
        pubkey: freeze_pda,
        is_signer: false,
        is_writable: true,
    });
    remaining_accounts.push(AccountMeta {
        pubkey: program.payer(),
        is_signer: true,
        is_writable: false,
    });

    match freeze_guard {
        GuardType::FreezeSolPayment => {
            remaining_accounts.push(AccountMeta {
                pubkey: *destination,
                is_signer: false,
                is_writable: true,
            });
            remaining_accounts.push(AccountMeta {
                pubkey: system_program::id(),
                is_signer: false,
                is_writable: false,
            });
        }
        GuardType::FreezeTokenPayment => {
            // retrieves the mint from the destination account
            let account_data = program
                .rpc()
                .get_account_data(destination)
                .map_err(|_| anyhow!("Could not load destination account"))?;
            let destination_account = spl_token::state::Account::unpack(&account_data)?;

            remaining_accounts.push(AccountMeta {
                pubkey: get_associated_token_address(&freeze_pda, &destination_account.mint),
                is_signer: false,
                is_writable: true,
            });
            remaining_accounts.push(AccountMeta {
                pubkey: destination.to_owned(),
                is_signer: false,
                is_writable: true,
            });
            remaining_accounts.push(AccountMeta {
                pubkey: spl_token::ID,
                is_signer: false,
                is_writable: false,
            });
            remaining_accounts.push(AccountMeta {
                pubkey: system_program::id(),
                is_signer: false,
                is_writable: false,
            });
        }
        _ => return Err(anyhow!("Invalid freeze guard type: {freeze_guard:?}")),
    };

    let priority_fee_ix = ComputeBudgetInstruction::set_compute_unit_price(*priority_fee);

    let builder = program
        .request()
        .instruction(priority_fee_ix)
        .accounts(RouteAccount {
            candy_guard: *candy_guard_id,
            candy_machine: *candy_machine_id,
            payer: program.payer(),
        })
        .accounts(remaining_accounts)
        .args(Route {
            args: RouteArgs {
                data: vec![FreezeInstruction::UnlockFunds as u8],
                guard: freeze_guard,
            },
            label: label.to_owned(),
        });
    let sig = builder.send()?;

    Ok(sig)
}