Skip to content

Commit

Permalink
add retries and cmv2 snapshot support
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelvanderwaal committed Dec 24, 2021
1 parent 554a027 commit 8fa6466
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 22 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "metaboss"
version = "0.3.1"
version = "0.3.3"
edition = "2021"
description="The Metaplex NFT-standard Swiss Army Knife tool."
repository="https://github.com/samuelvanderwaal/metaboss"
Expand Down
11 changes: 11 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
v0.3.3

* Added exponential backoff retries to network requests: 250 ms, 500 ms, 1000 ms then fails.
* Added support for snapshot mints and holders commands for v2 candy machine ids.
* Added `derive` subcommand for deriving PDAs.

v0.3.2

* Check first creator is verified in snapshot mints and snapshot holders commands.


v0.3.1

* Add `primary_sale_happened` flag to mint commands
Expand Down
8 changes: 6 additions & 2 deletions src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use indicatif::ParallelProgressIterator;
use log::{debug, error, info};
use metaplex_token_metadata::state::{Key, Metadata};
use rayon::prelude::*;
use retry::{delay::Exponential, retry};
use serde::Serialize;
use serde_json::{json, Value};
use solana_client::rpc_client::RpcClient;
Expand Down Expand Up @@ -147,10 +148,13 @@ pub fn decode(client: &RpcClient, mint_account: &String) -> Result<Metadata, Dec
};
let metadata_pda = get_metadata_pda(pubkey);

let account_data = match client.get_account_data(&metadata_pda) {
let account_data = match retry(
Exponential::from_millis_with_factor(250, 2.0).take(3),
|| client.get_account_data(&metadata_pda),
) {
Ok(data) => data,
Err(err) => {
return Err(DecodeError::ClientError(err.kind));
return Err(DecodeError::NetworkError(err.to_string()));
}
};

Expand Down
141 changes: 141 additions & 0 deletions src/derive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use metaplex_token_metadata::id;
use solana_sdk::pubkey::Pubkey;
use std::{convert::AsRef, str::FromStr};

pub fn get_generic_pda(str_seeds: String, program_id: String) {
let str_seeds = str_seeds
.split(",")
.map(|s| s.into())
.collect::<Vec<String>>();

let seeds: Vec<Vec<u8>> = str_seeds
.into_iter()
.map(|seed| pubkey_or_bytes(seed))
.collect();

let seeds: Vec<&[u8]> = seeds.iter().map(|seed| seed.as_slice()).collect();

let program_id =
Pubkey::from_str(&program_id).expect("Failed to parse pubkey from program_id!");
println!("{}", derive_generic_pda(seeds, program_id));
}

fn pubkey_or_bytes(seed: String) -> Vec<u8> {
let res = Pubkey::from_str(&seed);
let value: Vec<u8> = match res {
Ok(pubkey) => pubkey.as_ref().to_vec(),
Err(_) => seed.as_bytes().to_owned(),
};

value
}

pub fn get_metadata_pda(mint_account: String) {
let pubkey =
Pubkey::from_str(&mint_account).expect("Failed to parse pubkey from mint account!");
println!("{}", derive_metadata_pda(&pubkey));
}

pub fn get_edition_pda(mint_account: String) {
let pubkey =
Pubkey::from_str(&mint_account).expect("Failed to parse pubkey from mint account!");
println!("{}", derive_edition_pda(&pubkey));
}

pub fn get_cmv2_pda(candy_machine_id: String) {
let pubkey =
Pubkey::from_str(&candy_machine_id).expect("Failed to parse pubkey from candy_machine_id!");
println!("{}", derive_cmv2_pda(&pubkey));
}

fn derive_generic_pda(seeds: Vec<&[u8]>, program_id: Pubkey) -> Pubkey {
let (pda, _) = Pubkey::find_program_address(&seeds, &program_id);
pda
}

fn derive_metadata_pda(pubkey: &Pubkey) -> Pubkey {
let metaplex_pubkey = id();

let seeds = &[
"metadata".as_bytes(),
metaplex_pubkey.as_ref(),
pubkey.as_ref(),
];

let (pda, _) = Pubkey::find_program_address(seeds, &metaplex_pubkey);
pda
}

fn derive_edition_pda(pubkey: &Pubkey) -> Pubkey {
let metaplex_pubkey = id();

let seeds = &[
"metadata".as_bytes(),
metaplex_pubkey.as_ref(),
pubkey.as_ref(),
"edition".as_bytes(),
];

let (pda, _) = Pubkey::find_program_address(seeds, &metaplex_pubkey);
pda
}

pub fn derive_cmv2_pda(pubkey: &Pubkey) -> Pubkey {
let cmv2_pubkey = Pubkey::from_str("cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ")
.expect("Failed to parse pubkey from candy machine program id!");

let seeds = &["candy_machine".as_bytes(), pubkey.as_ref()];

let (pda, _) = Pubkey::find_program_address(seeds, &cmv2_pubkey);
pda
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_derive_generic_pda() {
let metadata_program_pubkey =
Pubkey::from_str("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s").unwrap();
let mint_pubkey = Pubkey::from_str("H9UJFx7HknQ9GUz7RBqqV9SRnht6XaVDh2cZS3Huogpf").unwrap();

let seeds = vec![
"metadata".as_bytes(),
metadata_program_pubkey.as_ref(),
mint_pubkey.as_ref(),
];

let expected_pda =
Pubkey::from_str("99pKPWsqi7bZaXKMvmwkxWV4nJjb5BS5SgKSNhW26ZNq").unwrap();
let program_pubkey =
Pubkey::from_str("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s").unwrap();

assert_eq!(derive_generic_pda(seeds, program_pubkey), expected_pda);
}

#[test]
fn test_derive_metadata_pda() {
let mint_pubkey = Pubkey::from_str("H9UJFx7HknQ9GUz7RBqqV9SRnht6XaVDh2cZS3Huogpf").unwrap();
let expected_pda =
Pubkey::from_str("99pKPWsqi7bZaXKMvmwkxWV4nJjb5BS5SgKSNhW26ZNq").unwrap();
assert_eq!(derive_metadata_pda(&mint_pubkey), expected_pda);
}

#[test]
fn test_derive_edition_pda() {
let mint_pubkey = Pubkey::from_str("H9UJFx7HknQ9GUz7RBqqV9SRnht6XaVDh2cZS3Huogpf").unwrap();
let expected_pda =
Pubkey::from_str("2vNgLPdTtfZYMNBR14vL5WXp6jYAvumfHauEHNc1BQim").unwrap();
assert_eq!(derive_edition_pda(&mint_pubkey), expected_pda);
}

#[test]
fn test_derive_cmv2_pda() {
let candy_machine_pubkey =
Pubkey::from_str("3qt9aBBmTSMxyzFEcwzZnFeV4tCZzPkTYVqPP7Bw5zUh").unwrap();
let expected_pda =
Pubkey::from_str("8J9W44AfgWFMSwE4iYyZMNCWV9mKqovS5YHiVoKuuA2b").unwrap();
assert_eq!(derive_cmv2_pda(&candy_machine_pubkey), expected_pda);
}
}
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub enum DecodeError {
#[error("failed to get account data")]
ClientError(ClientErrorKind),

#[error("network request failed after three attempts")]
NetworkError(String),

#[error("failed to parse string into Pubkey")]
PubkeyParseFailed(String),

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod constants;
pub mod data;
pub mod decode;
pub mod derive;
pub mod errors;
pub mod limiter;
pub mod mint;
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ fn main() -> Result<()> {
let client = RpcClient::new_with_timeout_and_commitment(rpc, timeout, commitment);
match options.cmd {
Command::Decode { decode_subcommands } => process_decode(&client, decode_subcommands)?,
Command::Derive { derive_subcommands } => process_derive(derive_subcommands),
Command::Mint { mint_subcommands } => process_mint(&client, mint_subcommands)?,
Command::Update { update_subcommands } => process_update(&client, update_subcommands)?,
Command::Set { set_subcommands } => process_set(&client, set_subcommands)?,
Expand Down
11 changes: 6 additions & 5 deletions src/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use metaplex_token_metadata::instruction::{
};
use rayon::prelude::*;
use reqwest;
use retry::{delay::Fixed, retry};
use retry::{delay::Exponential, retry};
use serde_json::Value;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
Expand Down Expand Up @@ -203,7 +203,7 @@ pub fn mint_one<P: AsRef<Path>>(
primary_sale_happened,
)?;
info!("Tx id: {:?}\nMint account: {:?}", &tx_id, &mint_account);
let message = format!("Tx id: {:?}\nMint account: {:?}!", &tx_id, &mint_account,);
let message = format!("Tx id: {:?}\nMint account: {:?}", &tx_id, &mint_account,);
println!("{}", message);

Ok(())
Expand Down Expand Up @@ -337,9 +337,10 @@ pub fn mint(
);

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

Ok((sig, mint.pubkey()))
Expand Down
34 changes: 34 additions & 0 deletions src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pub enum Command {
#[structopt(subcommand)]
decode_subcommands: DecodeSubcommands,
},
/// Derive PDAs for various account types
Derive {
#[structopt(subcommand)]
derive_subcommands: DeriveSubcommands,
},
/// Mint new NFTs from JSON files
#[structopt(name = "mint")]
Mint {
Expand Down Expand Up @@ -78,6 +83,27 @@ pub enum DecodeSubcommands {
},
}

#[derive(Debug, StructOpt)]
pub enum DeriveSubcommands {
/// Derive generic PDA from seeds and program id
#[structopt(name = "pda")]
Pda {
/// Seeds to derive PDA from
seeds: String,
/// Program id to derive PDA from
program_id: String,
},
/// Derive Metadata PDA
#[structopt(name = "metadata")]
Metadata { mint_account: String },
/// Derive Edition PDA
#[structopt(name = "edition")]
Edition { mint_account: String },
/// Derive CMV2 PDA
#[structopt(name = "cmv2-creator")]
CMV2Creator { candy_machine_id: String },
}

#[derive(Debug, StructOpt)]
pub enum MintSubcommands {
/// Mint a single NFT from a JSON file
Expand Down Expand Up @@ -224,6 +250,10 @@ pub enum SnapshotSubcommands {
#[structopt(short, long)]
candy_machine_id: Option<String>,

/// Candy machine v2 id
#[structopt(long = "v2")]
v2: bool,

/// Path to directory to save output files.
#[structopt(short, long, default_value = ".")]
output: String,
Expand All @@ -250,6 +280,10 @@ pub enum SnapshotSubcommands {
#[structopt(short, long)]
update_authority: Option<String>,

/// Candy machine v2 id
#[structopt(long = "v2")]
v2: bool,

/// Path to directory to save output file
#[structopt(short, long, default_value = ".")]
output: String,
Expand Down
16 changes: 14 additions & 2 deletions src/process_subcommands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::Result;
use solana_client::rpc_client::RpcClient;

use crate::decode::decode_metadata;
use crate::derive::{get_cmv2_pda, get_edition_pda, get_generic_pda, get_metadata_pda};
use crate::mint::{mint_list, mint_one};
use crate::opt::*;
use crate::sign::{sign_all, sign_one};
Expand All @@ -19,6 +20,15 @@ pub fn process_decode(client: &RpcClient, commands: DecodeSubcommands) -> Result
Ok(())
}

pub fn process_derive(commands: DeriveSubcommands) {
match commands {
DeriveSubcommands::Pda { seeds, program_id } => get_generic_pda(seeds, program_id),
DeriveSubcommands::Metadata { mint_account } => get_metadata_pda(mint_account),
DeriveSubcommands::Edition { mint_account } => get_edition_pda(mint_account),
DeriveSubcommands::CMV2Creator { candy_machine_id } => get_cmv2_pda(candy_machine_id),
}
}

pub fn process_mint(client: &RpcClient, commands: MintSubcommands) -> Result<()> {
match commands {
MintSubcommands::One {
Expand Down Expand Up @@ -95,17 +105,19 @@ pub fn process_snapshot(client: &RpcClient, commands: SnapshotSubcommands) -> Re
SnapshotSubcommands::Holders {
update_authority,
candy_machine_id,
v2,
output,
} => snapshot_holders(&client, &update_authority, &candy_machine_id, &output),
} => snapshot_holders(&client, &update_authority, &candy_machine_id, v2, &output),
SnapshotSubcommands::CMAccounts {
update_authority,
output,
} => snapshot_cm_accounts(&client, &update_authority, &output),
SnapshotSubcommands::Mints {
candy_machine_id,
update_authority,
v2,
output,
} => snapshot_mints(&client, candy_machine_id, update_authority, output),
} => snapshot_mints(&client, candy_machine_id, update_authority, v2, output),
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use metaplex_token_metadata::{
instruction::sign_metadata, state::Metadata, ID as METAPLEX_PROGRAM_ID,
};
use rayon::prelude::*;
use retry::{delay::Exponential, retry};
use solana_client::rpc_client::RpcClient;
use solana_program::borsh::try_from_slice_unchecked;
use solana_sdk::{
Expand Down Expand Up @@ -34,6 +35,7 @@ pub fn sign_one(client: &RpcClient, keypair: String, account: String) -> Result<
metadata_pubkey,
&creator.pubkey()
);

let sig = sign(client, &creator, metadata_pubkey)?;
info!("Tx sig: {}", sig);
println!("Tx sig: {}", sig);
Expand Down Expand Up @@ -78,7 +80,14 @@ pub fn sign(client: &RpcClient, creator: &Keypair, metadata_pubkey: Pubkey) -> R
&[creator],
recent_blockhash,
);
let sig = client.send_and_confirm_transaction(&tx)?;

// 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)
}

Expand Down
Loading

0 comments on commit 8fa6466

Please sign in to comment.