Skip to main content

jito_bundle/bundler/builder/
utils.rs

1use 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    /// Returns the mutable builder's current instruction slots view.
20    fn instruction_slots(&self) -> &BundleInstructionSlots {
21        &self.transactions_instructions
22    }
23}
24
25impl<'a> BundleBuilder<'a> {
26    // --- Construction ---
27    /// Creates a builder from validated build inputs.
28    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    // --- Build Pipeline ---
53    /// Builds a final `BuiltBundle` from fixed instruction slots.
54    ///
55    /// Jito hard-limits bundles to `MAX_BUNDLE_TRANSACTIONS` (5), so this builder
56    /// chooses tip placement based on remaining transaction capacity.
57    ///
58    /// Build flow:
59    /// 1. Compact sparse slots while preserving transaction order.
60    /// 2. Optionally apply `jitodontfront` account rewriting.
61    /// 3. Insert tip as separate tx when count < 5, or inline when count == 5.
62    /// 4. Validate tip account is not in LUTs for inline mode.
63    /// 5. Compile, sign, and size-check each transaction.
64    ///
65    /// Returns `JitoError` for invalid bundle size, compile/sign failures,
66    /// oversized transactions, and invalid LUT tip-account usage.
67    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    /// Compacts sparse slots while preserving transaction order.
113    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    /// Appends the tip as a dedicated transaction when the bundle has room (< 5 txs).
128    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    /// Appends the tip instruction to the last populated transaction when already at 5 txs.
146    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    /// Rewrites `jitodontfront` account usage so it appears only in the first transaction.
160    ///
161    /// The expected pubkey prefix is `jitodontfront`, while the suffix can vary,
162    /// so matching uses string prefix rather than exact full-string equality.
163    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    /// Compiles and signs one versioned transaction from instruction list.
181    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    /// Ensures the chosen tip account is not present in provided LUTs.
233    ///
234    /// If the tip account appears in a LUT for inline-tip mode, Jito bundle execution
235    /// will fail, so this is validated pre-send.
236    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}