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