use anyhow::anyhow;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer,
transaction::VersionedTransaction,
};
use solana_system_interface::instruction as system_instruction;
use std::sync::Arc;
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
use crate::{
common::nonce_cache::DurableNonceInfo,
trading::{
core::transaction_pool::{acquire_builder, release_builder},
MiddlewareManager,
},
};
const PACKET_DATA_SIZE: usize = 1232;
#[inline(always)]
fn sol_f64_to_lamports(sol: f64) -> u64 {
if sol <= 0.0 {
return 0;
}
let lamports = sol * 1_000_000_000.0;
(lamports.min(u64::MAX as f64)).round() as u64
}
pub fn build_transaction(
payer: &Arc<Keypair>,
unit_limit: u32,
unit_price: u64,
business_instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
is_buy: bool,
with_tip: bool,
tip_account: &Pubkey,
tip_amount: f64,
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<VersionedTransaction, anyhow::Error> {
let transaction = build_transaction_inner(
payer,
unit_limit,
unit_price,
business_instructions,
address_lookup_table_account,
recent_blockhash,
middleware_manager,
protocol_name,
is_buy,
with_tip,
tip_account,
tip_amount,
durable_nonce,
)?;
let serialized_len = bincode::serialized_size(&transaction)? as usize;
if crate::common::sdk_log::sdk_log_enabled() {
println!(
" [SDK][tx-size ] {} {} serialized={} bytes, business_ix={}, nonce={}, tip={}, cu_limit={}, cu_price={}, alt={}",
protocol_name,
if is_buy { "buy" } else { "sell" },
serialized_len,
business_instructions.len(),
durable_nonce.is_some(),
with_tip && tip_amount > 0.0,
unit_limit,
unit_price,
address_lookup_table_account.is_some()
);
}
if serialized_len <= PACKET_DATA_SIZE {
return Ok(transaction);
}
Err(anyhow!(
"transaction too large: {} > {}; SDK did not remove compute budget or relay tip because that changes transaction priority semantics. Use an address lookup table or pre-create token ATAs before submitting",
serialized_len,
PACKET_DATA_SIZE
))
}
fn build_transaction_inner(
payer: &Arc<Keypair>,
unit_limit: u32,
unit_price: u64,
business_instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
is_buy: bool,
with_tip: bool,
tip_account: &Pubkey,
tip_amount: f64,
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<VersionedTransaction, anyhow::Error> {
let mut instructions = Vec::with_capacity(business_instructions.len() + 5);
if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref(), durable_nonce) {
return Err(e);
}
if with_tip && tip_amount > 0.0 {
let tip_lamports = sol_f64_to_lamports(tip_amount);
instructions.push(system_instruction::transfer(&payer.pubkey(), tip_account, tip_lamports));
}
super::compute_budget_manager::extend_compute_budget_instructions(
&mut instructions,
unit_price,
unit_limit,
);
instructions.extend_from_slice(business_instructions);
let blockhash = get_transaction_blockhash(recent_blockhash, durable_nonce)?;
build_versioned_transaction(
payer,
instructions,
address_lookup_table_account,
blockhash,
middleware_manager,
protocol_name,
is_buy,
)
}
fn build_versioned_transaction(
payer: &Arc<Keypair>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<&AddressLookupTableAccount>,
blockhash: Hash,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
is_buy: bool,
) -> Result<VersionedTransaction, anyhow::Error> {
let full_instructions = match middleware_manager {
Some(middleware_manager) => middleware_manager
.apply_middlewares_process_full_instructions(instructions, protocol_name, is_buy)?,
None => instructions,
};
let mut builder = acquire_builder();
let build_result = builder.build_zero_alloc(
&payer.pubkey(),
&full_instructions,
address_lookup_table_account,
blockhash,
);
release_builder(builder);
let versioned_msg = build_result?;
let msg_bytes = versioned_msg.serialize();
let signature =
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
Ok(tx)
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::instruction::AccountMeta;
fn oversized_instruction(account_count: usize, data_len: usize) -> Instruction {
let accounts =
(0..account_count).map(|_| AccountMeta::new(Pubkey::new_unique(), false)).collect();
Instruction { program_id: Pubkey::new_unique(), accounts, data: vec![7; data_len] }
}
#[test]
fn oversized_transaction_returns_error_without_dropping_priority_semantics() {
let payer = Arc::new(Keypair::new());
let business_instructions = vec![oversized_instruction(36, 700)];
let err = build_transaction(
&payer,
80_000,
100_000,
&business_instructions,
None,
Some(Hash::new_unique()),
None,
"test",
true,
true,
&Pubkey::new_unique(),
0.001,
None,
)
.unwrap_err()
.to_string();
assert!(err.contains("transaction too large"), "{err}");
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
}
}