1use crate::{
2 bundle::{BundleError, JitoError},
3 config::Config,
4 constant::ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION,
5 fee::fee::{FeeConfigUtil, TransactionFeeUtil},
6 signer::bundle_signer::BundleSigner,
7 token::token::TokenUtil,
8 transaction::{TransactionUtil, VersionedTransactionResolved},
9 usage_limit::UsageTracker,
10 validator::transaction_validator::TransactionValidator,
11 KoraError,
12};
13use solana_client::nonblocking::rpc_client::RpcClient;
14use solana_commitment_config::CommitmentConfig;
15use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
16use std::sync::Arc;
17
18pub struct BundleProcessor {
19 pub resolved_transactions: Vec<VersionedTransactionResolved>,
20 pub total_required_lamports: u64,
21 pub total_payment_lamports: u64,
22 pub total_solana_estimated_fee: u64,
23}
24
25impl BundleProcessor {
26 pub async fn process_bundle(
27 encoded_txs: &[String],
28 fee_payer: Pubkey,
29 payment_destination: &Pubkey,
30 config: &Config,
31 rpc_client: &Arc<RpcClient>,
32 sig_verify: bool,
33 ) -> Result<Self, KoraError> {
34 let validator = TransactionValidator::new(config, fee_payer)?;
35 let mut resolved_transactions = Vec::with_capacity(encoded_txs.len());
36 let mut total_required_lamports = 0u64;
37 let mut all_bundle_instructions: Vec<Instruction> = Vec::new();
38 let mut txs_missing_payment_count = 0u64;
39
40 for encoded in encoded_txs {
42 let transaction = TransactionUtil::decode_b64_transaction(encoded)?;
43 UsageTracker::check_transaction_usage_limit(config, &transaction).await?;
44
45 let mut resolved_tx = VersionedTransactionResolved::from_transaction(
46 &transaction,
47 config,
48 rpc_client,
49 sig_verify,
50 )
51 .await?;
52
53 validator.validate_transaction(config, &mut resolved_tx, rpc_client).await?;
54
55 let fee_calc = FeeConfigUtil::estimate_kora_fee(
56 &mut resolved_tx,
57 &fee_payer,
58 config.validation.is_payment_required(),
59 rpc_client,
60 config,
61 )
62 .await?;
63
64 total_required_lamports =
65 total_required_lamports.checked_add(fee_calc.total_fee_lamports).ok_or_else(
66 || KoraError::ValidationError("Bundle fee calculation overflow".to_string()),
67 )?;
68
69 if fee_calc.payment_instruction_fee > 0 {
71 txs_missing_payment_count += 1;
72 }
73
74 all_bundle_instructions.extend(resolved_tx.all_instructions.clone());
75 resolved_transactions.push(resolved_tx);
76 }
77
78 if txs_missing_payment_count > 1 {
82 let overcount =
83 (txs_missing_payment_count - 1) * ESTIMATED_LAMPORTS_FOR_PAYMENT_INSTRUCTION;
84
85 total_required_lamports =
86 total_required_lamports.checked_sub(overcount).ok_or_else(|| {
87 KoraError::ValidationError("Bundle fee calculation overflow".to_string())
88 })?;
89 }
90
91 let mut total_payment_lamports = 0u64;
93 let mut total_solana_estimated_fee = 0u64;
94 for resolved in resolved_transactions.iter_mut() {
95 if let Some(payment) = TokenUtil::find_payment_in_transaction(
96 config,
97 resolved,
98 rpc_client,
99 payment_destination,
100 Some(&all_bundle_instructions),
101 )
102 .await?
103 {
104 total_payment_lamports =
105 total_payment_lamports.checked_add(payment).ok_or_else(|| {
106 KoraError::ValidationError("Payment calculation overflow".to_string())
107 })?;
108 }
109
110 let fee = TransactionFeeUtil::get_estimate_fee_resolved(rpc_client, resolved).await?;
111 total_solana_estimated_fee =
112 total_solana_estimated_fee.checked_add(fee).ok_or_else(|| {
113 KoraError::ValidationError("Bundle Solana fee calculation overflow".to_string())
114 })?;
115
116 validator.validate_lamport_fee(total_solana_estimated_fee)?;
117 }
118
119 Ok(Self {
120 resolved_transactions,
121 total_required_lamports,
122 total_payment_lamports,
123 total_solana_estimated_fee,
124 })
125 }
126
127 fn validate_payment(&self) -> Result<(), KoraError> {
128 if self.total_payment_lamports < self.total_required_lamports {
129 return Err(BundleError::Jito(JitoError::InsufficientBundlePayment(
130 self.total_required_lamports,
131 self.total_payment_lamports,
132 ))
133 .into());
134 }
135 Ok(())
136 }
137
138 pub async fn sign_all(
139 mut self,
140 signer: &Arc<solana_keychain::Signer>,
141 fee_payer: &Pubkey,
142 rpc_client: &RpcClient,
143 ) -> Result<Vec<VersionedTransactionResolved>, KoraError> {
144 self.validate_payment()?;
145
146 let mut blockhash = None;
147
148 for resolved in self.resolved_transactions.iter_mut() {
149 if blockhash.is_none() && resolved.transaction.signatures.is_empty() {
151 blockhash = Some(
152 rpc_client
153 .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
154 .await?
155 .0,
156 );
157 }
158
159 BundleSigner::sign_transaction_for_bundle(resolved, signer, fee_payer, &blockhash)
160 .await?;
161 }
162
163 Ok(self.resolved_transactions)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_validate_payment_sufficient() {
173 let processor = BundleProcessor {
174 resolved_transactions: vec![],
175 total_required_lamports: 1000,
176 total_payment_lamports: 1500,
177 total_solana_estimated_fee: 1000,
178 };
179
180 assert!(processor.validate_payment().is_ok());
181 }
182
183 #[test]
184 fn test_validate_payment_exact() {
185 let processor = BundleProcessor {
186 resolved_transactions: vec![],
187 total_required_lamports: 1000,
188 total_payment_lamports: 1000,
189 total_solana_estimated_fee: 1000,
190 };
191
192 assert!(processor.validate_payment().is_ok());
193 }
194
195 #[test]
196 fn test_validate_payment_insufficient() {
197 let processor = BundleProcessor {
198 resolved_transactions: vec![],
199 total_required_lamports: 2000,
200 total_payment_lamports: 1000,
201 total_solana_estimated_fee: 1000,
202 };
203
204 let result = processor.validate_payment();
205 assert!(result.is_err());
206 let err = result.unwrap_err();
207 assert!(matches!(err, KoraError::JitoError(_)));
208 if let KoraError::JitoError(msg) = err {
209 assert!(msg.contains("insufficient"));
210 assert!(msg.contains("2000"));
211 assert!(msg.contains("1000"));
212 }
213 }
214
215 #[test]
216 fn test_validate_payment_zero_required() {
217 let processor = BundleProcessor {
218 resolved_transactions: vec![],
219 total_required_lamports: 0,
220 total_payment_lamports: 0,
221 total_solana_estimated_fee: 1000,
222 };
223
224 assert!(processor.validate_payment().is_ok());
225 }
226
227 #[test]
228 fn test_validate_payment_max_values() {
229 let processor = BundleProcessor {
230 resolved_transactions: vec![],
231 total_required_lamports: u64::MAX,
232 total_payment_lamports: u64::MAX,
233 total_solana_estimated_fee: 1000,
234 };
235
236 assert!(processor.validate_payment().is_ok());
237 }
238
239 #[test]
240 fn test_validate_payment_one_lamport_short() {
241 let processor = BundleProcessor {
242 resolved_transactions: vec![],
243 total_required_lamports: 1001,
244 total_payment_lamports: 1000,
245 total_solana_estimated_fee: 500,
246 };
247
248 let result = processor.validate_payment();
249 assert!(result.is_err());
250 let err = result.unwrap_err();
251 assert!(matches!(err, KoraError::JitoError(_)));
252 }
253
254 #[test]
255 fn test_bundle_processor_fields() {
256 let processor = BundleProcessor {
257 resolved_transactions: vec![],
258 total_required_lamports: 5000,
259 total_payment_lamports: 6000,
260 total_solana_estimated_fee: 2500,
261 };
262
263 assert_eq!(processor.total_required_lamports, 5000);
264 assert_eq!(processor.total_payment_lamports, 6000);
265 assert_eq!(processor.total_solana_estimated_fee, 2500);
266 assert!(processor.resolved_transactions.is_empty());
267 }
268}