Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add delegate_collection_authority and revoke_collection_authority #77

Merged
merged 12 commits into from
Oct 22, 2024
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"
Expand Down
140 changes: 140 additions & 0 deletions examples/delegate_and_revoke_collection_authority.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
4 changes: 4 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ impl Helius {
pub fn ws(&self) -> Option<Arc<EnhancedWebsocket>> {
self.ws_client.clone()
}

pub fn config(&self) -> Arc<Config> {
self.config.clone()
}
}

/// A wrapper around the asynchronous Solana RPC client that provides thread-safe access
Expand Down
6 changes: 5 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -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
///
Expand Down Expand Up @@ -40,4 +40,8 @@ impl Config {
endpoints,
})
}

pub fn mint_api_authority(&self) -> MintApiAuthority {
MintApiAuthority::from_cluster(&self.cluster)
}
}
77 changes: 77 additions & 0 deletions src/mint_api.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -13,4 +20,74 @@ impl Helius {
pub async fn mint_compressed_nft(&self, request: MintCompressedNftRequest) -> Result<MintResponse> {
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<Signature> {
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<Pubkey>,
revoke_authority_keypair: &Keypair,
payer_keypair: Option<&Keypair>,
) -> Result<Signature> {
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())
}
}
2 changes: 0 additions & 2 deletions src/optimized_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 20 additions & 10 deletions src/types/enums.rs
Original file line number Diff line number Diff line change
@@ -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::*;

Expand Down Expand Up @@ -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<Self, &'static str> {
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<Pubkey> for MintApiAuthority {
fn into(self) -> Pubkey {
match self {
MintApiAuthority::Mainnet(s) => s,
MintApiAuthority::Devnet(s) => s,
}
}
}
Expand Down
64 changes: 64 additions & 0 deletions src/utils/collection_authority.rs
Original file line number Diff line number Diff line change
@@ -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()
}
Loading
Loading