Skip to content
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 .changeset/weak-peaches-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@orca-so/rust-tx-sender": minor
---

Provide the ability to skip simulation for compute units, set correct number of default signatures when creating a Versioned Transaction
72 changes: 72 additions & 0 deletions rust-sdk/tx-sender/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,78 @@ orca_tx_sender = { version = "0.1.0" }

## Usage Examples

## Per-Function Configuration Examples

Below is an example of how to use the per-function configuration for `build_and_send_transaction_with_config`. However, you can also use the per-function configuration for the build and send steps separately using the following functions: `build_transaction_with_config` and `send_transaction_with_config`.

### Basic Example

```rust
use orca_tx_sender::{
build_and_send_transaction_with_config,
PriorityFeeStrategy, Percentile,
set_priority_fee_strategy
};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::system_instruction;
use solana_sdk::commitment_config::CommitmentLevel;
use std::error::Error;
use std::str::FromStr;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com");

// Use the cluster URL to create a new instance of RpcConfig. You can also create the struct manually.
let rpc_config = RpcConfig::new(rpc_client.url());

// Configure the fee config with priority fees
let fee_config = FeeConfig {
priority_fee_strategy: PriorityFeeStrategy::Dynamic {
percentile: Percentile::P95,
max_lamports: 10_000,
},
..Default::default()
};

// Create a keypair for signing
let payer = Keypair::new();
println!("Using keypair: {}", payer.pubkey());

// Check balance
let balance = client.get_balance(&payer.pubkey()).await?;
println!("Account balance: {} lamports", balance);

// Jupiter Program address as an example recipient
let recipient = Pubkey::from_str("JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo").unwrap();

// Create transfer instruction
let transfer_ix = system_instruction::transfer(
&payer.pubkey(),
&recipient,
1_000_000, // 0.001 SOL
);

// Build and send transaction
println!("Sending transaction...");
let signature = build_and_send_transaction_with_config(
vec![transfer_ix],
&[&payer],
Some(CommitmentLevel::Confirmed),
None, // No address lookup tables
&rpc_client,
&rpc_config,
&fee_config,
).await?;

println!("Transaction sent: {}", signature);
Ok(())
}
```

## Global Configuration Examples

### Basic Example

```rust
Expand Down
60 changes: 19 additions & 41 deletions rust-sdk/tx-sender/src/compute_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::message::{v0::Message, VersionedMessage};
use solana_sdk::transaction::VersionedTransaction;

const SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR: u8 = 0x02;
/// Compute unit limit strategy to apply when building a transaction.
/// - Dynamic: Estimate compute units by simulating the transaction.
/// If the simulation fails, the transaction will not build.
/// - Exact: Directly use the provided compute unit limit.
#[derive(Debug, Default)]
pub enum ComputeUnitLimitStrategy {
#[default]
Dynamic,
Exact(u32),
}

#[derive(Debug, Default)]
pub struct ComputeConfig {
pub unit_limit: ComputeUnitLimitStrategy,
}

/// Estimate compute units by simulating a transaction
pub async fn estimate_compute_units(
Expand All @@ -25,10 +39,10 @@ pub async fn estimate_compute_units(
.await
.map_err(|e| format!("Failed to get recent blockhash: {}", e))?;

let mut simulation_instructions = instructions.to_vec();
if extract_compute_unit_limit(instructions).is_none() {
simulation_instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(1_400_000));
}
// Add max compute unit limit instruction so that the simulation does not fail
let mut simulation_instructions =
vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)];
simulation_instructions.extend_from_slice(instructions);

let message = Message::try_compile(payer, &simulation_instructions, &alt_accounts, blockhash)
.map_err(|e| format!("Failed to compile message: {}", e))?;
Expand Down Expand Up @@ -57,7 +71,6 @@ pub async fn estimate_compute_units(
if let Some(err) = simulation_result.value.err {
return Err(format!("Transaction simulation failed: {}", err));
}

match simulation_result.value.units_consumed {
Some(units) => Ok(units as u32),
None => Err("Transaction simulation didn't return consumed units".to_string()),
Expand Down Expand Up @@ -209,38 +222,3 @@ pub fn get_writable_accounts(instructions: &[Instruction]) -> Vec<Pubkey> {

writable.into_iter().collect()
}

/// Extract the compute unit limit from a list of instructions
fn extract_compute_unit_limit(instructions: &[Instruction]) -> Option<u32> {
for ix in instructions {
if ix.program_id == solana_sdk::compute_budget::ID {
if ix.data.first() == Some(&SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR) {
let limit_bytes_array: [u8; 4] = ix.data.get(1..5)?.try_into().ok()?;
return Some(u32::from_le_bytes(limit_bytes_array));
} else {
return None;
}
}
}
None
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_extract_compute_unit_limit() {
let instructions = vec![];
let compute_unit_limit = extract_compute_unit_limit(&instructions);
assert_eq!(compute_unit_limit, None);
}

#[tokio::test]
async fn test_extract_compute_unit_limit_with_limit() {
let instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)];

let compute_unit_limit = extract_compute_unit_limit(&instructions);
assert_eq!(compute_unit_limit, Some(1_400_000));
}
}
70 changes: 59 additions & 11 deletions rust-sdk/tx-sender/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,26 @@ pub async fn build_and_send_transaction<S: Signer>(
.await
}

#[derive(Debug, Default)]
pub struct BuildTransactionConfig {
pub rpc_config: RpcConfig,
pub fee_config: FeeConfig,
pub compute_config: ComputeConfig,
}

/// Build a transaction with compute budget and priority fees from the supplied configuration
///
/// This function handles:
/// 1. Building a transaction message with all instructions
/// 2. Adding compute budget instructions
/// 3. Adding any Jito tip instructions
/// 4. Supporting address lookup tables for account compression
pub async fn build_transaction_with_config(
pub async fn build_transaction_with_config_obj(
mut instructions: Vec<Instruction>,
payer: &Pubkey,
address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
rpc_client: &RpcClient,
rpc_config: &RpcConfig,
fee_config: &FeeConfig,
config: &BuildTransactionConfig,
) -> Result<VersionedTransaction, String> {
let recent_blockhash = rpc_client
.get_latest_blockhash()
Expand All @@ -113,13 +119,21 @@ pub async fn build_transaction_with_config(

let address_lookup_tables_clone = address_lookup_tables.clone();

let compute_units = compute_budget::estimate_compute_units(
rpc_client,
&instructions,
payer,
address_lookup_tables_clone,
)
.await?;
let compute_units = match config.compute_config.unit_limit {
ComputeUnitLimitStrategy::Dynamic => {
compute_budget::estimate_compute_units(
rpc_client,
&instructions,
payer,
address_lookup_tables_clone,
)
.await?
}
ComputeUnitLimitStrategy::Exact(units) => units,
};

let rpc_config = &config.rpc_config;
let fee_config = &config.fee_config;
let budget_instructions = compute_budget::get_compute_budget_instruction(
rpc_client,
compute_units,
Expand Down Expand Up @@ -153,12 +167,46 @@ pub async fn build_transaction_with_config(
Message::try_compile(payer, &instructions, &[], recent_blockhash)
.map_err(|e| format!("Failed to compile message: {}", e))?
};

// Provide the correct number of signatures for the transaction, otherwise (de)serialization can fail
Ok(VersionedTransaction {
signatures: vec![],
signatures: vec![
solana_sdk::signature::Signature::default();
message.header.num_required_signatures.into()
],
message: VersionedMessage::V0(message),
})
}

/// Build a transaction with compute budget and priority fees from the supplied configuration
///
/// This function handles:
/// 1. Building a transaction message with all instructions
/// 2. Adding compute budget instructions
/// 3. Adding any Jito tip instructions
/// 4. Supporting address lookup tables for account compression
pub async fn build_transaction_with_config(
instructions: Vec<Instruction>,
payer: &Pubkey,
address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
rpc_client: &RpcClient,
rpc_config: &RpcConfig,
fee_config: &FeeConfig,
) -> Result<VersionedTransaction, String> {
build_transaction_with_config_obj(
instructions,
payer,
address_lookup_tables,
rpc_client,
&BuildTransactionConfig {
rpc_config: (*rpc_config).clone(),
fee_config: (*fee_config).clone(),
..Default::default()
},
)
.await
}

/// Build a transaction with compute budget and priority fees from the global configuration
///
/// This function handles:
Expand Down
2 changes: 1 addition & 1 deletion rust-sdk/tx-sender/src/rpc_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl From<Hash> for ChainId {
}

/// RPC configuration for connecting to Solana nodes
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct RpcConfig {
pub url: String,
pub supports_priority_fee_percentile: bool,
Expand Down