Skip to content

Commit

Permalink
Merge pull request #103 from helius-labs/fix/send_smrt_tx_with_seeds
Browse files Browse the repository at this point in the history
fix(optimized_transaction): Refactor `send_smart_transaction_with_seeds`
  • Loading branch information
0xIchigo authored Dec 22, 2024
2 parents a6a16c5 + 9c872c3 commit bcd8a56
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 25 deletions.
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

0 comments on commit bcd8a56

Please sign in to comment.