From 8d608d1de90404e412e11db8b69d0fa2e43fb637 Mon Sep 17 00:00:00 2001 From: Samuel Vanderwaal Date: Tue, 12 Jul 2022 21:44:58 -0800 Subject: [PATCH] add update sfbp-all (#163) --- src/opt.rs | 25 +- src/process_subcommands.rs | 17 + src/update/common.rs | 2 +- src/update/creator.rs | 2 +- src/update/seller_fee_basis_points.rs | 90 ++++ src/update_metadata.rs | 632 -------------------------- 6 files changed, 133 insertions(+), 635 deletions(-) delete mode 100644 src/update_metadata.rs diff --git a/src/opt.rs b/src/opt.rs index c40c7aba..e93dcdeb 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -788,7 +788,7 @@ pub enum SnapshotSubcommands { #[derive(Debug, StructOpt)] pub enum UpdateSubcommands { /// Update the seller fee basis points field inside the data struct on an NFT - #[structopt(name = "seller-fee-basis-points")] + #[structopt(name = "sfbp")] SellerFeeBasisPoints { /// Path to the creator's keypair file #[structopt(short, long)] @@ -802,6 +802,29 @@ pub enum UpdateSubcommands { #[structopt(short, long)] new_seller_fee_basis_points: u16, }, + /// Update the seller fee basis points field inside the data struct on an NFT + #[structopt(name = "sfbp-all")] + SellerFeeBasisPointsAll { + /// Path to the creator's keypair file + #[structopt(short, long)] + keypair: Option, + + /// Path to the mint list file + #[structopt(short = "L", long)] + mint_list: Option, + + /// Cache file + #[structopt(short, long)] + cache_file: Option, + + /// New seller fee basis points for the metadata + #[structopt(short, long)] + new_sfbp: u16, + + /// Maximum retries: retry failed items up to this many times. + #[structopt(long, default_value = "1")] + retries: u8, + }, /// Update the name field inside the data struct on an NFT #[structopt(name = "name")] Name { diff --git a/src/process_subcommands.rs b/src/process_subcommands.rs index 46ad21d8..c3bf0b52 100644 --- a/src/process_subcommands.rs +++ b/src/process_subcommands.rs @@ -419,6 +419,23 @@ pub async fn process_update(client: RpcClient, commands: UpdateSubcommands) -> R &account, &new_seller_fee_basis_points, ), + UpdateSubcommands::SellerFeeBasisPointsAll { + keypair, + mint_list, + cache_file, + new_sfbp, + retries, + } => { + update_seller_fee_basis_points_all(UpdateSellerFeeBasisPointsAllArgs { + client, + keypair, + mint_list, + cache_file, + new_sfbp, + retries, + }) + .await + } UpdateSubcommands::Name { keypair, account, diff --git a/src/update/common.rs b/src/update/common.rs index ec37d952..c324c5f4 100644 --- a/src/update/common.rs +++ b/src/update/common.rs @@ -8,7 +8,7 @@ pub use solana_client::rpc_client::RpcClient; pub use solana_sdk::{ pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction, }; -pub use std::{cmp, str::FromStr, sync::Arc}; +pub use std::{cmp, fmt::Display, str::FromStr, sync::Arc}; pub use crate::cache::{Action, BatchActionArgs, RunActionArgs}; pub use crate::decode::{decode, get_metadata_pda}; diff --git a/src/update/creator.rs b/src/update/creator.rs index fa6ead3b..bc5e572c 100644 --- a/src/update/creator.rs +++ b/src/update/creator.rs @@ -52,7 +52,7 @@ pub async fn update_creator_by_position( Ok(()) } -pub async fn update_creator( +async fn update_creator( client: Arc, keypair: Arc, mint_account: String, diff --git a/src/update/seller_fee_basis_points.rs b/src/update/seller_fee_basis_points.rs index 2d534558..ddb65e95 100644 --- a/src/update/seller_fee_basis_points.rs +++ b/src/update/seller_fee_basis_points.rs @@ -1,5 +1,21 @@ use super::{common::*, update_data}; +pub struct UpdateSellerFeeBasisPointsArgs { + pub client: Arc, + pub keypair: Arc, + pub payer: Arc, + pub mint_account: String, + pub new_sfbp: u16, +} +pub struct UpdateSellerFeeBasisPointsAllArgs { + pub client: RpcClient, + pub keypair: Option, + pub mint_list: Option, + pub cache_file: Option, + pub new_sfbp: u16, + pub retries: u8, +} + pub fn update_seller_fee_basis_points_one( client: &RpcClient, keypair: Option, @@ -25,3 +41,77 @@ pub fn update_seller_fee_basis_points_one( update_data(client, &parsed_keypair, mint_account, new_data)?; Ok(()) } + +async fn update_sfbp(args: UpdateSellerFeeBasisPointsArgs) -> Result<(), ActionError> { + let old_md = decode(&args.client, &args.mint_account) + .map_err(|e| ActionError::ActionFailed(args.mint_account.to_string(), e.to_string()))?; + let old_data = old_md.data; + + let new_data = DataV2 { + creators: old_data.creators, + seller_fee_basis_points: args.new_sfbp, + name: old_data.name, + symbol: old_data.symbol, + uri: old_data.uri, + collection: old_md.collection, + uses: old_md.uses, + }; + + update_data(&args.client, &args.keypair, &args.mint_account, new_data) + .map_err(|e| ActionError::ActionFailed(args.mint_account.to_string(), e.to_string()))?; + + Ok(()) +} + +pub struct UpdateSellerFeeBasisPointsAll {} + +#[async_trait] +impl Action for UpdateSellerFeeBasisPointsAll { + fn name() -> &'static str { + "update-sfbp-all" + } + + async fn action(args: RunActionArgs) -> Result<(), ActionError> { + // Converting back and forth between String and u16 is dumb but I couldn't figure out a + // nice way to do this with generics. + let sfbp = args.new_value.parse::().map_err(|e| { + ActionError::ActionFailed( + args.mint_account.to_string(), + format!("Invalid new_sfbp: {}", e), + ) + })?; + + // Set Update Authority can have an optional payer. + update_sfbp(UpdateSellerFeeBasisPointsArgs { + client: args.client.clone(), + keypair: args.keypair.clone(), + payer: args.payer.clone(), + mint_account: args.mint_account, + new_sfbp: sfbp, + }) + .await + } +} + +pub async fn update_seller_fee_basis_points_all( + args: UpdateSellerFeeBasisPointsAllArgs, +) -> AnyResult<()> { + let solana_opts = parse_solana_config(); + let keypair = parse_keypair(args.keypair, solana_opts); + + // We don't support an optional payer for this action currently. + let payer = None; + + let args = BatchActionArgs { + client: args.client, + keypair, + payer, + mint_list: args.mint_list, + cache_file: args.cache_file, + new_value: args.new_sfbp.to_string(), + retries: args.retries, + }; + UpdateSellerFeeBasisPointsAll::run(args).await?; + + Ok(()) +} diff --git a/src/update_metadata.rs b/src/update_metadata.rs deleted file mode 100644 index db835009..00000000 --- a/src/update_metadata.rs +++ /dev/null @@ -1,632 +0,0 @@ -use anyhow::{anyhow, Result}; -use glob::glob; -use indicatif::ParallelProgressIterator; -use log::{error, info, warn}; -use mpl_token_metadata::{instruction::update_metadata_accounts_v2, state::DataV2}; -use rayon::prelude::*; -use retry::{delay::Exponential, retry}; -use solana_client::rpc_client::RpcClient; -use solana_sdk::{ - pubkey::Pubkey, - signer::{keypair::Keypair, Signer}, - transaction::Transaction, -}; -use std::{ - cmp, - fs::File, - path::Path, - str::FromStr, - sync::{Arc, Mutex}, -}; - -use crate::data::{NFTData, UpdateNFTData, UpdateUriData}; -use crate::decode::{decode, get_metadata_pda}; -use crate::limiter::create_default_rate_limiter; -use crate::parse::{convert_local_to_remote_data, parse_cli_creators, parse_keypair}; -use crate::{constants::*, parse::parse_solana_config}; - -pub fn update_seller_fee_basis_points_one( - client: &RpcClient, - keypair: Option, - mint_account: &str, - new_seller_fee_basis_points: &u16, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let parsed_keypair = parse_keypair(keypair, solana_opts); - - let old_md = decode(client, mint_account)?; - let data_with_old_seller_fee_basis_points = old_md.data; - - let new_data = DataV2 { - creators: data_with_old_seller_fee_basis_points.creators, - seller_fee_basis_points: new_seller_fee_basis_points.to_owned(), - name: data_with_old_seller_fee_basis_points.name, - symbol: data_with_old_seller_fee_basis_points.symbol, - uri: data_with_old_seller_fee_basis_points.uri, - collection: old_md.collection, - uses: old_md.uses, - }; - - update_data(client, &parsed_keypair, mint_account, new_data)?; - Ok(()) -} - -pub fn update_name_one( - client: &RpcClient, - keypair: Option, - mint_account: &str, - new_name: &str, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let parsed_keypair = parse_keypair(keypair, solana_opts); - - let old_md = decode(client, mint_account)?; - let data_with_old_name = old_md.data; - - let new_data = DataV2 { - creators: data_with_old_name.creators, - seller_fee_basis_points: data_with_old_name.seller_fee_basis_points, - name: new_name.to_owned(), - symbol: data_with_old_name.symbol, - uri: data_with_old_name.uri, - collection: old_md.collection, - uses: old_md.uses, - }; - - update_data(client, &parsed_keypair, mint_account, new_data)?; - Ok(()) -} - -pub fn update_symbol_one( - client: &RpcClient, - keypair_path: Option, - mint_account: &str, - new_symbol: &str, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let old_md = decode(client, mint_account)?; - let data_with_old_symbol = old_md.data; - - let new_data = DataV2 { - creators: data_with_old_symbol.creators, - seller_fee_basis_points: data_with_old_symbol.seller_fee_basis_points, - name: data_with_old_symbol.name, - symbol: new_symbol.to_owned(), - uri: data_with_old_symbol.uri, - collection: old_md.collection, - uses: old_md.uses, - }; - - update_data(client, &keypair, mint_account, new_data)?; - Ok(()) -} - -pub fn update_creator_by_position( - client: &RpcClient, - keypair_path: Option, - mint_account: &str, - new_creators: &str, - should_append: bool, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let old_md = decode(client, mint_account)?; - let data_with_old_creators = old_md.data; - let parsed_creators = parse_cli_creators(new_creators.to_string(), should_append)?; - - let new_creators = if let Some(mut old_creators) = data_with_old_creators.creators { - if !should_append { - parsed_creators - } else { - let remaining_space = 5 - old_creators.len(); - warn!( - "Appending {} new creators with old creators with shares of 0", - parsed_creators.len() - ); - let end_index = cmp::min(parsed_creators.len(), remaining_space); - old_creators.append(&mut parsed_creators[0..end_index].to_vec()); - old_creators - } - } else { - parsed_creators - }; - - let shares = new_creators.iter().fold(0, |acc, c| acc + c.share); - if shares != 100 { - return Err(anyhow!("Creators shares must sum to 100!")); - } - - let new_data = DataV2 { - creators: Some(new_creators), - seller_fee_basis_points: data_with_old_creators.seller_fee_basis_points, - name: data_with_old_creators.name, - symbol: data_with_old_creators.symbol, - uri: data_with_old_creators.uri, - collection: old_md.collection, - uses: old_md.uses, - }; - - update_data(client, &keypair, mint_account, new_data)?; - Ok(()) -} - -pub fn update_data_one( - client: &RpcClient, - keypair_path: Option, - mint_account: &str, - json_file: &str, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let old_md = decode(client, mint_account)?; - - let f = File::open(json_file)?; - let new_data: NFTData = serde_json::from_reader(f)?; - - let data = convert_local_to_remote_data(new_data)?; - - let data_v2 = DataV2 { - creators: data.creators, - seller_fee_basis_points: data.seller_fee_basis_points, - name: data.name, - symbol: data.symbol, - uri: data.uri, - collection: old_md.collection, - uses: old_md.uses, - }; - - update_data(client, &keypair, mint_account, data_v2)?; - - Ok(()) -} - -pub fn update_data_all( - client: &RpcClient, - keypair_path: Option, - data_dir: &str, -) -> Result<()> { - let use_rate_limit = *USE_RATE_LIMIT.read().unwrap(); - let handle = create_default_rate_limiter(); - - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let path = Path::new(&data_dir).join("*.json"); - let pattern = path - .to_str() - .ok_or_else(|| anyhow!("Invalid directory path"))?; - - let (paths, errors): (Vec<_>, Vec<_>) = glob(pattern)?.into_iter().partition(Result::is_ok); - - let paths: Vec<_> = paths.into_iter().map(Result::unwrap).collect(); - let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect(); - - let failed_mints: Arc>> = Arc::new(Mutex::new(Vec::new())); - - info!("Updating..."); - println!("Updating..."); - paths.par_iter().progress().for_each(|path| { - let mut handle = handle.clone(); - if use_rate_limit { - handle.wait(); - } - - let failed_mints = failed_mints.clone(); - let f = match File::open(path) { - Ok(f) => f, - Err(e) => { - error!("Failed to open file: {:?} error: {}", path, e); - return; - } - }; - - let update_nft_data: UpdateNFTData = match serde_json::from_reader(f) { - Ok(data) => data, - Err(e) => { - error!( - "Failed to parse JSON data from file: {:?} error: {}", - path, e - ); - return; - } - }; - - let old_md = match decode(client, &update_nft_data.mint_account) { - Ok(md) => md, - Err(e) => { - error!( - "Failed to decode mint account: {} error: {}", - update_nft_data.mint_account, e - ); - return; - } - }; - - let data = match convert_local_to_remote_data(update_nft_data.nft_data) { - Ok(data) => data, - Err(e) => { - error!( - "Failed to convert local data to remote data: {:?} error: {}", - path, e - ); - return; - } - }; - - let data_v2 = DataV2 { - creators: data.creators, - seller_fee_basis_points: data.seller_fee_basis_points, - name: data.name, - symbol: data.symbol, - uri: data.uri, - collection: old_md.collection, - uses: old_md.uses, - }; - - match update_data(client, &keypair, &update_nft_data.mint_account, data_v2) { - Ok(_) => (), - Err(e) => { - error!("Failed to update data: {:?} error: {}", path, e); - failed_mints - .lock() - .unwrap() - .push(update_nft_data.mint_account); - } - } - }); - - if !errors.is_empty() { - error!("Failed to read some of the files with the following errors:"); - for error in errors { - error!("{}", error); - } - } - - if !failed_mints.lock().unwrap().is_empty() { - error!("Failed to update the following mints:"); - for mint in failed_mints.lock().unwrap().iter() { - error!("{}", mint); - } - } - - Ok(()) -} - -pub fn update_data( - client: &RpcClient, - keypair: &Keypair, - mint_account: &str, - data: DataV2, -) -> Result<()> { - let program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID)?; - let mint_pubkey = Pubkey::from_str(mint_account)?; - let metadata_account = get_metadata_pda(mint_pubkey); - - let update_authority = keypair.pubkey(); - - let ix = update_metadata_accounts_v2( - program_id, - metadata_account, - update_authority, - None, - Some(data), - None, - None, - ); - let recent_blockhash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&update_authority), - &[keypair], - 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?; - - info!("Tx sig: {:?}", sig); - println!("Mint: {:?}, Tx sig: {:?}", mint_account, sig); - - Ok(()) -} - -pub fn update_uri_one( - client: &RpcClient, - keypair_path: Option, - mint_account: &str, - new_uri: &str, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - update_uri(client, &keypair, mint_account, new_uri)?; - - Ok(()) -} - -pub fn update_uri_all( - client: &RpcClient, - keypair_path: Option, - json_file: &str, -) -> Result<()> { - let use_rate_limit = *USE_RATE_LIMIT.read().unwrap(); - let handle = create_default_rate_limiter(); - - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let f = File::open(json_file)?; - let update_uris: Vec = serde_json::from_reader(f)?; - - update_uris.par_iter().for_each(|data| { - let mut handle = handle.clone(); - if use_rate_limit { - handle.wait(); - } - - match update_uri(client, &keypair, &data.mint_account, &data.new_uri) { - Ok(_) => (), - Err(e) => { - error!("Failed to update uri: {:?} error: {}", data, e); - } - } - }); - - Ok(()) -} - -pub fn update_uri( - client: &RpcClient, - keypair: &Keypair, - mint_account: &str, - new_uri: &str, -) -> Result<()> { - let mint_pubkey = Pubkey::from_str(mint_account)?; - let program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID)?; - let update_authority = keypair.pubkey(); - - let metadata_account = get_metadata_pda(mint_pubkey); - let metadata = decode(client, mint_account)?; - - let mut data = metadata.data; - if data.uri.trim_matches(char::from(0)) != new_uri.trim_matches(char::from(0)) { - data.uri = new_uri.to_string(); - - let data_v2 = DataV2 { - name: data.name, - symbol: data.symbol, - uri: data.uri, - seller_fee_basis_points: data.seller_fee_basis_points, - creators: data.creators, - collection: metadata.collection, - uses: metadata.uses, - }; - - let ix = update_metadata_accounts_v2( - program_id, - metadata_account, - update_authority, - None, - Some(data_v2), - None, - None, - ); - - let recent_blockhash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&update_authority), - &[keypair], - recent_blockhash, - ); - - let sig = client.send_and_confirm_transaction(&tx)?; - info!("Tx sig: {:?}", sig); - println!("Tx sig: {:?}", sig); - } else { - println!("URI is the same."); - } - - Ok(()) -} - -pub fn set_primary_sale_happened( - client: &RpcClient, - keypair_path: Option, - mint_account: &str, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID)?; - let mint_pubkey = Pubkey::from_str(mint_account)?; - - let update_authority = keypair.pubkey(); - - let metadata_account = get_metadata_pda(mint_pubkey); - - let ix = update_metadata_accounts_v2( - program_id, - metadata_account, - update_authority, - None, - None, - Some(true), - None, - ); - let recent_blockhash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&update_authority), - &[&keypair], - recent_blockhash, - ); - - let sig = client.send_and_confirm_transaction(&tx)?; - info!("Tx sig: {:?}", sig); - println!("Tx sig: {:?}", sig); - - Ok(()) -} - -pub fn set_update_authority( - client: &RpcClient, - keypair_path: Option, - mint_account: &str, - new_update_authority: &str, - keypair_payer_path: Option, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path.clone(), solana_opts); - - let solana_opts = parse_solana_config(); - let keypair_payer = parse_keypair(keypair_payer_path.or(keypair_path), solana_opts); - - let program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID)?; - let mint_pubkey = Pubkey::from_str(mint_account)?; - - let update_authority = keypair.pubkey(); - let new_update_authority = Pubkey::from_str(new_update_authority)?; - - let metadata_account = get_metadata_pda(mint_pubkey); - - let ix = update_metadata_accounts_v2( - program_id, - metadata_account, - update_authority, - Some(new_update_authority), - None, - None, - None, - ); - let recent_blockhash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&keypair_payer.pubkey()), - &[&keypair, &keypair_payer], - recent_blockhash, - ); - - let sig = client.send_and_confirm_transaction(&tx)?; - info!("Tx sig: {:?}", sig); - println!("Tx sig: {:?}", sig); - - Ok(()) -} - -pub fn set_update_authority_all( - client: &RpcClient, - keypair_path: Option, - json_file: &str, - new_update_authority: &str, - keypair_payer_path: Option, -) -> Result<()> { - let use_rate_limit = *USE_RATE_LIMIT.read().unwrap(); - let handle = create_default_rate_limiter(); - - let file = File::open(json_file)?; - let items: Vec = serde_json::from_reader(file)?; - - info!("Setting update_authority..."); - items.par_iter().progress().for_each(|item| { - let mut handle = handle.clone(); - if use_rate_limit { - handle.wait(); - } - - // If someone uses a json list that contains a mint account that has already - // been updated this will throw an error. We print that error and continue - match set_update_authority( - client, - keypair_path.clone(), - item, - new_update_authority, - keypair_payer_path.clone(), - ) { - Ok(_) => {} - Err(error) => { - error!("Error occurred! {}", error) - } - }; - }); - - Ok(()) -} - -pub fn set_immutable( - client: &RpcClient, - keypair_path: Option, - account: &str, -) -> Result<()> { - let solana_opts = parse_solana_config(); - let keypair = parse_keypair(keypair_path, solana_opts); - - let program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID)?; - let mint_account = Pubkey::from_str(account)?; - - let update_authority = keypair.pubkey(); - - let metadata_account = get_metadata_pda(mint_account); - - let ix = update_metadata_accounts_v2( - program_id, - metadata_account, - update_authority, - None, - None, - None, - Some(false), - ); - let recent_blockhash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&update_authority), - &[&keypair], - recent_blockhash, - ); - - let sig = client.send_and_confirm_transaction(&tx)?; - info!("Tx sig: {:?}", sig); - println!("Tx sig: {:?}", sig); - - Ok(()) -} - -pub fn set_immutable_all( - client: &RpcClient, - keypair_path: Option, - json_file: &str, -) -> Result<()> { - let use_rate_limit = *USE_RATE_LIMIT.read().unwrap(); - let handle = create_default_rate_limiter(); - - let file = File::open(json_file)?; - let items: Vec = serde_json::from_reader(file)?; - - info!("Setting immutable..."); - items.par_iter().progress().for_each(|item| { - let mut handle = handle.clone(); - if use_rate_limit { - handle.wait(); - } - - // If someone uses a json list that contains a mint account that has already - // been updated this will throw an error. We print that error and continue - match set_immutable(client, keypair_path.clone(), item) { - Ok(_) => {} - Err(error) => { - error!("Error occurred! {}", error) - } - }; - }); - - Ok(()) -}