Skip to content

Commit

Permalink
Merge pull request #36 from helius-labs/dev
Browse files Browse the repository at this point in the history
[Merge] Dev -> Main
  • Loading branch information
0xIchigo authored May 29, 2024
2 parents b8fac69 + 54059e8 commit 8c3dd7b
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 146 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "helius"
version = "0.1.4"
version = "0.1.5"
edition = "2021"
description = "An asynchronous Helius Rust SDK for building the future of Solana"
keywords = ["helius", "solana", "asynchronous-sdk", "das", "cryptocurrency"]
Expand Down
216 changes: 94 additions & 122 deletions src/optimized_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ use crate::types::{
};
use crate::Helius;

use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use bincode::{serialize, ErrorKind};
use reqwest::StatusCode;
use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig};
use solana_client::rpc_response::{Response, RpcSimulateTransactionResult};
use solana_sdk::{
address_lookup_table::AddressLookupTableAccount,
bs58::encode,
commitment_config::CommitmentConfig,
compute_budget::ComputeBudgetInstruction,
hash::Hash,
Expand Down Expand Up @@ -121,23 +120,58 @@ impl Helius {
/// The transaction signature, if successful
pub async fn send_smart_transaction(&self, config: SmartTransactionConfig<'_>) -> Result<Signature> {
let pubkey: Pubkey = config.from_keypair.pubkey();
let mut recent_blockhash: Hash = self.connection().get_latest_blockhash()?;
let recent_blockhash: Hash = self.connection().get_latest_blockhash()?;
let mut final_instructions: Vec<Instruction> = vec![];

// Check if any of the instructions provided set the compute unit price and/or limit, and throw an error if `true`
let existing_compute_budget_instructions: bool = config.instructions.iter().any(|instruction| {
instruction.program_id == ComputeBudgetInstruction::set_compute_unit_limit(0).program_id
|| instruction.program_id == ComputeBudgetInstruction::set_compute_unit_price(0).program_id
});

if existing_compute_budget_instructions {
return Err(HeliusError::InvalidInput(
"Cannot provide instructions that set the compute unit price and/or limit".to_string(),
));
}

// Get the optimal compute units
let units: Option<u64> = self
.get_compute_units(
config.instructions.clone(),
pubkey,
config.lookup_tables.clone().unwrap_or_default(),
&config.from_keypair,
)
.await?;

if units.is_none() {
return Err(HeliusError::InvalidInput(
"Error fetching compute units for the instructions provided".to_string(),
));
}

let compute_units: u64 = units.unwrap();
let customers_cu: u32 = if compute_units < 1000 {
1000
} else {
(compute_units as f64 * 1.5).ceil() as u32
};

// Add the compute unit limit instruction with a margin
let compute_units_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_limit(customers_cu);
final_instructions.push(compute_units_ix);

// Determine if we need to use a versioned transaction
let is_versioned: bool = config.lookup_tables.is_some();
let is_versioned: bool = false; //config.lookup_tables.is_some();
let mut legacy_transaction: Option<Transaction> = None;
let mut versioned_transaction: Option<VersionedTransaction> = None;

// Build the initial transaction based on whether lookup tables are present
if is_versioned {
// If lookup tables are present, we build a versioned transaction
let lookup_tables: &[AddressLookupTableAccount] = config.lookup_tables.as_deref().unwrap_or_default();
let mut instructions: Vec<Instruction> = vec![ComputeBudgetInstruction::set_compute_unit_price(1)];
instructions.extend(config.instructions.clone());

let v0_message: v0::Message =
v0::Message::try_compile(&pubkey, &instructions, lookup_tables, recent_blockhash)?;
v0::Message::try_compile(&pubkey, &config.instructions, lookup_tables, recent_blockhash)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);

// Sign the versioned transaction
Expand All @@ -158,83 +192,52 @@ impl Helius {
legacy_transaction = Some(tx);
}

let priority_fee: u64 = if let Some(tx) = &legacy_transaction {
// Serialize the transaction
let serialized_tx: Vec<u8> =
serialize(&tx).map_err(|e: Box<ErrorKind>| HeliusError::InvalidInput(e.to_string()))?;
let transaction_base64: String = STANDARD.encode(&serialized_tx);

// Get the priority fee estimate based on the serialized transaction
let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base64),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
recommended: Some(true),
..Default::default()
}),
};

let priority_fee_estimate: GetPriorityFeeEstimateResponse =
self.rpc().get_priority_fee_estimate(priority_fee_request).await?;

let priority_fee_f64: f64 =
priority_fee_estimate
.priority_fee_estimate
.ok_or(HeliusError::InvalidInput(
"Priority fee estimate not available".to_string(),
))?;

priority_fee_f64 as u64
// Serialize the transaction
let serialized_tx: Vec<u8> = if let Some(tx) = &legacy_transaction {
serialize(&tx).map_err(|e: Box<ErrorKind>| HeliusError::InvalidInput(e.to_string()))?
} else if let Some(tx) = &versioned_transaction {
// Serialize the transaction
let serialized_tx: Vec<u8> =
serialize(&tx).map_err(|e: Box<ErrorKind>| HeliusError::InvalidInput(e.to_string()))?;
let transaction_base64: String = STANDARD.encode(&serialized_tx);

// Get the priority fee estimate based on the serialized transaction
let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base64),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
recommended: Some(true),
..Default::default()
}),
};

let priority_fee_estimate: GetPriorityFeeEstimateResponse =
self.rpc().get_priority_fee_estimate(priority_fee_request).await?;

let priority_fee_f64: f64 =
priority_fee_estimate
.priority_fee_estimate
.ok_or(HeliusError::InvalidInput(
"Priority fee estimate not available".to_string(),
))?;
priority_fee_f64 as u64
serialize(&tx).map_err(|e: Box<ErrorKind>| HeliusError::InvalidInput(e.to_string()))?
} else {
return Err(HeliusError::InvalidInput("No transaction available".to_string()));
};

// Encode the transaction
let transaction_base58: String = encode(&serialized_tx).into_string();

// Get the priority fee estimate based on the serialized transaction
let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base58),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
recommended: Some(true),
..Default::default()
}),
};

let priority_fee_estimate: GetPriorityFeeEstimateResponse =
self.rpc().get_priority_fee_estimate(priority_fee_request).await?;

let priority_fee_recommendation: u64 =
priority_fee_estimate
.priority_fee_estimate
.ok_or(HeliusError::InvalidInput(
"Priority fee estimate not available".to_string(),
))? as u64;

let lamports_to_micro_lamports: u64 = 10_u64.pow(6);
let minimum_total_pfee_lamports: u64 = 10_000;
let microlamports_per_cu: u64 = std::cmp::max(
priority_fee_recommendation,
((minimum_total_pfee_lamports as f64 / customers_cu as f64) * lamports_to_micro_lamports as f64).round()
as u64,
);

// Add the compute unit price instruction with the estimated fee
let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(priority_fee);
let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(microlamports_per_cu);
final_instructions.push(compute_budget_ix);
final_instructions.extend(config.instructions.clone());

// Get the optimal compute units
if let Some(units) = self
.get_compute_units(
final_instructions.clone(),
pubkey,
config.lookup_tables.clone().unwrap_or_default(),
&config.from_keypair,
)
.await?
{
// Add some margin to the compute units to ensure the transaction does not fail
let compute_units_ix: Instruction =
ComputeBudgetInstruction::set_compute_unit_limit((units as f64 * 1.1).ceil() as u32);
final_instructions.insert(0, compute_units_ix);
}
// Add the original instructions back
final_instructions.extend(config.instructions.clone());

// Rebuild the transaction with the final instructions
if is_versioned {
Expand All @@ -253,21 +256,20 @@ impl Helius {
message: versioned_message,
});
} else {
let mut tx = Transaction::new_with_payer(&final_instructions, Some(&pubkey));
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&pubkey));
tx.try_sign(&[config.from_keypair], recent_blockhash)?;
legacy_transaction = Some(tx);
}

// Re-fetch interval of 60 seconds
let blockhash_refetch_interval: Duration = Duration::from_secs(60);
let mut last_blockhash_refetch: Instant = Instant::now();

// Common logic for sending transactions
let send_transaction_config: RpcSendTransactionConfig = RpcSendTransactionConfig {
skip_preflight: config.skip_preflight_checks.unwrap_or(true),
..Default::default()
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,
};

// Common logic for sending transactions
let send_result = |transaction: &Transaction| {
self.connection()
.send_transaction_with_config(transaction, send_transaction_config)
Expand All @@ -277,35 +279,11 @@ impl Helius {
.send_transaction_with_config(transaction, send_transaction_config)
};

// Send the transaction with retries and preflight checks
let mut retry_count: usize = 0;
let max_retries: usize = config.max_retries.unwrap_or(6);

while retry_count <= max_retries {
if last_blockhash_refetch.elapsed() >= blockhash_refetch_interval {
recent_blockhash = self.connection().get_latest_blockhash()?;
if is_versioned {
let signers: Vec<&dyn Signer> = vec![config.from_keypair];
let signed_message: Vec<Signature> = signers
.iter()
.map(|signer| {
signer.try_sign_message(
versioned_transaction.as_ref().unwrap().message.serialize().as_slice(),
)
})
.collect::<std::result::Result<Vec<_>, _>>()?;
versioned_transaction.as_mut().unwrap().signatures = signed_message;
} else {
legacy_transaction
.as_mut()
.unwrap()
.try_sign(&[config.from_keypair], recent_blockhash)?;
}

// Update the last refetch time
last_blockhash_refetch = Instant::now();
}
// Retry logic with a timeout of 60 seconds
let timeout: Duration = Duration::from_secs(60);
let start_time: Instant = Instant::now();

while Instant::now().duration_since(start_time) < timeout {
let result = if is_versioned {
send_versioned_result(versioned_transaction.as_ref().unwrap())
} else {
Expand All @@ -314,19 +292,13 @@ impl Helius {

match result {
Ok(signature) => return self.poll_transaction_confirmation(signature).await,
Err(error) => {
retry_count += 1;
if retry_count > max_retries {
return Err(HeliusError::ClientError(error));
}
continue;
}
Err(_) => continue,
}
}

Err(HeliusError::Timeout {
code: StatusCode::REQUEST_TIMEOUT,
text: "Reached an unexpected point in send_smart_transaction".to_string(),
text: "Transaction failed to confirm in 60s".to_string(),
})
}
}
2 changes: 2 additions & 0 deletions src/types/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ pub struct SearchAssetsOptions {
pub show_zero_balance: bool,
#[serde(default)]
pub show_closed_accounts: bool,
#[serde(default)]
pub show_native_balance: bool,
}
11 changes: 5 additions & 6 deletions src/types/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::types::{DisplayOptions, GetAssetOptions};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_sdk::{address_lookup_table::AddressLookupTableAccount, instruction::Instruction, signature::Keypair};

/// Defines the available clusters supported by Helius
Expand Down Expand Up @@ -572,8 +573,8 @@ pub struct FileQuality {
pub struct Metadata {
pub attributes: Option<Vec<Attribute>>,
pub description: Option<String>,
pub name: String,
pub symbol: String,
pub name: Option<String>,
pub symbol: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -946,8 +947,7 @@ pub struct EditWebhookRequest {
pub struct SmartTransactionConfig<'a> {
pub instructions: Vec<Instruction>,
pub from_keypair: &'a Keypair,
pub skip_preflight_checks: Option<bool>,
pub max_retries: Option<usize>,
pub send_options: RpcSendTransactionConfig,
pub lookup_tables: Option<Vec<AddressLookupTableAccount>>,
}

Expand All @@ -956,8 +956,7 @@ impl<'a> SmartTransactionConfig<'a> {
Self {
instructions,
from_keypair,
skip_preflight_checks: None,
max_retries: None,
send_options: RpcSendTransactionConfig::default(),
lookup_tables: None,
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/rpc/test_get_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ async fn test_get_asset_success() {
description: Some(
"Apt323 the 36 page Collectors Edition.".to_string(),
),
name: "Apt323 Collectors Edition #72".to_string(),
symbol: "".to_string(),
name: Some("Apt323 Collectors Edition #72".to_string()),
symbol: Some("".to_string()),
},
links: Some(
Links {
Expand Down
8 changes: 4 additions & 4 deletions tests/rpc/test_get_asset_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ async fn test_get_asset_batch_success() {
}
]),
description: Some("A hotspot NFT on Helium".to_string()),
name: "gentle-mandarin-ferret".to_string(),
symbol: "HOTSPOT".to_string(),
name: Some("gentle-mandarin-ferret".to_string()),
symbol: Some("HOTSPOT".to_string()),
},
links: Some(Links {
external_url: None,
Expand Down Expand Up @@ -156,8 +156,8 @@ async fn test_get_asset_batch_success() {
}
]),
description: Some("Aerial photograph of a parking structure in LA depicting the photographer, Andrew Mason, \"sliding\" through the image.".to_string()),
name: "Slide".to_string(),
symbol: "".to_string(),
name: Some("Slide".to_string()),
symbol: Some("".to_string()),
},
links: Some(Links {
external_url: Some("".to_string()),
Expand Down
4 changes: 2 additions & 2 deletions tests/rpc/test_get_assets_by_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ async fn test_get_assets_by_authority_success() {
description: Some(
"Fock it.".to_string(),
),
name: "Mad Lads #6867".to_string(),
symbol: "MAD".to_string(),
name: Some("Mad Lads #6867".to_string()),
symbol: Some("MAD".to_string()),
},
links: Some(
Links {
Expand Down
Loading

0 comments on commit 8c3dd7b

Please sign in to comment.