metaboss 0.49.0

The Metaplex NFT-standard Swiss Army Knife tool.
Documentation
use anyhow::{anyhow, Result};
use borsh::BorshDeserialize;
use indicatif::ParallelProgressIterator;
use log::{error, info};
use metaboss_lib::data::Priority;
use mpl_token_metadata::{accounts::Metadata, instructions::SignMetadata};
use rayon::prelude::*;
use retry::{delay::Exponential, retry};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    compute_budget::ComputeBudgetInstruction,
    pubkey::Pubkey,
    signature::Signature,
    signer::{keypair::Keypair, Signer},
    transaction::Transaction,
};
use std::{
    fs::File,
    str::FromStr,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
};

use crate::decode::get_metadata_pda;
use crate::derive::{derive_cmv2_pda, derive_cmv3_pda};
use crate::limiter::create_default_rate_limiter;
use crate::parse::{is_only_one_option, parse_keypair};
use crate::snapshot::get_cm_creator_accounts;
use crate::{constants::*, parse::parse_solana_config};

pub fn sign_one(
    client: &RpcClient,
    keypair_path: Option<String>,
    account: String,
    priority: Priority,
) -> Result<()> {
    let solana_opts = parse_solana_config();
    let creator = parse_keypair(keypair_path, solana_opts);

    let account_pubkey = Pubkey::from_str(&account)?;
    let metadata_pubkey = get_metadata_pda(account_pubkey);

    info!(
        "Signing metadata: {} with creator: {}",
        metadata_pubkey,
        &creator.pubkey()
    );

    let sig = sign(client, &creator, metadata_pubkey, priority)?;
    info!("Tx sig: {}", sig);
    println!("Tx sig: {sig}");

    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn sign_all(
    client: &RpcClient,
    keypair_path: Option<String>,
    creator: &Option<String>,
    position: usize,
    v2: bool,
    v3: bool,
    mint_accounts_file: Option<String>,
    priority: Priority,
) -> Result<()> {
    let solana_opts = parse_solana_config();
    let creator_keypair = parse_keypair(keypair_path, solana_opts);

    if !is_only_one_option(creator, &mint_accounts_file) {
        return Err(anyhow!(
            "Must specify exactly one of --creator or --mint-accounts-file"
        ));
    }

    if let Some(creator) = creator {
        let creator_pubkey =
            Pubkey::from_str(creator).expect("Failed to parse pubkey from creator!");
        if v2 {
            let cmv2_creator = derive_cmv2_pda(&creator_pubkey);
            sign_candy_machine_accounts(
                client,
                &cmv2_creator.to_string(),
                creator_keypair,
                position,
                priority,
            )?
        } else if v3 {
            let cmv3_creator = derive_cmv3_pda(&creator_pubkey);
            sign_candy_machine_accounts(
                client,
                &cmv3_creator.to_string(),
                creator_keypair,
                position,
                priority,
            )?
        } else {
            sign_candy_machine_accounts(client, creator, creator_keypair, position, priority)?
        }
    } else if let Some(mint_accounts_file) = mint_accounts_file {
        let file = File::open(mint_accounts_file)?;
        let mint_accounts: Vec<String> = serde_json::from_reader(&file)?;

        sign_mint_accounts(client, &creator_keypair, mint_accounts, priority)?;
    } else {
        unreachable!();
    }

    Ok(())
}

pub fn sign(
    client: &RpcClient,
    creator: &Keypair,
    metadata_pubkey: Pubkey,
    priority: Priority,
) -> Result<Signature> {
    let micro_lamports = match priority {
        Priority::None => 20,
        Priority::Low => 20_000,
        Priority::Medium => 200_000,
        Priority::High => 1_000_000,
        Priority::Max => 2_000_000,
    };

    let sign_ix = SignMetadata {
        metadata: metadata_pubkey,
        creator: creator.pubkey(),
    }
    .instruction();

    let ixs = vec![
        ComputeBudgetInstruction::set_compute_unit_price(micro_lamports),
        sign_ix,
    ];

    let recent_blockhash = client.get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &ixs,
        Some(&creator.pubkey()),
        &[creator],
        recent_blockhash,
    );

    // Send tx with retries.
    let res = retry(
        Exponential::from_millis_with_factor(250, 2.0).take(3),
        || client.send_and_confirm_transaction(&tx),
    );
    let sig = res?;

    Ok(sig)
}

pub fn sign_mint_accounts(
    client: &RpcClient,
    creator: &Keypair,
    mint_accounts: Vec<String>,
    priority: Priority,
) -> Result<()> {
    let use_rate_limit = *USE_RATE_LIMIT.read().unwrap();
    let handle = create_default_rate_limiter();

    mint_accounts
        .par_iter()
        .progress()
        .for_each(|mint_account| {
            let mut handle = handle.clone();
            if use_rate_limit {
                handle.wait();
            }

            let account_pubkey = match Pubkey::from_str(mint_account) {
                Ok(pubkey) => pubkey,
                Err(err) => {
                    error!("Invalid public key: {}, error: {}", mint_account, err);
                    return;
                }
            };

            let metadata_pubkey = get_metadata_pda(account_pubkey);

            // Try to sign all accounts, print any errors that crop up.
            match sign(client, creator, metadata_pubkey, priority.clone()) {
                Ok(sig) => info!("{}", sig),
                Err(e) => error!("{}", e),
            }
        });

    Ok(())
}

pub fn sign_candy_machine_accounts(
    client: &RpcClient,
    creator: &str,
    signing_creator: Keypair,
    position: usize,
    priority: Priority,
) -> Result<()> {
    let accounts = get_cm_creator_accounts(client, creator, position)?;

    // Only sign accounts that have not been signed yet
    let signed_at_least_one_account = Arc::new(AtomicBool::new(false));

    accounts
        .par_iter()
        .progress()
        .for_each(|(metadata_pubkey, account)| {
            let signed_at_least_one_account = signed_at_least_one_account.clone();
            let metadata: Metadata =
                match Metadata::deserialize(&mut account.data.clone().as_slice()) {
                    Ok(metadata) => metadata,
                    Err(_) => {
                        error!("Account {} has no metadata", metadata_pubkey);
                        return;
                    }
                };

            if let Some(creators) = metadata.creators {
                // Check whether the specific creator has already signed the account
                for creator in creators {
                    if creator.address == signing_creator.pubkey() && !creator.verified {
                        info!(
                            "Found creator unverified for mint account: {}",
                            metadata.mint
                        );
                        info!("Signing...");

                        let sig = match sign(
                            client,
                            &signing_creator,
                            *metadata_pubkey,
                            priority.clone(),
                        ) {
                            Ok(sig) => sig,
                            Err(e) => {
                                error!("Error signing: {}", e);
                                return;
                            }
                        };

                        info!("{}", sig);

                        signed_at_least_one_account.store(true, Ordering::Relaxed);
                    }
                }
            }
        });

    if !signed_at_least_one_account.load(Ordering::Relaxed) {
        info!("No unverified metadata for this creator and candy machine.");
        println!("No unverified metadata for this creator and candy machine.");
        return Ok(());
    }

    Ok(())
}