jito_bundle/bundler/builder/
utils.rs1use crate::JitoError;
2use crate::analysis::TransactionAnalysis;
3use crate::bundler::builder::types::{BundleBuilder, BundleBuilderInputs};
4use crate::bundler::bundle::types::BuiltBundle;
5use crate::bundler::types::{
6 BundleInstructionSlots, BundleSlotView, TipMode, empty_instruction_slots,
7};
8use crate::constants::MAX_BUNDLE_TRANSACTIONS;
9use crate::tip::TipHelper;
10use solana_compute_budget_interface::ComputeBudgetInstruction;
11use solana_instruction::{AccountMeta, Instruction};
12use solana_pubkey::Pubkey;
13use solana_sdk::address_lookup_table::AddressLookupTableAccount;
14use solana_sdk::message::{VersionedMessage, v0};
15use solana_sdk::signature::Signer;
16use solana_sdk::transaction::VersionedTransaction;
17
18impl BundleSlotView for BundleBuilder<'_> {
19 fn instruction_slots(&self) -> &BundleInstructionSlots {
21 &self.transactions_instructions
22 }
23}
24
25impl<'a> BundleBuilder<'a> {
26 fn new(inputs: BundleBuilderInputs<'a>) -> Self {
29 let BundleBuilderInputs {
30 payer,
31 transactions_instructions,
32 lookup_tables,
33 recent_blockhash,
34 tip_lamports,
35 jitodontfront_pubkey,
36 compute_unit_limit,
37 } = inputs;
38 let tip_account = TipHelper::get_random_tip_account();
39 Self {
40 payer,
41 transactions_instructions,
42 lookup_tables,
43 recent_blockhash,
44 tip_lamports,
45 jitodontfront_pubkey,
46 compute_unit_limit,
47 tip_account,
48 tip_mode: TipMode::InlineLastTx,
49 }
50 }
51
52 pub fn build(inputs: BundleBuilderInputs<'a>) -> Result<BuiltBundle, JitoError> {
68 let mut builder = Self::new(inputs);
69 builder.compact_transactions();
70 let count = builder.populated_count();
71 if count == 0 {
72 return Err(JitoError::InvalidBundleSize { count: 0 });
73 }
74
75 if let Some(jitodontfront_pubkey) = builder.jitodontfront_pubkey {
76 builder.apply_jitodont_front(jitodontfront_pubkey);
77 }
78
79 if count < MAX_BUNDLE_TRANSACTIONS {
80 builder.append_tip_transaction()?;
81 builder.tip_mode = TipMode::SeparateTx;
82 } else {
83 builder.append_tip_instruction();
84 builder.tip_mode = TipMode::InlineLastTx;
85 }
86
87 if matches!(builder.tip_mode, TipMode::InlineLastTx) {
88 Self::validate_tip_not_in_luts(&builder.tip_account, builder.lookup_tables)?;
89 }
90
91 let total = builder.populated_count();
92 let mut transactions = Vec::with_capacity(total);
93 for (compiled_index, ixs) in builder
94 .transactions_instructions
95 .iter()
96 .flatten()
97 .enumerate()
98 {
99 let txn = builder.build_versioned_transaction(compiled_index, total, ixs)?;
100 transactions.push(txn);
101 }
102
103 Ok(BuiltBundle::new(
104 transactions,
105 builder.tip_account,
106 builder.tip_lamports,
107 builder.tip_mode,
108 builder.transactions_instructions,
109 ))
110 }
111
112 fn compact_transactions(&mut self) {
114 let mut new_slots = empty_instruction_slots();
115 let mut idx = 0;
116 for slot in &mut self.transactions_instructions {
117 if let Some(ixs) = slot.take()
118 && idx < new_slots.len()
119 {
120 new_slots[idx] = Some(ixs);
121 idx += 1;
122 }
123 }
124 self.transactions_instructions = new_slots;
125 }
126
127 fn append_tip_transaction(&mut self) -> Result<(), JitoError> {
129 let tip_ix = TipHelper::create_tip_instruction_to(
130 &self.payer.pubkey(),
131 &self.tip_account,
132 self.tip_lamports,
133 );
134 let first_none = self
135 .transactions_instructions
136 .iter()
137 .position(|slot| slot.is_none())
138 .ok_or(JitoError::InvalidBundleSize {
139 count: MAX_BUNDLE_TRANSACTIONS,
140 })?;
141 self.transactions_instructions[first_none] = Some(vec![tip_ix]);
142 Ok(())
143 }
144
145 fn append_tip_instruction(&mut self) {
147 let tip_ix = TipHelper::create_tip_instruction_to(
148 &self.payer.pubkey(),
149 &self.tip_account,
150 self.tip_lamports,
151 );
152 if let Some(last_idx) = self.last_populated_index()
153 && let Some(ixs) = &mut self.transactions_instructions[last_idx]
154 {
155 ixs.push(tip_ix);
156 }
157 }
158
159 fn apply_jitodont_front(&mut self, jitodontfront_pubkey: &Pubkey) {
164 for ixs in self.transactions_instructions.iter_mut().flatten() {
165 for instruction in ixs.iter_mut() {
166 instruction
167 .accounts
168 .retain(|acct| !acct.pubkey.to_string().starts_with("jitodontfront"));
169 }
170 }
171 if let Some(Some(ixs)) = self.transactions_instructions.first_mut()
172 && let Some(instruction) = ixs.first_mut()
173 {
174 instruction
175 .accounts
176 .push(AccountMeta::new_readonly(*jitodontfront_pubkey, false));
177 }
178 }
179
180 fn build_versioned_transaction(
182 &self,
183 index: usize,
184 total: usize,
185 tx_instructions: &[Instruction],
186 ) -> Result<VersionedTransaction, JitoError> {
187 let compute_budget =
188 ComputeBudgetInstruction::set_compute_unit_limit(self.compute_unit_limit);
189 let mut instructions = vec![compute_budget];
190 instructions.extend_from_slice(tx_instructions);
191
192 let lut: &[AddressLookupTableAccount] =
193 if index == total - 1 && matches!(self.tip_mode, TipMode::SeparateTx) {
194 &[]
195 } else {
196 self.lookup_tables
197 };
198
199 let message = v0::Message::try_compile(
200 &self.payer.pubkey(),
201 &instructions,
202 lut,
203 self.recent_blockhash,
204 )
205 .map_err(|e| {
206 TransactionAnalysis::log_accounts_not_in_luts(
207 &instructions,
208 lut,
209 &format!("TX: {index} COMPILE_FAIL"),
210 );
211 JitoError::MessageCompileFailed {
212 index,
213 reason: e.to_string(),
214 }
215 })?;
216 let txn = VersionedTransaction::try_new(VersionedMessage::V0(message), &[self.payer])
217 .map_err(|e| JitoError::TransactionCreationFailed {
218 index,
219 reason: e.to_string(),
220 })?;
221 let size_info = TransactionAnalysis::analyze_transaction_size(&txn);
222 if size_info.is_oversized {
223 return Err(JitoError::TransactionOversized {
224 index,
225 size: size_info.size,
226 max: size_info.max_size,
227 });
228 }
229 Ok(txn)
230 }
231
232 fn validate_tip_not_in_luts(
237 tip_account: &Pubkey,
238 lookup_tables: &[AddressLookupTableAccount],
239 ) -> Result<(), JitoError> {
240 for lut in lookup_tables {
241 if lut.addresses.contains(tip_account) {
242 return Err(JitoError::TipAccountInLut {
243 tip_account: tip_account.to_string(),
244 });
245 }
246 }
247 Ok(())
248 }
249}