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