1use async_trait::async_trait;
2use base64::{engine::general_purpose::STANDARD, Engine as _};
3use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
4use solana_commitment_config::CommitmentConfig;
5use solana_keychain::{Signer, SolanaSigner};
6use solana_message::{
7 compiled_instruction::CompiledInstruction, v0::MessageAddressTableLookup, VersionedMessage,
8};
9use solana_sdk::{instruction::Instruction, pubkey::Pubkey, transaction::VersionedTransaction};
10use std::{collections::HashMap, ops::Deref};
11
12use solana_transaction_status_client_types::UiTransactionEncoding;
13
14use crate::{
15 config::Config,
16 error::KoraError,
17 fee::fee::{FeeConfigUtil, TransactionFeeUtil},
18 lighthouse::LighthouseUtil,
19 plugin::{PluginExecutionContext, TransactionPluginRunner},
20 sanitize_error,
21 transaction::{
22 instruction_util::IxUtils, ParsedSPLInstructionData, ParsedSPLInstructionType,
23 ParsedSystemInstructionData, ParsedSystemInstructionType,
24 },
25 validator::transaction_validator::TransactionValidator,
26 CacheUtil,
27};
28use solana_address_lookup_table_interface::state::AddressLookupTable;
29
30pub struct VersionedTransactionResolved {
32 pub transaction: VersionedTransaction,
33
34 pub all_account_keys: Vec<Pubkey>,
36
37 pub all_instructions: Vec<Instruction>,
39
40 parsed_system_instructions:
42 Option<HashMap<ParsedSystemInstructionType, Vec<ParsedSystemInstructionData>>>,
43
44 parsed_spl_instructions:
46 Option<HashMap<ParsedSPLInstructionType, Vec<ParsedSPLInstructionData>>>,
47}
48
49impl Deref for VersionedTransactionResolved {
50 type Target = VersionedTransaction;
51
52 fn deref(&self) -> &Self::Target {
53 &self.transaction
54 }
55}
56
57#[async_trait]
58pub trait VersionedTransactionOps {
59 fn encode_b64_transaction(&self) -> Result<String, KoraError>;
60 fn find_signer_position(&self, signer_pubkey: &Pubkey) -> Result<usize, KoraError>;
61
62 async fn sign_transaction(
63 &mut self,
64 config: &Config,
65 signer: &std::sync::Arc<Signer>,
66 rpc_client: &RpcClient,
67 will_send: bool,
68 ) -> Result<(VersionedTransaction, String), KoraError>;
69 async fn sign_and_send_transaction(
70 &mut self,
71 config: &Config,
72 signer: &std::sync::Arc<Signer>,
73 rpc_client: &RpcClient,
74 ) -> Result<(String, String), KoraError>;
75}
76
77impl VersionedTransactionResolved {
78 pub async fn from_transaction(
79 transaction: &VersionedTransaction,
80 config: &Config,
81 rpc_client: &RpcClient,
82 sig_verify: bool,
83 ) -> Result<Self, KoraError> {
84 let mut resolved = Self {
85 transaction: transaction.clone(),
86 all_account_keys: vec![],
87 all_instructions: vec![],
88 parsed_system_instructions: None,
89 parsed_spl_instructions: None,
90 };
91
92 let resolved_addresses = match &transaction.message {
94 VersionedMessage::Legacy(_) => {
95 vec![]
97 }
98 VersionedMessage::V0(v0_message) => {
99 LookupTableUtil::resolve_lookup_table_addresses(
101 config,
102 rpc_client,
103 &v0_message.address_table_lookups,
104 )
105 .await?
106 }
107 };
108
109 let mut all_account_keys = transaction.message.static_account_keys().to_vec();
111 all_account_keys.extend(resolved_addresses.clone());
112 resolved.all_account_keys = all_account_keys.clone();
113
114 let outer_instructions =
116 IxUtils::uncompile_instructions(transaction.message.instructions(), &all_account_keys)?;
117
118 let inner_instructions = resolved.fetch_inner_instructions(rpc_client, sig_verify).await?;
119
120 resolved.all_instructions.extend(outer_instructions);
121 resolved.all_instructions.extend(inner_instructions);
122
123 Ok(resolved)
124 }
125
126 pub fn from_kora_built_transaction(
128 transaction: &VersionedTransaction,
129 ) -> Result<Self, KoraError> {
130 Ok(Self {
131 transaction: transaction.clone(),
132 all_account_keys: transaction.message.static_account_keys().to_vec(),
133 all_instructions: IxUtils::uncompile_instructions(
134 transaction.message.instructions(),
135 transaction.message.static_account_keys(),
136 )?,
137 parsed_system_instructions: None,
138 parsed_spl_instructions: None,
139 })
140 }
141
142 async fn fetch_inner_instructions(
144 &mut self,
145 rpc_client: &RpcClient,
146 sig_verify: bool,
147 ) -> Result<Vec<Instruction>, KoraError> {
148 let simulation_result = rpc_client
149 .simulate_transaction_with_config(
150 &self.transaction,
151 RpcSimulateTransactionConfig {
152 commitment: Some(rpc_client.commitment()),
153 sig_verify,
154 inner_instructions: true,
155 replace_recent_blockhash: false,
156 encoding: Some(UiTransactionEncoding::Base64),
157 accounts: None,
158 min_context_slot: None,
159 },
160 )
161 .await
162 .map_err(|e| {
163 KoraError::RpcError(format!(
164 "Failed to simulate transaction: {}",
165 sanitize_error!(e)
166 ))
167 })?;
168
169 if let Some(err) = simulation_result.value.err {
170 return Err(KoraError::InvalidTransaction(format!(
171 "Transaction simulation failed: {err}"
172 )));
173 }
174
175 if let Some(inner_instructions) = simulation_result.value.inner_instructions {
176 let mut compiled_inner_instructions: Vec<CompiledInstruction> = vec![];
177 let mut extended_account_keys = self.all_account_keys.clone();
180 let mut account_keys_hashmap =
181 IxUtils::build_account_keys_hashmap(&extended_account_keys);
182
183 for ix in &inner_instructions {
184 for inner_ix in &ix.instructions {
185 let compiled = IxUtils::reconstruct_instruction_from_ui_with_account_key_cache(
186 inner_ix,
187 &mut extended_account_keys,
188 &mut account_keys_hashmap,
189 )?;
190 compiled_inner_instructions.push(compiled);
191 }
192 }
193
194 self.all_account_keys = extended_account_keys;
195 return IxUtils::uncompile_instructions(
196 &compiled_inner_instructions,
197 &self.all_account_keys,
198 );
199 }
200
201 Ok(vec![])
202 }
203
204 pub fn get_or_parse_system_instructions(
205 &mut self,
206 ) -> Result<&HashMap<ParsedSystemInstructionType, Vec<ParsedSystemInstructionData>>, KoraError>
207 {
208 if self.parsed_system_instructions.is_none() {
209 self.parsed_system_instructions = Some(IxUtils::parse_system_instructions(self)?);
210 }
211
212 self.parsed_system_instructions.as_ref().ok_or_else(|| {
213 KoraError::SerializationError("Parsed system instructions not found".to_string())
214 })
215 }
216
217 pub fn get_or_parse_spl_instructions(
218 &mut self,
219 ) -> Result<&HashMap<ParsedSPLInstructionType, Vec<ParsedSPLInstructionData>>, KoraError> {
220 if self.parsed_spl_instructions.is_none() {
221 self.parsed_spl_instructions = Some(IxUtils::parse_token_instructions(self)?);
222 }
223
224 self.parsed_spl_instructions.as_ref().ok_or_else(|| {
225 KoraError::SerializationError("Parsed SPL instructions not found".to_string())
226 })
227 }
228}
229
230#[async_trait]
232impl VersionedTransactionOps for VersionedTransactionResolved {
233 fn encode_b64_transaction(&self) -> Result<String, KoraError> {
234 let serialized = bincode::serialize(&self.transaction).map_err(|e| {
235 KoraError::SerializationError(format!(
236 "Base64 serialization failed: {}",
237 sanitize_error!(e)
238 ))
239 })?;
240 Ok(STANDARD.encode(serialized))
241 }
242
243 fn find_signer_position(&self, signer_pubkey: &Pubkey) -> Result<usize, KoraError> {
244 self.transaction
245 .message
246 .static_account_keys()
247 .iter()
248 .position(|key| key == signer_pubkey)
249 .ok_or_else(|| {
250 KoraError::InvalidTransaction(format!(
251 "Signer {signer_pubkey} not found in transaction account keys"
252 ))
253 })
254 }
255
256 async fn sign_transaction(
257 &mut self,
258 config: &Config,
259 signer: &std::sync::Arc<Signer>,
260 rpc_client: &RpcClient,
261 will_send: bool,
262 ) -> Result<(VersionedTransaction, String), KoraError> {
263 let fee_payer = signer.pubkey();
264 let validator = TransactionValidator::new(config, fee_payer)?;
265
266 validator.validate_transaction(config, self, rpc_client).await?;
268
269 let plugin_context = if will_send {
270 PluginExecutionContext::SignAndSendTransaction
271 } else {
272 PluginExecutionContext::SignTransaction
273 };
274 TransactionPluginRunner::from_config(config)
275 .run(self, config, rpc_client, &fee_payer, plugin_context)
276 .await?;
277
278 let fee_calculation = FeeConfigUtil::estimate_kora_fee(
280 self,
281 &fee_payer,
282 config.validation.is_payment_required(),
283 rpc_client,
284 config,
285 )
286 .await?;
287
288 let required_lamports = fee_calculation.total_fee_lamports;
289
290 if required_lamports > 0 {
292 log::info!("Payment validation: required_lamports={}", required_lamports);
293 let payment_destination = config.kora.get_payment_address(&fee_payer)?;
295
296 TransactionValidator::validate_token_payment(
298 config,
299 self,
300 required_lamports,
301 rpc_client,
302 &payment_destination,
303 )
304 .await?;
305
306 TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calculation)?;
308 }
309
310 let mut transaction = self.transaction.clone();
312
313 if transaction.signatures.is_empty() {
314 let blockhash = rpc_client
315 .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
316 .await?;
317 transaction.message.set_recent_blockhash(blockhash.0);
318 }
319
320 let estimated_fee = TransactionFeeUtil::get_estimate_fee_resolved(rpc_client, self).await?;
322 validator.validate_lamport_fee(estimated_fee)?;
323
324 LighthouseUtil::add_fee_payer_assertion(
325 &mut transaction,
326 rpc_client,
327 &fee_payer,
328 estimated_fee,
329 &config.kora.lighthouse,
330 will_send,
331 )
332 .await?;
333
334 let message_bytes = transaction.message.serialize();
336 let signature = signer
337 .sign_message(&message_bytes)
338 .await
339 .map_err(|e| KoraError::SigningError(sanitize_error!(e)))?;
340
341 let fee_payer_position = self.find_signer_position(&fee_payer)?;
343 transaction.signatures[fee_payer_position] = signature;
344
345 let serialized = bincode::serialize(&transaction)?;
347 let encoded = STANDARD.encode(serialized);
348
349 Ok((transaction, encoded))
350 }
351
352 async fn sign_and_send_transaction(
353 &mut self,
354 config: &Config,
355 signer: &std::sync::Arc<Signer>,
356 rpc_client: &RpcClient,
357 ) -> Result<(String, String), KoraError> {
358 let (transaction, encoded) =
360 self.sign_transaction(config, signer, rpc_client, true).await?;
361
362 let signature = rpc_client
364 .send_and_confirm_transaction(&transaction)
365 .await
366 .map_err(|e| KoraError::RpcError(sanitize_error!(e)))?;
367
368 Ok((signature.to_string(), encoded))
369 }
370}
371
372pub struct LookupTableUtil {}
373
374impl LookupTableUtil {
375 pub async fn resolve_lookup_table_addresses(
377 config: &Config,
378 rpc_client: &RpcClient,
379 lookup_table_lookups: &[MessageAddressTableLookup],
380 ) -> Result<Vec<Pubkey>, KoraError> {
381 let mut resolved_addresses = Vec::new();
382
383 for lookup in lookup_table_lookups {
385 let lookup_table_account =
386 CacheUtil::get_account(config, rpc_client, &lookup.account_key, false)
387 .await
388 .map_err(|e| {
389 KoraError::RpcError(format!(
390 "Failed to fetch lookup table: {}",
391 sanitize_error!(e)
392 ))
393 })?;
394
395 let address_lookup_table = AddressLookupTable::deserialize(&lookup_table_account.data)
397 .map_err(|e| {
398 KoraError::InvalidTransaction(format!(
399 "Failed to deserialize lookup table: {}",
400 sanitize_error!(e)
401 ))
402 })?;
403
404 for &index in &lookup.writable_indexes {
406 if let Some(address) = address_lookup_table.addresses.get(index as usize) {
407 resolved_addresses.push(*address);
408 } else {
409 return Err(KoraError::InvalidTransaction(format!(
410 "Lookup table index {index} out of bounds for writable addresses"
411 )));
412 }
413 }
414
415 for &index in &lookup.readonly_indexes {
417 if let Some(address) = address_lookup_table.addresses.get(index as usize) {
418 resolved_addresses.push(*address);
419 } else {
420 return Err(KoraError::InvalidTransaction(format!(
421 "Lookup table index {index} out of bounds for readonly addresses"
422 )));
423 }
424 }
425 }
426
427 Ok(resolved_addresses)
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use crate::{
434 config::SplTokenConfig,
435 tests::{
436 common::RpcMockBuilder, config_mock::mock_state::setup_config_mock,
437 toml_mock::ConfigBuilder,
438 },
439 transaction::TransactionUtil,
440 Config,
441 };
442 use serde_json::json;
443 use solana_client::rpc_request::RpcRequest;
444 use std::collections::HashMap;
445
446 use super::*;
447 use solana_address_lookup_table_interface::state::LookupTableMeta;
448 use solana_message::{compiled_instruction::CompiledInstruction, v0, Message};
449 use solana_sdk::{
450 account::Account,
451 hash::Hash,
452 instruction::{AccountMeta, Instruction},
453 signature::Keypair,
454 signer::Signer,
455 };
456
457 fn setup_test_config() -> Config {
458 ConfigBuilder::new()
459 .with_programs(vec![])
460 .with_tokens(vec![])
461 .with_spl_paid_tokens(SplTokenConfig::Allowlist(vec![]))
462 .with_free_price()
463 .with_cache_config(None, false, 60, 30) .build_config()
465 .expect("Failed to build test config")
466 }
467
468 #[test]
469 fn test_encode_transaction_b64() {
470 let keypair = Keypair::new();
471 let instruction = Instruction::new_with_bytes(
472 Pubkey::new_unique(),
473 &[1, 2, 3],
474 vec![AccountMeta::new(keypair.pubkey(), true)],
475 );
476 let message =
477 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
478 let tx = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
479
480 let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx).unwrap();
481 let encoded = resolved.encode_b64_transaction().unwrap();
482 assert!(!encoded.is_empty());
483 assert!(encoded
484 .chars()
485 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '='));
486 }
487
488 #[test]
489 fn test_encode_decode_b64_transaction() {
490 let keypair = Keypair::new();
491 let instruction = Instruction::new_with_bytes(
492 Pubkey::new_unique(),
493 &[1, 2, 3],
494 vec![AccountMeta::new(keypair.pubkey(), true)],
495 );
496 let message =
497 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
498 let tx = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
499
500 let resolved = VersionedTransactionResolved::from_kora_built_transaction(&tx).unwrap();
501 let encoded = resolved.encode_b64_transaction().unwrap();
502 let decoded = TransactionUtil::decode_b64_transaction(&encoded).unwrap();
503
504 assert_eq!(tx, decoded);
505 }
506
507 #[test]
508 fn test_find_signer_position_success() {
509 let keypair = Keypair::new();
510 let program_id = Pubkey::new_unique();
511 let instruction = Instruction::new_with_bytes(
512 program_id,
513 &[1, 2, 3],
514 vec![AccountMeta::new(keypair.pubkey(), true)],
515 );
516 let message =
517 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
518 let transaction =
519 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
520
521 let position = transaction.find_signer_position(&keypair.pubkey()).unwrap();
522 assert_eq!(position, 0); }
524
525 #[test]
526 fn test_find_signer_position_success_v0() {
527 let keypair = Keypair::new();
528 let program_id = Pubkey::new_unique();
529 let other_account = Pubkey::new_unique();
530
531 let v0_message = v0::Message {
532 header: solana_message::MessageHeader {
533 num_required_signatures: 1,
534 num_readonly_signed_accounts: 0,
535 num_readonly_unsigned_accounts: 2,
536 },
537 account_keys: vec![keypair.pubkey(), other_account, program_id],
538 recent_blockhash: Hash::default(),
539 instructions: vec![CompiledInstruction {
540 program_id_index: 2,
541 accounts: vec![0, 1],
542 data: vec![1, 2, 3],
543 }],
544 address_table_lookups: vec![],
545 };
546 let message = VersionedMessage::V0(v0_message);
547 let transaction =
548 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
549
550 let position = transaction.find_signer_position(&keypair.pubkey()).unwrap();
551 assert_eq!(position, 0);
552
553 let other_position = transaction.find_signer_position(&other_account).unwrap();
554 assert_eq!(other_position, 1);
555 }
556
557 #[test]
558 fn test_find_signer_position_middle_of_accounts() {
559 let keypair1 = Keypair::new();
560 let keypair2 = Keypair::new();
561 let keypair3 = Keypair::new();
562 let program_id = Pubkey::new_unique();
563
564 let v0_message = v0::Message {
565 header: solana_message::MessageHeader {
566 num_required_signatures: 3,
567 num_readonly_signed_accounts: 0,
568 num_readonly_unsigned_accounts: 1,
569 },
570 account_keys: vec![keypair1.pubkey(), keypair2.pubkey(), keypair3.pubkey(), program_id],
571 recent_blockhash: Hash::default(),
572 instructions: vec![CompiledInstruction {
573 program_id_index: 3,
574 accounts: vec![0, 1, 2],
575 data: vec![1, 2, 3],
576 }],
577 address_table_lookups: vec![],
578 };
579 let message = VersionedMessage::V0(v0_message);
580 let transaction =
581 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
582
583 assert_eq!(transaction.find_signer_position(&keypair1.pubkey()).unwrap(), 0);
584 assert_eq!(transaction.find_signer_position(&keypair2.pubkey()).unwrap(), 1);
585 assert_eq!(transaction.find_signer_position(&keypair3.pubkey()).unwrap(), 2);
586 }
587
588 #[test]
589 fn test_find_signer_position_not_found() {
590 let keypair = Keypair::new();
591 let missing_keypair = Keypair::new();
592 let instruction = Instruction::new_with_bytes(
593 Pubkey::new_unique(),
594 &[1, 2, 3],
595 vec![AccountMeta::new(keypair.pubkey(), true)],
596 );
597 let message =
598 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
599 let transaction =
600 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
601
602 let result = transaction.find_signer_position(&missing_keypair.pubkey());
603 assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
604
605 if let Err(KoraError::InvalidTransaction(msg)) = result {
606 assert!(msg.contains(&missing_keypair.pubkey().to_string()));
607 assert!(msg.contains("not found in transaction account keys"));
608 }
609 }
610
611 #[test]
612 fn test_find_signer_position_empty_account_keys() {
613 let v0_message = v0::Message {
615 header: solana_message::MessageHeader {
616 num_required_signatures: 0,
617 num_readonly_signed_accounts: 0,
618 num_readonly_unsigned_accounts: 0,
619 },
620 account_keys: vec![], recent_blockhash: Hash::default(),
622 instructions: vec![],
623 address_table_lookups: vec![],
624 };
625 let message = VersionedMessage::V0(v0_message);
626 let transaction =
627 TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
628 let search_key = Pubkey::new_unique();
629
630 let result = transaction.find_signer_position(&search_key);
631 assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
632 }
633
634 #[test]
635 fn test_from_kora_built_transaction() {
636 let keypair = Keypair::new();
637 let program_id = Pubkey::new_unique();
638 let instruction = Instruction::new_with_bytes(
639 program_id,
640 &[1, 2, 3, 4],
641 vec![
642 AccountMeta::new(keypair.pubkey(), true),
643 AccountMeta::new_readonly(Pubkey::new_unique(), false),
644 ],
645 );
646 let message = VersionedMessage::Legacy(Message::new(
647 std::slice::from_ref(&instruction),
648 Some(&keypair.pubkey()),
649 ));
650 let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap();
651
652 let resolved =
653 VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
654
655 assert_eq!(resolved.transaction, transaction);
656 assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys());
657 assert_eq!(resolved.all_instructions.len(), 1);
658
659 let resolved_instruction = &resolved.all_instructions[0];
662 assert_eq!(resolved_instruction.program_id, instruction.program_id);
663 assert_eq!(resolved_instruction.data, instruction.data);
664 assert_eq!(resolved_instruction.accounts.len(), instruction.accounts.len());
665
666 assert!(resolved.parsed_system_instructions.is_none());
667 assert!(resolved.parsed_spl_instructions.is_none());
668 }
669
670 #[test]
671 fn test_from_kora_built_transaction_v0() {
672 let keypair = Keypair::new();
673 let program_id = Pubkey::new_unique();
674 let other_account = Pubkey::new_unique();
675
676 let v0_message = v0::Message {
677 header: solana_message::MessageHeader {
678 num_required_signatures: 1,
679 num_readonly_signed_accounts: 0,
680 num_readonly_unsigned_accounts: 2,
681 },
682 account_keys: vec![keypair.pubkey(), other_account, program_id],
683 recent_blockhash: Hash::new_unique(),
684 instructions: vec![CompiledInstruction {
685 program_id_index: 2,
686 accounts: vec![0, 1],
687 data: vec![1, 2, 3],
688 }],
689 address_table_lookups: vec![],
690 };
691 let message = VersionedMessage::V0(v0_message);
692 let transaction = VersionedTransaction::try_new(message.clone(), &[&keypair]).unwrap();
693
694 let resolved =
695 VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
696
697 assert_eq!(resolved.transaction, transaction);
698 assert_eq!(resolved.all_account_keys, vec![keypair.pubkey(), other_account, program_id]);
699 assert_eq!(resolved.all_instructions.len(), 1);
700 assert_eq!(resolved.all_instructions[0].program_id, program_id);
701 assert_eq!(resolved.all_instructions[0].accounts.len(), 2);
702 assert_eq!(resolved.all_instructions[0].data, vec![1, 2, 3]);
703 }
704
705 #[tokio::test]
706 async fn test_from_transaction_legacy() {
707 let config = setup_test_config();
708 let _m = setup_config_mock(config.clone());
709
710 let keypair = Keypair::new();
711 let instruction = Instruction::new_with_bytes(
712 Pubkey::new_unique(),
713 &[1, 2, 3],
714 vec![AccountMeta::new(keypair.pubkey(), true)],
715 );
716 let message = VersionedMessage::Legacy(Message::new(
717 std::slice::from_ref(&instruction),
718 Some(&keypair.pubkey()),
719 ));
720 let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
721
722 let mut mocks = HashMap::new();
724 mocks.insert(
725 RpcRequest::SimulateTransaction,
726 json!({
727 "context": { "slot": 1 },
728 "value": {
729 "err": null,
730 "logs": [],
731 "accounts": null,
732 "unitsConsumed": 1000,
733 "innerInstructions": []
734 }
735 }),
736 );
737 let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
738
739 let resolved = VersionedTransactionResolved::from_transaction(
740 &transaction,
741 &config,
742 &rpc_client,
743 true,
744 )
745 .await
746 .unwrap();
747
748 assert_eq!(resolved.transaction, transaction);
749 assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys());
750 assert_eq!(resolved.all_instructions.len(), 1); let resolved_instruction = &resolved.all_instructions[0];
755 assert_eq!(resolved_instruction.program_id, instruction.program_id);
756 assert_eq!(resolved_instruction.data, instruction.data);
757 assert_eq!(resolved_instruction.accounts.len(), instruction.accounts.len());
758 assert_eq!(resolved_instruction.accounts[0].pubkey, instruction.accounts[0].pubkey);
759 assert_eq!(
760 resolved_instruction.accounts[0].is_writable,
761 instruction.accounts[0].is_writable
762 );
763 }
764
765 #[tokio::test]
766 async fn test_from_transaction_v0_with_lookup_tables() {
767 let config = setup_test_config();
768 let _m = setup_config_mock(config.clone());
769
770 let keypair = Keypair::new();
771 let program_id = Pubkey::new_unique();
772 let lookup_table_account = Pubkey::new_unique();
773 let resolved_address = Pubkey::new_unique();
774
775 let lookup_table = AddressLookupTable {
777 meta: LookupTableMeta {
778 deactivation_slot: u64::MAX,
779 last_extended_slot: 0,
780 last_extended_slot_start_index: 0,
781 authority: Some(Pubkey::new_unique()),
782 _padding: 0,
783 },
784 addresses: vec![resolved_address].into(),
785 };
786
787 let v0_message = v0::Message {
788 header: solana_message::MessageHeader {
789 num_required_signatures: 1,
790 num_readonly_signed_accounts: 0,
791 num_readonly_unsigned_accounts: 1,
792 },
793 account_keys: vec![keypair.pubkey(), program_id],
794 recent_blockhash: Hash::new_unique(),
795 instructions: vec![CompiledInstruction {
796 program_id_index: 1,
797 accounts: vec![0, 2], data: vec![42],
799 }],
800 address_table_lookups: vec![solana_message::v0::MessageAddressTableLookup {
801 account_key: lookup_table_account,
802 writable_indexes: vec![0],
803 readonly_indexes: vec![],
804 }],
805 };
806
807 let message = VersionedMessage::V0(v0_message);
808 let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
809
810 let mut mocks = HashMap::new();
812 let serialized_data = lookup_table.serialize_for_tests().unwrap();
813 let encoded_data = base64::engine::general_purpose::STANDARD.encode(&serialized_data);
814
815 mocks.insert(
816 RpcRequest::GetAccountInfo,
817 json!({
818 "context": { "slot": 1 },
819 "value": {
820 "data": [encoded_data, "base64"],
821 "executable": false,
822 "lamports": 0,
823 "owner": "AddressLookupTab1e1111111111111111111111111".to_string(),
824 "rentEpoch": 0
825 }
826 }),
827 );
828
829 mocks.insert(
830 RpcRequest::SimulateTransaction,
831 json!({
832 "context": { "slot": 1 },
833 "value": {
834 "err": null,
835 "logs": [],
836 "accounts": null,
837 "unitsConsumed": 1000,
838 "innerInstructions": []
839 }
840 }),
841 );
842
843 let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
844
845 let resolved = VersionedTransactionResolved::from_transaction(
846 &transaction,
847 &config,
848 &rpc_client,
849 true,
850 )
851 .await
852 .unwrap();
853
854 assert_eq!(resolved.transaction, transaction);
855
856 assert_eq!(resolved.all_account_keys.len(), 3); assert_eq!(resolved.all_account_keys[0], keypair.pubkey());
859 assert_eq!(resolved.all_account_keys[1], program_id);
860 assert_eq!(resolved.all_account_keys[2], resolved_address);
861 }
862
863 #[tokio::test]
864 async fn test_from_transaction_simulation_failure() {
865 let config = setup_test_config();
866 let _m = setup_config_mock(config.clone());
867
868 let keypair = Keypair::new();
869 let instruction = Instruction::new_with_bytes(
870 Pubkey::new_unique(),
871 &[1, 2, 3],
872 vec![AccountMeta::new(keypair.pubkey(), true)],
873 );
874 let message =
875 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
876 let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
877
878 let mut mocks = HashMap::new();
880 mocks.insert(
881 RpcRequest::SimulateTransaction,
882 json!({
883 "context": { "slot": 1 },
884 "value": {
885 "err": "InstructionError",
886 "logs": ["Some error log"],
887 "accounts": null,
888 "unitsConsumed": 0
889 }
890 }),
891 );
892 let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
893
894 let result = VersionedTransactionResolved::from_transaction(
895 &transaction,
896 &config,
897 &rpc_client,
898 true,
899 )
900 .await;
901
902 assert!(result.is_err());
905
906 match result {
907 Err(KoraError::RpcError(msg)) => {
908 assert!(msg.contains("Failed to simulate transaction"));
909 }
910 Err(KoraError::InvalidTransaction(msg)) => {
911 assert!(msg.contains("inner instructions fetching failed"));
912 }
913 _ => panic!("Expected RpcError or InvalidTransaction"),
914 }
915 }
916
917 #[tokio::test]
918 async fn test_fetch_inner_instructions_with_inner_instructions() {
919 let config = setup_test_config();
920 let _m = setup_config_mock(config);
921
922 let keypair = Keypair::new();
923 let instruction = Instruction::new_with_bytes(
924 Pubkey::new_unique(),
925 &[1, 2, 3],
926 vec![AccountMeta::new(keypair.pubkey(), true)],
927 );
928 let message =
929 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
930 let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
931
932 let inner_instruction_data = bs58::encode(&[10, 20, 30]).into_string();
934 let mut mocks = HashMap::new();
935 mocks.insert(
936 RpcRequest::SimulateTransaction,
937 json!({
938 "context": { "slot": 1 },
939 "value": {
940 "err": null,
941 "logs": [],
942 "accounts": null,
943 "unitsConsumed": 1000,
944 "innerInstructions": [
945 {
946 "index": 0,
947 "instructions": [
948 {
949 "programIdIndex": 1,
950 "accounts": [0],
951 "data": inner_instruction_data
952 }
953 ]
954 }
955 ]
956 }
957 }),
958 );
959 let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
960
961 let mut resolved =
962 VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
963 let inner_instructions =
964 resolved.fetch_inner_instructions(&rpc_client, true).await.unwrap();
965
966 assert_eq!(inner_instructions.len(), 1);
967 assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
968 }
969
970 #[tokio::test]
971 async fn test_fetch_inner_instructions_with_sig_verify_false() {
972 let config = setup_test_config();
973 let _m = setup_config_mock(config);
974
975 let keypair = Keypair::new();
976 let instruction = Instruction::new_with_bytes(
977 Pubkey::new_unique(),
978 &[1, 2, 3],
979 vec![AccountMeta::new(keypair.pubkey(), true)],
980 );
981 let message =
982 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
983 let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
984
985 let inner_instruction_data = bs58::encode(&[10, 20, 30]).into_string();
987 let mut mocks = HashMap::new();
988 mocks.insert(
989 RpcRequest::SimulateTransaction,
990 json!({
991 "context": { "slot": 1 },
992 "value": {
993 "err": null,
994 "logs": [],
995 "accounts": null,
996 "unitsConsumed": 1000,
997 "innerInstructions": [
998 {
999 "index": 0,
1000 "instructions": [
1001 {
1002 "programIdIndex": 1,
1003 "accounts": [0],
1004 "data": inner_instruction_data
1005 }
1006 ]
1007 }
1008 ]
1009 }
1010 }),
1011 );
1012 let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();
1013
1014 let mut resolved =
1015 VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
1016 let inner_instructions =
1017 resolved.fetch_inner_instructions(&rpc_client, false).await.unwrap();
1018
1019 assert_eq!(inner_instructions.len(), 1);
1020 assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
1021 }
1022
1023 #[tokio::test]
1024 async fn test_get_or_parse_system_instructions() {
1025 let config = setup_test_config();
1026 let _m = setup_config_mock(config);
1027
1028 let keypair = Keypair::new();
1029 let recipient = Pubkey::new_unique();
1030
1031 let instruction =
1033 solana_system_interface::instruction::transfer(&keypair.pubkey(), &recipient, 1000000);
1034 let message =
1035 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
1036 let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
1037
1038 let mut resolved =
1039 VersionedTransactionResolved::from_kora_built_transaction(&transaction).unwrap();
1040
1041 let parsed1_len = {
1043 let parsed1 = resolved.get_or_parse_system_instructions().unwrap();
1044 assert!(!parsed1.is_empty());
1045 parsed1.len()
1046 };
1047
1048 let parsed2 = resolved.get_or_parse_system_instructions().unwrap();
1050 assert_eq!(parsed1_len, parsed2.len());
1051
1052 assert!(
1054 parsed2.contains_key(&crate::transaction::ParsedSystemInstructionType::SystemTransfer)
1055 );
1056 }
1057
1058 #[tokio::test]
1059 async fn test_resolve_lookup_table_addresses() {
1060 let config = setup_test_config();
1061 let _m = setup_config_mock(config.clone());
1062
1063 let lookup_account_key = Pubkey::new_unique();
1064 let address1 = Pubkey::new_unique();
1065 let address2 = Pubkey::new_unique();
1066 let address3 = Pubkey::new_unique();
1067
1068 let lookup_table = AddressLookupTable {
1069 meta: LookupTableMeta {
1070 deactivation_slot: u64::MAX,
1071 last_extended_slot: 0,
1072 last_extended_slot_start_index: 0,
1073 authority: Some(Pubkey::new_unique()),
1074 _padding: 0,
1075 },
1076 addresses: vec![address1, address2, address3].into(),
1077 };
1078
1079 let serialized_data = lookup_table.serialize_for_tests().unwrap();
1080
1081 let rpc_client = RpcMockBuilder::new()
1082 .with_account_info(&Account {
1083 data: serialized_data,
1084 executable: false,
1085 lamports: 0,
1086 owner: Pubkey::new_unique(),
1087 rent_epoch: 0,
1088 })
1089 .build();
1090
1091 let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1092 account_key: lookup_account_key,
1093 writable_indexes: vec![0, 2], readonly_indexes: vec![1], }];
1096
1097 let resolved_addresses =
1098 LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups)
1099 .await
1100 .unwrap();
1101
1102 assert_eq!(resolved_addresses.len(), 3);
1103 assert_eq!(resolved_addresses[0], address1);
1104 assert_eq!(resolved_addresses[1], address3);
1105 assert_eq!(resolved_addresses[2], address2);
1106 }
1107
1108 #[tokio::test]
1109 async fn test_resolve_lookup_table_addresses_empty() {
1110 let config = setup_test_config();
1111 let _m = setup_config_mock(config.clone());
1112
1113 let rpc_client = RpcMockBuilder::new().with_account_not_found().build();
1114 let lookups = vec![];
1115
1116 let resolved_addresses =
1117 LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups)
1118 .await
1119 .unwrap();
1120
1121 assert_eq!(resolved_addresses.len(), 0);
1122 }
1123
1124 #[tokio::test]
1125 async fn test_resolve_lookup_table_addresses_account_not_found() {
1126 let config = setup_test_config();
1127 let _m = setup_config_mock(config.clone());
1128
1129 let rpc_client = RpcMockBuilder::new().with_account_not_found().build();
1130 let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1131 account_key: Pubkey::new_unique(),
1132 writable_indexes: vec![0],
1133 readonly_indexes: vec![],
1134 }];
1135
1136 let result =
1137 LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups).await;
1138 assert!(matches!(result, Err(KoraError::RpcError(_))));
1139
1140 if let Err(KoraError::RpcError(msg)) = result {
1141 assert!(msg.contains("Failed to fetch lookup table"));
1142 }
1143 }
1144
1145 #[tokio::test]
1146 async fn test_resolve_lookup_table_addresses_invalid_index() {
1147 let config = setup_test_config();
1148 let _m = setup_config_mock(config.clone());
1149
1150 let lookup_account_key = Pubkey::new_unique();
1151 let address1 = Pubkey::new_unique();
1152
1153 let lookup_table = AddressLookupTable {
1154 meta: LookupTableMeta {
1155 deactivation_slot: u64::MAX,
1156 last_extended_slot: 0,
1157 last_extended_slot_start_index: 0,
1158 authority: Some(Pubkey::new_unique()),
1159 _padding: 0,
1160 },
1161 addresses: vec![address1].into(), };
1163
1164 let serialized_data = lookup_table.serialize_for_tests().unwrap();
1165 let rpc_client = RpcMockBuilder::new()
1166 .with_account_info(&Account {
1167 data: serialized_data,
1168 executable: false,
1169 lamports: 0,
1170 owner: Pubkey::new_unique(),
1171 rent_epoch: 0,
1172 })
1173 .build();
1174
1175 let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1177 account_key: lookup_account_key,
1178 writable_indexes: vec![1], readonly_indexes: vec![],
1180 }];
1181
1182 let result =
1183 LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups).await;
1184 assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
1185
1186 if let Err(KoraError::InvalidTransaction(msg)) = result {
1187 assert!(msg.contains("index 1 out of bounds"));
1188 assert!(msg.contains("writable addresses"));
1189 }
1190 }
1191
1192 #[tokio::test]
1193 async fn test_resolve_lookup_table_addresses_invalid_readonly_index() {
1194 let config = setup_test_config();
1195 let _m = setup_config_mock(config.clone());
1196
1197 let lookup_account_key = Pubkey::new_unique();
1198 let address1 = Pubkey::new_unique();
1199
1200 let lookup_table = AddressLookupTable {
1201 meta: LookupTableMeta {
1202 deactivation_slot: u64::MAX,
1203 last_extended_slot: 0,
1204 last_extended_slot_start_index: 0,
1205 authority: Some(Pubkey::new_unique()),
1206 _padding: 0,
1207 },
1208 addresses: vec![address1].into(),
1209 };
1210
1211 let serialized_data = lookup_table.serialize_for_tests().unwrap();
1212 let rpc_client = RpcMockBuilder::new()
1213 .with_account_info(&Account {
1214 data: serialized_data,
1215 executable: false,
1216 lamports: 0,
1217 owner: Pubkey::new_unique(),
1218 rent_epoch: 0,
1219 })
1220 .build();
1221
1222 let lookups = vec![solana_message::v0::MessageAddressTableLookup {
1223 account_key: lookup_account_key,
1224 writable_indexes: vec![],
1225 readonly_indexes: vec![5], }];
1227
1228 let result =
1229 LookupTableUtil::resolve_lookup_table_addresses(&config, &rpc_client, &lookups).await;
1230 assert!(matches!(result, Err(KoraError::InvalidTransaction(_))));
1231
1232 if let Err(KoraError::InvalidTransaction(msg)) = result {
1233 assert!(msg.contains("index 5 out of bounds"));
1234 assert!(msg.contains("readonly addresses"));
1235 }
1236 }
1237}