diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f8ffb223..716369f9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,7 +46,7 @@ jobs: path: target \ key: ${{ runner.os }}-target-${{ hashFiles('**/Cargo.lock') }} \ restore-keys: ${{ runner.os }}-target- || echo 'Cache target step failed or timed out.'" - + - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -80,7 +80,7 @@ jobs: run: cargo build --verbose - name: Save build artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: build path: target diff --git a/Cargo.toml b/Cargo.toml index 738f57fe..42037cca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helius" -version = "0.2.1" +version = "0.2.2" edition = "2021" description = "An asynchronous Helius Rust SDK for building the future of Solana" keywords = ["helius", "solana", "asynchronous-sdk", "das", "cryptocurrency"] @@ -17,24 +17,43 @@ bincode = "1.3.3" chrono = { version = "0.4.11", features = ["serde"] } futures = "0.3.30" futures-util = "0.3.30" +mpl-token-metadata = { version = "5.0.0-beta.0" } phf = { version = "0.11.2", features = ["macros"] } rand = "0.8.5" -reqwest = { version = "0.12.3", features = ["json"] } +reqwest = { version = "0.12.8", features = ["json", "native-tls"] } semver = "1.0.23" serde = "1.0.198" serde-enum-str = "0.4.0" serde_json = "1.0.116" -solana-account-decoder = "1.18.12" -solana-client = "1.18.12" -solana-program = "1.18.12" -solana-rpc-client-api = "1.18.12" -solana-sdk = "1.18.11" -solana-transaction-status = "1.18.12" +solana-account-decoder = "2.0" +solana-client = "2.0" +solana-program = "2.0" +solana-rpc-client-api = "2.0" +solana-sdk = "2.0" +solana-transaction-status = "2.0" thiserror = "1.0.58" -tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "net"] } +tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "net", "time"] } tokio-stream = "0.1.15" -tokio-tungstenite = { version = "0.21.0", features = ["native-tls", "handshake"] } +tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] } url = "2.5.0" +spl-token = { version = "6.0", features = ["no-entrypoint"] } [dev-dependencies] mockito = "1.4.0" + +[features] +default = ["native-tls"] +native-tls = [ + "reqwest/native-tls", + "tokio-tungstenite/native-tls" +] +rustls = [ + "reqwest/rustls-tls", + "tokio-tungstenite/rustls-tls-webpki-roots" +] + +[patch.crates-io] +# https://github.com/solana-labs/solana/issues/26688#issuecomment-2136066879 +# For curve25519-dalek use the same revision Solana uses +# https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/Cargo.toml#L475-L563 +curve25519-dalek = { git = "https://github.com/solana-labs/curve25519-dalek.git", rev = "b500cdc2a920cd5bff9e2dd974d7b97349d61464" } diff --git a/README.md b/README.md index a8d40d71..d47b67fa 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,21 @@ where `x.y.z` is your desired version. Alternatively, use `cargo add helius` to Remember to run `cargo update` regularly to fetch the latest version of the SDK. +### TLS Options +The Helius Rust SDK uses the native TLS implementation by default via: +```toml +[dependencies] +helius = "x.y.z" +``` + +However, the SDK also supports `rustls`. Add the following to your `Cargo.toml` to use `rustls` instead of the native TLS implementation: +```toml +[dependencies] +helius = { version = "x.y.z", default-features = false, features = ["rustls"] } +``` + +Using `rustls` may be preferred in environments where OpenSSL is not available or when a pure Rust TLS implementation is desired. However, it may not support all the same features as the native TLS implementation + ## Usage ### `Helius` The SDK provides a [`Helius`](https://github.com/helius-labs/helius-rust-sdk/blob/dev/src/client.rs) instance that can be configured with an API key and a given Solana cluster. Developers can generate a new API key on the [Helius Developer Dashboard](https://dev.helius.xyz/dashboard/app). This instance acts as the main entry point for interacting with the SDK by providing methods to access different Solana and RPC client functionalities. The following code is an example of how to use the SDK to fetch info on [Mad Lad #8420](https://explorer.solana.com/address/F9Lw3ki3hJ7PF9HQXsBzoY8GyE6sPoEZZdXJBsTTD2rk?network=mainnet): @@ -118,17 +133,19 @@ Our SDK is designed to provide a seamless developer experience when building on - [`remove_addresses_from_webhook`](https://github.com/helius-labs/helius-rust-sdk/blob/bf24259e3333ae93126bb65b342c2c63e80e07a6/src/webhook.rs#L75-L105) - Removes a list of addresses from an existing webhook by its ID ### Smart Transactions -- [`create_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/705d66fb7d4004fc32c2a5f0d6ca4a1f2a7b175d/src/optimized_transaction.rs#L113-L312) - Creates an optimized transaction based on the provided configuration -- [`get_compute_units`](https://github.com/helius-labs/helius-rust-sdk/blob/a79a751e1a064125010bdb359068a366d635d005/src/optimized_transaction.rs#L29-L75) - Simulates a transaction to get the total compute units consumed -- [`poll_transaction_confirmation`](https://github.com/helius-labs/helius-rust-sdk/blob/a79a751e1a064125010bdb359068a366d635d005/src/optimized_transaction.rs#L77-L112) - Polls a transaction to check whether it has been confirmed in 5 second intervals with a 15 second timeout -- [`send_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/705d66fb7d4004fc32c2a5f0d6ca4a1f2a7b175d/src/optimized_transaction.rs#L314-L374) - Builds and sends an optimized transaction, and handles its confirmation status +- [`create_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/optimized_transaction.rs#L131-L331) - Creates an optimized transaction based on the provided configuration +- [`get_compute_units`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/optimized_transaction.rs#L34-L87) - Simulates a transaction to get the total compute units consumed +- [`poll_transaction_confirmation`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/optimized_transaction.rs#L89-L129) - Polls a transaction to check whether it has been confirmed in 5 second intervals with a 15 second timeout +- [`send_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/optimized_transaction.rs#L333-L364) - Builds and sends an optimized transaction, and handles its confirmation status +- [`send_and_confirm_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/optimized_transaction.rs#L366-L412) - Sends a transaction and handles its confirmation status with retry logic +- [`send_smart_transaction_with_seeds`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/optimized_transaction.rs#L414-L487) - Sends a smart transaction using seed bytes ### Jito Smart Transactions and Helper Methods - [`add_tip_instruction`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L66-L83) - Adds a tip instruction to the instructions provided -- [`create_smart_transaction_with_tip`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L85-L124) - Creates a smart transaction with a Jito tip -- [`get_bundle_statuses`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L169-L202) - Get the status of Jito bundles -- [`send_jito_bundle`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L126-L167) - Sends a bundle of transactions to the Jito Block Engine -- [`send_smart_transaction_with_tip`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L204-L269) - Sends a smart transaction as a Jito bundle with a tip +- [`create_smart_transaction_with_tip`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/jito.rs#L85-L125) - Creates a smart transaction with a Jito tip +- [`get_bundle_statuses`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/jito.rs#L170-L203) - Get the status of Jito bundles +- [`send_jito_bundle`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/jito.rs#L127-L168) - Sends a bundle of transactions to the Jito Block Engine +- [`send_smart_transaction_with_tip`](https://github.com/helius-labs/helius-rust-sdk/blob/bd9e0b10c81ab9ea56dfcd286336b086f6737b64/src/jito.rs#L205-L270) - Sends a smart transaction as a Jito bundle with a tip ### Helper Methods - [`get_priority_fee_estimate`](https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api) - Gets an estimate of the priority fees required for a transaction to be processed more quickly 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/examples/enhanced_websocket_accounts.rs b/examples/enhanced_websocket_accounts.rs index 5357bdc0..9a4af96a 100644 --- a/examples/enhanced_websocket_accounts.rs +++ b/examples/enhanced_websocket_accounts.rs @@ -9,7 +9,7 @@ async fn main() -> Result<()> { let api_key: &str = "your_api_key"; let cluster: Cluster = Cluster::MainnetBeta; - let helius: Helius = Helius::new_with_ws(api_key, cluster).await.unwrap(); + let helius: Helius = Helius::new_with_ws(api_key, cluster).await?; let key: pubkey::Pubkey = pubkey!("BtsmiEEvnSuUnKxqXj2PZRYpPJAc7C34mGz8gtJ1DAaH"); diff --git a/examples/send_smart_transaction_with_tip.rs b/examples/send_smart_transaction_with_tip.rs index 2f6fa765..9e3dc740 100644 --- a/examples/send_smart_transaction_with_tip.rs +++ b/examples/send_smart_transaction_with_tip.rs @@ -6,6 +6,7 @@ use solana_sdk::{ system_instruction::transfer, }; use std::str::FromStr; +use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; @@ -28,7 +29,7 @@ async fn main() { let create_config: CreateSmartTransactionConfig = CreateSmartTransactionConfig { instructions, - signers: vec![&from_keypair], + signers: vec![Arc::new(from_keypair)], lookup_tables: None, fee_payer: None, }; @@ -42,6 +43,7 @@ async fn main() { max_retries: None, min_context_slot: None, }, + timeout: Timeout::default(), }; // Send the optimized transaction with a 10k lamport tip using the New York region's API URL diff --git a/src/client.rs b/src/client.rs index 51a63b66..8d4ce9bc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -46,8 +46,9 @@ impl Helius { /// ``` pub fn new(api_key: &str, cluster: Cluster) -> Result { let config: Arc = Arc::new(Config::new(api_key, cluster)?); - let client: Client = Client::new(); + let client: Client = Client::builder().build().map_err(HeliusError::ReqwestError)?; let rpc_client: Arc = Arc::new(RpcClient::new(Arc::new(client.clone()), config.clone())?); + Ok(Helius { config, client, @@ -75,7 +76,7 @@ impl Helius { /// ``` pub fn new_with_async_solana(api_key: &str, cluster: Cluster) -> Result { let config: Arc = Arc::new(Config::new(api_key, cluster)?); - let client: Client = Client::new(); + let client: Client = Client::builder().build().map_err(HeliusError::ReqwestError)?; let url: String = format!("{}/?api-key={}", config.endpoints.rpc, config.api_key); let async_solana_client: Arc = Arc::new(AsyncSolanaRpcClient::new(url)); @@ -98,10 +99,11 @@ impl Helius { /// An instance of `Helius` if successful. A `HeliusError` is returned if an error occurs during configuration or initialization of the HTTP, RPC, or WS client pub async fn new_with_ws(api_key: &str, cluster: Cluster) -> Result { let config: Arc = Arc::new(Config::new(api_key, cluster)?); - let client: Client = Client::new(); + let client: Client = Client::builder().build().map_err(HeliusError::ReqwestError)?; let rpc_client: Arc = Arc::new(RpcClient::new(Arc::new(client.clone()), config.clone())?); let wss: String = format!("{}{}", ENHANCED_WEBSOCKET_URL, api_key); let ws_client: Arc = Arc::new(EnhancedWebsocket::new(&wss).await?); + Ok(Helius { config, client, @@ -144,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/error.rs b/src/error.rs index 0b02d2b1..468f36b3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -123,6 +123,9 @@ pub enum HeliusError { #[error("Url parse error")] UrlParseError(#[from] url::ParseError), + + #[error("TLS error: {0}")] + TlsError(String), } impl HeliusError { @@ -156,5 +159,15 @@ impl From for HeliusError { } } +impl From for HeliusError { + fn from(err: reqwest::Error) -> Self { + if err.is_builder() { + HeliusError::TlsError(err.to_string()) + } else { + HeliusError::ReqwestError(err) + } + } +} + /// A handy type alias for handling results across the Helius SDK pub type Result = std::result::Result; diff --git a/src/jito.rs b/src/jito.rs index 1534f0c6..af4828f9 100644 --- a/src/jito.rs +++ b/src/jito.rs @@ -92,7 +92,7 @@ impl Helius { /// A `Result` containing the serialized transaction as a base58-encoded string and the last valid block height pub async fn create_smart_transaction_with_tip( &self, - mut config: CreateSmartTransactionConfig<'_>, + mut config: CreateSmartTransactionConfig, tip_amount: Option, ) -> Result<(String, u64)> { if config.signers.is_empty() { @@ -105,6 +105,7 @@ impl Helius { let random_tip_account: &str = *JITO_TIP_ACCOUNTS.choose(&mut rand::thread_rng()).unwrap(); let payer_key: Pubkey = config .fee_payer + .as_ref() .map_or_else(|| config.signers[0].pubkey(), |signer| signer.pubkey()); self.add_tip_instruction(&mut config.instructions, payer_key, random_tip_account, tip_amount); @@ -212,7 +213,7 @@ impl Helius { /// A `Result` containing the bundle IDc pub async fn send_smart_transaction_with_tip( &self, - config: SmartTransactionConfig<'_>, + config: SmartTransactionConfig, tip_amount: Option, region: Option, ) -> Result { 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 53f3fd1f..108885ac 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -1,14 +1,19 @@ use crate::error::{HeliusError, Result}; use crate::types::{ - CreateSmartTransactionConfig, GetPriorityFeeEstimateOptions, GetPriorityFeeEstimateRequest, - GetPriorityFeeEstimateResponse, SmartTransaction, SmartTransactionConfig, + CreateSmartTransactionConfig, CreateSmartTransactionSeedConfig, GetPriorityFeeEstimateOptions, + GetPriorityFeeEstimateRequest, GetPriorityFeeEstimateResponse, SmartTransaction, SmartTransactionConfig, Timeout, }; use crate::Helius; +use std::sync::Arc; use bincode::{serialize, ErrorKind}; use reqwest::StatusCode; -use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig}; -use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; +use solana_client::{ + rpc_client::SerializableTransaction, + rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig}, + rpc_response::{Response, RpcSimulateTransactionResult}, +}; +use solana_sdk::signature::keypair_from_seed; use solana_sdk::{ address_lookup_table::AddressLookupTableAccount, bs58::encode, @@ -21,6 +26,7 @@ use solana_sdk::{ signature::{Signature, Signer}, transaction::{Transaction, VersionedTransaction}, }; +use solana_transaction_status::TransactionConfirmationStatus; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -40,7 +46,7 @@ impl Helius { instructions: Vec, payer: Pubkey, lookup_tables: Vec, - signers: &[&dyn Signer], + signers: Option<&[Arc]>, ) -> Result> { // Set the compute budget limit let test_instructions: Vec = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)] @@ -56,13 +62,20 @@ impl Helius { v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - // Create a signed VersionedTransaction - let transaction: VersionedTransaction = VersionedTransaction::try_new(versioned_message, signers) - .map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?; + // Create a VersionedTransaction (signed or unsigned) + let transaction: VersionedTransaction = if let Some(signers) = signers { + VersionedTransaction::try_new(versioned_message, signers) + .map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))? + } else { + VersionedTransaction { + signatures: vec![], + message: versioned_message, + } + }; // Simulate the transaction let config: RpcSimulateTransactionConfig = RpcSimulateTransactionConfig { - sig_verify: true, + sig_verify: signers.is_some(), ..Default::default() }; let result: Response = self @@ -86,8 +99,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 { @@ -96,16 +107,23 @@ impl Helius { }); } - match self - .connection() - .get_signature_status_with_commitment(&txt_sig, commitment_config) - { - Ok(Some(Ok(()))) => return Ok(txt_sig), - Ok(Some(Err(err))) => return Err(HeliusError::TransactionError(err)), - Ok(None) => { + let status = self.connection().get_signature_statuses(&[txt_sig])?; + + match status.value[0].clone() { + Some(status) => { + if status.err.is_none() + && (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed) + || status.confirmation_status == Some(TransactionConfirmationStatus::Finalized)) + { + return Ok(txt_sig); + } + if status.err.is_some() { + return Err(HeliusError::TransactionError(status.err.unwrap())); + } + } + None => { sleep(interval).await; } - Err(err) => return Err(HeliusError::ClientError(err)), } } } @@ -120,7 +138,7 @@ impl Helius { /// An optimized `SmartTransaction` (i.e., `Transaction` or `VersionedTransaction`) and the `last_valid_block_height` pub async fn create_smart_transaction( &self, - config: &CreateSmartTransactionConfig<'_>, + config: &CreateSmartTransactionConfig, ) -> Result<(SmartTransaction, u64)> { if config.signers.is_empty() { return Err(HeliusError::InvalidInput( @@ -130,6 +148,7 @@ impl Helius { let payer_pubkey: Pubkey = config .fee_payer + .as_ref() .map_or(config.signers[0].pubkey(), |signer| signer.pubkey()); let (recent_blockhash, last_valid_block_hash) = self .connection() @@ -160,10 +179,10 @@ impl Helius { v0::Message::try_compile(&payer_pubkey, &config.instructions, lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - let all_signers = if let Some(fee_payer) = config.fee_payer { - let mut all_signers: Vec<&dyn Signer> = config.signers.clone(); + let all_signers: Vec> = if let Some(fee_payer) = &config.fee_payer { + let mut all_signers = config.signers.clone(); if !all_signers.iter().any(|signer| signer.pubkey() == fee_payer.pubkey()) { - all_signers.push(fee_payer); + all_signers.push(fee_payer.clone()); } all_signers @@ -186,7 +205,7 @@ impl Helius { let mut tx: Transaction = Transaction::new_with_payer(&config.instructions, Some(&payer_pubkey)); tx.try_partial_sign(&config.signers, recent_blockhash)?; - if let Some(fee_payer) = config.fee_payer { + if let Some(fee_payer) = config.fee_payer.as_ref() { tx.try_partial_sign(&[fee_payer], recent_blockhash)?; } @@ -238,7 +257,7 @@ impl Helius { updated_instructions, payer_pubkey, config.lookup_tables.clone().unwrap_or_default(), - &config.signers, + Some(&config.signers), ) .await?; @@ -270,10 +289,10 @@ impl Helius { v0::Message::try_compile(&payer_pubkey, &final_instructions, lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - let all_signers: Vec<&dyn Signer> = if let Some(fee_payer) = config.fee_payer { + let all_signers: Vec> = if let Some(fee_payer) = config.fee_payer.as_ref() { let mut all_signers = config.signers.clone(); if !all_signers.iter().any(|signer| signer.pubkey() == fee_payer.pubkey()) { - all_signers.push(fee_payer); + all_signers.push(fee_payer.clone()); } all_signers } else { @@ -298,7 +317,7 @@ impl Helius { let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&payer_pubkey)); tx.try_partial_sign(&config.signers, recent_blockhash)?; - if let Some(fee_payer) = config.fee_payer { + if let Some(fee_payer) = config.fee_payer.as_ref() { tx.try_partial_sign(&[fee_payer], recent_blockhash)?; } @@ -319,38 +338,58 @@ impl Helius { /// /// # Returns /// The transaction signature, if successful - pub async fn send_smart_transaction(&self, config: SmartTransactionConfig<'_>) -> Result { + pub async fn send_smart_transaction(&self, config: SmartTransactionConfig) -> Result { let (transaction, last_valid_block_height) = self.create_smart_transaction(&config.create_config).await?; - // Common logic for sending transactions - let send_transaction_config: RpcSendTransactionConfig = RpcSendTransactionConfig { - skip_preflight: config.send_options.skip_preflight, - preflight_commitment: config.send_options.preflight_commitment, - encoding: config.send_options.encoding, - max_retries: config.send_options.max_retries, - min_context_slot: config.send_options.min_context_slot, - }; - - let send_result = |transaction: &Transaction| { - self.connection() - .send_transaction_with_config(transaction, send_transaction_config) - }; - let send_versioned_result = |transaction: &VersionedTransaction| { - self.connection() - .send_transaction_with_config(transaction, send_transaction_config) - }; + match transaction { + SmartTransaction::Legacy(tx) => { + self.send_and_confirm_transaction( + &tx, + config.send_options, + last_valid_block_height, + Some(config.timeout.into()), + ) + .await + } + SmartTransaction::Versioned(tx) => { + self.send_and_confirm_transaction( + &tx, + config.send_options, + last_valid_block_height, + Some(config.timeout.into()), + ) + .await + } + } + } - // Retry logic with a timeout of 60 seconds - let timeout: Duration = Duration::from_secs(60); + /// Sends a transaction and handles its confirmation status + /// + /// # Arguments + /// * `transaction` - The transaction to be sent, which implements `SerializableTransaction` + /// * `send_transaction_config` - Configuration options for sending the transaction + /// * `last_valid_block_height` - The last block height at which the transaction is valid + /// * `timeout` - Optional duration for polling transaction confirmation, defaults to 60 seconds + /// + /// # Returns + /// The transaction signature, if successful + pub async fn send_and_confirm_transaction( + &self, + transaction: &impl SerializableTransaction, + send_transaction_config: RpcSendTransactionConfig, + last_valid_block_height: u64, + timeout: Option, + ) -> Result { + // Retry logic with a timeout + let timeout: Duration = timeout.unwrap_or(Duration::from_secs(60)); let start_time: Instant = Instant::now(); while Instant::now().duration_since(start_time) < timeout || self.connection().get_block_height()? <= last_valid_block_height { - let result = match &transaction { - SmartTransaction::Legacy(tx) => send_result(tx), - SmartTransaction::Versioned(tx) => send_versioned_result(tx), - }; + let result = self + .connection() + .send_transaction_with_config(transaction, send_transaction_config); match result { Ok(signature) => { @@ -371,4 +410,78 @@ impl Helius { text: "Transaction failed to confirm in 60s".to_string(), }) } + + /// Sends a smart transaction using seed bytes + /// + /// This method allows for sending smart transactions in asynchronous contexts + /// where the Signer trait's lack of Send + Sync would otherwise cause issues. + /// It creates Keypairs from the provided seed bytes and uses them to sign the transaction. + /// + /// # Arguments + /// + /// * `create_config` - A `CreateSmartTransactionSeedConfig` containing: + /// - `instructions`: The instructions to be executed in the transaction. + /// - `signer_seeds`: Seed bytes for generating signer keypairs. + /// - `fee_payer_seed`: Optional seed bytes for generating the fee payer keypair. + /// - `lookup_tables`: Optional address lookup tables for the transaction. + /// * `send_options` - Optional `RpcSendTransactionConfig` for sending the transaction. + /// * `timeout` - Optional `Timeout` wait time for polling transaction confirmation. + /// + /// # Returns + /// + /// A `Result` containing the transaction signature if successful, or an error if not. + /// + /// # Errors + /// + /// This function will return an error if keypair creation from seeds fails, the underlying `send_smart_transaction` call fails, + /// or no signer seeds are provided + /// + /// # Notes + /// + /// If no `fee_payer_seed` is provided, the first signer (i.e., derived from the first seed in `signer_seeds`) will be used as the fee payer + pub async fn send_smart_transaction_with_seeds( + &self, + create_config: CreateSmartTransactionSeedConfig, + send_options: Option, + timeout: Option, + ) -> Result { + if create_config.signer_seeds.is_empty() { + return Err(HeliusError::InvalidInput( + "At least one signer seed must be provided".to_string(), + )); + } + + let mut signers: Vec> = create_config + .signer_seeds + .into_iter() + .map(|seed| { + Arc::new(keypair_from_seed(&seed).expect("Failed to create keypair from seed")) as Arc + }) + .collect(); + + // Determine the fee payer + let fee_payer_index: usize = if let Some(fee_payer_seed) = create_config.fee_payer_seed { + let fee_payer = + Arc::new(keypair_from_seed(&fee_payer_seed).expect("Failed to create fee payer keypair from seed")); + signers.push(fee_payer); + signers.len() - 1 // Index of the last signer (fee payer) + } else { + 0 // Index of the first signer + }; + let fee_payer = signers[fee_payer_index].clone(); + let create_smart_transaction_config: CreateSmartTransactionConfig = CreateSmartTransactionConfig { + instructions: create_config.instructions, + signers, + lookup_tables: create_config.lookup_tables, + fee_payer: Some(fee_payer), + }; + + let smart_transaction_config: SmartTransactionConfig = SmartTransactionConfig { + create_config: create_smart_transaction_config, + send_options: send_options.unwrap_or_default(), + timeout: timeout.unwrap_or_default(), + }; + + self.send_smart_transaction(smart_transaction_config).await + } } diff --git a/src/types/enhanced_transaction_types.rs b/src/types/enhanced_transaction_types.rs index 98d4dc7c..f3493936 100644 --- a/src/types/enhanced_transaction_types.rs +++ b/src/types/enhanced_transaction_types.rs @@ -78,7 +78,7 @@ pub struct TokenTransfer { #[serde(rename_all = "camelCase")] pub struct TransactionError { #[serde(rename = "InstructionError")] - pub instruciton_error: serde_json::Value, + pub instruction_error: Option, } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/types/enhanced_websocket.rs b/src/types/enhanced_websocket.rs index 2935a6da..21952419 100644 --- a/src/types/enhanced_websocket.rs +++ b/src/types/enhanced_websocket.rs @@ -96,4 +96,5 @@ pub struct RpcTransactionsConfig { pub struct TransactionNotification { pub transaction: EncodedTransactionWithStatusMeta, pub signature: String, + pub slot: u64, } diff --git a/src/types/enums.rs b/src/types/enums.rs index 86007452..4e17fa5b 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::*; @@ -27,13 +29,6 @@ pub enum Interface { ProgrammableNFT, #[serde(rename = "FungibleToken")] FungibleToken, - #[serde(rename = "V1_PRINT")] - V1PRINT, - #[allow(non_camel_case_types)] - #[serde(rename = "LEGACY_NFT")] - LEGACY_NFT, - #[serde(rename = "V2_NFT")] - Nft, #[serde(rename = "MplCoreAsset")] MplCoreAsset, #[serde(rename = "MplCoreCollection")] @@ -138,17 +133,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::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/types/types.rs b/src/types/types.rs index a6aa9091..3b0f97c9 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -4,9 +4,10 @@ use super::{ TransactionStatus, TransactionType, UiTransactionEncoding, WebhookType, }; use crate::types::{DisplayOptions, GetAssetOptions}; -// use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::sync::Arc; +use std::time::Duration; use solana_client::rpc_config::RpcSendTransactionConfig; use solana_sdk::{address_lookup_table::AddressLookupTableAccount, instruction::Instruction, signature::Signer}; @@ -16,6 +17,7 @@ use solana_sdk::{address_lookup_table::AddressLookupTableAccount, instruction::I pub enum Cluster { Devnet, MainnetBeta, + StakedMainnetBeta, } /// Stores the API and RPC endpoint URLs for a specific Helius cluster @@ -36,6 +38,10 @@ impl HeliusEndpoints { api: "https://api-mainnet.helius-rpc.com/".to_string(), rpc: "https://mainnet.helius-rpc.com/".to_string(), }, + Cluster::StakedMainnetBeta => HeliusEndpoints { + api: "https://api-mainnet.helius-rpc.com/".to_string(), + rpc: "https://staked.helius-rpc.com/".to_string(), + }, } } } @@ -795,7 +801,7 @@ pub struct GetPriorityFeeEstimateRequest { #[derive(Serialize, Deserialize, Debug, Default)] pub struct MicroLamportPriorityFeeLevels { - pub none: f64, + pub min: f64, pub low: f64, pub medium: f64, pub high: f64, @@ -945,15 +951,15 @@ pub struct EditWebhookRequest { pub encoding: AccountWebhookEncoding, } -pub struct CreateSmartTransactionConfig<'a> { +pub struct CreateSmartTransactionConfig { pub instructions: Vec, - pub signers: Vec<&'a dyn Signer>, + pub signers: Vec>, pub lookup_tables: Option>, - pub fee_payer: Option<&'a dyn Signer>, + pub fee_payer: Option>, } -impl<'a> CreateSmartTransactionConfig<'a> { - pub fn new(instructions: Vec, signers: Vec<&'a dyn Signer>) -> Self { +impl CreateSmartTransactionConfig { + pub fn new(instructions: Vec, signers: Vec>) -> Self { Self { instructions, signers, @@ -962,17 +968,36 @@ impl<'a> CreateSmartTransactionConfig<'a> { } } } +pub struct Timeout { + pub duration: Duration, +} -pub struct SmartTransactionConfig<'a> { - pub create_config: CreateSmartTransactionConfig<'a>, +impl Default for Timeout { + fn default() -> Self { + Self { + duration: Duration::from_secs(60), + } + } +} + +impl Into for Timeout { + fn into(self) -> Duration { + self.duration + } +} + +pub struct SmartTransactionConfig { + pub create_config: CreateSmartTransactionConfig, pub send_options: RpcSendTransactionConfig, + pub timeout: Timeout, } -impl<'a> SmartTransactionConfig<'a> { - pub fn new(instructions: Vec, signers: Vec<&'a dyn Signer>) -> Self { +impl SmartTransactionConfig { + pub fn new(instructions: Vec, signers: Vec>, timeout: Timeout) -> Self { Self { create_config: CreateSmartTransactionConfig::new(instructions, signers), send_options: RpcSendTransactionConfig::default(), + timeout, } } } @@ -984,3 +1009,32 @@ pub struct BasicRequest { pub method: String, pub params: Vec>, } + +#[derive(Clone)] +pub struct CreateSmartTransactionSeedConfig { + pub instructions: Vec, + pub signer_seeds: Vec<[u8; 32]>, + pub fee_payer_seed: Option<[u8; 32]>, + pub lookup_tables: Option>, +} + +impl CreateSmartTransactionSeedConfig { + pub fn new(instructions: Vec, signer_seeds: Vec<[u8; 32]>) -> Self { + Self { + instructions, + signer_seeds, + fee_payer_seed: None, + lookup_tables: None, + } + } + + pub fn with_fee_payer_seed(mut self, seed: [u8; 32]) -> Self { + self.fee_payer_seed = Some(seed); + self + } + + pub fn with_lookup_tables(mut self, lookup_tables: Vec) -> Self { + self.lookup_tables = Some(lookup_tables); + self + } +} 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/src/websocket.rs b/src/websocket.rs index cd6212d3..fc6c43c6 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -28,9 +28,8 @@ use tokio_tungstenite::{ }, MaybeTlsStream, WebSocketStream, }; -use url::Url; -pub const ENHANCED_WEBSOCKET_URL: &str = "wss://atlas-mainnet.helius-rpc.com?api-key="; +pub const ENHANCED_WEBSOCKET_URL: &str = "wss://atlas-mainnet.helius-rpc.com/?api-key="; const DEFAULT_PING_DURATION_SECONDS: u64 = 10; // pub type Result = Result; @@ -54,7 +53,6 @@ pub struct EnhancedWebsocket { impl EnhancedWebsocket { /// Expects enhanced websocket endpoint: wss://atlas-mainnet.helius-rpc.com?api-key= pub async fn new(url: &str) -> Result { - let url = Url::parse(url)?; let (ws, _response) = connect_async(url).await.map_err(HeliusError::Tungstenite)?; let (subscribe_sender, subscribe_receiver) = mpsc::unbounded_channel(); @@ -218,7 +216,6 @@ impl EnhancedWebsocket { request_id += 1; let method = format!("{operation}Subscribe"); let body = json!({"jsonrpc":"2.0","id":request_id,"method":method,"params":params}); - println!("subscription: {:#}", body); ws.send(Message::Text(body.to_string())).await?; requests_subscribe.insert(request_id, (operation, response_sender)); }, diff --git a/tests/rpc/test_get_priority_fee_estimate.rs b/tests/rpc/test_get_priority_fee_estimate.rs index 5e6e3910..c34d6d57 100644 --- a/tests/rpc/test_get_priority_fee_estimate.rs +++ b/tests/rpc/test_get_priority_fee_estimate.rs @@ -19,7 +19,7 @@ async fn test_get_nft_editions_success() { result: GetPriorityFeeEstimateResponse { priority_fee_estimate: Some(100.0), priority_fee_levels: Some(MicroLamportPriorityFeeLevels { - none: 0.0, + min: 0.0, low: 10.0, medium: 100.0, high: 500.0, diff --git a/tests/test_client_staked.rs b/tests/test_client_staked.rs new file mode 100644 index 00000000..f5297990 --- /dev/null +++ b/tests/test_client_staked.rs @@ -0,0 +1,28 @@ +use helius::client::Helius; +use helius::error::Result; +use helius::types::Cluster; + +#[test] +fn test_creating_new_client_staked_success() { + let api_key: &str = "valid-api-key"; + let cluster: Cluster = Cluster::StakedMainnetBeta; + + let result: Result = Helius::new(api_key, cluster); + assert!(result.is_ok()); + + let helius: Helius = result.unwrap(); + assert_eq!(helius.config.api_key, api_key); +} + +#[test] +fn test_creating_new_async_client_staked_success() { + let api_key: &str = "valid-api-key"; + let cluster: Cluster = Cluster::StakedMainnetBeta; + + let result: Result = Helius::new_with_async_solana(api_key, cluster); + assert!(result.is_ok()); + + let helius: Helius = result.unwrap(); + assert_eq!(helius.config.api_key, api_key); + assert!(helius.async_rpc_client.is_some()); +} diff --git a/tests/test_factory.rs b/tests/test_factory.rs index 92c6787d..55826eb4 100644 --- a/tests/test_factory.rs +++ b/tests/test_factory.rs @@ -21,6 +21,16 @@ fn test_factory_create_mainnet_instance() { assert_eq!(helius.config.endpoints.rpc, "https://mainnet.helius-rpc.com/"); } +#[test] +fn test_factory_create_staked_mainnet_instance() { + let factory: HeliusFactory = HeliusFactory::new("valid_api_key"); + let helius: Helius = factory.create(Cluster::StakedMainnetBeta).unwrap(); + + assert_eq!(helius.config.api_key, "valid_api_key"); + assert_eq!(helius.config.endpoints.api, "https://api-mainnet.helius-rpc.com/"); + assert_eq!(helius.config.endpoints.rpc, "https://staked.helius-rpc.com/"); +} + #[test] fn test_factory_create_with_reqwest() { let mut factory = HeliusFactory::new("valid_api_key"); 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); + } +}