1use solana_sdk::message::VersionedMessage;
33use solana_sdk::pubkey::Pubkey;
34use solana_sdk::transaction::VersionedTransaction;
35use std::collections::HashMap;
36use std::str::FromStr;
37use txgate_core::error::ParseError;
38use txgate_core::{ParsedTx, TxType, U256};
39use txgate_crypto::CurveType;
40
41use crate::Chain;
42
43const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111";
45
46const TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
48
49const TOKEN_2022_PROGRAM_ID: &str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
51
52#[derive(Debug, Clone, Copy, Default)]
61pub struct SolanaParser;
62
63impl SolanaParser {
64 #[must_use]
66 pub const fn new() -> Self {
67 Self
68 }
69
70 fn parse_system_transfer(data: &[u8], accounts: &[Pubkey]) -> Option<(String, u64)> {
72 if data.len() < 12 {
76 return None;
77 }
78
79 let instruction_type =
80 u32::from_le_bytes([*data.first()?, *data.get(1)?, *data.get(2)?, *data.get(3)?]);
81
82 if instruction_type != 2 {
84 return None;
85 }
86
87 let lamports = u64::from_le_bytes([
88 *data.get(4)?,
89 *data.get(5)?,
90 *data.get(6)?,
91 *data.get(7)?,
92 *data.get(8)?,
93 *data.get(9)?,
94 *data.get(10)?,
95 *data.get(11)?,
96 ]);
97
98 let destination = accounts.get(1)?;
100 Some((destination.to_string(), lamports))
101 }
102
103 fn is_token_instruction(program_id: &Pubkey) -> bool {
105 let program_str = program_id.to_string();
106 program_str == TOKEN_PROGRAM_ID || program_str == TOKEN_2022_PROGRAM_ID
107 }
108
109 fn parse_token_transfer(data: &[u8], accounts: &[Pubkey]) -> Option<(String, u64, bool)> {
111 if data.is_empty() {
112 return None;
113 }
114
115 let instruction_type = *data.first()?;
116
117 match instruction_type {
118 3 => {
120 if data.len() < 9 {
121 return None;
122 }
123 let amount = u64::from_le_bytes([
124 *data.get(1)?,
125 *data.get(2)?,
126 *data.get(3)?,
127 *data.get(4)?,
128 *data.get(5)?,
129 *data.get(6)?,
130 *data.get(7)?,
131 *data.get(8)?,
132 ]);
133 let destination = accounts.get(1)?;
135 Some((destination.to_string(), amount, false))
136 }
137 12 => {
139 if data.len() < 10 {
140 return None;
141 }
142 let amount = u64::from_le_bytes([
143 *data.get(1)?,
144 *data.get(2)?,
145 *data.get(3)?,
146 *data.get(4)?,
147 *data.get(5)?,
148 *data.get(6)?,
149 *data.get(7)?,
150 *data.get(8)?,
151 ]);
152 let destination = accounts.get(2)?;
154 Some((destination.to_string(), amount, true))
155 }
156 _ => None,
157 }
158 }
159
160 #[allow(dead_code)]
163 fn _determine_tx_type(message: &solana_sdk::message::Message) -> TxType {
164 let system_program = Pubkey::from_str(SYSTEM_PROGRAM_ID).ok();
165 let token_program = Pubkey::from_str(TOKEN_PROGRAM_ID).ok();
166 let token_2022_program = Pubkey::from_str(TOKEN_2022_PROGRAM_ID).ok();
167
168 for instruction in &message.instructions {
169 let program_idx = instruction.program_id_index as usize;
170 let program_id = message.account_keys.get(program_idx);
171
172 if let Some(program_id) = program_id {
173 if system_program.as_ref() == Some(program_id) && !instruction.data.is_empty() {
175 let instr_type = u32::from_le_bytes([
176 instruction.data.first().copied().unwrap_or(0),
177 instruction.data.get(1).copied().unwrap_or(0),
178 instruction.data.get(2).copied().unwrap_or(0),
179 instruction.data.get(3).copied().unwrap_or(0),
180 ]);
181 if instr_type == 2 {
182 return TxType::Transfer;
183 }
184 }
185
186 if (token_program.as_ref() == Some(program_id)
188 || token_2022_program.as_ref() == Some(program_id))
189 && !instruction.data.is_empty()
190 {
191 let instr_type = instruction.data.first().copied().unwrap_or(0);
192 if instr_type == 3 || instr_type == 12 {
193 return TxType::TokenTransfer;
194 }
195 if instr_type == 4 {
196 return TxType::TokenApproval;
197 }
198 }
199 }
200 }
201
202 TxType::ContractCall
204 }
205
206 fn get_account_keys(message: &VersionedMessage) -> &[Pubkey] {
208 match message {
209 VersionedMessage::Legacy(msg) => &msg.account_keys,
210 VersionedMessage::V0(msg) => &msg.account_keys,
211 }
212 }
213
214 fn get_instructions(message: &VersionedMessage) -> Vec<(Pubkey, Vec<Pubkey>, Vec<u8>)> {
216 let account_keys = Self::get_account_keys(message);
217
218 let instructions = match message {
219 VersionedMessage::Legacy(msg) => &msg.instructions,
220 VersionedMessage::V0(msg) => &msg.instructions,
221 };
222
223 instructions
224 .iter()
225 .filter_map(|instr| {
226 let program_id = account_keys.get(instr.program_id_index as usize)?;
227 let accounts: Vec<Pubkey> = instr
228 .accounts
229 .iter()
230 .filter_map(|&idx| account_keys.get(idx as usize).copied())
231 .collect();
232 Some((*program_id, accounts, instr.data.clone()))
233 })
234 .collect()
235 }
236}
237
238impl Chain for SolanaParser {
239 fn id(&self) -> &'static str {
240 "solana"
241 }
242
243 fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
244 if raw.is_empty() {
245 return Err(ParseError::MalformedTransaction {
246 context: "empty transaction data".to_string(),
247 });
248 }
249
250 let tx: VersionedTransaction =
252 bincode::deserialize(raw).map_err(|e| ParseError::MalformedTransaction {
253 context: format!("failed to decode Solana transaction: {e}"),
254 })?;
255
256 let message_bytes = tx.message.serialize();
258 let message_hash = solana_sdk::hash::hash(&message_bytes);
259 let mut hash = [0u8; 32];
260 hash.copy_from_slice(message_hash.as_ref());
261
262 let account_keys = Self::get_account_keys(&tx.message);
264
265 let fee_payer = account_keys.first().map(Pubkey::to_string);
267
268 let recent_blockhash = match &tx.message {
270 VersionedMessage::Legacy(msg) => msg.recent_blockhash,
271 VersionedMessage::V0(msg) => msg.recent_blockhash,
272 };
273
274 let instructions = Self::get_instructions(&tx.message);
276
277 let mut recipient = None;
278 let mut amount: Option<u64> = None;
279 let mut token_address = None;
280 let mut tx_type = TxType::ContractCall;
281
282 let system_program = Pubkey::from_str(SYSTEM_PROGRAM_ID).ok();
283
284 for (program_id, accounts, data) in &instructions {
285 if system_program.as_ref() == Some(program_id) {
287 if let Some((dest, lamports)) = Self::parse_system_transfer(data, accounts) {
288 recipient = Some(dest);
289 amount = Some(lamports);
290 tx_type = TxType::Transfer;
291 break;
292 }
293 }
294
295 if Self::is_token_instruction(program_id) {
297 if let Some((dest, amt, _checked)) = Self::parse_token_transfer(data, accounts) {
298 recipient = Some(dest);
299 amount = Some(amt);
300 token_address = Some(program_id.to_string());
301 tx_type = TxType::TokenTransfer;
302 break;
303 }
304 }
305 }
306
307 let mut metadata = HashMap::new();
309
310 if let Some(ref payer) = fee_payer {
312 metadata.insert(
313 "fee_payer".to_string(),
314 serde_json::Value::String(payer.clone()),
315 );
316 }
317
318 metadata.insert(
320 "recent_blockhash".to_string(),
321 serde_json::Value::String(recent_blockhash.to_string()),
322 );
323
324 metadata.insert(
326 "signature_count".to_string(),
327 serde_json::Value::Number(tx.signatures.len().into()),
328 );
329
330 metadata.insert(
332 "instruction_count".to_string(),
333 serde_json::Value::Number(instructions.len().into()),
334 );
335
336 let version = match &tx.message {
338 VersionedMessage::Legacy(_) => "legacy",
339 VersionedMessage::V0(_) => "v0",
340 };
341 metadata.insert(
342 "message_version".to_string(),
343 serde_json::Value::String(version.to_string()),
344 );
345
346 let programs: Vec<serde_json::Value> = instructions
348 .iter()
349 .map(|(program_id, _, _)| serde_json::Value::String(program_id.to_string()))
350 .collect();
351 metadata.insert("programs".to_string(), serde_json::Value::Array(programs));
352
353 Ok(ParsedTx {
354 hash,
355 recipient,
356 amount: amount.map(U256::from),
357 token: if token_address.is_some() {
358 None } else {
360 Some("SOL".to_string())
361 },
362 token_address,
363 tx_type,
364 chain: "solana".to_string(),
365 nonce: None, chain_id: None,
367 metadata,
368 })
369 }
370
371 fn curve(&self) -> CurveType {
372 CurveType::Ed25519
373 }
374
375 fn supports_version(&self, version: u8) -> bool {
376 version == 0 || version >= 128
378 }
379}
380
381#[cfg(test)]
386mod tests {
387 #![allow(
388 clippy::expect_used,
389 clippy::unwrap_used,
390 clippy::panic,
391 clippy::indexing_slicing
392 )]
393
394 use super::*;
395
396 #[test]
397 fn test_solana_parser_id() {
398 let parser = SolanaParser::new();
399 assert_eq!(parser.id(), "solana");
400 }
401
402 #[test]
403 fn test_solana_parser_curve() {
404 let parser = SolanaParser::new();
405 assert_eq!(parser.curve(), CurveType::Ed25519);
406 }
407
408 #[test]
409 fn test_solana_parser_default() {
410 let parser = SolanaParser::default();
411 assert_eq!(parser.id(), "solana");
412 }
413
414 #[test]
415 fn test_solana_parser_empty_input() {
416 let parser = SolanaParser::new();
417 let result = parser.parse(&[]);
418
419 assert!(result.is_err());
420 assert!(matches!(
421 result,
422 Err(ParseError::MalformedTransaction { .. })
423 ));
424 }
425
426 #[test]
427 fn test_solana_parser_invalid_input() {
428 let parser = SolanaParser::new();
429 let result = parser.parse(&[0x00, 0x01, 0x02]);
430
431 assert!(result.is_err());
432 assert!(matches!(
433 result,
434 Err(ParseError::MalformedTransaction { .. })
435 ));
436 }
437
438 #[test]
439 fn test_solana_parser_supports_version() {
440 let parser = SolanaParser::new();
441
442 assert!(parser.supports_version(0)); assert!(parser.supports_version(128)); assert!(parser.supports_version(129)); assert!(!parser.supports_version(1)); assert!(!parser.supports_version(127)); }
448
449 #[test]
450 fn test_solana_parser_is_send_sync() {
451 fn assert_send_sync<T: Send + Sync>() {}
452 assert_send_sync::<SolanaParser>();
453 }
454
455 #[test]
456 fn test_solana_parser_clone() {
457 let parser = SolanaParser::new();
458 let cloned = parser;
459 assert_eq!(parser.id(), cloned.id());
460 }
461
462 #[test]
463 fn test_solana_parser_debug() {
464 let parser = SolanaParser::new();
465 let debug_str = format!("{parser:?}");
466 assert!(debug_str.contains("SolanaParser"));
467 }
468
469 use solana_sdk::hash::Hash;
474 use solana_sdk::instruction::{AccountMeta, Instruction};
475 use solana_sdk::signature::Keypair;
476 use solana_sdk::signer::Signer;
477 use solana_sdk::system_instruction;
478 use solana_sdk::transaction::Transaction;
479
480 fn create_sol_transfer_tx(lamports: u64) -> Vec<u8> {
482 let from = Keypair::new();
483 let to = Pubkey::new_unique();
484 let recent_blockhash = Hash::new_unique();
485
486 let instruction = system_instruction::transfer(&from.pubkey(), &to, lamports);
487 let tx = Transaction::new_signed_with_payer(
488 &[instruction],
489 Some(&from.pubkey()),
490 &[&from],
491 recent_blockhash,
492 );
493
494 bincode::serialize(&tx).expect("failed to serialize transaction")
495 }
496
497 #[test]
498 fn test_parse_sol_transfer() {
499 let parser = SolanaParser::new();
500 let lamports = 1_000_000_000u64; let tx_bytes = create_sol_transfer_tx(lamports);
502
503 let result = parser.parse(&tx_bytes);
504 assert!(result.is_ok(), "Failed to parse SOL transfer: {:?}", result);
505
506 let parsed = result.unwrap();
507 assert_eq!(parsed.chain, "solana");
508 assert!(matches!(parsed.tx_type, TxType::Transfer));
509 assert!(parsed.recipient.is_some());
510 assert!(parsed.amount.is_some());
511 assert_eq!(parsed.amount.unwrap(), U256::from(lamports));
512 assert_eq!(parsed.token, Some("SOL".to_string()));
513 }
514
515 #[test]
516 fn test_parse_sol_transfer_small_amount() {
517 let parser = SolanaParser::new();
518 let lamports = 1u64; let tx_bytes = create_sol_transfer_tx(lamports);
520
521 let result = parser.parse(&tx_bytes);
522 assert!(result.is_ok());
523
524 let parsed = result.unwrap();
525 assert_eq!(parsed.amount.unwrap(), U256::from(1u64));
526 }
527
528 #[test]
529 fn test_parse_sol_transfer_large_amount() {
530 let parser = SolanaParser::new();
531 let lamports = u64::MAX; let tx_bytes = create_sol_transfer_tx(lamports);
533
534 let result = parser.parse(&tx_bytes);
535 assert!(result.is_ok());
536
537 let parsed = result.unwrap();
538 assert_eq!(parsed.amount.unwrap(), U256::from(u64::MAX));
539 }
540
541 #[test]
542 fn test_parse_transaction_metadata() {
543 let parser = SolanaParser::new();
544 let tx_bytes = create_sol_transfer_tx(1_000_000);
545
546 let result = parser.parse(&tx_bytes);
547 assert!(result.is_ok());
548
549 let parsed = result.unwrap();
550 assert!(parsed.metadata.contains_key("fee_payer"));
552 assert!(parsed.metadata.contains_key("recent_blockhash"));
553 assert!(parsed.metadata.contains_key("signature_count"));
554 assert!(parsed.metadata.contains_key("instruction_count"));
555 }
556
557 #[test]
558 fn test_parse_transaction_hash() {
559 let parser = SolanaParser::new();
560 let tx_bytes = create_sol_transfer_tx(1_000_000);
561
562 let result = parser.parse(&tx_bytes);
563 assert!(result.is_ok());
564
565 let parsed = result.unwrap();
566 assert!(!parsed.hash.iter().all(|&b| b == 0));
568 }
569
570 fn create_token_transfer_instruction(amount: u64, owner: &Pubkey) -> Instruction {
573 let token_program = Pubkey::from_str(TOKEN_PROGRAM_ID).unwrap();
574 let source = Pubkey::new_unique();
575 let destination = Pubkey::new_unique();
576
577 let mut data = vec![3u8]; data.extend_from_slice(&amount.to_le_bytes());
580
581 Instruction {
582 program_id: token_program,
583 accounts: vec![
584 AccountMeta::new(source, false),
585 AccountMeta::new(destination, false),
586 AccountMeta::new_readonly(*owner, true),
587 ],
588 data,
589 }
590 }
591
592 #[test]
593 fn test_parse_token_transfer() {
594 let parser = SolanaParser::new();
595
596 let from = Keypair::new();
597 let recent_blockhash = Hash::new_unique();
598 let token_amount = 1_000_000u64;
599
600 let instruction = create_token_transfer_instruction(token_amount, &from.pubkey());
602 let tx = Transaction::new_signed_with_payer(
603 &[instruction],
604 Some(&from.pubkey()),
605 &[&from],
606 recent_blockhash,
607 );
608
609 let tx_bytes = bincode::serialize(&tx).expect("failed to serialize");
610 let result = parser.parse(&tx_bytes);
611 assert!(
612 result.is_ok(),
613 "Failed to parse token transfer: {:?}",
614 result
615 );
616
617 let parsed = result.unwrap();
618 assert_eq!(parsed.chain, "solana");
619 assert!(matches!(parsed.tx_type, TxType::TokenTransfer));
620 assert!(parsed.recipient.is_some());
621 assert!(parsed.amount.is_some());
622 assert_eq!(parsed.amount.unwrap(), U256::from(token_amount));
623 }
624
625 fn create_transfer_checked_instruction(
628 amount: u64,
629 decimals: u8,
630 owner: &Pubkey,
631 ) -> Instruction {
632 let token_program = Pubkey::from_str(TOKEN_PROGRAM_ID).unwrap();
633 let source = Pubkey::new_unique();
634 let mint = Pubkey::new_unique();
635 let destination = Pubkey::new_unique();
636
637 let mut data = vec![12u8]; data.extend_from_slice(&amount.to_le_bytes());
640 data.push(decimals);
641
642 Instruction {
643 program_id: token_program,
644 accounts: vec![
645 AccountMeta::new(source, false),
646 AccountMeta::new_readonly(mint, false),
647 AccountMeta::new(destination, false),
648 AccountMeta::new_readonly(*owner, true),
649 ],
650 data,
651 }
652 }
653
654 #[test]
655 fn test_parse_transfer_checked() {
656 let parser = SolanaParser::new();
657
658 let from = Keypair::new();
659 let recent_blockhash = Hash::new_unique();
660 let token_amount = 500_000u64;
661 let decimals = 6u8;
662
663 let instruction =
665 create_transfer_checked_instruction(token_amount, decimals, &from.pubkey());
666 let tx = Transaction::new_signed_with_payer(
667 &[instruction],
668 Some(&from.pubkey()),
669 &[&from],
670 recent_blockhash,
671 );
672
673 let tx_bytes = bincode::serialize(&tx).expect("failed to serialize");
674 let result = parser.parse(&tx_bytes);
675 assert!(
676 result.is_ok(),
677 "Failed to parse TransferChecked: {:?}",
678 result
679 );
680
681 let parsed = result.unwrap();
682 assert!(matches!(parsed.tx_type, TxType::TokenTransfer));
683 assert_eq!(parsed.amount.unwrap(), U256::from(token_amount));
684 }
685
686 #[test]
687 fn test_parse_system_transfer_helper() {
688 let lamports = 5_000_000_000u64;
690
691 let mut data = vec![2u8, 0, 0, 0]; data.extend_from_slice(&lamports.to_le_bytes());
694
695 let source = Pubkey::new_unique();
696 let destination = Pubkey::new_unique();
697 let accounts = vec![source, destination];
698
699 let result = SolanaParser::parse_system_transfer(&data, &accounts);
700 assert!(result.is_some());
701
702 let (dest, amount) = result.unwrap();
703 assert_eq!(dest, destination.to_string());
704 assert_eq!(amount, lamports);
705 }
706
707 #[test]
708 fn test_parse_system_transfer_wrong_instruction_type() {
709 let mut data = vec![1u8, 0, 0, 0];
711 data.extend_from_slice(&1000u64.to_le_bytes());
712
713 let accounts = vec![Pubkey::new_unique(), Pubkey::new_unique()];
714
715 let result = SolanaParser::parse_system_transfer(&data, &accounts);
716 assert!(result.is_none());
717 }
718
719 #[test]
720 fn test_parse_system_transfer_insufficient_data() {
721 let data = vec![2u8, 0, 0, 0, 0, 0, 0, 0]; let accounts = vec![Pubkey::new_unique(), Pubkey::new_unique()];
725
726 let result = SolanaParser::parse_system_transfer(&data, &accounts);
727 assert!(result.is_none());
728 }
729
730 #[test]
731 fn test_parse_token_transfer_helper() {
732 let amount = 1_000_000u64;
733
734 let mut data = vec![3u8];
736 data.extend_from_slice(&amount.to_le_bytes());
737
738 let accounts = vec![
739 Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), ];
743
744 let result = SolanaParser::parse_token_transfer(&data, &accounts);
745 assert!(result.is_some());
746
747 let (dest, parsed_amount, is_checked) = result.unwrap();
748 assert_eq!(dest, accounts[1].to_string());
749 assert_eq!(parsed_amount, amount);
750 assert!(!is_checked);
751 }
752
753 #[test]
754 fn test_parse_token_transfer_checked_helper() {
755 let amount = 2_000_000u64;
756
757 let mut data = vec![12u8];
759 data.extend_from_slice(&amount.to_le_bytes());
760 data.push(9u8); let accounts = vec![
763 Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), Pubkey::new_unique(), ];
768
769 let result = SolanaParser::parse_token_transfer(&data, &accounts);
770 assert!(result.is_some());
771
772 let (dest, parsed_amount, is_checked) = result.unwrap();
773 assert_eq!(dest, accounts[2].to_string()); assert_eq!(parsed_amount, amount);
775 assert!(is_checked);
776 }
777
778 #[test]
779 fn test_parse_token_transfer_unknown_instruction() {
780 let data = vec![99u8, 0, 0, 0, 0, 0, 0, 0, 0];
782 let accounts = vec![Pubkey::new_unique(), Pubkey::new_unique()];
783
784 let result = SolanaParser::parse_token_transfer(&data, &accounts);
785 assert!(result.is_none());
786 }
787
788 #[test]
789 fn test_is_token_instruction() {
790 let token_program = Pubkey::from_str(TOKEN_PROGRAM_ID).unwrap();
791 let token_2022_program = Pubkey::from_str(TOKEN_2022_PROGRAM_ID).unwrap();
792 let system_program = Pubkey::from_str(SYSTEM_PROGRAM_ID).unwrap();
793 let random_program = Pubkey::new_unique();
794
795 assert!(SolanaParser::is_token_instruction(&token_program));
796 assert!(SolanaParser::is_token_instruction(&token_2022_program));
797 assert!(!SolanaParser::is_token_instruction(&system_program));
798 assert!(!SolanaParser::is_token_instruction(&random_program));
799 }
800}