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

fix(optimized_transaction): Refactor send_smart_transaction_with_seeds #103

Merged
merged 5 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/get_parsed_transaction_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ async fn main() -> Result<()> {
let request: ParsedTransactionHistoryRequest = ParsedTransactionHistoryRequest {
address: "2k5AXX4guW9XwRQ1AKCpAuUqgWDpQpwFfpVFh3hnm2Ha".to_string(),
before: None,
until: None,
transaction_type: None,
commitment: None,
limit: None,
source: None,
};

let response: Result<Vec<EnhancedTransaction>> = helius.parsed_transaction_history(request).await;
Expand Down
103 changes: 103 additions & 0 deletions examples/send_smart_transaction_with_seeds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use helius::types::*;
use helius::Helius;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_sdk::{bs58, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, system_instruction};
use std::{str::FromStr, time::Duration};
use tokio::time::sleep;

#[tokio::main]
async fn main() {
tokio::spawn(async {
let api_key: &str = "your_api_key";
let cluster: Cluster = Cluster::MainnetBeta;
let helius: Helius = Helius::new(api_key, cluster).unwrap();

// Convert your base58 private key to a seed
let keypair_base58: &str = "your_keypair_as_base58";
let keypair_bytes: Vec<u8> = bs58::decode(keypair_base58).into_vec().unwrap();

// Create the recipient address
let to_pubkey: Pubkey = Pubkey::from_str("recipient_address").unwrap();

// Get the sender's public key for balance checking
let from_pubkey: Pubkey = Keypair::from_bytes(&keypair_bytes).unwrap().pubkey();

println!("From wallet address: {}", from_pubkey);
println!("To wallet address: {}", to_pubkey);

// Get initial balances
let balance_from: u64 = helius.connection().get_balance(&from_pubkey).unwrap_or(0);
let balance_to: u64 = helius.connection().get_balance(&to_pubkey).unwrap_or(0);

println!(
"From wallet balance: {} SOL",
balance_from as f64 / LAMPORTS_PER_SOL as f64
);
println!("To wallet balance: {} SOL", balance_to as f64 / LAMPORTS_PER_SOL as f64);

// Create the transfer instruction
let transfer_amount: u64 = (0.01 * LAMPORTS_PER_SOL as f64) as u64;
let instruction: solana_sdk::instruction::Instruction =
system_instruction::transfer(&from_pubkey, &to_pubkey, transfer_amount);

// Convert keypair bytes to a 32-byte seed array
let mut seed: [u8; 32] = [0u8; 32];
seed.copy_from_slice(&keypair_bytes[..32]);

// For testing purposes. In a production setting, you'd actually create or pass in an existing ATL
let address_lut: Vec<solana_sdk::address_lookup_table::AddressLookupTableAccount> = vec![];

// Configure the smart transaction
let config: CreateSmartTransactionSeedConfig = CreateSmartTransactionSeedConfig {
instructions: vec![instruction],
signer_seeds: vec![seed],
fee_payer_seed: None,
lookup_tables: Some(address_lut),
priority_fee_cap: Some(100000),
};

// Configure send options (optional)
let send_options: Option<RpcSendTransactionConfig> = Some(RpcSendTransactionConfig {
skip_preflight: true,
preflight_commitment: None,
encoding: None,
max_retries: None,
min_context_slot: None,
});

// Set a timeout (optional)
let timeout: Option<Timeout> = Some(Timeout {
duration: Duration::from_secs(60),
});

// Send the transaction
match helius
.send_smart_transaction_with_seeds(config, send_options, timeout)
.await
{
Ok(signature) => {
println!("Transaction sent successfully: {}", signature);
sleep(Duration::from_secs(5)).await;

// Get final balances
let balance_from: u64 = helius.connection().get_balance(&from_pubkey).unwrap_or(0);
println!(
"Final From Wallet Balance: {} SOL",
balance_from as f64 / LAMPORTS_PER_SOL as f64
);
let balance_to: u64 = helius.connection().get_balance(&to_pubkey).unwrap_or(0);
println!(
"Final To Wallet Balance: {} SOL",
balance_to as f64 / LAMPORTS_PER_SOL as f64
);
}
Err(e) => {
eprintln!("Failed to send transaction: {:?}", e);
}
}
})
.await
.unwrap();
}
216 changes: 191 additions & 25 deletions src/optimized_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use solana_sdk::{
message::{v0, VersionedMessage},
pubkey::Pubkey,
signature::{Signature, Signer},
signer::keypair::Keypair,
transaction::{Transaction, VersionedTransaction},
};
use solana_transaction_status::TransactionConfirmationStatus;
Expand All @@ -37,7 +38,7 @@ impl Helius {
/// * `instructions` - The transaction instructions
/// * `payer` - The public key of the payer
/// * `lookup_tables` - The address lookup tables
/// * `from_keypair` - The keypair signing the transaction (needed to simulate the transaction)
/// * `signers` - The signers for the transaction
///
/// # Returns
/// The compute units consumed, or None if unsuccessful
Expand Down Expand Up @@ -417,6 +418,63 @@ impl Helius {
})
}

/// Thread safe version of get_compute_units to simulate a transaction to get the total compute units consumed
///
/// # Arguments
/// * `instructions` - The transaction instructions
/// * `payer` - The public key of the payer
/// * `lookup_tables` - The address lookup tables
/// * `keypairs` - The keypairs for the transaction
///
/// # Returns
/// The compute units consumed, or None if unsuccessful
pub async fn get_compute_units_thread_safe(
&self,
instructions: Vec<Instruction>,
payer: Pubkey,
lookup_tables: Vec<AddressLookupTableAccount>,
keypairs: Option<&[&Keypair]>,
) -> Result<Option<u64>> {
let test_instructions: Vec<Instruction> = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)]
.into_iter()
.chain(instructions)
.collect::<Vec<_>>();

let recent_blockhash: Hash = self.connection().get_latest_blockhash()?;
let v0_message: v0::Message =
v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_blockhash)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);

let transaction: VersionedTransaction = if let Some(keypairs) = keypairs {
let mut tx = VersionedTransaction {
signatures: vec![Signature::default(); keypairs.len()],
message: versioned_message.clone(),
};

for (i, keypair) in keypairs.iter().enumerate() {
tx.signatures[i] = keypair.sign_message(&versioned_message.serialize());
}

tx
} else {
VersionedTransaction {
signatures: vec![],
message: versioned_message,
}
};

let config: RpcSimulateTransactionConfig = RpcSimulateTransactionConfig {
sig_verify: keypairs.is_some(),
..Default::default()
};

let result: Response<RpcSimulateTransactionResult> = self
.connection()
.simulate_transaction_with_config(&transaction, config)?;

Ok(result.value.units_consumed)
}

/// Sends a smart transaction using seed bytes
///
/// This method allows for sending smart transactions in asynchronous contexts
Expand All @@ -439,7 +497,7 @@ impl Helius {
///
/// # Errors
///
/// This function will return an error if keypair creation from seeds fails, the underlying `send_smart_transaction` call fails,
/// This function will return an error if keypair creation from seeds fails, the transaction sending fails,
/// or no signer seeds are provided
///
/// # Notes
Expand All @@ -457,38 +515,146 @@ impl Helius {
));
}

let mut signers: Vec<Arc<dyn Signer>> = create_config
let keypairs: Vec<Keypair> = create_config
.signer_seeds
.into_iter()
.map(|seed| {
Arc::new(keypair_from_seed(&seed).expect("Failed to create keypair from seed")) as Arc<dyn Signer>
})
.map(|seed| keypair_from_seed(&seed).expect("Failed to create keypair from seed"))
.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)
// Create the fee payer keypair if provided. Otherwise, we default to the first signer
let fee_payer: Keypair = if let Some(fee_payer_seed) = create_config.fee_payer_seed {
keypair_from_seed(&fee_payer_seed).expect("Failed to create keypair from seed")
} else {
0 // Index of the first signer
Keypair::from_bytes(&keypairs[0].to_bytes()).unwrap()
};
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),
priority_fee_cap: create_config.priority_fee_cap,

let (recent_blockhash, last_valid_block_hash) = self
.connection()
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())?;

let mut final_instructions: Vec<Instruction> = vec![];

// Get priority fee estimate
let transaction: Transaction = Transaction::new_signed_with_payer(
&create_config.instructions,
Some(&fee_payer.pubkey()),
&[&fee_payer],
recent_blockhash,
);

let serialized_tx: Vec<u8> = serialize(&transaction).map_err(|e| HeliusError::InvalidInput(e.to_string()))?;
let transaction_base58: String = encode(&serialized_tx).into_string();

let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base58),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
recommended: Some(true),
..Default::default()
}),
};

let smart_transaction_config: SmartTransactionConfig = SmartTransactionConfig {
create_config: create_smart_transaction_config,
send_options: send_options.unwrap_or_default(),
timeout: timeout.unwrap_or_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 priority_fee: u64 = if let Some(provided_fee) = create_config.priority_fee_cap {
std::cmp::min(priority_fee_recommendation, provided_fee)
} else {
priority_fee_recommendation
};

// Add compute budget instructions
final_instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee));

// Get optimal compute units
let mut test_instructions: Vec<Instruction> = final_instructions.clone();
test_instructions.extend(create_config.instructions.clone());

let units: Option<u64> = self
.get_compute_units_thread_safe(
test_instructions,
fee_payer.pubkey(),
create_config.lookup_tables.clone().unwrap_or_default(),
Some(&[&fee_payer]),
)
.await?;

let compute_units: u64 = units.ok_or(HeliusError::InvalidInput(
"Error fetching compute units for the instructions provided".to_string(),
))?;

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

self.send_smart_transaction(smart_transaction_config).await
final_instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(customers_cu));
final_instructions.extend(create_config.instructions);

// Create the final transaction
let transaction: SmartTransaction = if let Some(lookup_tables) = create_config.lookup_tables {
let message: v0::Message = v0::Message::try_compile(
&fee_payer.pubkey(),
&final_instructions,
&lookup_tables,
recent_blockhash,
)?;

let versioned_message: VersionedMessage = VersionedMessage::V0(message);

let fee_payer_copy: Keypair = Keypair::from_bytes(&fee_payer.to_bytes()).unwrap();
let mut all_signers: Vec<Keypair> = vec![fee_payer_copy];
all_signers.extend(keypairs.into_iter().filter(|k| k.pubkey() != fee_payer.pubkey()));

let mut tx: VersionedTransaction = VersionedTransaction {
signatures: vec![Signature::default(); all_signers.len()],
message: versioned_message.clone(),
};

// Sign message with all keypairs
for (i, keypair) in all_signers.iter().enumerate() {
tx.signatures[i] = keypair.sign_message(&versioned_message.serialize());
}

SmartTransaction::Versioned(tx)
} else {
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&fee_payer.pubkey()));

let mut signers: Vec<&Keypair> = vec![&fee_payer];
signers.extend(keypairs.iter().filter(|k| k.pubkey() != fee_payer.pubkey()));

tx.sign(&signers, recent_blockhash);

SmartTransaction::Legacy(tx)
};

// Send and confirm the transaction
match transaction {
SmartTransaction::Legacy(tx) => {
self.send_and_confirm_transaction(
&tx,
send_options.unwrap_or_default(),
last_valid_block_hash,
Some(timeout.unwrap_or_default().into()),
)
.await
}
SmartTransaction::Versioned(tx) => {
self.send_and_confirm_transaction(
&tx,
send_options.unwrap_or_default(),
last_valid_block_hash,
Some(timeout.unwrap_or_default().into()),
)
.await
}
}
}
}
1 change: 1 addition & 0 deletions tests/rpc/test_get_assets_by_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ async fn test_get_assets_by_authority_success() {
},
],
errors: None,
native_balance: None,
},
id: "1".to_string(),
};
Expand Down
1 change: 1 addition & 0 deletions tests/rpc/test_get_assets_by_creator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ async fn test_get_assets_by_creator_success() {
},
],
errors: None,
native_balance: None,
},
id: "1".to_string(),
};
Expand Down
1 change: 1 addition & 0 deletions tests/rpc/test_get_assets_by_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async fn test_get_assets_by_group_success() {
},
],
errors: None,
native_balance: None,
},
id: "1".to_string(),
};
Expand Down
Loading
Loading