diff --git a/Cargo.lock b/Cargo.lock index 0f52b01..5fbe345 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -14,6 +14,56 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + [[package]] name = "ark-bn254" version = "0.4.0" @@ -155,6 +205,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -371,6 +427,39 @@ dependencies = [ "num-traits", ] +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -744,6 +833,12 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -1975,7 +2070,9 @@ dependencies = [ name = "solana_parser" version = "0.1.0" dependencies = [ + "base64 0.13.1", "bincode", + "clap", "hex", "log", "solana-program", @@ -2099,6 +2196,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2187,6 +2290,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index a33fd89..91f8861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ solana-sdk = "2.0.3" solana-program = "2.0.3" bincode = "1.3.3" hex = "0.4.3" +base64 = "0.13.0" +clap = "4.5.31" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 453ba4e..8c81d6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,46 +1,64 @@ -use std::env; - mod solana; use crate::solana::parser::parse_transaction; use crate::solana::structs::SolanaParsedTransactionPayload; +use clap::{Arg, Command}; fn main() { - let args: Vec = env::args().collect(); - - if args.len() != 4 { - println!("Usage: `cargo run parse --message OR cargo run parse --transaction`"); - return; - } + let matches = Command::new("solana-parser") + .subcommand( + Command::new("parse") + .arg( + Arg::new("format") + .long("format") + .value_parser(["message", "transaction"]) + .required(true), + ) + .arg( + Arg::new("input") + .required(true) + .help("The transaction/message to parse"), + ) + .arg( + Arg::new("encoding") + .long("encoding") + .value_parser(["base64", "hex"]) + .default_value("hex"), + ), + ) + .get_matches(); + + if let Some(parse_matches) = matches.subcommand_matches("parse") { + let unsigned_tx = parse_matches.get_one::("input").unwrap(); + let is_transaction = parse_matches.get_one::("format").unwrap() == "transaction"; + let encoding = parse_matches.get_one::("encoding").unwrap(); - let command = &args[1]; - match command.as_str() { - "parse" => { - let unsigned_tx = &args[3]; - let flag = if args.len() > 3 { Some(&args[2]) } else { None }; - match flag { - Some(flag) if flag == "--message" || flag == "--transaction" => { - let is_transaction = flag == "--transaction"; - let result = parse_transaction(unsigned_tx.to_string(), is_transaction); - match result { - Ok(response) => { - print_parsed_transaction(response.solana_parsed_transaction.payload.unwrap()); - }, - Err(e) => println!("Error: {}", e), - } - } - _ => { - println!("Invalid or missing flag. Use either --message or --transaction."); - } - } + println!("Parsing transaction: {}", unsigned_tx); + println!( + "Format: {}", + if is_transaction { + "transaction" + } else { + "message" } - _ => println!("Unknown command: {}", command), + ); + println!("Encoding: {}", encoding); + + match parse_transaction( + unsigned_tx.to_string(), + is_transaction, + encoding.to_string(), + ) { + Ok(response) => { + print_parsed_transaction(response.solana_parsed_transaction.payload.unwrap()); + } + Err(e) => println!("Error: {}", e), } + } } fn print_parsed_transaction(transaction_payload: SolanaParsedTransactionPayload) { println!("Solana Parsed Transaction Payload:"); - println!(" Unsigned Payload: {}", transaction_payload.unsigned_payload); if let Some(metadata) = transaction_payload.transaction_metadata { println!(" Transaction Metadata:"); println!(" Signatures: {:?}", metadata.signatures); @@ -52,8 +70,14 @@ fn print_parsed_transaction(transaction_payload: SolanaParsedTransactionPayload) println!(" Instruction {}:", i + 1); println!(" Program Key: {}", instruction.program_key); println!(" Accounts: {:?}", instruction.accounts); - println!(" Instruction Data (hex): {}", instruction.instruction_data_hex); - println!(" Address Table Lookups: {:?}", instruction.address_table_lookups); + println!( + " Instruction Data (hex): {}", + instruction.instruction_data_hex + ); + println!( + " Address Table Lookups: {:?}", + instruction.address_table_lookups + ); } println!(" Transfers:"); for (i, transfer) in metadata.transfers.iter().enumerate() { @@ -82,6 +106,9 @@ fn print_parsed_transaction(transaction_payload: SolanaParsedTransactionPayload) println!(" Fee: {}", fee); } } - println!(" Address Table Lookups: {:?}", metadata.address_table_lookups); + println!( + " Address Table Lookups: {:?}", + metadata.address_table_lookups + ); } } diff --git a/src/solana/parser.rs b/src/solana/parser.rs index 2f02992..57142a4 100644 --- a/src/solana/parser.rs +++ b/src/solana/parser.rs @@ -1,4 +1,8 @@ -use std::error::Error; +use super::structs::{ + SolTransfer, SolanaAccount, SolanaAddressTableLookup, SolanaInstruction, SolanaMetadata, + SolanaParseResponse, SolanaParsedTransaction, SolanaParsedTransactionPayload, + SolanaSingleAddressTableLookup, SplTransfer, +}; use hex; use solana_sdk::{ hash::Hash, @@ -8,9 +12,9 @@ use solana_sdk::{ Message as LegacyMessage, MessageHeader, VersionedMessage, }, pubkey::Pubkey, - system_instruction::SystemInstruction, + system_instruction::SystemInstruction, }; -use super::structs::{SolTransfer, SolanaAccount, SolanaAddressTableLookup, SolanaInstruction, SolanaMetadata, SolanaParseResponse, SolanaParsedTransaction, SolanaParsedTransactionPayload, SolanaSingleAddressTableLookup, SplTransfer}; +use std::error::Error; // Length of a solana signature in bytes (64 bytes long) pub const LEN_SOL_SIGNATURE_BYTES: usize = 64; @@ -22,7 +26,7 @@ pub const LEN_ARRAY_HEADER_BYTES: usize = 1; pub const LEN_MESSAGE_HEADER_BYTES: usize = 3; // This is a string representation of the account address of the Solana System Program -- the main native program that "owns" user accounts and is in charge of facilitating basic SOL transfers among other things pub const SOL_SYSTEM_PROGRAM_KEY: &str = "11111111111111111111111111111111"; -// This is a string representation of the account address of the Token Program -- Used for transferring SPL tokens +// This is a string representation of the account address of the Token Program -- Used for transferring SPL tokens pub const TOKEN_PROGRAM_KEY: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; // This is a string representation of the account address for the Token 2022 Program which is a strict superset of the old Token Program, used to add extra functionality -- Used for transferring SPL tokens pub const TOKEN_2022_PROGRAM_KEY: &str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"; @@ -30,12 +34,21 @@ pub const TOKEN_2022_PROGRAM_KEY: &str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEp const V0_TRANSACTION_INDICATOR: u8 = 0x80; // Entrypoint to parsing -pub fn parse_transaction(unsigned_tx: String, full_transaction: bool) -> Result> { +pub fn parse_transaction( + unsigned_tx: String, + full_transaction: bool, + format: String, +) -> Result> { if unsigned_tx.is_empty() { return Err("Transaction is empty".into()); } - let tx = SolanaTransaction::new(&unsigned_tx, full_transaction).map_err(|e| { + let tx = match format.as_str() { + "hex" => SolanaTransaction::new(&unsigned_tx, full_transaction), + "base64" => SolanaTransaction::from_base64(&unsigned_tx, full_transaction), + _ => return Err("Unsupported format".into()), + } + .map_err(|e| { Box::::from(format!("Unable to parse transaction: {}", e)) })?; @@ -71,17 +84,22 @@ fn parse_solana_transaction( if full_transaction { let (signatures, tx_body) = parse_signatures(unsigned_tx_bytes)?; let message = match tx_body[0] { - V0_TRANSACTION_INDICATOR => parse_solana_v0_transaction(&tx_body[LEN_ARRAY_HEADER_BYTES..tx_body.len()]).map_err(|e| format!("Error parsing full transaction. If this is just a message instead of a full transaction, parse using the --message flag. Parsing Error: {:#?}", e))?, - _ => parse_solana_legacy_transaction(tx_body).map_err(|e| format!("Error parsing full transaction. If this is just a message instead of a full transaction, parse using the --message flag. Parsing Error: {:#?}", e))?, + V0_TRANSACTION_INDICATOR => parse_solana_v0_transaction(&tx_body[LEN_ARRAY_HEADER_BYTES..tx_body.len()]).map_err(|e| format!("Error parsing full transaction. If this is just a message instead of a full transaction, parse using the --format message flag. Parsing Error: {:#?}", e))?, + _ => parse_solana_legacy_transaction(tx_body).map_err(|e| format!("Error parsing full transaction. If this is just a message instead of a full transaction, parse using the --format message flag. Parsing Error: {:#?}", e))?, }; - return Ok(SolanaTransaction{ message, signatures }); + return Ok(SolanaTransaction { + message, + signatures, + }); } let message = match unsigned_tx_bytes[0] { - V0_TRANSACTION_INDICATOR => parse_solana_v0_transaction(&unsigned_tx_bytes[LEN_ARRAY_HEADER_BYTES..unsigned_tx_bytes.len()]).map_err(|e| format!("Error parsing message. If this is a serialized Solana transaction with signatures, parse using the --transaction flag. Parsing error: {:#?}", e))?, - _ => parse_solana_legacy_transaction(unsigned_tx_bytes).map_err(|e| format!("Error parsing message. If this is a full solana transaction with signatures or signature placeholders, parse using the --transaction flag. Parsing Error: {:#?}", e))?, + V0_TRANSACTION_INDICATOR => parse_solana_v0_transaction(&unsigned_tx_bytes[LEN_ARRAY_HEADER_BYTES..unsigned_tx_bytes.len()]).map_err(|e| format!("Error parsing message. If this is a serialized Solana transaction with signatures, parse using the --format transaction flag. Parsing error: {:#?}", e))?, + _ => parse_solana_legacy_transaction(unsigned_tx_bytes).map_err(|e| format!("Error parsing message. If this is a full solana transaction with signatures or signature placeholders, parse using the --format transaction flag. Parsing Error: {:#?}", e))?, }; - return Ok(SolanaTransaction{ message, signatures: vec![] }); // Signatures array is empty when we are parsing a message (using --message) as opposed to a full transaction - + return Ok(SolanaTransaction { + message, + signatures: vec![], + }); // Signatures array is empty when we are parsing a message (using --message) as opposed to a full transaction } /* @@ -161,7 +179,7 @@ fn validate_length( /* Parse Signatures -- Context: Solana transactions contain a compact array of signatures at the beginning of a transaction +- Context: Solana transactions contain a compact array of signatures at the beginning of a transaction - This function parses these signatures. - NOTE: This is only relevant for when we are parsing FULL TRANSACTIONS (using the flag --transasction) not when we are parsing only the message (using --message) */ @@ -181,7 +199,10 @@ fn parse_signatures( .take(num_signatures) .map(<[u8]>::to_vec) .collect(); - Ok((signatures, &unsigned_tx_bytes[parse_len..unsigned_tx_bytes.len()])) + Ok(( + signatures, + &unsigned_tx_bytes[parse_len..unsigned_tx_bytes.len()], + )) } /* @@ -389,7 +410,9 @@ Read Compact u16 - FINALLY -- if the final format is zzyyyyyyyxxxxxxx it is represented in compactu16 as 1xxxxxxx1yyyyyyy000000zz - In a 3 byte representation, the first byte has the 7 least significant digits, and the 3rd byte has the two most significant digits of the final u16 */ -fn read_compact_u16(tx_body_remainder: &[u8]) -> Result<(usize, &[u8]), Box> { +fn read_compact_u16( + tx_body_remainder: &[u8], +) -> Result<(usize, &[u8]), Box> { let mut value = 0u16; let mut shift = 0; let mut bytes_read = 0; @@ -417,7 +440,10 @@ fn read_compact_u16(tx_body_remainder: &[u8]) -> Result<(usize, &[u8]), Box Result<(usize, &[u8]), Box Result> { - let (&tag, rest) = instruction_data.split_first().ok_or("Error while parsing spl instruction data header")?; + fn parse_spl_transfer_data( + instruction_data: &[u8], + ) -> Result> { + let (&tag, rest) = instruction_data + .split_first() + .ok_or("Error while parsing spl instruction data header")?; Ok(match tag { 3 => { - let (amount, _rest) = unpack_u64(rest).map_err(|_| format!("Error while parsing spl instruction Transfer -- amount"))?; + let (amount, _rest) = unpack_u64(rest).map_err(|_| { + format!("Error while parsing spl instruction Transfer -- amount") + })?; Self::Transfer { amount } } 12 => { - let (amount, rest) = unpack_u64(rest).map_err(|_| format!("Error while parsing spl instruction TransferChecked -- amount"))?; - let (&decimals, _rest) = rest.split_first().ok_or("Error while parsing spl instruction TransferChecked -- decimals")?; + let (amount, rest) = unpack_u64(rest).map_err(|_| { + format!("Error while parsing spl instruction TransferChecked -- amount") + })?; + let (&decimals, _rest) = rest + .split_first() + .ok_or("Error while parsing spl instruction TransferChecked -- decimals")?; Self::TransferChecked { amount, decimals } } 26 => { @@ -454,12 +490,20 @@ impl SplInstructionData { 1 => { let (amount, rest) = unpack_u64(rest).map_err(|_| format!("Error while parsing spl instruction TransferCheckedWithFee -- amount"))?; let (&decimals, rest) = rest.split_first().ok_or("Error while parsing spl instruction TransferCheckedWithFee -- decimals")?; - let (fee, _rest) = unpack_u64(rest).map_err(|_| format!("Error while parsing spl instruction TransferCheckedWithFee -- fee"))?; + let (fee, _rest) = unpack_u64(rest).map_err(|_| { + format!( + "Error while parsing spl instruction TransferCheckedWithFee -- fee" + ) + })?; (amount, decimals, fee) } _ => return Ok(Self::Unsupported), }; - Self::TransferCheckedWithFee { amount, decimals, fee } + Self::TransferCheckedWithFee { + amount, + decimals, + fee, + } } _ => Self::Unsupported, }) @@ -488,6 +532,12 @@ impl SolanaTransaction { parse_solana_transaction(hex_tx, full_transaction) } + pub fn from_base64(base64_tx: &str, full_transaction: bool) -> Result> { + let bytes_tx = base64::decode(base64_tx)?; + let hex_tx = hex::encode(bytes_tx); + parse_solana_transaction(&hex_tx, full_transaction) + } + fn all_account_key_strings(&self) -> Vec { self.message .static_account_keys() @@ -584,7 +634,10 @@ impl SolanaTransaction { fn all_instructions_and_transfers( &self, - ) -> Result<(Vec, Vec, Vec), Box> { + ) -> Result< + (Vec, Vec, Vec), + Box, + > { let mut instructions: Vec = vec![]; let mut transfers: Vec = vec![]; let mut spl_transfers: Vec = vec![]; @@ -614,10 +667,10 @@ impl SolanaTransaction { match program_key.as_str() { SOL_SYSTEM_PROGRAM_KEY => { let system_instruction: SystemInstruction = bincode::deserialize(&i.data) - .map_err(|_| "Could not parse system instruction")?; + .map_err(|_| "Could not parse system instruction")?; if let SystemInstruction::Transfer { lamports } = system_instruction { if accounts.len() != 2 { - return Err("System Program Transfer Instruction should have exactly 2 arguments".into()) + return Err("System Program Transfer Instruction should have exactly 2 arguments".into()); } let transfer = SolTransfer { amount: lamports.to_string(), @@ -628,16 +681,22 @@ impl SolanaTransaction { } } TOKEN_PROGRAM_KEY => { - let token_program_instruction: SplInstructionData = SplInstructionData::parse_spl_transfer_data(&i.data)?; - let spl_tranfer_opt = self.parse_spl_instruction_data(token_program_instruction, accounts.clone())?; + let token_program_instruction: SplInstructionData = + SplInstructionData::parse_spl_transfer_data(&i.data)?; + let spl_tranfer_opt = self + .parse_spl_instruction_data(token_program_instruction, accounts.clone())?; match spl_tranfer_opt { Some(spl_transfer) => spl_transfers.push(spl_transfer), None => (), } } TOKEN_2022_PROGRAM_KEY => { - let token_program_22_instruction: SplInstructionData = SplInstructionData::parse_spl_transfer_data(&i.data)?; - let spl_tranfer_opt = self.parse_spl_instruction_data(token_program_22_instruction, accounts.clone())?; + let token_program_22_instruction: SplInstructionData = + SplInstructionData::parse_spl_transfer_data(&i.data)?; + let spl_tranfer_opt = self.parse_spl_instruction_data( + token_program_22_instruction, + accounts.clone(), + )?; match spl_tranfer_opt { Some(spl_transfer) => spl_transfers.push(spl_transfer), None => (), @@ -658,7 +717,11 @@ impl SolanaTransaction { } // Parse Instruction to Solana Token Program OR Solana Token Program 2022 and return something if it is an SPL transfer - fn parse_spl_instruction_data(&self, token_instruction: SplInstructionData, accounts: Vec) -> Result, Box> { + fn parse_spl_instruction_data( + &self, + token_instruction: SplInstructionData, + accounts: Vec, + ) -> Result, Box> { if let SplInstructionData::Transfer { amount } = token_instruction { let signers = self.get_spl_multisig_signers_if_exist(&accounts, 3)?; let spl_transfer = SplTransfer { @@ -667,12 +730,12 @@ impl SolanaTransaction { from: accounts[0].account_key.clone(), owner: accounts[2].account_key.clone(), signers, - decimals: None, + decimals: None, fee: None, token_mint: None, }; - return Ok(Some(spl_transfer)) - } else if let SplInstructionData::TransferChecked{ amount, decimals } = token_instruction { + return Ok(Some(spl_transfer)); + } else if let SplInstructionData::TransferChecked { amount, decimals } = token_instruction { let signers = self.get_spl_multisig_signers_if_exist(&accounts, 4)?; let spl_transfer = SplTransfer { amount: amount.to_string(), @@ -684,8 +747,13 @@ impl SolanaTransaction { decimals: Some(decimals.to_string()), fee: None, }; - return Ok(Some(spl_transfer)) - } else if let SplInstructionData::TransferCheckedWithFee { amount, decimals, fee } = token_instruction { + return Ok(Some(spl_transfer)); + } else if let SplInstructionData::TransferCheckedWithFee { + amount, + decimals, + fee, + } = token_instruction + { let signers = self.get_spl_multisig_signers_if_exist(&accounts, 4)?; let spl_transfer = SplTransfer { amount: amount.to_string(), @@ -697,16 +765,27 @@ impl SolanaTransaction { decimals: Some(decimals.to_string()), fee: Some(fee.to_string()), }; - return Ok(Some(spl_transfer)) + return Ok(Some(spl_transfer)); } - return Ok(None) + return Ok(None); } - fn get_spl_multisig_signers_if_exist(&self, accounts: &Vec, num_accts_before_signer: usize) -> Result, Box> { + fn get_spl_multisig_signers_if_exist( + &self, + accounts: &Vec, + num_accts_before_signer: usize, + ) -> Result, Box> { if accounts.len() < num_accts_before_signer { - return Err(format!("Invalid number of accounts provided for spl token transfer instruction").into()) + return Err(format!( + "Invalid number of accounts provided for spl token transfer instruction" + ) + .into()); } - Ok(accounts[num_accts_before_signer..accounts.len()].to_vec().into_iter().map(|a| a.account_key).collect()) + Ok(accounts[num_accts_before_signer..accounts.len()] + .to_vec() + .into_iter() + .map(|a| a.account_key) + .collect()) } fn recent_blockhash(&self) -> String { @@ -737,7 +816,8 @@ impl SolanaTransaction { } fn signatures(&self) -> Result, Box> { - Ok(self.signatures + Ok(self + .signatures .iter() .map(|sig| sig.iter().map(|b| format!("{:02x}", b)).collect::()) .collect()) @@ -772,7 +852,7 @@ mod tests { Ok(unsigned_tx_bytes) } -#[test] + #[test] fn test_read_compact_u16() { // Test compact header with a single byte let test_input_1 = "05FFFFFFFFFF"; @@ -813,4 +893,4 @@ mod tests { "error parsing unsigned transaction: unable to parse compact array header, not enough bytes", ); } -} \ No newline at end of file +} diff --git a/src/solana/structs.rs b/src/solana/structs.rs index 8bbef08..5b0759a 100644 --- a/src/solana/structs.rs +++ b/src/solana/structs.rs @@ -45,8 +45,8 @@ pub struct SplTransfer { pub to: String, pub amount: String, pub owner: String, - pub signers: Vec, // This is an empty array if ths is not a multisig account with multiple signers - pub token_mint: Option, + pub signers: Vec, // This is an empty array if ths is not a multisig account with multiple signers + pub token_mint: Option, pub decimals: Option, pub fee: Option, } diff --git a/src/solana/tests.rs b/src/solana/tests.rs index b576382..d712619 100644 --- a/src/solana/tests.rs +++ b/src/solana/tests.rs @@ -1,894 +1,906 @@ use std::vec; use super::*; +use crate::solana::parser::{SolanaTransaction, TOKEN_2022_PROGRAM_KEY, TOKEN_PROGRAM_KEY}; +use crate::solana::structs::{ + SolTransfer, SolanaAccount, SolanaAddressTableLookup, SolanaInstruction, + SolanaSingleAddressTableLookup, +}; use parser::SOL_SYSTEM_PROGRAM_KEY; use structs::SolanaMetadata; -use crate::solana::structs::{SolanaInstruction, SolanaAccount, SolanaAddressTableLookup, SolanaSingleAddressTableLookup, SolTransfer}; -use crate::solana::parser::{SolanaTransaction, TOKEN_PROGRAM_KEY, TOKEN_2022_PROGRAM_KEY}; - - - #[test] - fn parses_valid_legacy_transactions() { - let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, true).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - // All expected values - let exp_signature = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - let sender_account_key = "3uC8tBZQQA1RCKv9htCngTfYm4JK4ezuYx4M4nFsZQVp"; - let recipient_account_key = "tkhqC9QX2gkqJtUFk2QKhBmQfFyyqZXSpr73VFRi35C"; - let expected_account_keys = vec![ - sender_account_key.to_string(), - recipient_account_key.to_string(), - SOL_SYSTEM_PROGRAM_KEY.to_string(), - ]; - let expected_program_keys = vec![SOL_SYSTEM_PROGRAM_KEY.to_string()]; - let expected_instruction_hex = "020000006f00000000000000"; - let expected_amount_opt = Some("111".to_string()); - let expected_amount = expected_amount_opt.clone().unwrap(); - - // All output checks - assert_eq!(tx_metadata.signatures, vec![exp_signature]); - assert_eq!(tx_metadata.account_keys, expected_account_keys); - assert_eq!(tx_metadata.program_keys, expected_program_keys); - assert_eq!(tx_metadata.instructions.len(), 1); - - // Assert instructions array is correct - let inst = &tx_metadata.instructions[0]; - assert_eq!(inst.program_key, SOL_SYSTEM_PROGRAM_KEY.to_string()); - assert_eq!(inst.accounts.len(), 2); - assert_eq!(inst.accounts[0].account_key, sender_account_key); - assert!(inst.accounts[0].signer); - assert!(inst.accounts[0].writable); - assert_eq!(inst.accounts[1].account_key, recipient_account_key); - assert!(!inst.accounts[1].signer); - assert!(inst.accounts[1].writable); - assert_eq!(inst.instruction_data_hex, expected_instruction_hex); - - // Assert transfers array is correct - assert_eq!(tx_metadata.transfers.len(), 1); - assert_eq!(tx_metadata.transfers[0].amount, expected_amount); - assert_eq!(tx_metadata.transfers[0].from, sender_account_key); - assert_eq!(tx_metadata.transfers[0].to, recipient_account_key); - } - - #[test] - fn parses_valid_legacy_transaction_message_only() { - let unsigned_payload = "010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, false).unwrap(); // check that a message is parsed correctly - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - // All expected values - let sender_account_key = "3uC8tBZQQA1RCKv9htCngTfYm4JK4ezuYx4M4nFsZQVp"; - let recipient_account_key = "tkhqC9QX2gkqJtUFk2QKhBmQfFyyqZXSpr73VFRi35C"; - let expected_account_keys = vec![ - sender_account_key.to_string(), - recipient_account_key.to_string(), - SOL_SYSTEM_PROGRAM_KEY.to_string(), - ]; - let expected_program_keys = vec![SOL_SYSTEM_PROGRAM_KEY.to_string()]; - let expected_instruction_hex = "020000006f00000000000000"; - let expected_amount_opt = Some("111".to_string()); - let expected_amount = expected_amount_opt.clone().unwrap(); - - // All output checks - assert_eq!(tx_metadata.signatures, vec![] as Vec); // Check that there is an empty signature - assert_eq!(tx_metadata.account_keys, expected_account_keys); - assert_eq!(tx_metadata.program_keys, expected_program_keys); - assert_eq!(tx_metadata.instructions.len(), 1); - - // Assert instructions array is correct - let inst = &tx_metadata.instructions[0]; - assert_eq!(inst.program_key, SOL_SYSTEM_PROGRAM_KEY.to_string()); - assert_eq!(inst.accounts.len(), 2); - assert_eq!(inst.accounts[0].account_key, sender_account_key); - assert!(inst.accounts[0].signer); - assert!(inst.accounts[0].writable); - assert_eq!(inst.accounts[1].account_key, recipient_account_key); - assert!(!inst.accounts[1].signer); - assert!(inst.accounts[1].writable); - assert_eq!(inst.instruction_data_hex, expected_instruction_hex); - - // Assert transfers array is correct - assert_eq!(tx_metadata.transfers.len(), 1); - assert_eq!(tx_metadata.transfers[0].amount, expected_amount); - assert_eq!(tx_metadata.transfers[0].from, sender_account_key); - assert_eq!(tx_metadata.transfers[0].to, recipient_account_key); - } - - #[test] - fn parses_invalid_transactions() { - // Invalid bytes, odd number length string - let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, true); - - let inst_error_message = parsed_tx.unwrap_err().to_string(); // Unwrap the error - assert_eq!( - inst_error_message, - "unsigned transaction provided is invalid when converted to bytes" - ); - - // Invalid length for Instruction Data Array - let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f000000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, true); - let inst_error_message = parsed_tx.unwrap_err().to_string(); // Convert to String - assert_eq!( +#[test] +fn parses_valid_legacy_transactions() { + let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, true).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + // All expected values + let exp_signature = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + let sender_account_key = "3uC8tBZQQA1RCKv9htCngTfYm4JK4ezuYx4M4nFsZQVp"; + let recipient_account_key = "tkhqC9QX2gkqJtUFk2QKhBmQfFyyqZXSpr73VFRi35C"; + let expected_account_keys = vec![ + sender_account_key.to_string(), + recipient_account_key.to_string(), + SOL_SYSTEM_PROGRAM_KEY.to_string(), + ]; + let expected_program_keys = vec![SOL_SYSTEM_PROGRAM_KEY.to_string()]; + let expected_instruction_hex = "020000006f00000000000000"; + let expected_amount_opt = Some("111".to_string()); + let expected_amount = expected_amount_opt.clone().unwrap(); + + // All output checks + assert_eq!(tx_metadata.signatures, vec![exp_signature]); + assert_eq!(tx_metadata.account_keys, expected_account_keys); + assert_eq!(tx_metadata.program_keys, expected_program_keys); + assert_eq!(tx_metadata.instructions.len(), 1); + + // Assert instructions array is correct + let inst = &tx_metadata.instructions[0]; + assert_eq!(inst.program_key, SOL_SYSTEM_PROGRAM_KEY.to_string()); + assert_eq!(inst.accounts.len(), 2); + assert_eq!(inst.accounts[0].account_key, sender_account_key); + assert!(inst.accounts[0].signer); + assert!(inst.accounts[0].writable); + assert_eq!(inst.accounts[1].account_key, recipient_account_key); + assert!(!inst.accounts[1].signer); + assert!(inst.accounts[1].writable); + assert_eq!(inst.instruction_data_hex, expected_instruction_hex); + + // Assert transfers array is correct + assert_eq!(tx_metadata.transfers.len(), 1); + assert_eq!(tx_metadata.transfers[0].amount, expected_amount); + assert_eq!(tx_metadata.transfers[0].from, sender_account_key); + assert_eq!(tx_metadata.transfers[0].to, recipient_account_key); +} + +#[test] +fn parses_valid_legacy_transaction_message_only() { + let unsigned_payload = "010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, false).unwrap(); // check that a message is parsed correctly + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + // All expected values + let sender_account_key = "3uC8tBZQQA1RCKv9htCngTfYm4JK4ezuYx4M4nFsZQVp"; + let recipient_account_key = "tkhqC9QX2gkqJtUFk2QKhBmQfFyyqZXSpr73VFRi35C"; + let expected_account_keys = vec![ + sender_account_key.to_string(), + recipient_account_key.to_string(), + SOL_SYSTEM_PROGRAM_KEY.to_string(), + ]; + let expected_program_keys = vec![SOL_SYSTEM_PROGRAM_KEY.to_string()]; + let expected_instruction_hex = "020000006f00000000000000"; + let expected_amount_opt = Some("111".to_string()); + let expected_amount = expected_amount_opt.clone().unwrap(); + + // All output checks + assert_eq!(tx_metadata.signatures, vec![] as Vec); // Check that there is an empty signature + assert_eq!(tx_metadata.account_keys, expected_account_keys); + assert_eq!(tx_metadata.program_keys, expected_program_keys); + assert_eq!(tx_metadata.instructions.len(), 1); + + // Assert instructions array is correct + let inst = &tx_metadata.instructions[0]; + assert_eq!(inst.program_key, SOL_SYSTEM_PROGRAM_KEY.to_string()); + assert_eq!(inst.accounts.len(), 2); + assert_eq!(inst.accounts[0].account_key, sender_account_key); + assert!(inst.accounts[0].signer); + assert!(inst.accounts[0].writable); + assert_eq!(inst.accounts[1].account_key, recipient_account_key); + assert!(!inst.accounts[1].signer); + assert!(inst.accounts[1].writable); + assert_eq!(inst.instruction_data_hex, expected_instruction_hex); + + // Assert transfers array is correct + assert_eq!(tx_metadata.transfers.len(), 1); + assert_eq!(tx_metadata.transfers[0].amount, expected_amount); + assert_eq!(tx_metadata.transfers[0].from, sender_account_key); + assert_eq!(tx_metadata.transfers[0].to, recipient_account_key); +} + +#[test] +fn parses_invalid_transactions() { + // Invalid bytes, odd number length string + let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, true); + + let inst_error_message = parsed_tx.unwrap_err().to_string(); // Unwrap the error + assert_eq!( + inst_error_message, + "unsigned transaction provided is invalid when converted to bytes" + ); + + // Invalid length for Instruction Data Array + let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f000000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, true); + + let inst_error_message = parsed_tx.unwrap_err().to_string(); // Convert to String + assert_eq!( inst_error_message, "Error parsing full transaction. If this is just a message instead of a full transaction, parse using the --message flag. Parsing Error: \"Unsigned transaction provided is incorrectly formatted, error while parsing Instruction Data Array\"" ); - // Invalid length for Accounts Array - let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001192b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f000000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, true); + // Invalid length for Accounts Array + let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001192b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f000000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, true); - let inst_error_message = parsed_tx.unwrap_err().to_string(); // Unwrap the error - assert_eq!( + let inst_error_message = parsed_tx.unwrap_err().to_string(); // Unwrap the error + assert_eq!( inst_error_message, "Error parsing full transaction. If this is just a message instead of a full transaction, parse using the --message flag. Parsing Error: \"Unsigned transaction provided is incorrectly formatted, error while parsing Accounts\"" ); - } - - #[test] - fn parses_v0_transactions() { - // You can also ensure that the output of this transaction makes sense yourself using the below references - // Transaction reference: https://solscan.io/tx/4tkFaZQPGNYTBag6sNTawpBnAodqiBNF494y86s2qBLohQucW1AHRaq9Mm3vWTSxFRaUTmtdYp67pbBRz5RDoAdr - // Address Lookup Table Account key: https://explorer.solana.com/address/6yJwigBRYdkrpfDEsCRj7H5rrzdnAYv8LHzYbb5jRFKy/entries - - // Invalid bytes, odd number length string - let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100070ae05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc186a35856664b03306690c1c0fbd4b5821aea1c64ffb8c368a0422e47ae0d2895de288ba87b903021e6c8c2abf12c2484e98b040792b1fbb87091bc8e0dd76b6600000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000000479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a98c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d616419cee70b839eb4eadd1411aa73eea6fd8700da5f0ea730136db1dd6fb2de660804000502c05c150004000903caa200000000000007060002000e03060101030200020c0200000080f0fa02000000000601020111070600010009030601010515060002010509050805100f0a0d01020b0c0011060524e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000060302000001090158b73fa66d1fb4a0562610136ebc84c7729542a8d792cb9bd2ad1bf75c30d5a404bdc2c1ba0497bcbbbf".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, true).unwrap(); - let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); - - // verify that the signatures array contains a single placeholder signature - let exp_signature = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - assert_eq!(transaction_metadata.signatures, vec![exp_signature]); - - verify_jupiter_message(transaction_metadata) - } - - #[test] - fn parses_v0_transaction_message_only() { - // NOTE: This is the same transaction as above however only the message is included, without the signatures array - // You can also ensure that the output of this transaction makes sense yourself using the below references - // Transaction reference: https://solscan.io/tx/4tkFaZQPGNYTBag6sNTawpBnAodqiBNF494y86s2qBLohQucW1AHRaq9Mm3vWTSxFRaUTmtdYp67pbBRz5RDoAdr - // Address Lookup Table Account key: https://explorer.solana.com/address/6yJwigBRYdkrpfDEsCRj7H5rrzdnAYv8LHzYbb5jRFKy/entries - - // Invalid bytes, odd number length string - let unsigned_payload = "800100070ae05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc186a35856664b03306690c1c0fbd4b5821aea1c64ffb8c368a0422e47ae0d2895de288ba87b903021e6c8c2abf12c2484e98b040792b1fbb87091bc8e0dd76b6600000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000000479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a98c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d616419cee70b839eb4eadd1411aa73eea6fd8700da5f0ea730136db1dd6fb2de660804000502c05c150004000903caa200000000000007060002000e03060101030200020c0200000080f0fa02000000000601020111070600010009030601010515060002010509050805100f0a0d01020b0c0011060524e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000060302000001090158b73fa66d1fb4a0562610136ebc84c7729542a8d792cb9bd2ad1bf75c30d5a404bdc2c1ba0497bcbbbf".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_payload, false).unwrap(); - let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); - - // verify that the signatures array is empty - assert_eq!(transaction_metadata.signatures, vec![] as Vec); - - verify_jupiter_message(transaction_metadata) - } - - #[allow(clippy::too_many_lines)] - fn verify_jupiter_message(transaction_metadata: SolanaMetadata) { - // All Expected accounts - let signer_acct_key = "G6fEj2pt4YYAxLS8JAsY5BL6hea7Fpe8Xyqscg2e7pgp"; // Signer account key - let usdc_mint_acct_key = "A4a6VbNvKA58AGpXBEMhp7bPNN9bDCFS9qze4qWDBBQ8"; // USDC Mint account key - let receiving_acct_key = "FxDNKZ14p3W7o1tpinH935oiwUo3YiZowzP1hUcUzUFw"; // receiving account key - let compute_budget_acct_key = "ComputeBudget111111111111111111111111111111"; // compute budget program account key - let jupiter_program_acct_key = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; // Jupiter program account key - let token_acct_key = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; // token program account key - let assoc_token_acct_key = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; // associated token program account key - let jupiter_event_authority_key = "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"; // Jupiter aggregator event authority account key - let usdc_acct_key = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC account key - - // All expected static account keys - let expected_static_acct_keys = vec![ - signer_acct_key, - usdc_mint_acct_key, - receiving_acct_key, - SOL_SYSTEM_PROGRAM_KEY, - compute_budget_acct_key, - jupiter_program_acct_key, - token_acct_key, - assoc_token_acct_key, - jupiter_event_authority_key, - usdc_acct_key, - ]; - // all expected program account keys - let expected_program_keys = vec![ - SOL_SYSTEM_PROGRAM_KEY, - compute_budget_acct_key, - jupiter_program_acct_key, - token_acct_key, - assoc_token_acct_key, - ]; - - // All expected full account objects - let signer_acct = SolanaAccount { - account_key: signer_acct_key.to_string(), - signer: true, - writable: true, - }; - let usdc_mint_acct = SolanaAccount { - account_key: usdc_mint_acct_key.to_string(), - signer: false, - writable: true, - }; - let receiving_acct = SolanaAccount { - account_key: receiving_acct_key.to_string(), - signer: false, - writable: true, - }; - let system_program_acct = SolanaAccount { - account_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), - signer: false, - writable: false, - }; - let jupiter_program_acct = SolanaAccount { - account_key: jupiter_program_acct_key.to_string(), - signer: false, - writable: false, - }; - let token_acct = SolanaAccount { - account_key: token_acct_key.to_string(), - signer: false, - writable: false, - }; - let jupiter_event_authority_acct = SolanaAccount { - account_key: jupiter_event_authority_key.to_string(), - signer: false, - writable: false, - }; - let usdc_acct = SolanaAccount { - account_key: usdc_acct_key.to_string(), - signer: false, +} + +#[test] +fn parses_v0_transactions() { + // You can also ensure that the output of this transaction makes sense yourself using the below references + // Transaction reference: https://solscan.io/tx/4tkFaZQPGNYTBag6sNTawpBnAodqiBNF494y86s2qBLohQucW1AHRaq9Mm3vWTSxFRaUTmtdYp67pbBRz5RDoAdr + // Address Lookup Table Account key: https://explorer.solana.com/address/6yJwigBRYdkrpfDEsCRj7H5rrzdnAYv8LHzYbb5jRFKy/entries + + // Invalid bytes, odd number length string + let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100070ae05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc186a35856664b03306690c1c0fbd4b5821aea1c64ffb8c368a0422e47ae0d2895de288ba87b903021e6c8c2abf12c2484e98b040792b1fbb87091bc8e0dd76b6600000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000000479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a98c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d616419cee70b839eb4eadd1411aa73eea6fd8700da5f0ea730136db1dd6fb2de660804000502c05c150004000903caa200000000000007060002000e03060101030200020c0200000080f0fa02000000000601020111070600010009030601010515060002010509050805100f0a0d01020b0c0011060524e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000060302000001090158b73fa66d1fb4a0562610136ebc84c7729542a8d792cb9bd2ad1bf75c30d5a404bdc2c1ba0497bcbbbf".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, true).unwrap(); + let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); + + // verify that the signatures array contains a single placeholder signature + let exp_signature = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + assert_eq!(transaction_metadata.signatures, vec![exp_signature]); + + verify_jupiter_message(transaction_metadata) +} + +#[test] +fn parses_v0_transaction_message_only() { + // NOTE: This is the same transaction as above however only the message is included, without the signatures array + // You can also ensure that the output of this transaction makes sense yourself using the below references + // Transaction reference: https://solscan.io/tx/4tkFaZQPGNYTBag6sNTawpBnAodqiBNF494y86s2qBLohQucW1AHRaq9Mm3vWTSxFRaUTmtdYp67pbBRz5RDoAdr + // Address Lookup Table Account key: https://explorer.solana.com/address/6yJwigBRYdkrpfDEsCRj7H5rrzdnAYv8LHzYbb5jRFKy/entries + + // Invalid bytes, odd number length string + let unsigned_payload = "800100070ae05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc186a35856664b03306690c1c0fbd4b5821aea1c64ffb8c368a0422e47ae0d2895de288ba87b903021e6c8c2abf12c2484e98b040792b1fbb87091bc8e0dd76b6600000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000000479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a98c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d616419cee70b839eb4eadd1411aa73eea6fd8700da5f0ea730136db1dd6fb2de660804000502c05c150004000903caa200000000000007060002000e03060101030200020c0200000080f0fa02000000000601020111070600010009030601010515060002010509050805100f0a0d01020b0c0011060524e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000060302000001090158b73fa66d1fb4a0562610136ebc84c7729542a8d792cb9bd2ad1bf75c30d5a404bdc2c1ba0497bcbbbf".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, false).unwrap(); + let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); + + // verify that the signatures array is empty + assert_eq!(transaction_metadata.signatures, vec![] as Vec); + + verify_jupiter_message(transaction_metadata) +} + +#[allow(clippy::too_many_lines)] +fn verify_jupiter_message(transaction_metadata: SolanaMetadata) { + // All Expected accounts + let signer_acct_key = "G6fEj2pt4YYAxLS8JAsY5BL6hea7Fpe8Xyqscg2e7pgp"; // Signer account key + let usdc_mint_acct_key = "A4a6VbNvKA58AGpXBEMhp7bPNN9bDCFS9qze4qWDBBQ8"; // USDC Mint account key + let receiving_acct_key = "FxDNKZ14p3W7o1tpinH935oiwUo3YiZowzP1hUcUzUFw"; // receiving account key + let compute_budget_acct_key = "ComputeBudget111111111111111111111111111111"; // compute budget program account key + let jupiter_program_acct_key = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; // Jupiter program account key + let token_acct_key = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; // token program account key + let assoc_token_acct_key = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; // associated token program account key + let jupiter_event_authority_key = "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"; // Jupiter aggregator event authority account key + let usdc_acct_key = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC account key + + // All expected static account keys + let expected_static_acct_keys = vec![ + signer_acct_key, + usdc_mint_acct_key, + receiving_acct_key, + SOL_SYSTEM_PROGRAM_KEY, + compute_budget_acct_key, + jupiter_program_acct_key, + token_acct_key, + assoc_token_acct_key, + jupiter_event_authority_key, + usdc_acct_key, + ]; + // all expected program account keys + let expected_program_keys = vec![ + SOL_SYSTEM_PROGRAM_KEY, + compute_budget_acct_key, + jupiter_program_acct_key, + token_acct_key, + assoc_token_acct_key, + ]; + + // All expected full account objects + let signer_acct = SolanaAccount { + account_key: signer_acct_key.to_string(), + signer: true, + writable: true, + }; + let usdc_mint_acct = SolanaAccount { + account_key: usdc_mint_acct_key.to_string(), + signer: false, + writable: true, + }; + let receiving_acct = SolanaAccount { + account_key: receiving_acct_key.to_string(), + signer: false, + writable: true, + }; + let system_program_acct = SolanaAccount { + account_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), + signer: false, + writable: false, + }; + let jupiter_program_acct = SolanaAccount { + account_key: jupiter_program_acct_key.to_string(), + signer: false, + writable: false, + }; + let token_acct = SolanaAccount { + account_key: token_acct_key.to_string(), + signer: false, + writable: false, + }; + let jupiter_event_authority_acct = SolanaAccount { + account_key: jupiter_event_authority_key.to_string(), + signer: false, + writable: false, + }; + let usdc_acct = SolanaAccount { + account_key: usdc_acct_key.to_string(), + signer: false, + writable: false, + }; + + let lookup_table_key = "6yJwigBRYdkrpfDEsCRj7H5rrzdnAYv8LHzYbb5jRFKy"; + + // Assert that accounts and programs are correct + assert_eq!(expected_static_acct_keys, transaction_metadata.account_keys); + assert_eq!(expected_program_keys, transaction_metadata.program_keys); + + // Assert ALL instructions as expected --> REFERENCE: https://solscan.io/tx/4tkFaZQPGNYTBag6sNTawpBnAodqiBNF494y86s2qBLohQucW1AHRaq9Mm3vWTSxFRaUTmtdYp67pbBRz5RDoAdr + + // Instruction 1 -- SetComputeUnitLimit + let exp_instruction_1 = SolanaInstruction { + program_key: compute_budget_acct_key.to_string(), + accounts: vec![], + address_table_lookups: vec![], + instruction_data_hex: "02c05c1500".to_string(), + }; + assert_eq!(exp_instruction_1, transaction_metadata.instructions[0]); + + // Instruction 2 -- SetComputeUnitPrice + let exp_instruction_2 = SolanaInstruction { + program_key: compute_budget_acct_key.to_string(), + accounts: vec![], + address_table_lookups: vec![], + instruction_data_hex: "03caa2000000000000".to_string(), + }; + assert_eq!(exp_instruction_2, transaction_metadata.instructions[1]); + + // Instruction 3 - CreateIdempotent + let exp_instruction_3 = SolanaInstruction { + program_key: assoc_token_acct_key.to_string(), + accounts: vec![ + signer_acct.clone(), + receiving_acct.clone(), + signer_acct.clone(), + system_program_acct.clone(), + token_acct.clone(), + ], + address_table_lookups: vec![SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key.to_string(), + index: 151, writable: false, - }; - - let lookup_table_key = "6yJwigBRYdkrpfDEsCRj7H5rrzdnAYv8LHzYbb5jRFKy"; - - // Assert that accounts and programs are correct - assert_eq!(expected_static_acct_keys, transaction_metadata.account_keys); - assert_eq!(expected_program_keys, transaction_metadata.program_keys); - - // Assert ALL instructions as expected --> REFERENCE: https://solscan.io/tx/4tkFaZQPGNYTBag6sNTawpBnAodqiBNF494y86s2qBLohQucW1AHRaq9Mm3vWTSxFRaUTmtdYp67pbBRz5RDoAdr - - // Instruction 1 -- SetComputeUnitLimit - let exp_instruction_1 = SolanaInstruction { - program_key: compute_budget_acct_key.to_string(), - accounts: vec![], - address_table_lookups: vec![], - instruction_data_hex: "02c05c1500".to_string(), - }; - assert_eq!(exp_instruction_1, transaction_metadata.instructions[0]); - - // Instruction 2 -- SetComputeUnitPrice - let exp_instruction_2 = SolanaInstruction { - program_key: compute_budget_acct_key.to_string(), - accounts: vec![], - address_table_lookups: vec![], - instruction_data_hex: "03caa2000000000000".to_string(), - }; - assert_eq!(exp_instruction_2, transaction_metadata.instructions[1]); - - // Instruction 3 - CreateIdempotent - let exp_instruction_3 = SolanaInstruction { - program_key: assoc_token_acct_key.to_string(), - accounts: vec![ - signer_acct.clone(), - receiving_acct.clone(), - signer_acct.clone(), - system_program_acct.clone(), - token_acct.clone(), - ], - address_table_lookups: vec![SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key.to_string(), - index: 151, - writable: false, - }], - instruction_data_hex: "01".to_string(), - }; - assert_eq!(exp_instruction_3, transaction_metadata.instructions[2]); - - // Instruction 4 -- This is a basic SOL transfer - let exp_instruction_4 = SolanaInstruction { - program_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), - accounts: vec![signer_acct.clone(), receiving_acct.clone()], - address_table_lookups: vec![], - instruction_data_hex: "0200000080f0fa0200000000".to_string(), - }; - assert_eq!(exp_instruction_4, transaction_metadata.instructions[3]); - - // Instruction 5 -- SyncNative - let exp_instruction_5 = SolanaInstruction { - program_key: token_acct_key.to_string(), - accounts: vec![receiving_acct.clone()], - address_table_lookups: vec![], - instruction_data_hex: "11".to_string(), - }; - assert_eq!(exp_instruction_5, transaction_metadata.instructions[4]); - - // Instruction 6 -- CreateIdempotent - let exp_instruction_6 = SolanaInstruction { - program_key: assoc_token_acct_key.to_string(), - accounts: vec![ - signer_acct.clone(), - usdc_mint_acct.clone(), - signer_acct.clone(), - usdc_acct.clone(), - system_program_acct.clone(), - token_acct.clone(), - ], - address_table_lookups: vec![], - instruction_data_hex: "01".to_string(), - }; - assert_eq!(exp_instruction_6, transaction_metadata.instructions[5]); - - // Instruction 7 Jupiter Aggregator V6 Route - let mut lookups_7: Vec = vec![]; - let lookups_7_inds: Vec = vec![187, 188, 189, 186, 194, 193, 191]; - let lookups_7_writable: Vec = vec![false, false, true, true, true, true, false]; - for i in 0..lookups_7_inds.len() { - lookups_7.push(SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key.to_string(), - index: lookups_7_inds[i], - writable: lookups_7_writable[i], - }); - } - let exp_instruction_7 = SolanaInstruction { - program_key: jupiter_program_acct_key.to_string(), - accounts: vec![ - token_acct.clone(), - signer_acct.clone(), - receiving_acct.clone(), - usdc_mint_acct.clone(), - jupiter_program_acct.clone(), - usdc_acct.clone(), - jupiter_program_acct.clone(), - jupiter_event_authority_acct.clone(), - jupiter_program_acct.clone(), - usdc_mint_acct.clone(), - receiving_acct.clone(), - signer_acct.clone(), - token_acct.clone(), - jupiter_program_acct.clone(), - ], - address_table_lookups: lookups_7, - instruction_data_hex: - "e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000" - .to_string(), - }; - assert_eq!(exp_instruction_7, transaction_metadata.instructions[6]); - - // Instruction 8 -- CloseAccount - let exp_instruction_8 = SolanaInstruction { - program_key: token_acct_key.to_string(), - accounts: vec![ - receiving_acct.clone(), - signer_acct.clone(), - signer_acct.clone(), - ], - address_table_lookups: vec![], - instruction_data_hex: "09".to_string(), - }; - assert_eq!(exp_instruction_8, transaction_metadata.instructions[7]); - - // ASSERT top level transfers array - let exp_transf_arr: Vec = vec![SolTransfer { - amount: "50000000".to_string(), - to: receiving_acct_key.to_string(), - from: signer_acct_key.to_string(), - }]; - assert_eq!(exp_transf_arr, transaction_metadata.transfers); - - // ASSERT Address table lookups - let exp_lookups: Vec = vec![SolanaAddressTableLookup { + }], + instruction_data_hex: "01".to_string(), + }; + assert_eq!(exp_instruction_3, transaction_metadata.instructions[2]); + + // Instruction 4 -- This is a basic SOL transfer + let exp_instruction_4 = SolanaInstruction { + program_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), + accounts: vec![signer_acct.clone(), receiving_acct.clone()], + address_table_lookups: vec![], + instruction_data_hex: "0200000080f0fa0200000000".to_string(), + }; + assert_eq!(exp_instruction_4, transaction_metadata.instructions[3]); + + // Instruction 5 -- SyncNative + let exp_instruction_5 = SolanaInstruction { + program_key: token_acct_key.to_string(), + accounts: vec![receiving_acct.clone()], + address_table_lookups: vec![], + instruction_data_hex: "11".to_string(), + }; + assert_eq!(exp_instruction_5, transaction_metadata.instructions[4]); + + // Instruction 6 -- CreateIdempotent + let exp_instruction_6 = SolanaInstruction { + program_key: assoc_token_acct_key.to_string(), + accounts: vec![ + signer_acct.clone(), + usdc_mint_acct.clone(), + signer_acct.clone(), + usdc_acct.clone(), + system_program_acct.clone(), + token_acct.clone(), + ], + address_table_lookups: vec![], + instruction_data_hex: "01".to_string(), + }; + assert_eq!(exp_instruction_6, transaction_metadata.instructions[5]); + + // Instruction 7 Jupiter Aggregator V6 Route + let mut lookups_7: Vec = vec![]; + let lookups_7_inds: Vec = vec![187, 188, 189, 186, 194, 193, 191]; + let lookups_7_writable: Vec = vec![false, false, true, true, true, true, false]; + for i in 0..lookups_7_inds.len() { + lookups_7.push(SolanaSingleAddressTableLookup { address_table_key: lookup_table_key.to_string(), - writable_indexes: vec![189, 194, 193, 186], - readonly_indexes: vec![151, 188, 187, 191], - }]; - assert_eq!(exp_lookups, transaction_metadata.address_table_lookups); + index: lookups_7_inds[i], + writable: lookups_7_writable[i], + }); } - - #[test] - #[allow(clippy::too_many_lines)] - fn parses_valid_v0_transaction_with_complex_address_table_lookups() { - // You can also ensure that the output of this transaction makes sense yourself using the below references - // Transaction reference: https://solscan.io/tx/5onMRTbaqb1xjotSedpvSzS4bzPzRSYFwoBb5Rc7qaVwYUy2o9nNE59j8p23zkMBDTd7JRZ4phMfkPz6VikDW32P - // Address Lookup Table Accounts: - // https://solscan.io/account/BkAbXZuNv1prbDh5q6HAQgkGgkX14UpBSfDnuLHKoQho - // https://solscan.io/account/3yg3PND9XDBd7VnZAoHXFRvyFfjPzR8RNb1G1AS9GwH6 - - let unsigned_transaction = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100090fe05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc115376f3f97590a9c65d068b64e24a1f0f3ab9798c17fdddc38bf54d15ab56df477047a381c391538f7a3ba42bafe841d453f26d52e71a66443f6af1edd748afd86a35856664b03306690c1c0fbd4b5821aea1c64ffb8c368a0422e47ae0d2895de288ba87b903021e6c8c2abf12c2484e98b040792b1fbb87091bc8e0dd76b66e9d4488b07fe399b1a9155e5821b697d43016c0a3c4f3bbca2afb41d0163305700000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000000479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f0000000000106ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a98c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859ac1ae3d087f29237062548f70c4c04aec2a995694986e7cbb467520621d38630b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61f0686da7719b0fd854cbc86dd72ec0c438b509b1e57ad61ea9dc8de9efbbcdba0707000502605f04000700090327530500000000000b0600040009060a0101060200040c0200000080969800000000000a0104011108280a0c0004020503090e08080d081d180201151117131614100f120c1c0a08081e1f190c01051a1b0a29c1209b3341d69c810502000000136400011c016401028096980000000000b2a31700000000006400000a030400000109029fa3b18857ed4adbd196e5fa77c76029c0ea1084a9671d2ad0643a027d29ad8a0a410104400705021103090214002c3c0b092d97db350aa90b53afe1d13d3a5b6ff46c97be630ca2779983794df503fbfeff02fdfc".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); - let parsed_tx_sigs = transaction_metadata.signatures; - assert_eq!(1, parsed_tx_sigs.len()); - assert_eq!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(), parsed_tx_sigs[0]); - - // All Expected accounts - let signer_acct_key = "G6fEj2pt4YYAxLS8JAsY5BL6hea7Fpe8Xyqscg2e7pgp"; // Signer account key - let pyth_intermediate_acct_key = "2Rpb7v4vNS6d4k936hVA3176BW3yxqvrpXx21C8tY9xw"; // PYTH account key - let wsol_intermediate_acct_key = "91bUbswo6Di8235jAPwim1At4cPZLbG2pkpneyqKg4NQ"; // WSOL account key - let usdc_destination_acct_key = "A4a6VbNvKA58AGpXBEMhp7bPNN9bDCFS9qze4qWDBBQ8"; // USDC account key - let wsol_mint_acct_key = "FxDNKZ14p3W7o1tpinH935oiwUo3YiZowzP1hUcUzUFw"; // WSOL Mint account key - let usdc_intermediate_acct_key = "Gjmjory7TWKJXD2Jc6hKzAG991wWutFhtbXudzJqgx3p"; // USDC account key - let compute_budget_acct_key = "ComputeBudget111111111111111111111111111111"; // compute budget program account key - let jupiter_program_acct_key = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; // Jupiter program account key - let wsol_acct_key = "So11111111111111111111111111111111111111112"; // WSOL program account key - let token_acct_key = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; // token program account key - let assoc_token_acct_key = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; // associated token program account key - let jupiter_event_authority_key = "CapuXNQoDviLvU1PxFiizLgPNQCxrsag1uMeyk6zLVps"; // Jupiter aggregator event authority account key - let jupiter_aggregator_authority_key = "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"; // Jupiter aggregator authority account key - let usdc_acct_key = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC account key - - // All expected static account keys - let expected_static_acct_keys = vec![ - signer_acct_key, - pyth_intermediate_acct_key, - wsol_intermediate_acct_key, - usdc_destination_acct_key, - wsol_mint_acct_key, - usdc_intermediate_acct_key, - SOL_SYSTEM_PROGRAM_KEY, - compute_budget_acct_key, - jupiter_program_acct_key, - wsol_acct_key, - token_acct_key, - assoc_token_acct_key, - jupiter_event_authority_key, - jupiter_aggregator_authority_key, - usdc_acct_key, - ]; - // all expected program account keys - let expected_program_keys = vec![ - SOL_SYSTEM_PROGRAM_KEY, - compute_budget_acct_key, - jupiter_program_acct_key, - token_acct_key, - assoc_token_acct_key, - ]; - - // All expected full account objects - let signer_acct = SolanaAccount { - account_key: signer_acct_key.to_string(), - signer: true, - writable: true, - }; - let usdc_destination_acct = SolanaAccount { - account_key: usdc_destination_acct_key.to_string(), - signer: false, - writable: true, - }; - let system_program_acct = SolanaAccount { - account_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), - signer: false, - writable: false, - }; - let jupiter_program_acct = SolanaAccount { - account_key: jupiter_program_acct_key.to_string(), - signer: false, - writable: false, - }; - let token_acct = SolanaAccount { - account_key: token_acct_key.to_string(), - signer: false, - writable: false, - }; - let jupiter_event_authority_acct: SolanaAccount = SolanaAccount { - account_key: jupiter_event_authority_key.to_string(), - signer: false, - writable: false, - }; - let jupiter_aggregator_authority_acct = SolanaAccount { - account_key: jupiter_aggregator_authority_key.to_string(), - signer: false, - writable: false, - }; - let usdc_acct = SolanaAccount { - account_key: usdc_acct_key.to_string(), - signer: false, - writable: false, - }; - let wsol_acct = SolanaAccount { - account_key: wsol_acct_key.to_string(), - signer: false, - writable: false, - }; - let wsol_mint_acct = SolanaAccount { - account_key: wsol_mint_acct_key.to_string(), - signer: false, - writable: true, - }; - let pyth_intermediate_acct = SolanaAccount { - account_key: pyth_intermediate_acct_key.to_string(), - signer: false, - writable: true, - }; - let wsol_intermediate_acct = SolanaAccount { - account_key: wsol_intermediate_acct_key.to_string(), - signer: false, - writable: true, - }; - let usdc_intermediate_acct = SolanaAccount { - account_key: usdc_intermediate_acct_key.to_string(), - signer: false, - writable: true, - }; - - let lookup_table_key_1 = "BkAbXZuNv1prbDh5q6HAQgkGgkX14UpBSfDnuLHKoQho"; - let lookup_table_key_2 = "3yg3PND9XDBd7VnZAoHXFRvyFfjPzR8RNb1G1AS9GwH6"; - - // Assert that accounts and programs are correct - assert_eq!(expected_static_acct_keys, transaction_metadata.account_keys); - assert_eq!(expected_program_keys, transaction_metadata.program_keys); - - // Assert ALL instructions as expected --> REFERENCE: https://solscan.io/tx/5onMRTbaqb1xjotSedpvSzS4bzPzRSYFwoBb5Rc7qaVwYUy2o9nNE59j8p23zkMBDTd7JRZ4phMfkPz6VikDW32P - - // Assert expected number of instructions - assert_eq!(7, transaction_metadata.instructions.len()); - - // Instruction 1 -- SetComputeUnitLimit - let exp_instruction_1 = SolanaInstruction { - program_key: compute_budget_acct_key.to_string(), - accounts: vec![], - address_table_lookups: vec![], - instruction_data_hex: "02605f0400".to_string(), - }; - assert_eq!(exp_instruction_1, transaction_metadata.instructions[0]); - - // Instruction 2 -- SetComputeUnitPrice - let exp_instruction_2 = SolanaInstruction { - program_key: compute_budget_acct_key.to_string(), - accounts: vec![], - address_table_lookups: vec![], - instruction_data_hex: "032753050000000000".to_string(), - }; - assert_eq!(exp_instruction_2, transaction_metadata.instructions[1]); - - // Instruction 3 - CreateIdempotent - let exp_instruction_3 = SolanaInstruction { - program_key: assoc_token_acct_key.to_string(), - accounts: vec![ - signer_acct.clone(), - wsol_mint_acct.clone(), - signer_acct.clone(), - wsol_acct.clone(), - system_program_acct.clone(), - token_acct.clone(), - ], - address_table_lookups: vec![], - instruction_data_hex: "01".to_string(), - }; - assert_eq!(exp_instruction_3, transaction_metadata.instructions[2]); - - // Instruction 4 -- This is a basic SOL transfer - let exp_instruction_4 = SolanaInstruction { - program_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), - accounts: vec![signer_acct.clone(), wsol_mint_acct.clone()], - address_table_lookups: vec![], - instruction_data_hex: "020000008096980000000000".to_string(), - }; - assert_eq!(exp_instruction_4, transaction_metadata.instructions[3]); - - // Instruction 5 -- SyncNative - let exp_instruction_5 = SolanaInstruction { - program_key: token_acct_key.to_string(), - accounts: vec![wsol_mint_acct.clone()], - address_table_lookups: vec![], - instruction_data_hex: "11".to_string(), - }; - assert_eq!(exp_instruction_5, transaction_metadata.instructions[4]); - - // Instruction 6 -- Jupiter Aggregator v6: sharedAccountsRoute - let exp_instruction_6 = SolanaInstruction { - program_key: jupiter_program_acct_key.to_string(), - accounts: vec![ - token_acct.clone(), - jupiter_event_authority_acct.clone(), - signer_acct.clone(), - wsol_mint_acct.clone(), - wsol_intermediate_acct.clone(), - usdc_intermediate_acct.clone(), - usdc_destination_acct.clone(), - wsol_acct.clone(), - usdc_acct.clone(), - jupiter_program_acct.clone(), - jupiter_program_acct.clone(), - jupiter_aggregator_authority_acct.clone(), - jupiter_program_acct.clone(), - wsol_intermediate_acct.clone(), - pyth_intermediate_acct.clone(), - jupiter_event_authority_acct.clone(), - token_acct.clone(), - jupiter_program_acct.clone(), - jupiter_program_acct.clone(), - jupiter_event_authority_acct.clone(), - pyth_intermediate_acct.clone(), - usdc_intermediate_acct.clone(), - token_acct.clone(), - ], - instruction_data_hex: "c1209b3341d69c810502000000136400011c016401028096980000000000b2a3170000000000640000" + let exp_instruction_7 = SolanaInstruction { + program_key: jupiter_program_acct_key.to_string(), + accounts: vec![ + token_acct.clone(), + signer_acct.clone(), + receiving_acct.clone(), + usdc_mint_acct.clone(), + jupiter_program_acct.clone(), + usdc_acct.clone(), + jupiter_program_acct.clone(), + jupiter_event_authority_acct.clone(), + jupiter_program_acct.clone(), + usdc_mint_acct.clone(), + receiving_acct.clone(), + signer_acct.clone(), + token_acct.clone(), + jupiter_program_acct.clone(), + ], + address_table_lookups: lookups_7, + instruction_data_hex: + "e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000".to_string(), + }; + assert_eq!(exp_instruction_7, transaction_metadata.instructions[6]); + + // Instruction 8 -- CloseAccount + let exp_instruction_8 = SolanaInstruction { + program_key: token_acct_key.to_string(), + accounts: vec![ + receiving_acct.clone(), + signer_acct.clone(), + signer_acct.clone(), + ], + address_table_lookups: vec![], + instruction_data_hex: "09".to_string(), + }; + assert_eq!(exp_instruction_8, transaction_metadata.instructions[7]); + + // ASSERT top level transfers array + let exp_transf_arr: Vec = vec![SolTransfer { + amount: "50000000".to_string(), + to: receiving_acct_key.to_string(), + from: signer_acct_key.to_string(), + }]; + assert_eq!(exp_transf_arr, transaction_metadata.transfers); + + // ASSERT Address table lookups + let exp_lookups: Vec = vec![SolanaAddressTableLookup { + address_table_key: lookup_table_key.to_string(), + writable_indexes: vec![189, 194, 193, 186], + readonly_indexes: vec![151, 188, 187, 191], + }]; + assert_eq!(exp_lookups, transaction_metadata.address_table_lookups); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn parses_valid_v0_transaction_with_complex_address_table_lookups() { + // You can also ensure that the output of this transaction makes sense yourself using the below references + // Transaction reference: https://solscan.io/tx/5onMRTbaqb1xjotSedpvSzS4bzPzRSYFwoBb5Rc7qaVwYUy2o9nNE59j8p23zkMBDTd7JRZ4phMfkPz6VikDW32P + // Address Lookup Table Accounts: + // https://solscan.io/account/BkAbXZuNv1prbDh5q6HAQgkGgkX14UpBSfDnuLHKoQho + // https://solscan.io/account/3yg3PND9XDBd7VnZAoHXFRvyFfjPzR8RNb1G1AS9GwH6 + + let unsigned_transaction = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100090fe05271368f77a2c5fefe77ce50e2b2f93ceb671eee8b172734c8d4df9d9eddc115376f3f97590a9c65d068b64e24a1f0f3ab9798c17fdddc38bf54d15ab56df477047a381c391538f7a3ba42bafe841d453f26d52e71a66443f6af1edd748afd86a35856664b03306690c1c0fbd4b5821aea1c64ffb8c368a0422e47ae0d2895de288ba87b903021e6c8c2abf12c2484e98b040792b1fbb87091bc8e0dd76b66e9d4488b07fe399b1a9155e5821b697d43016c0a3c4f3bbca2afb41d0163305700000000000000000000000000000000000000000000000000000000000000000306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000000479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138f069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f0000000000106ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a98c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859ac1ae3d087f29237062548f70c4c04aec2a995694986e7cbb467520621d38630b43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61f0686da7719b0fd854cbc86dd72ec0c438b509b1e57ad61ea9dc8de9efbbcdba0707000502605f04000700090327530500000000000b0600040009060a0101060200040c0200000080969800000000000a0104011108280a0c0004020503090e08080d081d180201151117131614100f120c1c0a08081e1f190c01051a1b0a29c1209b3341d69c810502000000136400011c016401028096980000000000b2a31700000000006400000a030400000109029fa3b18857ed4adbd196e5fa77c76029c0ea1084a9671d2ad0643a027d29ad8a0a410104400705021103090214002c3c0b092d97db350aa90b53afe1d13d3a5b6ff46c97be630ca2779983794df503fbfeff02fdfc".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); + let parsed_tx_sigs = transaction_metadata.signatures; + assert_eq!(1, parsed_tx_sigs.len()); + assert_eq!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(), parsed_tx_sigs[0]); + + // All Expected accounts + let signer_acct_key = "G6fEj2pt4YYAxLS8JAsY5BL6hea7Fpe8Xyqscg2e7pgp"; // Signer account key + let pyth_intermediate_acct_key = "2Rpb7v4vNS6d4k936hVA3176BW3yxqvrpXx21C8tY9xw"; // PYTH account key + let wsol_intermediate_acct_key = "91bUbswo6Di8235jAPwim1At4cPZLbG2pkpneyqKg4NQ"; // WSOL account key + let usdc_destination_acct_key = "A4a6VbNvKA58AGpXBEMhp7bPNN9bDCFS9qze4qWDBBQ8"; // USDC account key + let wsol_mint_acct_key = "FxDNKZ14p3W7o1tpinH935oiwUo3YiZowzP1hUcUzUFw"; // WSOL Mint account key + let usdc_intermediate_acct_key = "Gjmjory7TWKJXD2Jc6hKzAG991wWutFhtbXudzJqgx3p"; // USDC account key + let compute_budget_acct_key = "ComputeBudget111111111111111111111111111111"; // compute budget program account key + let jupiter_program_acct_key = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; // Jupiter program account key + let wsol_acct_key = "So11111111111111111111111111111111111111112"; // WSOL program account key + let token_acct_key = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; // token program account key + let assoc_token_acct_key = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; // associated token program account key + let jupiter_event_authority_key = "CapuXNQoDviLvU1PxFiizLgPNQCxrsag1uMeyk6zLVps"; // Jupiter aggregator event authority account key + let jupiter_aggregator_authority_key = "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"; // Jupiter aggregator authority account key + let usdc_acct_key = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC account key + + // All expected static account keys + let expected_static_acct_keys = vec![ + signer_acct_key, + pyth_intermediate_acct_key, + wsol_intermediate_acct_key, + usdc_destination_acct_key, + wsol_mint_acct_key, + usdc_intermediate_acct_key, + SOL_SYSTEM_PROGRAM_KEY, + compute_budget_acct_key, + jupiter_program_acct_key, + wsol_acct_key, + token_acct_key, + assoc_token_acct_key, + jupiter_event_authority_key, + jupiter_aggregator_authority_key, + usdc_acct_key, + ]; + // all expected program account keys + let expected_program_keys = vec![ + SOL_SYSTEM_PROGRAM_KEY, + compute_budget_acct_key, + jupiter_program_acct_key, + token_acct_key, + assoc_token_acct_key, + ]; + + // All expected full account objects + let signer_acct = SolanaAccount { + account_key: signer_acct_key.to_string(), + signer: true, + writable: true, + }; + let usdc_destination_acct = SolanaAccount { + account_key: usdc_destination_acct_key.to_string(), + signer: false, + writable: true, + }; + let system_program_acct = SolanaAccount { + account_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), + signer: false, + writable: false, + }; + let jupiter_program_acct = SolanaAccount { + account_key: jupiter_program_acct_key.to_string(), + signer: false, + writable: false, + }; + let token_acct = SolanaAccount { + account_key: token_acct_key.to_string(), + signer: false, + writable: false, + }; + let jupiter_event_authority_acct: SolanaAccount = SolanaAccount { + account_key: jupiter_event_authority_key.to_string(), + signer: false, + writable: false, + }; + let jupiter_aggregator_authority_acct = SolanaAccount { + account_key: jupiter_aggregator_authority_key.to_string(), + signer: false, + writable: false, + }; + let usdc_acct = SolanaAccount { + account_key: usdc_acct_key.to_string(), + signer: false, + writable: false, + }; + let wsol_acct = SolanaAccount { + account_key: wsol_acct_key.to_string(), + signer: false, + writable: false, + }; + let wsol_mint_acct = SolanaAccount { + account_key: wsol_mint_acct_key.to_string(), + signer: false, + writable: true, + }; + let pyth_intermediate_acct = SolanaAccount { + account_key: pyth_intermediate_acct_key.to_string(), + signer: false, + writable: true, + }; + let wsol_intermediate_acct = SolanaAccount { + account_key: wsol_intermediate_acct_key.to_string(), + signer: false, + writable: true, + }; + let usdc_intermediate_acct = SolanaAccount { + account_key: usdc_intermediate_acct_key.to_string(), + signer: false, + writable: true, + }; + + let lookup_table_key_1 = "BkAbXZuNv1prbDh5q6HAQgkGgkX14UpBSfDnuLHKoQho"; + let lookup_table_key_2 = "3yg3PND9XDBd7VnZAoHXFRvyFfjPzR8RNb1G1AS9GwH6"; + + // Assert that accounts and programs are correct + assert_eq!(expected_static_acct_keys, transaction_metadata.account_keys); + assert_eq!(expected_program_keys, transaction_metadata.program_keys); + + // Assert ALL instructions as expected --> REFERENCE: https://solscan.io/tx/5onMRTbaqb1xjotSedpvSzS4bzPzRSYFwoBb5Rc7qaVwYUy2o9nNE59j8p23zkMBDTd7JRZ4phMfkPz6VikDW32P + + // Assert expected number of instructions + assert_eq!(7, transaction_metadata.instructions.len()); + + // Instruction 1 -- SetComputeUnitLimit + let exp_instruction_1 = SolanaInstruction { + program_key: compute_budget_acct_key.to_string(), + accounts: vec![], + address_table_lookups: vec![], + instruction_data_hex: "02605f0400".to_string(), + }; + assert_eq!(exp_instruction_1, transaction_metadata.instructions[0]); + + // Instruction 2 -- SetComputeUnitPrice + let exp_instruction_2 = SolanaInstruction { + program_key: compute_budget_acct_key.to_string(), + accounts: vec![], + address_table_lookups: vec![], + instruction_data_hex: "032753050000000000".to_string(), + }; + assert_eq!(exp_instruction_2, transaction_metadata.instructions[1]); + + // Instruction 3 - CreateIdempotent + let exp_instruction_3 = SolanaInstruction { + program_key: assoc_token_acct_key.to_string(), + accounts: vec![ + signer_acct.clone(), + wsol_mint_acct.clone(), + signer_acct.clone(), + wsol_acct.clone(), + system_program_acct.clone(), + token_acct.clone(), + ], + address_table_lookups: vec![], + instruction_data_hex: "01".to_string(), + }; + assert_eq!(exp_instruction_3, transaction_metadata.instructions[2]); + + // Instruction 4 -- This is a basic SOL transfer + let exp_instruction_4 = SolanaInstruction { + program_key: SOL_SYSTEM_PROGRAM_KEY.to_string(), + accounts: vec![signer_acct.clone(), wsol_mint_acct.clone()], + address_table_lookups: vec![], + instruction_data_hex: "020000008096980000000000".to_string(), + }; + assert_eq!(exp_instruction_4, transaction_metadata.instructions[3]); + + // Instruction 5 -- SyncNative + let exp_instruction_5 = SolanaInstruction { + program_key: token_acct_key.to_string(), + accounts: vec![wsol_mint_acct.clone()], + address_table_lookups: vec![], + instruction_data_hex: "11".to_string(), + }; + assert_eq!(exp_instruction_5, transaction_metadata.instructions[4]); + + // Instruction 6 -- Jupiter Aggregator v6: sharedAccountsRoute + let exp_instruction_6 = SolanaInstruction { + program_key: jupiter_program_acct_key.to_string(), + accounts: vec![ + token_acct.clone(), + jupiter_event_authority_acct.clone(), + signer_acct.clone(), + wsol_mint_acct.clone(), + wsol_intermediate_acct.clone(), + usdc_intermediate_acct.clone(), + usdc_destination_acct.clone(), + wsol_acct.clone(), + usdc_acct.clone(), + jupiter_program_acct.clone(), + jupiter_program_acct.clone(), + jupiter_aggregator_authority_acct.clone(), + jupiter_program_acct.clone(), + wsol_intermediate_acct.clone(), + pyth_intermediate_acct.clone(), + jupiter_event_authority_acct.clone(), + token_acct.clone(), + jupiter_program_acct.clone(), + jupiter_program_acct.clone(), + jupiter_event_authority_acct.clone(), + pyth_intermediate_acct.clone(), + usdc_intermediate_acct.clone(), + token_acct.clone(), + ], + instruction_data_hex: + "c1209b3341d69c810502000000136400011c016401028096980000000000b2a3170000000000640000" .to_string(), - address_table_lookups: vec![ - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 0, - writable: false, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 9, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 2, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 4, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 3, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 7, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 17, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 5, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 1, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 65, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 64, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_1.to_string(), - index: 20, - writable: false, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_2.to_string(), - index: 253, - writable: false, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_2.to_string(), - index: 252, - writable: false, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_2.to_string(), - index: 251, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_2.to_string(), - index: 254, - writable: true, - }, - SolanaSingleAddressTableLookup { - address_table_key: lookup_table_key_2.to_string(), - index: 255, - writable: true, - }, - ], - }; - assert_eq!(exp_instruction_6, transaction_metadata.instructions[5]); - - // Instruction 7 -- Close Account - let exp_instruction_7: SolanaInstruction = SolanaInstruction { - program_key: token_acct_key.to_string(), - accounts: vec![wsol_mint_acct.clone(), signer_acct.clone(), signer_acct.clone()], - address_table_lookups: vec![], - instruction_data_hex: "09".to_string(), - }; - assert_eq!(exp_instruction_7, transaction_metadata.instructions[6]); - - // ASSERT top level transfers array - let exp_transf_arr: Vec = vec![SolTransfer { - amount: "10000000".to_string(), - to: wsol_mint_acct_key.to_string(), - from: signer_acct_key.to_string(), - }]; - assert_eq!(exp_transf_arr, transaction_metadata.transfers); - - // ASSERT Address table lookups - let exp_lookups: Vec = vec![ - SolanaAddressTableLookup { + address_table_lookups: vec![ + SolanaSingleAddressTableLookup { address_table_key: lookup_table_key_1.to_string(), - writable_indexes: vec![65, 1, 4, 64, 7, 5, 2, 17, 3, 9], - readonly_indexes: vec![20, 0], + index: 0, + writable: false, }, - SolanaAddressTableLookup { + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 9, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 2, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 4, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 3, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 7, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 17, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 5, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 1, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 65, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 64, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + index: 20, + writable: false, + }, + SolanaSingleAddressTableLookup { address_table_key: lookup_table_key_2.to_string(), - writable_indexes: vec![251, 254, 255], - readonly_indexes: vec![253, 252], + index: 253, + writable: false, }, - ]; - assert_eq!(exp_lookups, transaction_metadata.address_table_lookups); - } - - #[test] - fn parses_transaction_with_multi_byte_compact_array_header() { - // The purpose of this test is to ensure that transactions with compact array headers that are multiple bytes long are parsed correctly - // multiple byte array headers are possible based on the compact-u16 format as described in solana documentation here: https://solana.com/docs/core/transactions#compact-array-format - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100071056837517cb604056d3d10dca4553663be1e7a8f0cb7a78abd50862eb2073fbd827a00dfa20ba5511ba322e07293a47397c9e842de88aa4d359ff9a0073f88217740218a08252e747966f313cef860d86d095a76f033098f0cb383d4a5078cc8dedfee61094ac6637619b7c78339527ef0a4460a9e32a0f37fda8c68aea1b751dd39306efe4d9bbb93cfa6c484c1016bb7a52fe3feeca3157d7d0791a25f345798b18611a5b9ca7a6a37eac499749d95233f53f18ec5e692915e2582ade72d68962bedcf3cccf19cbf35daaa34926be22b1fc6bfc7a0938bbb6ee5593046168974592a5996b45dcf0f07ef85b77388f204a784bbf8b212806048c3f9276485de4c353609a6762251896323d9bcd3e70b7bf0ddb03ff381afd5601e994ab9b5f9c0306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e80d0720fe448de59d8811e24d6df917dc8d0d98b392ddf4dd2b622a747a60fded9b48fc124b1d8ff29225062e50ea775462ef424c3c21bda18e3bf47b835bbdd90909000502c027090009000903098b0200000000000a06000100150b0c01010b0200010c0200000000e1f505000000000c010101110a06000200160b0c01010d1d0c0001020d160d0e0d1710171112010215161317000c0c18170304050d23e517cb977ae3ad2a010000002664000100e1f505000000006d2d4a01000000002100000c0301000001090f0c001916061a020708140b0c0a8f02828362be28ce44327e16490100000000000000000000000000000000000000000000000000000000000000000000210514000000532f27101965dd16442e59d40670faf5ebb142e40000000000000000000000000000000000000000000000075858938cec63c6b3140000009528cf48a8deb982b5549d72abbb764ffdbce3010056837517cb604056d3d10dca4553663be1e7a8f0cb7a78abd50862eb2073fbd800140000009528cf48a8deb982b5549d72abbb764ffdbce301000001000000009a06e62b93010000420000000101000000d831640000000000000000000000000000000000b3c663ec8c935858070000000000000000000000000000000000000000000000000000000000000000026f545fe588dd627fb93f2295f47652ccd56feab015ec282c500bf33679e3b3d10423222928042b2a26256a88a76573c8d9d435fad46f194977a3aead561e0c01a6d9b5873c9f05e4dd8e010302020c".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); - - // sanity check signatures - let parsed_tx_sigs = transaction_metadata.signatures; - assert_eq!(1, parsed_tx_sigs.len()); - assert_eq!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(), parsed_tx_sigs[0]); - } - - #[test] - fn parse_spl_token_transfer() { - // The below transaction hex involves two instructions, one to the system program which is irrelevant for this test, and an SPL token transfer described below. - - // The below transaction is an SPL token transfer of the following type: - // - A full transaction (legacy message) - // - Calls the original Token Program (NOT 2022) - // - Calls the TransferCheckedWithFee instruction - // - Not a mutisig owner - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000307533b5b0116e5bd434b30300c28f3814712637545ae345cc63d2f23709c75894d3bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb976044a1460dfb457c122a8fe4d4c180b21a6078e67ea08c271acfd1b7ff3d88a2bbf4ca107ce11d55b05bdb209feaeeac8120fea5598cabbf91df2862fc36c5cf83a2000000000000000000000000000000000000000000000000000000000000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea940000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9eefd656548c17a30f2d97998a7ec413e2304464841f817bfc5c73c2c9a36bf6f020403020500040400000006030301000903a086010000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - // initialize expected values - let exp_from = "EbmwLZmuugxuQb8ksm4TBXf2qPbSK8N4uxNmakvRaUyX"; - let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; - let exp_owner = "6buLKuZFhVNtAFkyRituTZNNVyjHSYLx4NyfD8cKr1uW"; - let exp_amount = "100000"; - - // Test assertions for SPL Transfer fields - let spl_transfer = &tx_metadata.spl_transfers[0]; - assert_eq!(spl_transfer.from, exp_from); - assert_eq!(spl_transfer.to, exp_to); - assert_eq!(spl_transfer.owner, exp_owner); - assert_eq!(spl_transfer.amount, exp_amount); - assert_eq!(spl_transfer.signers, Vec::::new()); - assert_eq!(spl_transfer.decimals, None); - assert_eq!(spl_transfer.token_mint, None); - assert_eq!(spl_transfer.fee, None); - - // Test Program called in the instruction - assert_eq!(tx_metadata.instructions[1].program_key, TOKEN_PROGRAM_KEY) - } - - #[test] - fn parse_spl_token_22_transfer_checked_with_fee() { - // The below transaction is an SPL token transfer of the following type: - // - A full transaction (legacy message) - // - Calls Token Program 2022 - // - Calls the TransferCheckedWithFee instruction - // - Not a mutisig owner - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "01000205864624d78f936e02c49acfd0320a66b8baec813f00df938ed2505b1242504fa9e3db1d9522e05705cf23ac1d3f5a1db2ef9f23ff78d7fcf699da1cf4902463263bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb97604406ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfcbc07c56e60ad3d3f177382eac6548fba1fd32cfd90ca02b3e7cfa185fdce7398b97a42135e0503573230dfadebb740b6e206b513208e90a489f2b46684462bc801030401040200131a0100ca9a3b00000000097b00000000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, false).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - // Initialize expected value - let exp_from = "GLTLPbA1XJctLCsaErmbzgouaLsLm2CLGzWyi8xangNq"; - let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; - let exp_owner = "A39fhEiRvz4YsSrrpqU8z3zF6n1t9S48CsDjL2ibDFrx"; - let exp_amount = "1000000000"; - let exp_mint = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"; - let exp_decimals = "9"; - let exp_fee = "123"; - - // Test assertions for SPL Transfer fields - let spl_transfer = &tx_metadata.spl_transfers[0]; - assert_eq!(spl_transfer.from, exp_from); - assert_eq!(spl_transfer.to, exp_to); - assert_eq!(spl_transfer.owner, exp_owner); - assert_eq!(spl_transfer.amount, exp_amount); - assert_eq!(spl_transfer.signers, Vec::::new()); - assert_eq!(spl_transfer.decimals, Some(exp_decimals.to_string())); - assert_eq!(spl_transfer.token_mint, Some(exp_mint.to_string())); - assert_eq!(spl_transfer.fee, Some(exp_fee.to_string())); - - // Test Program called in the instruction - assert_eq!(tx_metadata.instructions[0].program_key, TOKEN_2022_PROGRAM_KEY) - } - - #[test] - fn parse_spl_token_program_tranfer_multiple_signers() { - // The below Transaction is an SPL token transfer of the following type: - // - Versioned Transaction Message - // - Calls the original Token Program (NOT 2022) - // - Calls the simple Transfer instruction - // - Mutlisig owner with 2 signers - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "8003020106864624d78f936e02c49acfd0320a66b8baec813f00df938ed2505b1242504fa98b2e0a1e9310dc03bfc0432ac8c9f290d15cbc57b2ed367f43aeefc28c7a4d7a5078df268c218e5c9ebe650a7f90c8879bba318b35ce9046cb505b7ed5724a9de3db1d9522e05705cf23ac1d3f5a1db2ef9f23ff78d7fcf699da1cf4902463263bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb97604406ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9b97a42135e0503573230dfadebb740b6e206b513208e90a489f2b46684462bc80105050304000102090300ca9a3b0000000000".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, false).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - // Initialize expected value - let exp_from = "GLTLPbA1XJctLCsaErmbzgouaLsLm2CLGzWyi8xangNq"; - let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; - let exp_owner = "A39fhEiRvz4YsSrrpqU8z3zF6n1t9S48CsDjL2ibDFrx"; - let exp_signer_1 = "ANJPUpqXC1Qn8uhHVXLTsRKjving6kPfjCATJzg7EJjB"; - let exp_signer_2 = "6R8WtdoanEVNJfkeGfbQDMsCrqeHE1sGXjsReJsSbmxQ"; - let exp_amount = "1000000000"; - - // Test assertions for SPL Transfer fields - let spl_transfer = &tx_metadata.spl_transfers[0]; - assert_eq!(spl_transfer.from, exp_from); - assert_eq!(spl_transfer.to, exp_to); - assert_eq!(spl_transfer.owner, exp_owner); - assert_eq!(spl_transfer.amount, exp_amount); - assert_eq!(spl_transfer.signers, vec![exp_signer_1, exp_signer_2]); - assert_eq!(spl_transfer.decimals, None); - assert_eq!(spl_transfer.token_mint, None); - assert_eq!(spl_transfer.fee, None); - - // Test Program called in the instruction - assert_eq!(tx_metadata.instructions[0].program_key, TOKEN_PROGRAM_KEY) - } - - #[test] - fn parse_spl_token_program_2022_transfer_checked_multiple_signers() { - // The below Transaction is an SPL token transfer of the following type: - // - Versioned Transaction Message - // - Calls Token Program 2022 - // - Calls the TransferChecked instruction - // - Mutlisig owner with 2 signers - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "8003020207864624d78f936e02c49acfd0320a66b8baec813f00df938ed2505b1242504fa98b2e0a1e9310dc03bfc0432ac8c9f290d15cbc57b2ed367f43aeefc28c7a4d7a5078df268c218e5c9ebe650a7f90c8879bba318b35ce9046cb505b7ed5724a9de3db1d9522e05705cf23ac1d3f5a1db2ef9f23ff78d7fcf699da1cf4902463263bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb97604406ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfcbc07c56e60ad3d3f177382eac6548fba1fd32cfd90ca02b3e7cfa185fdce7398b97a42135e0503573230dfadebb740b6e206b513208e90a489f2b46684462bc80105060306040001020a0c00ca9a3b000000000900".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, false).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - // Initialize expected value - let exp_from = "GLTLPbA1XJctLCsaErmbzgouaLsLm2CLGzWyi8xangNq"; - let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; - let exp_owner = "A39fhEiRvz4YsSrrpqU8z3zF6n1t9S48CsDjL2ibDFrx"; - let exp_signer_1 = "ANJPUpqXC1Qn8uhHVXLTsRKjving6kPfjCATJzg7EJjB"; - let exp_signer_2 = "6R8WtdoanEVNJfkeGfbQDMsCrqeHE1sGXjsReJsSbmxQ"; - let exp_amount = "1000000000"; - let exp_mint = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"; - let exp_decimals = 9; - - // Test assertions for SPL Transfer fields - let spl_transfer = &tx_metadata.spl_transfers[0]; - assert_eq!(spl_transfer.from, exp_from); - assert_eq!(spl_transfer.to, exp_to); - assert_eq!(spl_transfer.owner, exp_owner); - assert_eq!(spl_transfer.amount, exp_amount); - assert_eq!(spl_transfer.signers, vec![exp_signer_1, exp_signer_2]); - assert_eq!(spl_transfer.decimals, Some(exp_decimals.to_string())); - assert_eq!(spl_transfer.token_mint, Some(exp_mint.to_string())); - assert_eq!(spl_transfer.fee, None); - - // Test Program called in the instruction - assert_eq!(tx_metadata.instructions[0].program_key, TOKEN_2022_PROGRAM_KEY) - } + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_2.to_string(), + index: 252, + writable: false, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_2.to_string(), + index: 251, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_2.to_string(), + index: 254, + writable: true, + }, + SolanaSingleAddressTableLookup { + address_table_key: lookup_table_key_2.to_string(), + index: 255, + writable: true, + }, + ], + }; + assert_eq!(exp_instruction_6, transaction_metadata.instructions[5]); + + // Instruction 7 -- Close Account + let exp_instruction_7: SolanaInstruction = SolanaInstruction { + program_key: token_acct_key.to_string(), + accounts: vec![ + wsol_mint_acct.clone(), + signer_acct.clone(), + signer_acct.clone(), + ], + address_table_lookups: vec![], + instruction_data_hex: "09".to_string(), + }; + assert_eq!(exp_instruction_7, transaction_metadata.instructions[6]); + + // ASSERT top level transfers array + let exp_transf_arr: Vec = vec![SolTransfer { + amount: "10000000".to_string(), + to: wsol_mint_acct_key.to_string(), + from: signer_acct_key.to_string(), + }]; + assert_eq!(exp_transf_arr, transaction_metadata.transfers); + + // ASSERT Address table lookups + let exp_lookups: Vec = vec![ + SolanaAddressTableLookup { + address_table_key: lookup_table_key_1.to_string(), + writable_indexes: vec![65, 1, 4, 64, 7, 5, 2, 17, 3, 9], + readonly_indexes: vec![20, 0], + }, + SolanaAddressTableLookup { + address_table_key: lookup_table_key_2.to_string(), + writable_indexes: vec![251, 254, 255], + readonly_indexes: vec![253, 252], + }, + ]; + assert_eq!(exp_lookups, transaction_metadata.address_table_lookups); +} + +#[test] +fn parses_transaction_with_multi_byte_compact_array_header() { + // The purpose of this test is to ensure that transactions with compact array headers that are multiple bytes long are parsed correctly + // multiple byte array headers are possible based on the compact-u16 format as described in solana documentation here: https://solana.com/docs/core/transactions#compact-array-format + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800100071056837517cb604056d3d10dca4553663be1e7a8f0cb7a78abd50862eb2073fbd827a00dfa20ba5511ba322e07293a47397c9e842de88aa4d359ff9a0073f88217740218a08252e747966f313cef860d86d095a76f033098f0cb383d4a5078cc8dedfee61094ac6637619b7c78339527ef0a4460a9e32a0f37fda8c68aea1b751dd39306efe4d9bbb93cfa6c484c1016bb7a52fe3feeca3157d7d0791a25f345798b18611a5b9ca7a6a37eac499749d95233f53f18ec5e692915e2582ade72d68962bedcf3cccf19cbf35daaa34926be22b1fc6bfc7a0938bbb6ee5593046168974592a5996b45dcf0f07ef85b77388f204a784bbf8b212806048c3f9276485de4c353609a6762251896323d9bcd3e70b7bf0ddb03ff381afd5601e994ab9b5f9c0306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e80d0720fe448de59d8811e24d6df917dc8d0d98b392ddf4dd2b622a747a60fded9b48fc124b1d8ff29225062e50ea775462ef424c3c21bda18e3bf47b835bbdd90909000502c027090009000903098b0200000000000a06000100150b0c01010b0200010c0200000000e1f505000000000c010101110a06000200160b0c01010d1d0c0001020d160d0e0d1710171112010215161317000c0c18170304050d23e517cb977ae3ad2a010000002664000100e1f505000000006d2d4a01000000002100000c0301000001090f0c001916061a020708140b0c0a8f02828362be28ce44327e16490100000000000000000000000000000000000000000000000000000000000000000000210514000000532f27101965dd16442e59d40670faf5ebb142e40000000000000000000000000000000000000000000000075858938cec63c6b3140000009528cf48a8deb982b5549d72abbb764ffdbce3010056837517cb604056d3d10dca4553663be1e7a8f0cb7a78abd50862eb2073fbd800140000009528cf48a8deb982b5549d72abbb764ffdbce301000001000000009a06e62b93010000420000000101000000d831640000000000000000000000000000000000b3c663ec8c935858070000000000000000000000000000000000000000000000000000000000000000026f545fe588dd627fb93f2295f47652ccd56feab015ec282c500bf33679e3b3d10423222928042b2a26256a88a76573c8d9d435fad46f194977a3aead561e0c01a6d9b5873c9f05e4dd8e010302020c".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let transaction_metadata = parsed_tx.transaction_metadata().unwrap(); + + // sanity check signatures + let parsed_tx_sigs = transaction_metadata.signatures; + assert_eq!(1, parsed_tx_sigs.len()); + assert_eq!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string(), parsed_tx_sigs[0]); +} + +#[test] +fn parse_spl_token_transfer() { + // The below transaction hex involves two instructions, one to the system program which is irrelevant for this test, and an SPL token transfer described below. + + // The below transaction is an SPL token transfer of the following type: + // - A full transaction (legacy message) + // - Calls the original Token Program (NOT 2022) + // - Calls the TransferCheckedWithFee instruction + // - Not a mutisig owner + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000307533b5b0116e5bd434b30300c28f3814712637545ae345cc63d2f23709c75894d3bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb976044a1460dfb457c122a8fe4d4c180b21a6078e67ea08c271acfd1b7ff3d88a2bbf4ca107ce11d55b05bdb209feaeeac8120fea5598cabbf91df2862fc36c5cf83a2000000000000000000000000000000000000000000000000000000000000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea940000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9eefd656548c17a30f2d97998a7ec413e2304464841f817bfc5c73c2c9a36bf6f020403020500040400000006030301000903a086010000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + // initialize expected values + let exp_from = "EbmwLZmuugxuQb8ksm4TBXf2qPbSK8N4uxNmakvRaUyX"; + let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; + let exp_owner = "6buLKuZFhVNtAFkyRituTZNNVyjHSYLx4NyfD8cKr1uW"; + let exp_amount = "100000"; + + // Test assertions for SPL Transfer fields + let spl_transfer = &tx_metadata.spl_transfers[0]; + assert_eq!(spl_transfer.from, exp_from); + assert_eq!(spl_transfer.to, exp_to); + assert_eq!(spl_transfer.owner, exp_owner); + assert_eq!(spl_transfer.amount, exp_amount); + assert_eq!(spl_transfer.signers, Vec::::new()); + assert_eq!(spl_transfer.decimals, None); + assert_eq!(spl_transfer.token_mint, None); + assert_eq!(spl_transfer.fee, None); + + // Test Program called in the instruction + assert_eq!(tx_metadata.instructions[1].program_key, TOKEN_PROGRAM_KEY) +} + +#[test] +fn parse_spl_token_22_transfer_checked_with_fee() { + // The below transaction is an SPL token transfer of the following type: + // - A full transaction (legacy message) + // - Calls Token Program 2022 + // - Calls the TransferCheckedWithFee instruction + // - Not a mutisig owner + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000205864624d78f936e02c49acfd0320a66b8baec813f00df938ed2505b1242504fa9e3db1d9522e05705cf23ac1d3f5a1db2ef9f23ff78d7fcf699da1cf4902463263bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb97604406ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfcbc07c56e60ad3d3f177382eac6548fba1fd32cfd90ca02b3e7cfa185fdce7398b97a42135e0503573230dfadebb740b6e206b513208e90a489f2b46684462bc801030401040200131a0100ca9a3b00000000097b00000000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, false).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + // Initialize expected value + let exp_from = "GLTLPbA1XJctLCsaErmbzgouaLsLm2CLGzWyi8xangNq"; + let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; + let exp_owner = "A39fhEiRvz4YsSrrpqU8z3zF6n1t9S48CsDjL2ibDFrx"; + let exp_amount = "1000000000"; + let exp_mint = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"; + let exp_decimals = "9"; + let exp_fee = "123"; + + // Test assertions for SPL Transfer fields + let spl_transfer = &tx_metadata.spl_transfers[0]; + assert_eq!(spl_transfer.from, exp_from); + assert_eq!(spl_transfer.to, exp_to); + assert_eq!(spl_transfer.owner, exp_owner); + assert_eq!(spl_transfer.amount, exp_amount); + assert_eq!(spl_transfer.signers, Vec::::new()); + assert_eq!(spl_transfer.decimals, Some(exp_decimals.to_string())); + assert_eq!(spl_transfer.token_mint, Some(exp_mint.to_string())); + assert_eq!(spl_transfer.fee, Some(exp_fee.to_string())); + + // Test Program called in the instruction + assert_eq!( + tx_metadata.instructions[0].program_key, + TOKEN_2022_PROGRAM_KEY + ) +} + +#[test] +fn parse_spl_token_program_tranfer_multiple_signers() { + // The below Transaction is an SPL token transfer of the following type: + // - Versioned Transaction Message + // - Calls the original Token Program (NOT 2022) + // - Calls the simple Transfer instruction + // - Mutlisig owner with 2 signers + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "8003020106864624d78f936e02c49acfd0320a66b8baec813f00df938ed2505b1242504fa98b2e0a1e9310dc03bfc0432ac8c9f290d15cbc57b2ed367f43aeefc28c7a4d7a5078df268c218e5c9ebe650a7f90c8879bba318b35ce9046cb505b7ed5724a9de3db1d9522e05705cf23ac1d3f5a1db2ef9f23ff78d7fcf699da1cf4902463263bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb97604406ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9b97a42135e0503573230dfadebb740b6e206b513208e90a489f2b46684462bc80105050304000102090300ca9a3b0000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, false).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + // Initialize expected value + let exp_from = "GLTLPbA1XJctLCsaErmbzgouaLsLm2CLGzWyi8xangNq"; + let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; + let exp_owner = "A39fhEiRvz4YsSrrpqU8z3zF6n1t9S48CsDjL2ibDFrx"; + let exp_signer_1 = "ANJPUpqXC1Qn8uhHVXLTsRKjving6kPfjCATJzg7EJjB"; + let exp_signer_2 = "6R8WtdoanEVNJfkeGfbQDMsCrqeHE1sGXjsReJsSbmxQ"; + let exp_amount = "1000000000"; + + // Test assertions for SPL Transfer fields + let spl_transfer = &tx_metadata.spl_transfers[0]; + assert_eq!(spl_transfer.from, exp_from); + assert_eq!(spl_transfer.to, exp_to); + assert_eq!(spl_transfer.owner, exp_owner); + assert_eq!(spl_transfer.amount, exp_amount); + assert_eq!(spl_transfer.signers, vec![exp_signer_1, exp_signer_2]); + assert_eq!(spl_transfer.decimals, None); + assert_eq!(spl_transfer.token_mint, None); + assert_eq!(spl_transfer.fee, None); + + // Test Program called in the instruction + assert_eq!(tx_metadata.instructions[0].program_key, TOKEN_PROGRAM_KEY) +} + +#[test] +fn parse_spl_token_program_2022_transfer_checked_multiple_signers() { + // The below Transaction is an SPL token transfer of the following type: + // - Versioned Transaction Message + // - Calls Token Program 2022 + // - Calls the TransferChecked instruction + // - Mutlisig owner with 2 signers + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "8003020207864624d78f936e02c49acfd0320a66b8baec813f00df938ed2505b1242504fa98b2e0a1e9310dc03bfc0432ac8c9f290d15cbc57b2ed367f43aeefc28c7a4d7a5078df268c218e5c9ebe650a7f90c8879bba318b35ce9046cb505b7ed5724a9de3db1d9522e05705cf23ac1d3f5a1db2ef9f23ff78d7fcf699da1cf4902463263bcae0fb76cc461d85bd05a078f887cf646fd27011e12edaaeb5091cdb97604406ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfcbc07c56e60ad3d3f177382eac6548fba1fd32cfd90ca02b3e7cfa185fdce7398b97a42135e0503573230dfadebb740b6e206b513208e90a489f2b46684462bc80105060306040001020a0c00ca9a3b000000000900".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, false).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + // Initialize expected value + let exp_from = "GLTLPbA1XJctLCsaErmbzgouaLsLm2CLGzWyi8xangNq"; + let exp_to = "52QUutfwWMDDVNZSjovpmtD1ZmMe3Uf3n1ENE7JgBMkP"; + let exp_owner = "A39fhEiRvz4YsSrrpqU8z3zF6n1t9S48CsDjL2ibDFrx"; + let exp_signer_1 = "ANJPUpqXC1Qn8uhHVXLTsRKjving6kPfjCATJzg7EJjB"; + let exp_signer_2 = "6R8WtdoanEVNJfkeGfbQDMsCrqeHE1sGXjsReJsSbmxQ"; + let exp_amount = "1000000000"; + let exp_mint = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"; + let exp_decimals = 9; + + // Test assertions for SPL Transfer fields + let spl_transfer = &tx_metadata.spl_transfers[0]; + assert_eq!(spl_transfer.from, exp_from); + assert_eq!(spl_transfer.to, exp_to); + assert_eq!(spl_transfer.owner, exp_owner); + assert_eq!(spl_transfer.amount, exp_amount); + assert_eq!(spl_transfer.signers, vec![exp_signer_1, exp_signer_2]); + assert_eq!(spl_transfer.decimals, Some(exp_decimals.to_string())); + assert_eq!(spl_transfer.token_mint, Some(exp_mint.to_string())); + assert_eq!(spl_transfer.fee, None); + + // Test Program called in the instruction + assert_eq!( + tx_metadata.instructions[0].program_key, + TOKEN_2022_PROGRAM_KEY + ) +}