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