From 37e5a2508303b06e880ead70f3d0318d4c09d359 Mon Sep 17 00:00:00 2001 From: Dmytro Medynskyi Date: Tue, 8 Oct 2024 18:34:36 +0200 Subject: [PATCH] feat: Add revoke_collection_authority --- Cargo.toml | 1 + src/client.rs | 2 + src/config.rs | 6 +- src/mint_api.rs | 30 ++++++- src/types/enums.rs | 26 +++--- src/utils/collection_authority.rs | 50 +++++++++++ src/utils/mod.rs | 2 + tests/test_mint_api.rs | 107 ++++++++++++++--------- tests/utils/test_collection_authority.rs | 68 ++++++++++++++ 9 files changed, 240 insertions(+), 52 deletions(-) create mode 100644 src/utils/collection_authority.rs create mode 100644 tests/utils/test_collection_authority.rs diff --git a/Cargo.toml b/Cargo.toml index 9a2f72d..cb78b7f 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 } diff --git a/src/client.rs b/src/client.rs index 8cb7649..012c3ec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -146,6 +146,8 @@ 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 c5df168..3630d66 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 527ff25..60a7c43 100644 --- a/src/mint_api.rs +++ b/src/mint_api.rs @@ -1,6 +1,9 @@ +use solana_program::pubkey::Pubkey; +use solana_sdk::signature::{Keypair, Signature}; use crate::error::Result; -use crate::types::{MintCompressedNftRequest, MintResponse}; +use crate::types::{MintCompressedNftRequest, MintResponse, SmartTransactionConfig}; use crate::Helius; +use crate::utils::collection_authority::revoke_collection_authority_instruction; impl Helius { /// The easiest way to mint a compressed NFT (cNFT) @@ -13,4 +16,29 @@ impl Helius { pub async fn mint_compressed_nft(&self, request: MintCompressedNftRequest) -> Result { self.rpc_client.post_rpc_request("mintCompressedNft", request).await } + + /// 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 transaction_config = SmartTransactionConfig::new(vec![revoke_instruction], vec![payer_keypair]); + self.send_smart_transaction(transaction_config).await + } } diff --git a/src/types/enums.rs b/src/types/enums.rs index bed9b46..4ad62b7 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,24 @@ 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 0000000..bbcddd7 --- /dev/null +++ b/src/utils/collection_authority.rs @@ -0,0 +1,50 @@ +use solana_program::pubkey::Pubkey; +use mpl_token_metadata::ID; +use solana_sdk::signature::{Keypair, Signer}; +use mpl_token_metadata::instructions::RevokeCollectionAuthority; +use solana_program::instruction::Instruction; + +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() +} + diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 006f589..a91bc6c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,3 +5,5 @@ pub use self::make_keypairs::make_keypairs; mod deserialize_str_to_number; mod is_valid_solana_address; mod make_keypairs; + +pub mod collection_authority; \ No newline at end of file diff --git a/tests/test_mint_api.rs b/tests/test_mint_api.rs index 8b7d422..8e2fc7a 100644 --- a/tests/test_mint_api.rs +++ b/tests/test_mint_api.rs @@ -160,44 +160,71 @@ 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; - - let devnet_authority: std::result::Result = MintApiAuthority::from_cluster(devnet_cluster); - let mainnet_authority: std::result::Result = - MintApiAuthority::from_cluster(mainnet_cluster); - - 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" - ); -} -#[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" - ); -} +#[cfg(test)] +mod tests { + use super::*; + use solana_program::pubkey::Pubkey; + use std::str::FromStr; + + #[test] + fn test_into_pubkey() { + let pubkey_str = "HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R"; + let expected_pubkey = Pubkey::from_str(pubkey_str).unwrap(); + + let mint_authority = MintApiAuthority::Mainnet(expected_pubkey); + let converted_pubkey: Pubkey = mint_authority.into(); + + 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"), + } + } +} \ No newline at end of file diff --git a/tests/utils/test_collection_authority.rs b/tests/utils/test_collection_authority.rs new file mode 100644 index 0000000..ffb4741 --- /dev/null +++ b/tests/utils/test_collection_authority.rs @@ -0,0 +1,68 @@ +#[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_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); + + } +} \ No newline at end of file