kora_lib/bundle/
helper.rs

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        // Phase 1: Decode, resolve, validate, calc fees, collect instructions
39        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        // Phase 2: Calculate payments with cross-tx ATA visibility
72        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            // Get latest blockhash if signatures are empty and blockhash is not set
130            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}