diff --git a/Cargo.toml b/Cargo.toml index 9a2f72d3..f5c2db12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ bincode = "1.3.3" chrono = { version = "0.4.11", features = ["serde"] } futures = "0.3.30" futures-util = "0.3.30" +mpl-token-metadata = "4.1.2" phf = { version = "0.11.2", features = ["macros"] } rand = "0.8.5" reqwest = { version = "0.11", features = ["json"], default-features = false } @@ -35,6 +36,7 @@ tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "net"] } tokio-stream = "0.1.15" tokio-tungstenite = { version = "0.21.0", features = ["native-tls", "handshake"] } url = "2.5.0" +spl-token = { version = "3.5.0", features = ["no-entrypoint"] } [dev-dependencies] mockito = "1.4.0" diff --git a/examples/delegate_and_revoke_collection_authority.rs b/examples/delegate_and_revoke_collection_authority.rs new file mode 100644 index 00000000..0ab88a86 --- /dev/null +++ b/examples/delegate_and_revoke_collection_authority.rs @@ -0,0 +1,140 @@ +use helius::error::Result; +use helius::types::Cluster; +use helius::utils::collection_authority::{get_collection_authority_record, get_collection_metadata_account}; +use helius::Helius; +use mpl_token_metadata::instructions::CreateMetadataAccountV3; +use mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs; +use mpl_token_metadata::types::DataV2; +use solana_program::system_instruction::create_account; +use solana_program::system_program; +use solana_sdk::{ + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::solana_program::program_pack::Pack; +use spl_token::{instruction::initialize_mint, state::Mint}; + +#[tokio::main] +async fn main() -> Result<()> { + let api_key = ""; + let payer = Keypair::from_base58_string(""); + let cluster = Cluster::MainnetBeta; + let helius = Helius::new_with_async_solana(api_key, cluster)?; + let rpc_client = helius.async_connection()?; + + let collection_mint_keypair = Keypair::new(); + let rent = rpc_client + .get_minimum_balance_for_rent_exemption(Mint::LEN) + .await + .expect("Failed to get rent exemption amount"); + let create_mint_account_ix = create_account( + &payer.pubkey(), + &collection_mint_keypair.pubkey(), + rent, + Mint::LEN as u64, + &spl_token::id(), + ); + let collection_authority_keypair = Keypair::new(); + let initialize_mint_ix = initialize_mint( + &spl_token::id(), + &collection_mint_keypair.pubkey(), + &collection_authority_keypair.pubkey(), + None, + 9, + ) + .expect("Failed to create initialize mint instruction"); + let recent_blockhash = rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[create_mint_account_ix, initialize_mint_ix], + Some(&payer.pubkey()), + &[&payer, &collection_mint_keypair], + recent_blockhash, + ); + rpc_client + .send_and_confirm_transaction(&transaction) + .await + .expect("Failed to create and initialize mint"); + println!( + "Collection mint created and initialized: {}", + collection_mint_keypair.pubkey() + ); + + let metadata_pubkey = get_collection_metadata_account(&collection_mint_keypair.pubkey()); + let create_metadata_accounts_ix = CreateMetadataAccountV3 { + metadata: metadata_pubkey, + mint: collection_mint_keypair.pubkey(), + mint_authority: collection_authority_keypair.pubkey(), + payer: payer.pubkey(), + update_authority: (collection_authority_keypair.pubkey(), true), + system_program: system_program::ID, + rent: None, + } + .instruction(CreateMetadataAccountV3InstructionArgs { + data: DataV2 { + name: "".to_string(), + symbol: "".to_string(), + uri: "".to_string(), + seller_fee_basis_points: 0, + creators: None, + collection: None, + uses: None, + }, + is_mutable: true, + collection_details: None, + }); + let recent_blockhash = rpc_client.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[create_metadata_accounts_ix], + Some(&payer.pubkey()), + &[&payer, &collection_authority_keypair], + recent_blockhash, + ); + rpc_client.send_and_confirm_transaction(&transaction).await?; + println!("Metadata account created: {}", metadata_pubkey.to_string()); + + let delegated_authority_keypair = Keypair::new(); + let result = helius + .delegate_collection_authority( + collection_mint_keypair.pubkey(), + delegated_authority_keypair.pubkey(), + &collection_authority_keypair, + Some(&payer), + ) + .await; + assert!( + result.is_ok(), + "Failed to delegate collection authority to {}", + delegated_authority_keypair.pubkey() + ); + println!( + "Delegate collection authority to {} transaction signature: {}", + delegated_authority_keypair.pubkey(), + result? + ); + + let collection_authority_record = + get_collection_authority_record(&collection_mint_keypair.pubkey(), &delegated_authority_keypair.pubkey()); + let account = rpc_client.get_account(&collection_authority_record).await; + assert!(account.is_ok(), "Collection authority record account should exist"); + + let result = helius + .revoke_collection_authority( + collection_mint_keypair.pubkey(), + Some(delegated_authority_keypair.pubkey()), + &collection_authority_keypair, + Some(&payer), + ) + .await; + assert!(result.is_ok(), "Failed to revoke collection authority"); + println!( + "Revoke collection authority from {} transaction signature: {}", + delegated_authority_keypair.pubkey(), + result? + ); + + let account = rpc_client.get_account(&collection_authority_record).await; + assert!(account.is_err(), "Collection authority record account should be closed"); + + println!("Delegated collection authority successfully revoked"); + Ok(()) +} diff --git a/src/client.rs b/src/client.rs index 8cb76495..8d4ce9bc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -146,6 +146,10 @@ impl Helius { pub fn ws(&self) -> Option> { self.ws_client.clone() } + + pub fn config(&self) -> Arc { + self.config.clone() + } } /// A wrapper around the asynchronous Solana RPC client that provides thread-safe access diff --git a/src/config.rs b/src/config.rs index c5df168c..3630d66d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use crate::error::{HeliusError, Result}; -use crate::types::{Cluster, HeliusEndpoints}; +use crate::types::{Cluster, HeliusEndpoints, MintApiAuthority}; /// Configuration settings for the Helius client /// @@ -40,4 +40,8 @@ impl Config { endpoints, }) } + + pub fn mint_api_authority(&self) -> MintApiAuthority { + MintApiAuthority::from_cluster(&self.cluster) + } } diff --git a/src/mint_api.rs b/src/mint_api.rs index 527ff258..ffca3fed 100644 --- a/src/mint_api.rs +++ b/src/mint_api.rs @@ -1,6 +1,13 @@ use crate::error::Result; use crate::types::{MintCompressedNftRequest, MintResponse}; +use crate::utils::collection_authority::{ + delegate_collection_authority_instruction, revoke_collection_authority_instruction, +}; use crate::Helius; +use solana_program::pubkey::Pubkey; +use solana_sdk::signature::{Keypair, Signature}; +use solana_sdk::signer::Signer; +use solana_sdk::transaction::Transaction; impl Helius { /// The easiest way to mint a compressed NFT (cNFT) @@ -13,4 +20,74 @@ impl Helius { pub async fn mint_compressed_nft(&self, request: MintCompressedNftRequest) -> Result { self.rpc_client.post_rpc_request("mintCompressedNft", request).await } + + /// Delegates collection authority to a new authority for a given collection mint. + /// + /// # Arguments + /// * `collection_mint` - The public key of the collection mint. + /// * `new_collection_authority` - The public key of the new authority to delegate to. + /// * `update_authority_keypair` - The keypair of the current update authority who is delegating the authority. + /// * `payer_keypair` - Optional keypair to pay for the transaction fees. If `None`, `update_authority_keypair` is used as the payer. + /// + /// # Returns + /// A `Result` containing the transaction `Signature` if successful. + pub async fn delegate_collection_authority( + &self, + collection_mint: Pubkey, + new_collection_authority: Pubkey, + update_authority_keypair: &Keypair, + payer_keypair: Option<&Keypair>, + ) -> Result { + let payer_keypair = payer_keypair.unwrap_or(update_authority_keypair); + let delegate_instruction = delegate_collection_authority_instruction( + collection_mint, + new_collection_authority, + update_authority_keypair, + payer_keypair.pubkey(), + ); + let recent_blockhash = self.async_connection()?.get_latest_blockhash().await?; + let transaction = Transaction::new_signed_with_payer( + &[delegate_instruction], + Some(&payer_keypair.pubkey()), + &[payer_keypair, update_authority_keypair], + recent_blockhash, + ); + self.async_connection()? + .send_and_confirm_transaction(&transaction) + .await + .map_err(|e| e.into()) + } + + /// Revokes a delegated collection authority for a given collection mint. + /// + /// # Arguments + /// * `collection_mint` - The public key of the collection mint. + /// * `delegated_collection_authority` - Optional public key of the delegated authority to revoke. If `None`, the default mint API authority is used. + /// * `revoke_authority_keypair` - The keypair of the authority revoking the delegated authority. + /// * `payer_keypair` - Optional keypair to pay for the transaction fees. If `None`, `revoke_authority_keypair` is used as the payer. + /// + /// # Returns + /// A `Result` containing the transaction `Signature` if successful. + pub async fn revoke_collection_authority( + &self, + collection_mint: Pubkey, + delegated_collection_authority: Option, + revoke_authority_keypair: &Keypair, + payer_keypair: Option<&Keypair>, + ) -> Result { + let collection_authority = delegated_collection_authority.unwrap_or(self.config().mint_api_authority().into()); + let revoke_instruction = + revoke_collection_authority_instruction(collection_mint, collection_authority, revoke_authority_keypair); + let payer_keypair = payer_keypair.unwrap_or(revoke_authority_keypair); + let recent_blockhash = self.async_connection()?.get_latest_blockhash().await?; + self.async_connection()? + .send_and_confirm_transaction(&Transaction::new_signed_with_payer( + &vec![revoke_instruction], + Some(&payer_keypair.pubkey()), + &vec![&payer_keypair, &revoke_authority_keypair], + recent_blockhash, + )) + .await + .map_err(|e| e.into()) + } } diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 5d9eb8fa..23233eb1 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -95,8 +95,6 @@ impl Helius { let interval: Duration = Duration::from_secs(5); let start: Instant = Instant::now(); - let commitment_config: CommitmentConfig = CommitmentConfig::confirmed(); - loop { if start.elapsed() >= timeout { return Err(HeliusError::Timeout { diff --git a/src/types/enums.rs b/src/types/enums.rs index bed9b469..01a82add 100644 --- a/src/types/enums.rs +++ b/src/types/enums.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str}; +use solana_program::pubkey::Pubkey; use solana_sdk::transaction::{Transaction, VersionedTransaction}; +use std::str::FromStr; use super::*; @@ -138,20 +140,28 @@ pub enum TokenType { #[derive(Debug, Clone, Copy, PartialEq)] pub enum MintApiAuthority { - Mainnet(&'static str), - Devnet(&'static str), + Mainnet(Pubkey), + Devnet(Pubkey), } impl MintApiAuthority { - pub fn from_cluster(cluster: Cluster) -> Result { + pub fn from_cluster(cluster: &Cluster) -> Self { match cluster { - Cluster::Devnet => Ok(MintApiAuthority::Devnet("2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC")), - Cluster::MainnetBeta => Ok(MintApiAuthority::Mainnet( - "HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R", - )), - Cluster::StakedMainnetBeta => Ok(MintApiAuthority::Mainnet( - "HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R", - )), + Cluster::MainnetBeta | Cluster::StakedMainnetBeta => { + MintApiAuthority::Mainnet(Pubkey::from_str("HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R").unwrap()) + } + Cluster::Devnet => { + MintApiAuthority::Devnet(Pubkey::from_str("2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC").unwrap()) + } + } + } +} + +impl Into for MintApiAuthority { + fn into(self) -> Pubkey { + match self { + MintApiAuthority::Mainnet(s) => s, + MintApiAuthority::Devnet(s) => s, } } } diff --git a/src/utils/collection_authority.rs b/src/utils/collection_authority.rs new file mode 100644 index 00000000..8d29291d --- /dev/null +++ b/src/utils/collection_authority.rs @@ -0,0 +1,64 @@ +use mpl_token_metadata::instructions::{ApproveCollectionAuthority, RevokeCollectionAuthority}; +use mpl_token_metadata::ID; +use solana_program::instruction::Instruction; +use solana_program::pubkey::Pubkey; +use solana_sdk::signature::{Keypair, Signer}; + +pub fn get_collection_authority_record(collection_mint: &Pubkey, collection_authority: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[ + "metadata".as_bytes(), + ID.as_ref(), + &collection_mint.to_bytes(), + "collection_authority".as_bytes(), + &collection_authority.to_bytes(), + ], + &ID, + ) + .0 +} +pub fn get_collection_metadata_account(collection_mint: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&["metadata".as_bytes(), ID.as_ref(), &collection_mint.to_bytes()], &ID).0 +} + +pub fn revoke_collection_authority_instruction( + collection_mint: Pubkey, + collection_authority: Pubkey, + revoke_authority_keypair: &Keypair, +) -> Instruction { + let collection_metadata = get_collection_metadata_account(&collection_mint); + let collection_authority_record = get_collection_authority_record(&collection_mint, &collection_authority); + + let revoke_instruction = RevokeCollectionAuthority { + collection_authority_record, + delegate_authority: collection_authority, + revoke_authority: revoke_authority_keypair.pubkey(), + metadata: collection_metadata, + mint: collection_mint, + }; + + revoke_instruction.instruction() +} + +pub fn delegate_collection_authority_instruction( + collection_mint: Pubkey, + new_collection_authority: Pubkey, + update_authority_keypair: &Keypair, + payer_pubkey: Pubkey, +) -> Instruction { + let collection_metadata = get_collection_metadata_account(&collection_mint); + let collection_authority_record = get_collection_authority_record(&collection_mint, &new_collection_authority); + + let approve_instruction = ApproveCollectionAuthority { + collection_authority_record, + new_collection_authority, + update_authority: update_authority_keypair.pubkey(), + payer: payer_pubkey, + metadata: collection_metadata, + mint: collection_mint, + system_program: solana_program::system_program::ID, + rent: None, + }; + + approve_instruction.instruction() +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 006f5896..0c8a29b1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,7 @@ pub use self::deserialize_str_to_number::deserialize_str_to_number; pub use self::is_valid_solana_address::is_valid_solana_address; pub use self::make_keypairs::make_keypairs; +pub mod collection_authority; mod deserialize_str_to_number; mod is_valid_solana_address; mod make_keypairs; diff --git a/tests/test_mint_api.rs b/tests/test_mint_api.rs index 8b7d4221..3f1b345a 100644 --- a/tests/test_mint_api.rs +++ b/tests/test_mint_api.rs @@ -160,44 +160,67 @@ async fn test_get_asset_proof_failure() { assert!(result.is_err(), "Expected an error but got success"); } -#[tokio::test] -async fn test_mint_api_authority_from_cluster_success() { - let devnet_cluster: Cluster = Cluster::Devnet; - let mainnet_cluster: Cluster = Cluster::MainnetBeta; +#[cfg(test)] +mod tests { + use super::*; + use solana_program::pubkey::Pubkey; + use std::str::FromStr; - let devnet_authority: std::result::Result = MintApiAuthority::from_cluster(devnet_cluster); - let mainnet_authority: std::result::Result = - MintApiAuthority::from_cluster(mainnet_cluster); + #[test] + fn test_into_pubkey() { + let pubkey_str = "HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R"; + let expected_pubkey = Pubkey::from_str(pubkey_str).unwrap(); - assert_eq!( - devnet_authority.unwrap(), - MintApiAuthority::Devnet("2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC"), - "Devnet authority did not match expected value" - ); - assert_eq!( - mainnet_authority.unwrap(), - MintApiAuthority::Mainnet("HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R"), - "Mainnet authority did not match expected value" - ); -} + let mint_authority = MintApiAuthority::Mainnet(expected_pubkey); + let converted_pubkey: Pubkey = mint_authority.into(); -#[tokio::test] -async fn test_mint_api_authority_from_cluster_failure() { - let devnet_cluster: Cluster = Cluster::Devnet; - let mainnet_cluster: Cluster = Cluster::MainnetBeta; - - let devnet_authority: std::result::Result = MintApiAuthority::from_cluster(devnet_cluster); - let mainnet_authority: std::result::Result = - MintApiAuthority::from_cluster(mainnet_cluster); - - assert_ne!( - devnet_authority.unwrap(), - MintApiAuthority::Devnet("Blade"), - "Devnet authority did not match expected value" - ); - assert_ne!( - mainnet_authority.unwrap(), - MintApiAuthority::Mainnet("Deacon Frost"), - "Mainnet authority did not match expected value" - ); + assert_eq!(converted_pubkey, expected_pubkey); + + let pubkey_str = "2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC"; + let expected_pubkey = Pubkey::from_str(pubkey_str).unwrap(); + + let mint_authority = MintApiAuthority::Devnet(expected_pubkey); + let converted_pubkey: Pubkey = mint_authority.into(); + + assert_eq!(converted_pubkey, expected_pubkey); + } + + #[test] + fn test_from_cluster() { + let cluster = Cluster::Devnet; + let mint_api_authority = MintApiAuthority::from_cluster(&cluster); + + let expected_pubkey = Pubkey::from_str("2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC").unwrap(); + + match mint_api_authority { + MintApiAuthority::Devnet(pubkey) => { + assert_eq!(pubkey, expected_pubkey); + } + _ => panic!("Expected MintApiAuthority::Devnet variant"), + } + + let cluster = Cluster::MainnetBeta; + let mint_api_authority = MintApiAuthority::from_cluster(&cluster); + + let expected_pubkey = Pubkey::from_str("HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R").unwrap(); + + match mint_api_authority { + MintApiAuthority::Mainnet(pubkey) => { + assert_eq!(pubkey, expected_pubkey); + } + _ => panic!("Expected MintApiAuthority::Mainnet variant"), + } + + let cluster = Cluster::StakedMainnetBeta; + let mint_api_authority = MintApiAuthority::from_cluster(&cluster); + + let expected_pubkey = Pubkey::from_str("HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R").unwrap(); + + match mint_api_authority { + MintApiAuthority::Mainnet(pubkey) => { + assert_eq!(pubkey, expected_pubkey); + } + _ => panic!("Expected MintApiAuthority::Mainnet variant"), + } + } } diff --git a/tests/tests.rs b/tests/tests.rs index fba8dbcf..a5a2be8c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,4 +1,5 @@ mod utils { + mod test_collection_authority; mod test_deserialize_str_to_number; mod test_is_valid_solana_address; mod test_make_keypairs; diff --git a/tests/utils/test_collection_authority.rs b/tests/utils/test_collection_authority.rs new file mode 100644 index 00000000..97f99581 --- /dev/null +++ b/tests/utils/test_collection_authority.rs @@ -0,0 +1,97 @@ +#[cfg(test)] +mod tests { + use helius::utils::collection_authority::*; + use mpl_token_metadata::ID; + use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + use solana_sdk::signature::{Keypair, Signer}; + + #[test] + fn test_get_collection_authority_record() { + let collection_mint = Pubkey::new_unique(); + let collection_authority = Pubkey::new_unique(); + + let result = get_collection_authority_record(&collection_mint, &collection_authority); + + let (expected_pubkey, _bump_seed) = Pubkey::find_program_address( + &[ + b"metadata", + ID.as_ref(), + &collection_mint.to_bytes(), + b"collection_authority", + &collection_authority.to_bytes(), + ], + &ID, + ); + + assert_eq!(result, expected_pubkey); + } + + #[test] + fn test_get_collection_metadata_account() { + let collection_mint = Pubkey::new_unique(); + + let result = get_collection_metadata_account(&collection_mint); + + let (expected_pubkey, _bump_seed) = + Pubkey::find_program_address(&[b"metadata", ID.as_ref(), &collection_mint.to_bytes()], &ID); + + assert_eq!(result, expected_pubkey); + } + + #[test] + fn test_get_delegate_collection_authority_instruction() { + let collection_mint = Pubkey::new_unique(); + let new_collection_authority = Pubkey::new_unique(); + let update_authority_keypair = Keypair::new(); + let payer_pubkey = Pubkey::new_unique(); + + let instruction = delegate_collection_authority_instruction( + collection_mint, + new_collection_authority, + &update_authority_keypair, + payer_pubkey, + ); + + assert_eq!(instruction.program_id, ID); + + let collection_metadata = get_collection_metadata_account(&collection_mint); + let collection_authority_record = get_collection_authority_record(&collection_mint, &new_collection_authority); + + let expected_accounts = vec![ + AccountMeta::new(collection_authority_record, false), + AccountMeta::new_readonly(new_collection_authority, false), + AccountMeta::new(update_authority_keypair.pubkey(), true), + AccountMeta::new(payer_pubkey, true), + AccountMeta::new_readonly(collection_metadata, false), + AccountMeta::new_readonly(collection_mint, false), + AccountMeta::new_readonly(solana_program::system_program::ID, false), + ]; + + assert_eq!(instruction.accounts, expected_accounts); + } + + #[test] + fn test_get_revoke_collection_authority_instruction() { + let collection_mint = Pubkey::new_unique(); + let collection_authority = Pubkey::new_unique(); + let revoke_authority_keypair = Keypair::new(); + + let instruction = + revoke_collection_authority_instruction(collection_mint, collection_authority, &revoke_authority_keypair); + + assert_eq!(instruction.program_id, ID); + + let expected_accounts = vec![ + AccountMeta::new( + get_collection_authority_record(&collection_mint, &collection_authority), + false, + ), + AccountMeta::new(collection_authority, false), + AccountMeta::new(revoke_authority_keypair.pubkey(), true), + AccountMeta::new_readonly(get_collection_metadata_account(&collection_mint), false), + AccountMeta::new_readonly(collection_mint, false), + ]; + + assert_eq!(instruction.accounts, expected_accounts); + } +}