diff --git a/src/lib.rs b/src/lib.rs index 7ea349e..09baeca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,14 @@ -pub mod solana { - pub mod parser; - pub mod structs; - pub mod idl_parser; - pub mod idl_db; -} \ No newline at end of file +pub mod solana; + +// Re-export commonly used types and functions for convenience +pub use solana::idl_parser::{ + compute_idl_hash, construct_custom_idl_records_map, + construct_custom_idl_records_map_with_overrides, construct_idl_records_map, decode_idl_data, + parse_instruction_with_idl, +}; +pub use solana::parser::{parse_transaction, parse_transaction_with_idls}; +pub use solana::structs::{ + CustomIdl, CustomIdlConfig, Idl, IdlSource, ProgramType, SolanaInstruction, SolanaMetadata, + SolanaParseResponse, SolanaParsedInstructionData, SolanaParsedTransaction, + SolanaParsedTransactionPayload, +}; diff --git a/src/main.rs b/src/main.rs index be720bb..fd3538d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,47 +1,135 @@ +use std::collections::HashMap; use std::env; +use std::fs; mod solana; use crate::solana::parser::parse_transaction; -use crate::solana::structs::{SolanaParsedTransactionPayload, SolanaParsedInstructionData}; - +use crate::solana::structs::{ + IdlSource, SolanaParsedInstructionData, SolanaParsedTransactionPayload, +}; fn main() { let args: Vec = env::args().collect(); - if args.len() != 4 { - println!("Usage: `cargo run parse --message OR cargo run parse --transaction`"); + if args.len() < 4 { + print_usage(); return; } 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), + match command.as_str() { + "parse" => { + let flag = &args[2]; + let unsigned_tx = &args[3]; + + match flag.as_str() { + "--message" | "--transaction" => { + let is_transaction = flag == "--transaction"; + + // Check for optional custom IDL parameters + let custom_idls = parse_custom_idl_args(&args[4..]); + + let result = + parse_transaction(unsigned_tx.to_string(), is_transaction, custom_idls); + + 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!("Invalid flag. Use either --message or --transaction."); + print_usage(); + } + } + } + _ => { + println!("Unknown command: {}", command); + print_usage(); + } + } +} + +fn print_usage() { + println!("Usage:"); + println!(" cargo run parse --message "); + println!(" cargo run parse --transaction "); + println!(); + println!("Optional custom IDL parameters:"); + println!(" --custom-idl [--override]"); + println!(); + println!("Examples:"); + println!(" cargo run parse --message "); + println!(" cargo run parse --message --custom-idl /path/to/idl.json"); + println!(" cargo run parse --message --custom-idl /path/to/idl.json --override"); +} + +fn parse_custom_idl_args(args: &[String]) -> Option> { + if args.is_empty() { + return None; + } + + let mut custom_idls = HashMap::new(); + let mut i = 0; + + while i < args.len() { + if args[i] == "--custom-idl" { + if i + 2 >= args.len() { + eprintln!( + "Error: --custom-idl requires and " + ); + return None; } - _ => println!("Unknown command: {}", command), + + let program_id = args[i + 1].clone(); + let idl_arg = &args[i + 2]; + + // Check if it's a file path or JSON string + let idl_json = if std::path::Path::new(idl_arg).exists() { + match fs::read_to_string(idl_arg) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading IDL file {}: {}", idl_arg, e); + return None; + } + } + } else { + idl_arg.clone() + }; + + // Check for --override flag + let override_builtin = if i + 3 < args.len() && args[i + 3] == "--override" { + i += 1; // Skip the --override flag + true + } else { + false + }; + + custom_idls.insert(program_id, (idl_json, override_builtin)); + i += 3; + } else { + i += 1; } + } + + if custom_idls.is_empty() { + None + } else { + Some(custom_idls) + } } fn print_parsed_transaction(transaction_payload: SolanaParsedTransactionPayload) { println!("Solana Parsed Transaction Payload:"); - println!(" Unsigned Payload: {}", transaction_payload.unsigned_payload); + println!( + " Unsigned Payload: {}", + transaction_payload.unsigned_payload + ); if let Some(metadata) = transaction_payload.transaction_metadata { println!(" Transaction Metadata:"); println!(" Signatures: {:?}", metadata.signatures); @@ -53,8 +141,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 + ); print_parsed_instruction_data(instruction.parsed_instruction.clone()); } println!(" Transfers:"); @@ -84,14 +178,35 @@ 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 + ); } } fn print_parsed_instruction_data(p_inst_data: Option) { println!(" Parsed Instruction Data:"); if let Some(parsed_data) = p_inst_data { - println!(" Instruction Name: {}", parsed_data.instruction_name); + println!( + " Instruction Name: {}", + parsed_data.instruction_name + ); + + // Display IDL source information + match &parsed_data.idl_source { + IdlSource::BuiltIn(program_type) => { + println!( + " IDL Source: Built-in ({})", + program_type.program_name() + ); + } + IdlSource::Custom => { + println!(" IDL Source: Custom"); + } + } + println!(" IDL Hash: {}", parsed_data.idl_hash); + println!(" Named Accounts:"); for k in parsed_data.named_accounts.keys() { let acct_string = parsed_data.named_accounts[k].clone(); @@ -105,4 +220,4 @@ fn print_parsed_instruction_data(p_inst_data: Option String { + idl_json.chars().filter(|c| !c.is_whitespace()).collect() +} + +/// Computes SHA256 hash of the compressed IDL JSON string +/// this doesn't canonicalize the JSON by sorting it or anything, so if your JSON isn't exactly same, you will get some inconsistencies +/// we can resolve this by implementing a custom serializer for IDLs +pub fn compute_idl_hash(idl_json: &str) -> String { + let compressed = compress_idl_json(idl_json); + let mut hasher = Sha256::new(); + hasher.update(compressed.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) +} // Required Top-Level fields in Solana IDL JSON's /* @@ -31,26 +50,161 @@ const MAX_DEFINED_TYPE_DEPTH: usize = 10; // Max depth for defined types const MAX_CURSOR_LENGTH: usize = 1232; // Max size in bytes of a serialized Solana transaction const MAX_ALLOC_PER_CURSOR_LENGTH: usize = 24; // Typical heap allocation overhead for pointers +/// Constructs a mapping from program_id to IdlRecord for all built-in IDLs. +/// IDLs are embedded at compile time and do not require file system access. +#[allow(dead_code)] // Public API - exported from lib.rs +pub fn construct_custom_idl_records_map( +) -> Result, Box> { + let mut idl_map = HashMap::new(); + + for program_type in ProgramType::all() { + let program_id = program_type.program_id().to_string(); + let idl_record = IdlRecord { + program_name: program_type.program_name().to_string(), + program_id: program_id.clone(), + program_type: Some(program_type.clone()), + custom_idl: None, + custom_idl_json: None, + override_builtin: false, + }; + + idl_map.insert(program_id, idl_record); + } + + Ok(idl_map) +} + +/// Constructs custom IDL records map with optional custom IDLs (JSON string version). +/// This is the legacy API that accepts JSON strings. +#[allow(dead_code)] // Public API - exported from lib.rs +pub fn construct_custom_idl_records_map_with_overrides( + custom_idls: Option>, // program_id -> (idl_json, override_builtin) +) -> Result, Box> { + // Convert old API to new API + let custom_configs = custom_idls.map(|idls| { + idls.into_iter() + .map(|(program_id, (json, override_builtin))| { + ( + program_id, + CustomIdlConfig::from_json(json, override_builtin), + ) + }) + .collect() + }); + construct_idl_records_map(custom_configs) +} -// This method takes all IDL's that have been uploaded to the IDL DB and constructs a mapping from PROGRAM_ID --> IDL_RECORD_INFO -pub fn construct_custom_idl_records_map() -> Result, Box> { +/// Constructs IDL records map with optional custom IDLs. +/// This is the new API that supports both pre-parsed Idl structs and JSON strings. +/// +/// # Arguments +/// * `custom_idls` - Optional map of program_id -> CustomIdlConfig +pub fn construct_idl_records_map( + custom_idls: Option>, +) -> Result, Box> { let mut idl_map = HashMap::new(); - - for entry in IDL_DB { - let program_id = entry.1.to_string(); + + // First, load all built-in IDLs (using embedded IDL data) + for program_type in ProgramType::all() { + let program_id = program_type.program_id().to_string(); let idl_record = IdlRecord { - program_name: entry.0.to_string(), - program_id: entry.1.to_string(), - file_path: entry.2.to_string(), + program_name: program_type.program_name().to_string(), + program_id: program_id.clone(), + program_type: Some(program_type.clone()), + custom_idl: None, + custom_idl_json: None, + override_builtin: false, }; - - // Use insert() instead of indexing syntax + idl_map.insert(program_id, idl_record); } - + + // Then, add or override with custom IDLs if provided + if let Some(custom_idls) = custom_idls { + for (program_id, config) in custom_idls { + // Parse the custom IDL if it's JSON, otherwise use the pre-parsed version + let (custom_idl, custom_idl_json) = match config.idl { + CustomIdl::Parsed(idl) => { + // For pre-parsed IDLs, we serialize to JSON for hash computation + let json = serde_json::to_string(&idl)?; + (idl, json) + } + CustomIdl::Json(json) => { + let idl = decode_idl_data(&json)?; + (idl, json) + } + }; + + if let Some(existing_record) = idl_map.get_mut(&program_id) { + // Update existing record with custom IDL + existing_record.custom_idl = Some(custom_idl); + existing_record.custom_idl_json = Some(custom_idl_json); + existing_record.override_builtin = config.override_builtin; + } else { + // Create new record for unknown program + let short_id = if program_id.len() >= 8 { + &program_id[..8] + } else { + &program_id + }; + let idl_record = IdlRecord { + program_name: format!("Custom Program {short_id}"), + program_id: program_id.clone(), + program_type: None, + custom_idl: Some(custom_idl), + custom_idl_json: Some(custom_idl_json), + override_builtin: true, // Always override for unknown programs + }; + idl_map.insert(program_id, idl_record); + } + } + } + Ok(idl_map) } +/// Get the resolved IDL and its JSON string for an IdlRecord. +/// Returns (Idl, idl_json_str, IdlSource) +pub fn resolve_idl_for_record( + idl_record: &IdlRecord, + program_key: &str, +) -> Result<(Idl, String, crate::solana::structs::IdlSource), Box> { + use crate::solana::structs::IdlSource; + + // Determine which IDL to use + if let Some(ref custom_idl) = idl_record.custom_idl { + // Custom IDL provided + if idl_record.override_builtin || idl_record.program_type.is_none() { + // Use custom IDL (either override is set or no built-in exists) + let json = idl_record + .custom_idl_json + .clone() + .ok_or("Custom IDL present but JSON string missing")?; + return Ok((custom_idl.clone(), json, IdlSource::Custom)); + } + } + + // Use built-in IDL + if let Some(ref program_type) = idl_record.program_type { + let builtin_json = program_type.idl_json(); + let builtin_idl = decode_idl_data(builtin_json)?; + Ok(( + builtin_idl, + builtin_json.to_string(), + IdlSource::BuiltIn(program_type.clone()), + )) + } else if let Some(ref custom_idl) = idl_record.custom_idl { + // Fallback to custom IDL if no built-in + let json = idl_record + .custom_idl_json + .clone() + .ok_or("Custom IDL present but JSON string missing")?; + Ok((custom_idl.clone(), json, IdlSource::Custom)) + } else { + Err(format!("No IDL available for program: {program_key}").into()) + } +} + // The TypeResolver struct helps resolved defined types within an IDL during the parsing of instruction call data struct TypeResolver<'a> { type_cache: HashMap, @@ -62,7 +216,8 @@ impl<'a> TypeResolver<'a> { for ty in &idl.types { if type_cache.contains_key(&ty.name) { return Err( - format!("multiple types with the same name detected: {}", &ty.name).into()) + format!("multiple types with the same name detected: {}", &ty.name).into(), + ); } type_cache.insert(ty.name.clone(), ty); } @@ -90,7 +245,10 @@ impl SizeGuard { } // This method checks to see whether there is enough memory left in the budget to be allocated, and if so, creates a byte vector of the correct length - fn create_allocated_buffer(&mut self, len: usize) -> Result, Box> { + fn create_allocated_buffer( + &mut self, + len: usize, + ) -> Result, Box> { if len > self.remaining_budget { return Err( "memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data".into(), @@ -102,7 +260,10 @@ impl SizeGuard { } // This method checks to see whether there is enough memory left in the budget to be allocated, and if so, creates a vector of serde json value type objects of the correct length - fn create_allocated_arg_vector(&mut self, len: usize) -> Result, Box> { + fn create_allocated_arg_vector( + &mut self, + len: usize, + ) -> Result, Box> { let amount = len * size_of::(); if amount > self.remaining_budget { @@ -122,17 +283,19 @@ impl SizeGuard { // the Decode IDL Data method takes an IDL json string and parses it into IDL rust structs to be used to parse passed in instruction data pub fn decode_idl_data(idl_json: &str) -> Result> { // Parse IDL from JSON string into Maps - let idl_map: Map = from_str(idl_json).map_err(|e| -> Box { + let idl_map: Map = + from_str(idl_json).map_err(|e| -> Box { format!("unable to parse IDL: Invalid JSON with error: {e}").into() - })?; + })?; // Parse instructions array let instructions = validate_idl_array(&idl_map, IDL_INSTRUCTIONS_KEY)?; let mut parsed_instructions: Vec = vec![]; for i in instructions { - let parsed_i: IdlInstruction = from_value(i).map_err(|e| -> Box { + let parsed_i: IdlInstruction = + from_value(i).map_err(|e| -> Box { format!("failed to parse instructions array in uploaded IDL with error: {e}").into() - })?; + })?; parsed_instructions.push(parsed_i); } @@ -147,9 +310,10 @@ pub fn decode_idl_data(idl_json: &str) -> Result let types = validate_idl_array(&idl_map, IDL_TYPES_KEY)?; let mut parsed_types: Vec = vec![]; for t in types { - let parsed_t: IdlTypeDefinition = from_value(t).map_err(|e| -> Box { + let parsed_t: IdlTypeDefinition = + from_value(t).map_err(|e| -> Box { format!("failed to parse types array in uploaded IDL with error: {e}").into() - })?; + })?; parsed_types.push(parsed_t); } @@ -166,21 +330,28 @@ pub fn decode_idl_data(idl_json: &str) -> Result // This method takes in a json object, and validates the existence of an ARRAY at a particular key (for example the top level instructions array within all IDL Json's) // It then returns the value at the provided key, cast as an array type -fn validate_idl_array(idl_map: &Map, key: &str) -> Result, Box> { +fn validate_idl_array( + idl_map: &Map, + key: &str, +) -> Result, Box> { let value = idl_map .get(key) - .ok_or_else(|| -> Box { - format!("key '{key}' not found in uploaded IDL").into() + .ok_or_else(|| -> Box { + format!("key '{key}' not found in uploaded IDL").into() + })?; + let checked_value = value + .as_array() + .ok_or_else(|| -> Box { + format!("value for key '{key}' must be a JSON array").into() })?; - let checked_value = value.as_array().ok_or_else(|| -> Box { - format!("value for key '{key}' must be a JSON array").into() - })?; Ok(checked_value.clone()) } // This method computes the default anchor discriminator for an instruction using it's instruction name (only computes if the instruction discriminators are not EXPLICITLY provided) // Reference for calculating the default discriminator - https://www.anchor-lang.com/docs/basics/idl#discriminators -pub fn compute_default_anchor_discriminator(instruction_name: &str) -> Result, Box> { +pub fn compute_default_anchor_discriminator( + instruction_name: &str, +) -> Result, Box> { if instruction_name.is_empty() { return Err("attempted to compute the default anchor instruction discriminator for an instruction with no name".into()); } @@ -220,16 +391,16 @@ pub fn create_accounts_map( instruction_spec: &IdlInstruction, ) -> Result, Box> { if accounts.len() < instruction_spec.accounts.len() { - return Err( - format!( - "too few accounts provided in transaction payload for instruction {}", - instruction_spec.name - ).into()); + return Err(format!( + "too few accounts provided in transaction payload for instruction {}", + instruction_spec.name + ) + .into()); } let mut acct_map: HashMap = HashMap::new(); - for i in 0..instruction_spec.accounts.len() { - acct_map.insert(instruction_spec.accounts[i].name.clone(), accounts[i].to_string()); + for (account_spec, account) in instruction_spec.accounts.iter().zip(accounts.iter()) { + acct_map.insert(account_spec.name.clone(), account.to_string()); } Ok(acct_map) } @@ -244,10 +415,16 @@ pub fn find_instruction_by_discriminator( } for i in instructions { - let disc = i.clone().discriminator.ok_or_else(|| -> Box { - format!("no discriminator found for instruction {} found in IDL", i.name).into() - } - )?; + let disc = i + .clone() + .discriminator + .ok_or_else(|| -> Box { + format!( + "no discriminator found for instruction {} found in IDL", + i.name + ) + .into() + })?; // Validate length of instruction data, to make sure it has enough bytes for the discriminator if instruction_data.len() < disc.len() { @@ -260,9 +437,10 @@ pub fn find_instruction_by_discriminator( } } let inst_data_string = hex::encode(instruction_data); - Err( - format!("no matching instruction discriminator found for instruction data: {inst_data_string:?}").into() + Err(format!( + "no matching instruction discriminator found for instruction data: {inst_data_string:?}" ) + .into()) } // Parse data into args -- takes in the IDL instruction object corresponding to the instruction call data, as well as the instruction call data and parses the data into a vector of arguments @@ -275,12 +453,17 @@ pub fn parse_data_into_args( let resolver = TypeResolver::new(idl)?; // Validate discriminator length and set cursor to correct position - let disc = idl_instruction.clone().discriminator.ok_or_else(|| -> Box { - format!( - "no discriminator found for instruction {} not found in IDL", - idl_instruction.name - ).into() - })?; + let disc = + idl_instruction + .clone() + .discriminator + .ok_or_else(|| -> Box { + format!( + "no discriminator found for instruction {} not found in IDL", + idl_instruction.name + ) + .into() + })?; if data.len() < disc.len() { // we should not get here since we've checked the length of the discriminator return Err( @@ -298,19 +481,27 @@ pub fn parse_data_into_args( // parse all arguments let mut args = serde_json::Map::new(); for arg in &idl_instruction.args { - let parsed_arg = parse_type(&mut data_cursor, &arg.r#type, &resolver, &mut size_guard).map_err(|e| -> Box { - format!("failed to parse IDL argument with error: {}", e).into() - })?; - args.insert( arg.name.clone(), parsed_arg); + let parsed_arg = parse_type(&mut data_cursor, &arg.r#type, &resolver, &mut size_guard) + .map_err(|e| -> Box { + format!("failed to parse IDL argument with error: {}", e).into() + })?; + args.insert(arg.name.clone(), parsed_arg); } // Error if data bytes still remaining after parsing all expected arguments - let cursor_position = usize::try_from(data_cursor.position()).map_err(|e| -> Box { - format!("invalid cursor position while parsing solana IDL data {}", e).into() - })?; + let cursor_position = + usize::try_from(data_cursor.position()).map_err(|e| -> Box { + format!( + "invalid cursor position while parsing solana IDL data {}", + e + ) + .into() + })?; if cursor_position != data.len() { if cursor_position < data.len() { - return Err("extra unexpected bytes remaining at the end of instruction call data".into()); + return Err( + "extra unexpected bytes remaining at the end of instruction call data".into(), + ); } return Err("cursor out of bounds".into()); } @@ -335,7 +526,7 @@ fn parse_type( } else { serde_json::Value::Bool(true) }) - }, + } IdlType::I8 => Ok(reader.read_i8()?.into()), IdlType::I16 => Ok(reader.read_i16::()?.into()), IdlType::I32 => Ok(reader.read_i32::()?.into()), @@ -362,7 +553,7 @@ fn parse_type( let mut buf = [0u8; 32]; reader.read_exact(&mut buf)?; Ok(bs58::encode(buf).into_string().into()) - }, + } IdlType::String => { let len = reader.read_u32::()? as usize; // Check size guard & allocate memory @@ -370,7 +561,7 @@ fn parse_type( reader.read_exact(&mut buf)?; Ok(String::from_utf8(buf)?.into()) - }, + } IdlType::Bytes => { let len = reader.read_u32::()? as usize; // Check size guard & allocate memory @@ -378,7 +569,7 @@ fn parse_type( reader.read_exact(&mut buf)?; Ok(serde_json::Value::String(hex::encode(&buf))) - }, + } // Container types IdlType::Array(ty, size) => { @@ -389,11 +580,15 @@ fn parse_type( arr.push(parse_type(reader, ty, resolver, size_guard)?); } Ok(arr.into()) - }, + } IdlType::Vec(ty) => { - let len = reader.read_u32::().map_err(|e| -> Box { - format!("failed while parsing length header of argument of type vec: {e}").into() - })?; + let len = + reader + .read_u32::() + .map_err(|e| -> Box { + format!("failed while parsing length header of argument of type vec: {e}") + .into() + })?; // Check size guard & allocate memory let mut vec = size_guard.create_allocated_arg_vector(len as usize)?; @@ -402,7 +597,7 @@ fn parse_type( vec.push(parse_type(reader, ty, resolver, size_guard)?); } Ok(vec.into()) - }, + } IdlType::Option(ty) => { let flag = reader.read_u8()?; Ok(if flag == 0 { @@ -410,7 +605,7 @@ fn parse_type( } else { parse_type(reader, ty, resolver, size_guard)? }) - }, + } // Custom types IdlType::Defined(defined) => { let type_name = match defined { @@ -418,7 +613,7 @@ fn parse_type( Defined::Object { name } => name, }; parse_defined_type(reader, type_name, resolver, size_guard) - }, + } } } @@ -429,7 +624,9 @@ fn parse_defined_type( resolver: &TypeResolver, size_guard: &mut SizeGuard, ) -> Result> { - let ty_def = resolver.resolve(type_name).ok_or_else(|| format!("type {} not found in IDL", type_name))?; + let ty_def = resolver + .resolve(type_name) + .ok_or_else(|| format!("type {} not found in IDL", type_name))?; match &ty_def.r#type { IdlTypeDefinitionType::Struct { fields } => { @@ -437,14 +634,15 @@ fn parse_defined_type( for field in fields { map.insert( field.name.clone(), - parse_type(reader, &field.r#type, resolver, size_guard)? + parse_type(reader, &field.r#type, resolver, size_guard)?, ); } Ok(map.into()) } IdlTypeDefinitionType::Enum { variants } => { let variant_index = reader.read_u8()?; - let variant = variants.get(variant_index as usize) + let variant = variants + .get(variant_index as usize) .ok_or("invalid variant index")?; let value = match &variant.fields { @@ -460,7 +658,7 @@ fn parse_defined_type( for field in fields { map.insert( field.name.clone(), - parse_type(reader, &field.r#type, resolver, size_guard)? + parse_type(reader, &field.r#type, resolver, size_guard)?, ); } serde_json::Value::Object(map) @@ -472,18 +670,86 @@ fn parse_defined_type( variant.name.clone(): value })) } - IdlTypeDefinitionType::Alias { value } => { - parse_type(reader, value, resolver, size_guard) - } + IdlTypeDefinitionType::Alias { value } => parse_type(reader, value, resolver, size_guard), } } +/// Parses individual instruction data using a provided IDL. +/// +/// This function allows parsing instruction data without requiring full transaction context. +/// It matches the instruction by discriminator and parses the arguments according to the IDL. +/// +/// # Arguments +/// * `instruction_data` - The raw instruction data bytes (including discriminator) +/// * `program_id` - The program ID this instruction belongs to (used for context in output) +/// * `idl` - The IDL to use for parsing +/// +/// # Returns +/// * `SolanaParsedInstructionData` containing the parsed instruction name, discriminator, +/// and arguments. Note that `named_accounts` will be empty since no accounts are provided. +/// +/// # Example +/// ```ignore +/// use solana_parser::{parse_instruction_with_idl, decode_idl_data}; +/// +/// let idl_json = r#"{"instructions": [...], "types": [...]}"#; +/// let idl = decode_idl_data(idl_json).unwrap(); +/// let instruction_data = hex::decode("abc123...").unwrap(); +/// +/// let parsed = parse_instruction_with_idl(&instruction_data, "program_id", &idl).unwrap(); +/// println!("Instruction: {}", parsed.instruction_name); +/// ``` +#[allow(dead_code)] // Public API - exported from lib.rs +pub fn parse_instruction_with_idl( + instruction_data: &[u8], + _program_id: &str, + idl: &Idl, +) -> Result> { + use crate::solana::structs::IdlSource; + + // Find the matching instruction by discriminator + let instruction = + find_instruction_by_discriminator(instruction_data, idl.instructions.clone())?; + + // Parse the instruction data into arguments + let parsed_args = parse_data_into_args(instruction_data, &instruction, idl).map_err( + |e| -> Box { + format!( + "failed to parse instruction data for instruction '{}': {e}", + instruction.name + ) + .into() + }, + )?; + + // Get the discriminator (we know it exists since find_instruction_by_discriminator succeeded) + let discriminator_bytes = instruction + .discriminator + .ok_or("instruction matched but discriminator is missing")?; + + // Compute IDL hash from serialized IDL + let idl_json = serde_json::to_string(idl)?; + let idl_hash = compute_idl_hash(&idl_json); + + Ok(SolanaParsedInstructionData { + instruction_name: instruction.name, + discriminator: hex::encode(&discriminator_bytes), + program_call_args: parsed_args, + named_accounts: std::collections::HashMap::new(), + idl_source: IdlSource::Custom, + idl_hash, + }) +} + /* Cycle Checking in Defined types */ // Check IDL for Cycles -- takes in an IDL and checks to see whether the defined types contains any cycles (invalid case) -fn check_idl_for_cycles(idl: Idl, type_cache: &HashMap) -> Result<(), Box> { +fn check_idl_for_cycles( + idl: Idl, + type_cache: &HashMap, +) -> Result<(), Box> { for t in idl.types { cycle_recursive_check(type_cache.clone(), &t.name, HashSet::new())?; } @@ -498,22 +764,26 @@ fn cycle_recursive_check( ) -> Result<(), Box> { // Check to see whether max depth has been exceeded if path.len() > MAX_DEFINED_TYPE_DEPTH { - return Err(format!("defined types resolution max depth exceeded on type: {type_name}").into()); + return Err( + format!("defined types resolution max depth exceeded on type: {type_name}").into(), + ); } // Check to see whether a cycle has been detected if path.contains(type_name) { - return Err(format!("defined types cycle check failed. Recursive type found: {type_name}").into()); + return Err( + format!("defined types cycle check failed. Recursive type found: {type_name}").into(), + ); } path.insert(type_name.to_string()); - let ty_def = type_cache - .get(type_name) - .copied() - .ok_or_else(|| -> Box { + let ty_def = + type_cache + .get(type_name) + .copied() + .ok_or_else(|| -> Box { format!("type {type_name} not found in IDL").into() - } - )?; + })?; match &ty_def.r#type { IdlTypeDefinitionType::Struct { fields } => { @@ -546,4 +816,4 @@ fn cycle_recursive_check( Ok(()) } } -} \ No newline at end of file +} diff --git a/src/solana/mod.rs b/src/solana/mod.rs index 946a42b..0ee34f2 100644 --- a/src/solana/mod.rs +++ b/src/solana/mod.rs @@ -1,7 +1,8 @@ +pub mod embedded_idls; +pub mod idl_db; +pub mod idl_parser; pub mod parser; pub mod structs; -pub mod idl_parser; -pub mod idl_db; #[cfg(test)] mod tests; diff --git a/src/solana/parser.rs b/src/solana/parser.rs index 4ddc3f5..968f3f8 100644 --- a/src/solana/parser.rs +++ b/src/solana/parser.rs @@ -1,4 +1,10 @@ -use std::{error::Error, collections::HashMap, fs}; +use super::structs::{ + AccountAddress, CustomIdlConfig, IdlRecord, SolTransfer, SolanaAccount, + SolanaAddressTableLookup, SolanaInstruction, SolanaMetadata, SolanaParseResponse, + SolanaParsedInstructionData, SolanaParsedTransaction, SolanaParsedTransactionPayload, + SolanaSingleAddressTableLookup, SplTransfer, +}; +use crate::solana::idl_parser; use hex; use solana_sdk::{ hash::Hash, @@ -8,12 +14,9 @@ use solana_sdk::{ Message as LegacyMessage, MessageHeader, VersionedMessage, }, pubkey::Pubkey, - system_instruction::SystemInstruction, + system_instruction::SystemInstruction, }; -use super::structs::{AccountAddress, SolTransfer, SolanaAccount, SolanaParsedInstructionData, SolanaAddressTableLookup, SolanaInstruction, SolanaMetadata, SolanaParseResponse, SolanaParsedTransaction, SolanaParsedTransactionPayload, SolanaSingleAddressTableLookup, SplTransfer, IdlRecord}; -use crate::solana::idl_parser; - -pub const IDL_DIRECTORY: &str = "src/solana/idls/"; +use std::{collections::HashMap, error::Error}; // Length of a solana signature in bytes (64 bytes long) pub const LEN_SOL_SIGNATURE_BYTES: usize = 64; @@ -25,26 +28,88 @@ 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"; // Versioned transactions have a prefix of 0x80 const V0_TRANSACTION_INDICATOR: u8 = 0x80; -// Entrypoint to parsing -pub fn parse_transaction(unsigned_tx: String, full_transaction: bool) -> Result> { +/// Entrypoint to parsing (legacy API) +/// +/// # Arguments +/// * `unsigned_tx` - Hex string of the transaction to parse +/// * `full_transaction` - Whether the input is a full transaction or just a message +/// * `custom_idls` - Optional map of program_id -> (idl_json, override_builtin) +/// - idl_json: JSON string of the IDL +/// - override_builtin: if true, use custom IDL even if a built-in exists for this program_id +/// - Pass `None` to use only built-in IDLs (default behavior) +pub fn parse_transaction( + unsigned_tx: String, + full_transaction: bool, + custom_idls: Option>, +) -> Result> { + // Convert old API to new API + let custom_configs = custom_idls.map(|idls| { + idls.into_iter() + .map(|(program_id, (json, override_builtin))| { + ( + program_id, + CustomIdlConfig::from_json(json, override_builtin), + ) + }) + .collect() + }); + parse_transaction_with_idls(unsigned_tx, full_transaction, custom_configs) +} + +/// Parse a Solana transaction with custom IDLs +/// +/// This is the preferred API for passing custom IDLs. It supports both pre-parsed +/// `Idl` structs and JSON strings via `CustomIdlConfig`. +/// +/// # Arguments +/// * `unsigned_tx` - Hex string of the transaction to parse +/// * `full_transaction` - Whether the input is a full transaction or just a message +/// * `custom_idls` - Optional map of program_id -> CustomIdlConfig +/// - Use `CustomIdlConfig::from_idl(idl, override)` to pass a pre-parsed Idl struct +/// - Use `CustomIdlConfig::from_json(json, override)` to pass a JSON string +/// - Pass `None` to use only built-in IDLs (default behavior) +/// +/// # Example +/// ```ignore +/// use solana_parser::{parse_transaction_with_idls, CustomIdlConfig, Idl}; +/// use std::collections::HashMap; +/// +/// // Using a pre-parsed IDL +/// let idl: Idl = /* ... */; +/// let mut custom_idls = HashMap::new(); +/// custom_idls.insert( +/// "program_id".to_string(), +/// CustomIdlConfig::from_idl(idl, true), // override built-in +/// ); +/// +/// let result = parse_transaction_with_idls(tx_hex, true, Some(custom_idls)); +/// ``` +pub fn parse_transaction_with_idls( + unsigned_tx: String, + full_transaction: bool, + custom_idls: Option>, +) -> Result> { if unsigned_tx.is_empty() { return Err("Transaction is empty".into()); } - let tx = SolanaTransaction::new(&unsigned_tx, full_transaction).map_err(|e| { - Box::::from(format!("Unable to parse transaction: {}", e)) - })?; + let tx = SolanaTransaction::new_with_idls(&unsigned_tx, full_transaction, custom_idls) + .map_err(|e| { + Box::::from(format!("Unable to parse transaction: {e}")) + })?; // use the sanitize message to check for malformed transactions tx.message.sanitize().map_err(|e| { - Box::::from(format!("Solana transaction message failed sanitization check: {}", e)) + Box::::from(format!( + "Solana transaction message failed sanitization check: {e}" + )) })?; let payload = SolanaParsedTransactionPayload { @@ -63,36 +128,63 @@ pub fn parse_transaction(unsigned_tx: String, full_transaction: bool) -> Result< Parse Solana Transaction - This function takes an unsigned solana transaction hex string and parses it either as a v0 transaction or as legacy transaction (v0 transactions include Address Lookup Tables which allow more addresses to be included in a transaction by only including references to the addresses instead of the whole string) */ +#[allow(dead_code)] // Kept for backwards compatibility, use parse_solana_transaction_with_idls fn parse_solana_transaction( unsigned_tx: &str, full_transaction: bool, + custom_idls: Option>, ) -> Result> { - let unsigned_tx_bytes: Vec = hex::decode(unsigned_tx).map_err(|_| { - "unsigned Solana transaction provided is invalid hex" - })?; + // Convert old API to new API + let custom_configs = custom_idls.map(|idls| { + idls.into_iter() + .map(|(program_id, (json, override_builtin))| { + ( + program_id, + CustomIdlConfig::from_json(json, override_builtin), + ) + }) + .collect() + }); + parse_solana_transaction_with_idls(unsigned_tx, full_transaction, custom_configs) +} + +fn parse_solana_transaction_with_idls( + unsigned_tx: &str, + full_transaction: bool, + custom_idls: Option>, +) -> Result> { + let unsigned_tx_bytes: Vec = hex::decode(unsigned_tx) + .map_err(|_| "unsigned Solana transaction provided is invalid hex")?; if unsigned_tx_bytes.is_empty() { - return Err( - Box::::from("unsigned Solana transaction provided must be non-empty"), - ); + return Err(Box::::from( + "unsigned Solana transaction provided must be non-empty", + )); } - let custom_idl_records = idl_parser::construct_custom_idl_records_map()?; + let custom_idl_records = idl_parser::construct_idl_records_map(custom_idls)?; 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 --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:#?}"))?, }; - return Ok(SolanaTransaction{ message, signatures, custom_idl_records }); + return Ok(SolanaTransaction { + message, + signatures, + custom_idl_records, + }); } 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 --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:#?}"))?, }; - return Ok(SolanaTransaction{ message, signatures: vec![], custom_idl_records }); // Signatures array is empty when we are parsing a message (using --message) as opposed to a full transaction - + Ok(SolanaTransaction { + message, + signatures: vec![], + custom_idl_records, + }) // Signatures array is empty when we are parsing a message (using --message) as opposed to a full transaction } /* @@ -172,10 +264,11 @@ 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) */ +#[allow(clippy::type_complexity)] fn parse_signatures( unsigned_tx_bytes: &[u8], ) -> Result<(Vec, &[u8]), Box> { @@ -192,7 +285,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()], + )) } /* @@ -223,6 +319,7 @@ fn parse_header( Parse Accounts - This function parses the compact array of all static account keys (as opposed to address table lookups) included in this transaction */ +#[allow(clippy::type_complexity)] fn parse_accounts( tx_body_remainder: &[u8], ) -> Result<(Vec, &[u8]), Box> { @@ -267,6 +364,7 @@ fn parse_block_hash(tx_body_remainder: &[u8]) -> Result<(Hash, &[u8]), Box Result<(Vec, &[u8]), Box> { @@ -318,6 +416,7 @@ fn parse_single_instruction( Parse Address Table Lookups - This function parses all address table lookups included in the transaction into a vector of MessageAddressTableLookup objects as described by the Solana SDK */ +#[allow(clippy::type_complexity)] fn parse_address_table_lookups( tx_body_remainder: &[u8], ) -> Result<(Vec, &[u8]), Box> { @@ -371,6 +470,7 @@ Parse Compact Array of Bytes - Context: there are various cases in a solana transaction where a compact array of bytes is included with the first byte being how many bytes there are in the array. These byte arrays include Instruction account indexes and the instruction data - This method parses a compact array of individual bytes */ +#[allow(clippy::type_complexity)] fn parse_compact_array_of_bytes<'a>( tx_body_remainder: &'a [u8], section: &str, @@ -380,7 +480,7 @@ fn parse_compact_array_of_bytes<'a>( LEN_ARRAY_HEADER_BYTES, &format!("{section} Array Header"), )?; - let (length, tx_body_remainder) = read_compact_u16(&tx_body_remainder)?; + let (length, tx_body_remainder) = read_compact_u16(tx_body_remainder)?; let parse_len = length * LEN_ARRAY_HEADER_BYTES; validate_length(tx_body_remainder, parse_len, &format!("{section} Array"))?; let bytes: Vec = tx_body_remainder[0..parse_len].to_vec(); @@ -400,7 +500,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; @@ -408,10 +510,9 @@ fn read_compact_u16(tx_body_remainder: &[u8]) -> Result<(usize, &[u8]), Box= tx_body_remainder.len() { - return Err(format!( - "error parsing unsigned transaction: unable to parse compact array header, not enough bytes" - ) - .into()); + return Err( + "error parsing unsigned transaction: unable to parse compact array header, not enough bytes".into() + ); } let byte = tx_body_remainder[bytes_read]; @@ -419,16 +520,18 @@ 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(|_| "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(|_| "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 => { @@ -463,14 +574,22 @@ impl SplInstructionData { // Given the extension prefix of 26 ensure that we're calling instruciton 01, which is TransferCheckedWithFee let (amount, decimals, fee) = match transfer_fee_tag { 1 => { - let (amount, rest) = unpack_u64(rest).map_err(|_| format!("error while parsing spl instruction TransferCheckedWithFee -- amount"))?; + let (amount, rest) = unpack_u64(rest).map_err(|_| { + "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(|_| { + "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, }) @@ -493,11 +612,24 @@ pub type Signature = Vec; pub struct SolanaTransaction { message: VersionedMessage, signatures: Vec, - custom_idl_records: HashMap + custom_idl_records: HashMap, } impl SolanaTransaction { - pub fn new(hex_tx: &str, full_transaction: bool) -> Result> { - parse_solana_transaction(hex_tx, full_transaction) + #[allow(dead_code)] // Kept for backwards compatibility, use new_with_idls + pub fn new( + hex_tx: &str, + full_transaction: bool, + custom_idls: Option>, + ) -> Result> { + parse_solana_transaction(hex_tx, full_transaction, custom_idls) + } + + pub fn new_with_idls( + hex_tx: &str, + full_transaction: bool, + custom_idls: Option>, + ) -> Result> { + parse_solana_transaction_with_idls(hex_tx, full_transaction, custom_idls) } fn all_account_key_strings(&self) -> Vec { @@ -594,9 +726,13 @@ impl SolanaTransaction { } } + #[allow(clippy::type_complexity)] fn all_instructions_and_transfers( &self, - ) -> Result<(Vec, Vec, Vec), Box> { + ) -> Result< + (Vec, Vec, Vec), + Box, + > { // use the sanitize message to check for malformed transactions self.message.sanitize().map_err(|e| { Box::::from(format!("Solana transaction message failed sanitization check while parsing instructions: {}", e)) @@ -617,7 +753,8 @@ impl SolanaTransaction { let atlu = self.resolve_address_table_lookup(a as usize)?; // push the parsed address table lookup to both the lookups array AND the combined all transaction address array atlu_addresses.push(atlu.clone()); - all_transaction_addresses.push(AccountAddress::AddressTableLookUp(atlu.clone())); + all_transaction_addresses + .push(AccountAddress::AddressTableLookUp(atlu.clone())); continue; } let account_key = self @@ -637,17 +774,19 @@ impl SolanaTransaction { } // Make sure that program id is a statically included address -- Including program ID's as Address Table Lookups is INVALID if i.program_id_index as usize >= self.message.static_account_keys().len() { - return Err(Box::::from("Solana Instruction program index must be within static account keys")) + return Err(Box::::from( + "Solana Instruction program index must be within static account keys", + )); } let program_key = i.program_id(self.message.static_account_keys()).to_string(); - + 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 all_transaction_addresses.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(), @@ -658,25 +797,34 @@ 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, all_transaction_addresses.clone())?; - match spl_tranfer_opt { - Some(spl_transfer) => spl_transfers.push(spl_transfer), - None => (), + let token_program_instruction: SplInstructionData = + SplInstructionData::parse_spl_transfer_data(&i.data)?; + if let Some(spl_transfer) = self.parse_spl_instruction_data( + token_program_instruction, + all_transaction_addresses.clone(), + )? { + spl_transfers.push(spl_transfer); } } 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, all_transaction_addresses.clone())?; - match spl_tranfer_opt { - Some(spl_transfer) => spl_transfers.push(spl_transfer), - None => (), + let token_program_22_instruction: SplInstructionData = + SplInstructionData::parse_spl_transfer_data(&i.data)?; + if let Some(spl_transfer) = self.parse_spl_instruction_data( + token_program_22_instruction, + all_transaction_addresses.clone(), + )? { + spl_transfers.push(spl_transfer); } } _ => {} } - let parsed_inst_option: Option = parse_idl(&program_key, &all_transaction_addresses, i, &self.custom_idl_records)?; + let parsed_inst_option: Option = parse_idl( + &program_key, + &all_transaction_addresses, + i, + &self.custom_idl_records, + )?; let instruction_data_hex: String = hex::encode(&i.data); let inst = SolanaInstruction { @@ -684,7 +832,7 @@ impl SolanaTransaction { accounts: static_accounts, instruction_data_hex, address_table_lookups: atlu_addresses, - parsed_instruction: parsed_inst_option + parsed_instruction: parsed_inst_option, }; instructions.push(inst); } @@ -692,7 +840,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, all_transaction_addresses: Vec) -> Result, Box> { + fn parse_spl_instruction_data( + &self, + token_instruction: SplInstructionData, + all_transaction_addresses: Vec, + ) -> Result, Box> { if let SplInstructionData::Transfer { amount } = token_instruction { let signers = self.get_spl_multisig_signers_if_exist(&all_transaction_addresses, 3)?; let spl_transfer = SplTransfer { @@ -701,12 +853,12 @@ impl SolanaTransaction { to: all_transaction_addresses[1].to_string(), // the "to" address is the address at index 1 in the address parameter array in an SPL Transfer Instruction owner: all_transaction_addresses[2].to_string(), // the "owner" address is the address at index 2 in the address parameter array in an SPL Transfer Instruction 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(&all_transaction_addresses, 4)?; let spl_transfer = SplTransfer { amount: amount.to_string(), @@ -718,8 +870,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(&all_transaction_addresses, 4)?; let spl_transfer = SplTransfer { amount: amount.to_string(), @@ -731,16 +888,25 @@ 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) + Ok(None) } - fn get_spl_multisig_signers_if_exist(&self, all_transaction_addresses: &Vec, num_accts_before_signer: usize) -> Result, Box> { + fn get_spl_multisig_signers_if_exist( + &self, + all_transaction_addresses: &[AccountAddress], + num_accts_before_signer: usize, + ) -> Result, Box> { if all_transaction_addresses.len() < num_accts_before_signer { - return Err(format!("invalid number of accounts provided for spl token transfer instruction").into()) + return Err( + "invalid number of accounts provided for spl token transfer instruction".into(), + ); } - Ok(all_transaction_addresses[num_accts_before_signer..all_transaction_addresses.len()].to_vec().into_iter().map(|a| a.to_string()).collect()) + Ok(all_transaction_addresses[num_accts_before_signer..] + .iter() + .map(|a| a.to_string()) + .collect()) } fn recent_blockhash(&self) -> String { @@ -771,7 +937,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()) @@ -793,60 +960,60 @@ impl SolanaTransaction { } } -/* - This method is used for parsing custom IDL's uploaded by the end user - */ - fn parse_idl( - program_key: &str, - all_transaction_addresses: &[AccountAddress], - inst: &CompiledInstruction, - custom_idls: &HashMap, - ) -> Result, Box> { - if let Some(idl_record) = custom_idls.get(program_key) { - // read idl data into json string - let idl_json_str = fs::read_to_string(IDL_DIRECTORY.to_string() + &idl_record.file_path)?; - - // Parse idl interface json string into idl type - let idl = idl_parser::decode_idl_data(&idl_json_str).map_err(|e| -> Box { - format!( - "failed to parse Solana IDL with program key: {program_key} with error: {}", - e - ).into() - })?; - - let instruction = idl_parser::find_instruction_by_discriminator(&inst.data, idl.instructions.clone())?; - - // Parse data into args map - let parsed_args = idl_parser::parse_data_into_args(&inst.data, &instruction, &idl).map_err(|e| -> Box { - format!( - "failed to parse instruction call data into IDL instruction for instruction name: {} with error: {}", - instruction.name, e - ).into() - })?; +/// Parses instruction data using the IDL for the given program. +/// IDLs are embedded at compile time - no file system access required. +fn parse_idl( + program_key: &str, + all_transaction_addresses: &[AccountAddress], + inst: &CompiledInstruction, + custom_idls: &HashMap, +) -> Result, Box> { + if let Some(idl_record) = custom_idls.get(program_key) { + // Resolve which IDL to use (built-in or custom) - uses embedded IDLs + let (idl, idl_json_str, idl_source) = + idl_parser::resolve_idl_for_record(idl_record, program_key)?; + + // Compute IDL hash + let idl_hash = idl_parser::compute_idl_hash(&idl_json_str); + + let instruction = + idl_parser::find_instruction_by_discriminator(&inst.data, idl.instructions.clone())?; + + // Parse data into args map + let parsed_args = idl_parser::parse_data_into_args(&inst.data, &instruction, &idl).map_err(|e| -> Box { + format!( + "failed to parse instruction call data into IDL instruction for instruction name: {} with error: {}", + instruction.name, e + ).into() + })?; - // Create named accounts map - let named_accounts = idl_parser::create_accounts_map(all_transaction_addresses, &instruction).map_err(|e| -> Box { + // Create named accounts map + let named_accounts = + idl_parser::create_accounts_map(all_transaction_addresses, &instruction).map_err( + |e| -> Box { format!( - "failed to create named accounts map for instruction name: {} with error: {}", - instruction.name, e - ).into() - })?; - - if let Some(discriminator_bytes) = instruction.discriminator { - return Ok(Some(SolanaParsedInstructionData { - program_call_args: parsed_args, - discriminator: hex::encode(&discriminator_bytes), - instruction_name: instruction.name, - named_accounts, - })); - } - // We shouldn't get here because we found the instruction by the discriminator above - return Err( - "attempted to parse IDL into instruction without discriminator".into() - ); + "failed to create named accounts map for instruction name: {} with error: {}", + instruction.name, e + ) + .into() + }, + )?; + + if let Some(discriminator_bytes) = instruction.discriminator { + return Ok(Some(SolanaParsedInstructionData { + program_call_args: parsed_args, + discriminator: hex::encode(&discriminator_bytes), + instruction_name: instruction.name, + named_accounts, + idl_source, + idl_hash, + })); } - Ok(None) + // We shouldn't get here because we found the instruction by the discriminator above + return Err("attempted to parse IDL into instruction without discriminator".into()); } + Ok(None) +} #[cfg(test)] mod tests { @@ -861,7 +1028,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"; @@ -909,13 +1076,13 @@ mod tests { // Test 1: Empty input to parse_solana_transaction() let unsigned_tx_too_short1 = ""; - let empty_err = parse_solana_transaction(unsigned_tx_too_short1, false).unwrap_err(); + let empty_err = parse_solana_transaction(unsigned_tx_too_short1, false, None).unwrap_err(); assert_eq!( empty_err.to_string(), "unsigned Solana transaction provided must be non-empty" ); let unsigned_tx_too_short2 = "0x"; - let empty_err = parse_solana_transaction(unsigned_tx_too_short2, false).unwrap_err(); + let empty_err = parse_solana_transaction(unsigned_tx_too_short2, false, None).unwrap_err(); assert_eq!( empty_err.to_string(), "unsigned Solana transaction provided is invalid hex" @@ -926,7 +1093,7 @@ mod tests { fn solana_fuzzer_regression_test2() { // Test 2: Parsing hex bytes with characters with multi-byte widths let unsigned_tx_2 = ".Ñ‘%"; - let empty_err = parse_solana_transaction(unsigned_tx_2, false).unwrap_err(); + let empty_err = parse_solana_transaction(unsigned_tx_2, false, None).unwrap_err(); assert_eq!( empty_err.to_string(), "unsigned Solana transaction provided is invalid hex" @@ -939,17 +1106,20 @@ mod tests { // data extracted from fuzz case let unsigned_tx_instruction_problem1_raw = vec![ - 0x01, 0x01, 0x03, 0x00, 0x0a, 0x0a, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x82, 0x82, 0x82, 0x82, - 0x82, 0x82, 0x00, 0x00, 0xf1, 0xf1, 0xf1, 0xf1, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, - 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0xf1, 0xf1, 0xf1, 0xf1, 0xf1, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, + 0x01, 0x01, 0x03, 0x00, 0x0a, 0x0a, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x66, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0xf1, 0xf1, + 0xf1, 0xf1, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, + 0xf1, 0xf1, 0xf1, 0xf1, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - let unsigned_tx_instruction_problem1_hex_string = hex::encode(&unsigned_tx_instruction_problem1_raw); + let unsigned_tx_instruction_problem1_hex_string = + hex::encode(&unsigned_tx_instruction_problem1_raw); let unsigned_tx_instruction_problem1 = - parse_solana_transaction(&unsigned_tx_instruction_problem1_hex_string, true).unwrap(); + parse_solana_transaction(&unsigned_tx_instruction_problem1_hex_string, true, None) + .unwrap(); // symptom: this panics with "index out of bounds: the len is 0 but the index is 0" let fuzz_err = unsigned_tx_instruction_problem1 @@ -968,19 +1138,21 @@ mod tests { // data extracted from fuzz case let unsigned_tx_instruction_problem2_string = String::from_utf8(vec![ - 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x35, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x35, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x35, 0x46, 0x46, 0x46, - 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, - 0x30, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, - 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x35, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, + 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x35, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, + 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x35, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, + 0x46, 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, + 0x30, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x30, 0x30, 0x30, 0x31, + 0x30, 0x30, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, ]) .unwrap(); let unsigned_tx_instruction_problem2 = - parse_solana_transaction(&unsigned_tx_instruction_problem2_string, true).unwrap(); + parse_solana_transaction(&unsigned_tx_instruction_problem2_string, true, None).unwrap(); // symptom: this panics with "attempt to subtract with overflow" let fuzz_err = unsigned_tx_instruction_problem2 @@ -992,4 +1164,4 @@ mod tests { "Solana transaction message failed sanitization check while parsing instructions: index out of bounds" ); } -} \ No newline at end of file +} diff --git a/src/solana/structs.rs b/src/solana/structs.rs index 1995453..f81acff 100644 --- a/src/solana/structs.rs +++ b/src/solana/structs.rs @@ -1,6 +1,152 @@ -use std::{fmt, collections::HashMap}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::{collections::HashMap, fmt}; + +/// ProgramType represents the built-in IDL types supported by the library +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProgramType { + ApePro, + CandyMachine, + Drift, + JupiterLimit, + Jupiter, + Kamino, + Lifinity, + Meteora, + Openbook, + Orca, + Raydium, + Stabble, + JupiterAggregatorV6, +} + +impl ProgramType { + /// Returns the program ID associated with this ProgramType + pub fn program_id(&self) -> &str { + match self { + ProgramType::ApePro => "JSW99DKmxNyREQM14SQLDykeBvEUG63TeohrvmofEiw", + ProgramType::CandyMachine => "cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ", + ProgramType::Drift => "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH", + ProgramType::JupiterLimit => "j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X", + ProgramType::Jupiter => "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB", + ProgramType::Kamino => "6LtLpnUFNByNXLyCoK9wA2MykKAmQNZKBdY8s47dehDc", + ProgramType::Lifinity => "2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c", + ProgramType::Meteora => "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo", + ProgramType::Openbook => "opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb", + ProgramType::Orca => "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", + ProgramType::Raydium => "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", + ProgramType::Stabble => "swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ", + ProgramType::JupiterAggregatorV6 => "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", + } + } + + /// Returns the file path for this ProgramType (deprecated, IDLs are now embedded) + #[allow(dead_code)] // Kept for backwards compatibility + pub fn file_path(&self) -> &str { + match self { + ProgramType::ApePro => "ape_pro.json", + ProgramType::CandyMachine => "cndy.json", + ProgramType::Drift => "drift.json", + ProgramType::JupiterLimit => "jupiter_limit.json", + ProgramType::Jupiter => "jupiter.json", + ProgramType::Kamino => "kamino.json", + ProgramType::Lifinity => "lifinity.json", + ProgramType::Meteora => "meteora.json", + ProgramType::Openbook => "openbook.json", + ProgramType::Orca => "orca.json", + ProgramType::Raydium => "raydium.json", + ProgramType::Stabble => "stabble.json", + ProgramType::JupiterAggregatorV6 => "jupiter_agg_v6.json", + } + } + + /// Returns the program name for this ProgramType + pub fn program_name(&self) -> &str { + match self { + ProgramType::ApePro => "Ape Pro", + ProgramType::CandyMachine => "Metaplex Candy Machine", + ProgramType::Drift => "Drift Protocol V2", + ProgramType::JupiterLimit => "Jupiter Limit", + ProgramType::Jupiter => "Jupiter Swap", + ProgramType::Kamino => "Kamino", + ProgramType::Lifinity => "Lifinity Swap V2", + ProgramType::Meteora => "Meteora", + ProgramType::Openbook => "Openbook", + ProgramType::Orca => "Orca Whirlpool", + ProgramType::Raydium => "Raydium", + ProgramType::Stabble => "Stabble", + ProgramType::JupiterAggregatorV6 => "Jupiter Aggregator V6", + } + } + + /// Looks up a ProgramType by program_id + #[allow(dead_code)] // Public API + pub fn from_program_id(program_id: &str) -> Option { + match program_id { + "JSW99DKmxNyREQM14SQLDykeBvEUG63TeohrvmofEiw" => Some(ProgramType::ApePro), + "cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ" => Some(ProgramType::CandyMachine), + "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" => Some(ProgramType::Drift), + "j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X" => Some(ProgramType::JupiterLimit), + "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB" => Some(ProgramType::Jupiter), + "6LtLpnUFNByNXLyCoK9wA2MykKAmQNZKBdY8s47dehDc" => Some(ProgramType::Kamino), + "2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c" => Some(ProgramType::Lifinity), + "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo" => Some(ProgramType::Meteora), + "opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb" => Some(ProgramType::Openbook), + "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc" => Some(ProgramType::Orca), + "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" => Some(ProgramType::Raydium), + "swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ" => Some(ProgramType::Stabble), + "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" => Some(ProgramType::JupiterAggregatorV6), + _ => None, + } + } + + /// Returns all built-in program types + pub fn all() -> &'static [ProgramType] { + &[ + ProgramType::ApePro, + ProgramType::CandyMachine, + ProgramType::Drift, + ProgramType::JupiterLimit, + ProgramType::Jupiter, + ProgramType::Kamino, + ProgramType::Lifinity, + ProgramType::Meteora, + ProgramType::Openbook, + ProgramType::Orca, + ProgramType::Raydium, + ProgramType::Stabble, + ProgramType::JupiterAggregatorV6, + ] + } + + /// Returns the embedded IDL JSON string for this program type. + /// IDLs are compiled into the binary. + pub fn idl_json(&self) -> &'static str { + use crate::solana::embedded_idls::*; + match self { + ProgramType::ApePro => APE_PRO_IDL, + ProgramType::CandyMachine => CANDY_MACHINE_IDL, + ProgramType::Drift => DRIFT_IDL, + ProgramType::JupiterLimit => JUPITER_LIMIT_IDL, + ProgramType::Jupiter => JUPITER_IDL, + ProgramType::Kamino => KAMINO_IDL, + ProgramType::Lifinity => LIFINITY_IDL, + ProgramType::Meteora => METEORA_IDL, + ProgramType::Openbook => OPENBOOK_IDL, + ProgramType::Orca => ORCA_IDL, + ProgramType::Raydium => RAYDIUM_IDL, + ProgramType::Stabble => STABBLE_IDL, + ProgramType::JupiterAggregatorV6 => JUPITER_AGG_V6_IDL, + } + } +} + +/// IdlSource indicates whether a built-in or custom IDL was used for parsing +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum IdlSource { + BuiltIn(ProgramType), + Custom, +} #[derive(Debug, Clone, PartialEq)] pub struct SolanaMetadata { @@ -50,18 +196,22 @@ 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, } #[derive(Debug, Clone, PartialEq)] pub struct SolanaParsedInstructionData { - pub instruction_name: String, + pub instruction_name: String, pub discriminator: String, - pub named_accounts: HashMap, + pub named_accounts: HashMap, pub program_call_args: serde_json::Map, + /// Indicates whether a built-in or custom IDL was used + pub idl_source: IdlSource, + /// SHA256 hash of the compressed (whitespace removed) IDL JSON string + pub idl_hash: String, } #[derive(Debug, Clone, PartialEq)] @@ -115,12 +265,20 @@ impl fmt::Display for AccountAddress { - vendor docs reference: */ -// Contains a reference to "uploaded" IDL's +/// Contains resolved IDL information for a program. +/// IDLs are now stored as parsed structs, not file paths. #[derive(Debug, PartialEq, Eq, Clone)] pub struct IdlRecord { pub program_id: String, pub program_name: String, - pub file_path: String, + /// The built-in program type (if this is a known program) + pub program_type: Option, + /// Custom IDL provided by the caller (already parsed) + pub custom_idl: Option, + /// The JSON string for the custom IDL (used for hash computation) + pub custom_idl_json: Option, + /// Whether to override built-in IDL with custom one (if both exist) + pub override_builtin: bool, } /// IDL that is compatible with what anchor and shank extract from a solana program. @@ -291,3 +449,54 @@ impl fmt::Display for Defined { } } +/// Represents a custom IDL that can be passed to the parser. +/// Supports both pre-parsed `Idl` structs and JSON strings. +#[derive(Debug, Clone)] +#[allow(dead_code)] // Public API - variants used by library consumers +pub enum CustomIdl { + /// A pre-parsed IDL struct (avoids re-parsing) + Parsed(Idl), + /// An IDL as a JSON string (will be parsed) + Json(String), +} + +#[allow(dead_code)] // Public API +impl CustomIdl { + /// Create a CustomIdl from a pre-parsed Idl struct + pub fn from_idl(idl: Idl) -> Self { + CustomIdl::Parsed(idl) + } + + /// Create a CustomIdl from a JSON string + pub fn from_json(json: String) -> Self { + CustomIdl::Json(json) + } +} + +/// Configuration for a custom IDL to be used during parsing +#[derive(Debug, Clone)] +pub struct CustomIdlConfig { + /// The custom IDL (either pre-parsed or as JSON) + pub idl: CustomIdl, + /// If true, override built-in IDL even if one exists for this program + pub override_builtin: bool, +} + +#[allow(dead_code)] // Public API +impl CustomIdlConfig { + /// Create a new custom IDL config from a pre-parsed Idl + pub fn from_idl(idl: Idl, override_builtin: bool) -> Self { + Self { + idl: CustomIdl::Parsed(idl), + override_builtin, + } + } + + /// Create a new custom IDL config from a JSON string + pub fn from_json(json: String, override_builtin: bool) -> Self { + Self { + idl: CustomIdl::Json(json), + override_builtin, + } + } +} diff --git a/src/solana/tests.rs b/src/solana/tests.rs index 5d2bde0..1dcc22f 100644 --- a/src/solana/tests.rs +++ b/src/solana/tests.rs @@ -1,1690 +1,1848 @@ -use std::{vec, collections::HashMap}; -use serde_json::{Value, Number, Map}; +use serde_json::{Map, Number, Value}; +use std::{collections::HashMap, vec}; use super::*; +use crate::solana::idl_parser; +use crate::solana::parser::{SolanaTransaction, TOKEN_2022_PROGRAM_KEY, TOKEN_PROGRAM_KEY}; +use crate::solana::structs::{ + IdlSource, ProgramType, SolTransfer, SolanaAccount, SolanaAddressTableLookup, + SolanaInstruction, SolanaSingleAddressTableLookup, +}; + +// Test-only IDL directory for test fixtures +const TEST_IDL_DIRECTORY: &str = "src/solana/idls/"; 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, IDL_DIRECTORY}; -use crate::solana::idl_parser; - - - #[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 Solana transaction provided is invalid hex" - ); - - // 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, None).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, None).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, None); + + let inst_error_message = parsed_tx.unwrap_err().to_string(); // Unwrap the error + assert_eq!( + inst_error_message, + "unsigned Solana transaction provided is invalid hex" + ); + + // Invalid length for Instruction Data Array + let unsigned_payload = "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f000000000000".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_payload, true, None); + + 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, None); - 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, None).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, None).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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - }; - 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(), - parsed_instruction: None, - 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(), - parsed_instruction: None, - }; - 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 { + let exp_instruction_7 = &transaction_metadata.instructions[6]; + assert_eq!( + exp_instruction_7.program_key, + jupiter_program_acct_key.to_string() + ); + assert_eq!( + exp_instruction_7.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(), + ] + ); + assert_eq!(exp_instruction_7.address_table_lookups, lookups_7); + assert_eq!( + exp_instruction_7.instruction_data_hex, + "e517cb977ae3ad2a01000000120064000180f0fa02000000005d34700000000000320000" + ); + // Verify Jupiter IDL was parsed with correct metadata + let parsed = exp_instruction_7 + .parsed_instruction + .as_ref() + .expect("Jupiter instruction should be parsed"); + assert_eq!(parsed.instruction_name, "route"); + assert!(matches!( + parsed.idl_source, + IdlSource::BuiltIn(ProgramType::JupiterAggregatorV6) + )); + + // 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(), + parsed_instruction: None, + }; + 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, None).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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + 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(), + parsed_instruction: None, + }; + assert_eq!(exp_instruction_5, transaction_metadata.instructions[4]); + + // Instruction 6 -- Jupiter Aggregator v6: sharedAccountsRoute + let exp_instruction_6 = &transaction_metadata.instructions[5]; + assert_eq!( + exp_instruction_6.program_key, + jupiter_program_acct_key.to_string() + ); + assert_eq!( + exp_instruction_6.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(), + ] + ); + assert_eq!( + exp_instruction_6.instruction_data_hex, + "c1209b3341d69c810502000000136400011c016401028096980000000000b2a3170000000000640000" + ); + assert_eq!( + exp_instruction_6.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(), - writable_indexes: vec![65, 1, 4, 64, 7, 5, 2, 17, 3, 9], - readonly_indexes: vec![20, 0], + index: 17, + writable: true, }, - SolanaAddressTableLookup { + 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); - } - + 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, + }, + ] + ); + // Verify Jupiter IDL was parsed + assert!(exp_instruction_6.parsed_instruction.is_some()); + let parsed = exp_instruction_6.parsed_instruction.as_ref().unwrap(); + assert_eq!(parsed.instruction_name, "shared_accounts_route"); + assert_eq!(parsed.discriminator, "c1209b3341d69c81"); + assert!(matches!( + parsed.idl_source, + IdlSource::BuiltIn(ProgramType::JupiterAggregatorV6) + )); + + // 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(), + parsed_instruction: None, + }; + 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, None).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, None).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); + assert_eq!( + tx_metadata.instructions[1].instruction_data_hex, + "03a086010000000000" + ) +} + +#[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, None).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, None).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, None).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 + ) +} + +#[test] +fn parse_spl_transfer_using_address_table_lookups() { + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04021303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true, None).unwrap(); + let _ = parsed_tx.transaction_metadata().unwrap(); +} + +#[test] +fn parse_spl_transfer_using_address_table_lookups_mint() { + // This transaction contains two SPL transfer instructions + // BOTH Spl transfer instructions use an Address Table look up to represent the Token Mint address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04021303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true, None).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!( + spl_transfer_1.token_mint, + Some("ADDRESS_TABLE_LOOKUP".to_string()) + ); // EMPTY BECAUSE OF ATLU + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup token mint address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!( + spl_transfer_2.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_2.to, + "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + ); + assert_eq!( + spl_transfer_2.token_mint, + Some("ADDRESS_TABLE_LOOKUP".to_string()) + ); // EMPTY BECAUSE OF ATLU + assert_eq!( + spl_transfer_2.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); +} + +#[test] +fn parse_spl_transfer_using_address_table_lookups_recipient() { + // This transaction contains two SPL transfer instructions + // 1. Uses an Address Table look up to represent the Token Mint address + // 2. Uses an Address Table look up to represent the Recipient address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04020313000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true, None).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!( + spl_transfer_1.token_mint, + Some("ADDRESS_TABLE_LOOKUP".to_string()) + ); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup for receiving "to" address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!( + spl_transfer_2.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!(spl_transfer_2.to, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_2.token_mint, + Some("EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string()) + ); + assert_eq!( + spl_transfer_2.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); +} + +#[test] +fn parse_spl_transfer_using_address_table_lookups_sender() { + // This transaction contains two SPL transfer instructions + // 1. Uses an Address Table look up to represent the Token Mint address + // 2. Uses an Address Table look up to represent the Sending address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04130303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true, None).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!( + spl_transfer_1.token_mint, + Some("ADDRESS_TABLE_LOOKUP".to_string()) + ); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup for sending "from" address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!(spl_transfer_2.from, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_2.to, + "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + ); + assert_eq!( + spl_transfer_2.token_mint, + Some("EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string()) + ); + assert_eq!( + spl_transfer_2.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); +} + +#[test] +fn parse_spl_transfer_using_address_table_lookups_owner() { + // This transaction contains two SPL transfer instructions + // 1. Uses an Address Table look up to represent the Token Mint address + // 2. Uses an Address Table look up to represent the Owner address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04020003130a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true, None).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!( + spl_transfer_1.token_mint, + Some("ADDRESS_TABLE_LOOKUP".to_string()) + ); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup for owner address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!( + spl_transfer_2.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_2.to, + "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + ); + assert_eq!( + spl_transfer_2.token_mint, + Some("DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string()) + ); + assert_eq!(spl_transfer_2.owner, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction +} + +#[cfg(test)] +#[allow(clippy::module_inception)] +mod tests { + use super::*; + use std::fs; + + // JUPITER AGGREGATOR V6 TESTS -- IDL PARSING #[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]); - } + fn test_idl_parsing_jupiter_instruction_1() { + let jup_idl_file = "jupiter_agg_v6.json"; + + // Instruction #3 at this link -- https://solscan.io/tx/JcGkMSRd5iyXH5dar6MnjyrpEhQi5XR9mV2TfQpxLbTZH53geZsToDY4nzbcLGnvV32RvuEe4BPTSTkobV6rjEx + // Instruction Name: route + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode( + "e517cb977ae3ad2a0200000007640001266401026ca35b0400000000a5465c0400000000000000", + ) + .unwrap(), + ) + .unwrap(); - #[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); - assert_eq!(tx_metadata.instructions[1].instruction_data_hex, "03a086010000000000") + // Instruction #6 at this link -- https://solscan.io/tx/eQqquSewgVCP3jBkKeTstnEGGMuGaT7g7G7uPL9kfFgJk163BYFHhqE3hxNTsaoWWX6gJHRYNr2sSeRtEP3nnA3 + // Instruction Name: shared_accounts_route + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode("c1209b3341d69c810502000000266400014701006401028a28000000000000f405000000000000460014") + .unwrap(), + ) + .unwrap(); + + // Instruction #5 at this link -- https://solscan.io/tx/huK6BK5iUUvGZHHGJZbPrTjQrWgQY7vDQ4umFQTw8he1rvxPV6DH41XcwgEMZWXr9irgGKomxotQGzueCARqfkM + // Instruction Name: set_token_ledger + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode("e455b9704e4f4d02").unwrap(), + ) + .unwrap(); + + // Instruction #14 at this link -- https://solscan.io/tx/huK6BK5iUUvGZHHGJZbPrTjQrWgQY7vDQ4umFQTw8he1rvxPV6DH41XcwgEMZWXr9irgGKomxotQGzueCARqfkM + // Instruction Name: shared_account_route_with_token_ledger + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode("e6798f50779f6aaa0402000000076400012f0000640102201fd10200000000080700") + .unwrap(), + ) + .unwrap(); + + // Instruction #9 at this link -- https://solscan.io/tx/34JeXyn1YgX2VXFeasJwFhp135dWN4xaHJquYUNP6ymgHksvM9wi4GSR6DRXmHwJ2vguB5swuauG9GPP9zEDCMds + // Instruction Name: route_with_token_ledger + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode( + "96564774a75d0e680300000011006400013d016401021a6402036142950900000000320000", + ) + .unwrap(), + ) + .unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/DB5hZyNoUSo7JxNmpZkrx3Fz4rUPRLhyfkvBSp2RaqiMnEaJkgV7gUhrUAcKKtoJFzGpmsBQdmUuka2fQHY4WyR + // Instruction Name: shared_accounts_exact_out_route + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode( + "b0d169a89a7d453e07020000001a6400011a6401028beb033902000000ec0a3e0500000000f40100", + ) + .unwrap(), + ) + .unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/dUQfdooGYYxRRMNYYD3fCJ6aPVCK8aEcHsgZiujZK9yZqaS3zWTfFmygQHWSRdcKYwz19u7HtHqcxvuUrbQBa79 + // Instruction Name: claim_token + let _ = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode("74ce1bbfa613004908").unwrap(), + ) + .unwrap(); } + // APE PRO SMART WALLET PROGRAM -- IDL PARSING TESTS #[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) + fn test_idl_ape_pro() { + let ape_idl_file = "ape_pro.json"; + + // Instruction #4 at this link -- https://solscan.io/tx/5Yifk8KmHjGWxLP3haiMsyzuUBT7EUoqzHphW5qQoazvAqhzREf3Lwu5mRFEK7o9xHMrLuM1UavDGmRetP8sDKrB + // Instruction Name: preFlashSwapApprove + let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("315e88b6bffd5c280246218496010000cd6e86c4290c162621bd683dcf8de9da5aed6ffcbe1c3e0fb03581739510ba8137e3cba7093f1e888c7726a4f56809f90d38ed4cf1d85e2aae04b51016dee2ec01809698000000000000ca9a3b000000000000000000000000092d00000000ffffffffffffffff").unwrap()).unwrap(); + + // Instruction #4 at this link -- https://solscan.io/tx/4JwtqYCqan3DPSbjGqwxYKzuhSZndSdX5b1JEieJr4AZD2pm1nEzCnGHKRNPRf7NvurvnvNSiECy1jV9hxiPgvy7 + // Instruction Name: postSwap + let _ = get_idl_parsed_value_given_data( + ape_idl_file, + &hex::decode("9fd5b739b38a75a1").unwrap(), + ) + .unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/4K89QAFV5EiB4AEp344XxKYxB2gdqDEcFEWj8jN6F8teKURY8zwGQbNkGoP1vFN6JvDBo9Z9M6V97qX9RG4xBYh4 + // Instruction Name: withdrawToken + let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("88ebb505656d395121353b84960100009ef7641be0a9b34d7f8a97129c3f5711a7eaa5d35d9b7fc5b09f5746cec2165378babd55434c969c16946f4414ef59e78136f2c3c0a7bf619ecb08b4361d0af301bfa79caf05000000").unwrap()).unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/5BoC44EDb33DAhxmkZrFkQsBnG6ALJ3A5VzYXYkeCvkcWSLEXorZ2X4jeHP8V42kST8tNCxDkTE4cnt1efjSAFSn + // Instruction Name: flashSwapApprove + let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("f5d2c7fb443c609454baaf9492010000ed4f9590d2243e5fe524dcf665c758b4567a5e1e58949f504f1688fcd77fdc3b4a552d0e8caf9499d41c6b013bc1a6f39ec1191b8ba6e97a3bea0e5fc22453750040420f00000000004c38c175510000000000000000000000").unwrap()).unwrap(); } + // RaydiumCMPP -- IDL PARSING TESTS #[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) + fn test_idl_raydium() { + let rd_idl_file = "raydium.json"; + + // Instruction #4 at this link -- https://solscan.io/tx/1FyuW7Pwxip4RZ5BpR3jwnDZQ5z9uUQ4Pa74hBUZw2z1k4eVWgAGBkXptSamNCkWxV58a4aXt4UHWywCERRP54a + // Instruction Name: swapBaseInput + let _ = get_idl_parsed_value_given_data( + rd_idl_file, + &hex::decode("8fbe5adac41e33de1027000000000000b80d020200000000").unwrap(), + ) + .unwrap(); + + // Instruction #5 at this link -- https://solscan.io/tx/5LETS5M435N9tEUynZy2DZX5PToNJSZZ4Cxab89zF9iHk5p6PPpgPFUsKn2aa8GVGyCHukQ7a69tafweYGS3Rk2A + // Instruction Name: withdraw + let _ = get_idl_parsed_value_given_data( + rd_idl_file, + &hex::decode("b712469c946da1229c2fef7dba0200007679e100000000000a90393145d5fa08") + .unwrap(), + ) + .unwrap(); + + // Instruction #3.1 at this link -- https://solscan.io/tx/4s6ajUvRdsLLorb6w1N9GyJDmmzDksUrSgHHLfdZ4NPjgDe7emGWTFBt2JSWMxq9pq1bxx6bMqfgGGZ68pPb5LjU + // Instruction Name: createAmmConfig + let _ = get_idl_parsed_value_given_data( + rd_idl_file, + &hex::decode("8934edd4d7756c6804008813000000000000c0d4010000000000409c00000000000080d1f00800000000") + .unwrap(), + ) + .unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/5frS5mvdNJvnnLH4VhboGKzGMWTnL1HPuc9jbgpFTRavC7qZjcV92LNQBCPQLUhpsUybjfXqxy9xWgwgjuu4Hy9h + // Instruction Name: collectProtocolFee + let _ = get_idl_parsed_value_given_data( + rd_idl_file, + &hex::decode("8888fcddc2427e59ffffffffffffffffffffffffffffffff").unwrap(), + ) + .unwrap(); } + // Kamino Program -- IDL PARSING TESTS #[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) + fn test_idl_kamino() { + let kamino_idl_file = "kamino.json"; + + // Instruction #3 at this link -- https://solscan.io/tx/2RhYLjmRbKry2tyHKpaTfGYaEXSUmUtBTxWLRZ347nfKVVknBGgYNf5BrcykjVRjcmc8zNwhanLDcQy8ex6SfS3s + // Instruction Name: initializeStrategy + let _ = get_idl_parsed_value_given_data( + kamino_idl_file, + &hex::decode("d0779091b23969fc0100000000000000ea000000000000000000000000000000") + .unwrap(), + ) + .unwrap(); + + // Instruction #3.1 at this link -- https://solscan.io/tx/2qWFsUVJKkKYz19fzQnf2eaapYEXQXgnvmHvNZksmw7Co89Ez9BTZB7bkBrP6CicjbpwvM1VLmHwshQV58WVUsUF + // Instruction Name: insertCollateralInfo + let _ = get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("1661044ea6bc33beea000000000000000b86be66bc1f98b47d20a3be615a4905a825b826864e2a0f4c948467d33ee7090000000000000000000000000000000000000000000000000000000000000000e2003e00ffffffffe0001400ffffffff774d0000000000000000000000000000000000000000000000000000000000002c010000000000004001000000000000000000000000000000ffffffffffffffff").unwrap()).unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/PuKd8JeLhvSwA8VxN7hkeV5ZHdyj1hk8EoFawjn2X9YVu9GrHza93yPT1AxvMgTFzckkSBCVXhtc4eocwVxx68u + // Instruction Name: flashSwapUnevenVaultsStart + let _ = get_idl_parsed_value_given_data( + kamino_idl_file, + &hex::decode("816fae0c0a3c95c19786991c0000000000").unwrap(), + ) + .unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/5fJXiyDoUxffhPm2riHTugu2zSsx16Rq1ezGv4oomjw6jEUgyrPLG3vrFkGazXgDspQpcFsJ8UapAUuNX8ph6yD8 + // Instruction Name: collectFeesAndRewards + let _ = get_idl_parsed_value_given_data( + kamino_idl_file, + &hex::decode("71124b08b61f69ba").unwrap(), + ) + .unwrap(); + + // Instruction #3 at this link -- https://solscan.io/tx/oYVVbND3bbpBuL3eb9jyyET2wWu1nEKxacC6BsRHP4KdsA9WHNjD7tHSEcNJkt4R83NFTquESp2xrhR92DRFCEW + // Instruction Name: invest + let _ = get_idl_parsed_value_given_data( + kamino_idl_file, + &hex::decode("0df5b467feb67904").unwrap(), + ) + .unwrap(); + + // Instruction #2 at this link -- https://solscan.io/tx/yWdjGMsPP4zNVFpbqXRSVTwZJQogXzqjpV4kFLMH5G7T4Dc6hjrQ8QCkJpKLPi2sq8PyEBgedntRLVervR8Eg6o + // Instruction Name: initializeSharesMetadata + let _ = get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("030fac72c8008320180000004b616d696e6f20774d2d5553444320285261796469756d29080000006b574d2d555344435800000068747470733a2f2f6170692e6b616d696e6f2e66696e616e63652f6b746f6b656e732f4273334c5757747165435641714b75315032574839674b5a344d4a736b57484e786631453666686f7667597a2f6d65746164617461").unwrap()).unwrap(); } + // Meteora Program -- IDL PARSING TESTS #[test] - fn parse_spl_transfer_using_address_table_lookups() { - // ensure that transaction gets parsed without errors - let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04021303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let _ = parsed_tx.transaction_metadata().unwrap(); + fn test_idl_meteora() { + let mtr_idl_file = "meteora.json"; + + // Instruction #3.6 at this link -- https://solscan.io/tx/3Veo71aP5mVi4VT35hYDQRaxqvdkEbcgPmjh9dsBVNK2XLVusrvk3PLzrS36sYsY2h3wL2S7Z76DYW3B13uispv4 + // Instruction Name: swap + let _ = get_idl_parsed_value_given_data( + mtr_idl_file, + &hex::decode("f8c69e91e17587c8d0b49c7a000000000000000000000000").unwrap(), + ) + .unwrap(); } + // Test Calculating Default Anchor discriminator across different contracts #[test] - fn parse_spl_transfer_using_address_table_lookups_mint() { - // This transaction contains two SPL transfer instructions - // BOTH Spl transfer instructions use an Address Table look up to represent the Token Mint address - // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04021303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - let spl_transfers = tx_metadata.spl_transfers; - - // SPL transfer 1 (Uses an address table lookup token mint address) - let spl_transfer_1 = spl_transfers[0].clone(); - assert_eq!( - spl_transfer_1.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); - assert_eq!( - spl_transfer_1.to, - "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() - ); - assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // EMPTY BECAUSE OF ATLU - assert_eq!( - spl_transfer_1.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() - ); - - // SPL transfer 2 (Uses an address table lookup token mint address) - let spl_transfer_2 = spl_transfers[1].clone(); - assert_eq!( - spl_transfer_2.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); - assert_eq!( - spl_transfer_2.to, - "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() - ); - assert_eq!(spl_transfer_2.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // EMPTY BECAUSE OF ATLU + fn test_anchor_default_discriminator_generation() { + // Test Ape Pro contract instruction + // Check that the instruction name collectFeesAndRewards parses to the correct instruction discriminator + // Reference for the correct instruction discriminator is the first 8 bytes of instruction #3 at this link - https://solscan.io/tx/5fJXiyDoUxffhPm2riHTugu2zSsx16Rq1ezGv4oomjw6jEUgyrPLG3vrFkGazXgDspQpcFsJ8UapAUuNX8ph6yD8 + let inst_name = "collectFeesAndRewards"; + let exp_disc = hex::decode("71124b08b61f69ba").unwrap(); + let calc_disc = idl_parser::compute_default_anchor_discriminator(inst_name).unwrap(); + assert_eq!(exp_disc, calc_disc); + + // Test Orca Whirlpools contract instruction + // Check that the instruction name openPosition parses to the correct instruction discriminator + // Reference for the correct instruction discriminator is the first 8 bytes of instruction #3.5 at this link - https://solscan.io/tx/3TxJ1wMMBpfXucDzv5wbgXY2qx5W6pbxRGwPmkNgAhBRBqZViZNUwBWBzi2UXvsbpPf4rjQj3j2GFLT4gmX9WpKQ + let inst_name = "openPosition"; + let exp_disc = hex::decode("87802f4d0f98f031").unwrap(); + let calc_disc = idl_parser::compute_default_anchor_discriminator(inst_name).unwrap(); + assert_eq!(exp_disc, calc_disc); + + // Test OpenBook instruction + // Check that the instruction name consumeEvents parses to the correct instruction discriminator + // Reference for the correct instruction discriminator is the first 8 bytes of instruction #4.1 at this link - https://solscan.io/tx/sCJp1GLAZfGNVGBZxhCE3ZQ4uyXPy8ui7L63bWF2bSPbEUhEkzPtztzr7G18DaJK64yR88RojTXPwDdvkNeDgzL + let inst_name = "consumeEvents"; + let exp_disc = hex::decode("dd91b1341f2f3fc9").unwrap(); + let calc_disc = idl_parser::compute_default_anchor_discriminator(inst_name).unwrap(); + assert_eq!(exp_disc, calc_disc); + + // Test empty instruction name -- ERROR CASE + let empty_inst_name = ""; + let empty_err_str = idl_parser::compute_default_anchor_discriminator(empty_inst_name) + .unwrap_err() + .to_string(); assert_eq!( - spl_transfer_2.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() - ); + empty_err_str, + "attempted to compute the default anchor instruction discriminator for an instruction with no name" + .to_string() + ); } + // Test cycle detection in defined types resolution #[test] - fn parse_spl_transfer_using_address_table_lookups_recipient() { - // This transaction contains two SPL transfer instructions - // 1. Uses an Address Table look up to represent the Token Mint address - // 2. Uses an Address Table look up to represent the Recipient address - // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04020313000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - let spl_transfers = tx_metadata.spl_transfers; - - // SPL transfer 1 (Uses an address table lookup token mint address) - let spl_transfer_1 = spl_transfers[0].clone(); - assert_eq!( - spl_transfer_1.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); - assert_eq!( - spl_transfer_1.to, - "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() - ); - assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + fn test_cyclic_types_idl() { + let idl_json_string = + fs::read_to_string(TEST_IDL_DIRECTORY.to_string() + "cyclic.json").unwrap(); + let cyclic_err_str = idl_parser::decode_idl_data(&idl_json_string) + .unwrap_err() + .to_string(); assert_eq!( - spl_transfer_1.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + cyclic_err_str, + "defined types cycle check failed. Recursive type found: TypeA".to_string() ); + } - // SPL transfer 2 (Uses an address table lookup for receiving "to" address) - let spl_transfer_2 = spl_transfers[1].clone(); - assert_eq!( - spl_transfer_2.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); - assert_eq!(spl_transfer_2.to, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction - assert_eq!( - spl_transfer_2.token_mint, - Some("EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string()) - ); + // Test type name collision detection + #[test] + fn test_type_names_collision() { + let idl_json_string = + fs::read_to_string(TEST_IDL_DIRECTORY.to_string() + "collision.json").unwrap(); + let cyclic_err_str = idl_parser::decode_idl_data(&idl_json_string) + .unwrap_err() + .to_string(); assert_eq!( - spl_transfer_2.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + cyclic_err_str, + "multiple types with the same name detected: TypeA".to_string() ); } + // Test detection of extraneous bytes at the end of an instruction #[test] - fn parse_spl_transfer_using_address_table_lookups_sender() { - // This transaction contains two SPL transfer instructions - // 1. Uses an Address Table look up to represent the Token Mint address - // 2. Uses an Address Table look up to represent the Sending address - // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04130303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - let spl_transfers = tx_metadata.spl_transfers; - - // SPL transfer 1 (Uses an address table lookup token mint address) - let spl_transfer_1 = spl_transfers[0].clone(); + fn test_extraneous_bytes() { + let kamino_idl_file = "kamino.json"; + // Below is Kamino instruction data with an extra byte "00" added on to the end + // Original instruction #3 at this link -- https://solscan.io/tx/oYVVbND3bbpBuL3eb9jyyET2wWu1nEKxacC6BsRHP4KdsA9WHNjD7tHSEcNJkt4R83NFTquESp2xrhR92DRFCEW + let kamino_extra_bytes_err = get_idl_parsed_value_given_data( + kamino_idl_file, + &hex::decode("0df5b467feb6790400").unwrap(), + ) + .unwrap_err() + .to_string(); assert_eq!( - spl_transfer_1.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); - assert_eq!( - spl_transfer_1.to, - "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() - ); - assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction - assert_eq!( - spl_transfer_1.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + kamino_extra_bytes_err, + "extra unexpected bytes remaining at the end of instruction call data".to_string() ); - // SPL transfer 2 (Uses an address table lookup for sending "from" address) - let spl_transfer_2 = spl_transfers[1].clone(); - assert_eq!(spl_transfer_2.from, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction - assert_eq!( - spl_transfer_2.to, - "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() - ); + let rd_pid = "raydium.json"; + // Below is Raydium instruction data with some extra bytes "0012" added on to the end + // Original Instruction #4 at this link -- https://solscan.io/tx/1FyuW7Pwxip4RZ5BpR3jwnDZQ5z9uUQ4Pa74hBUZw2z1k4eVWgAGBkXptSamNCkWxV58a4aXt4UHWywCERRP54a + let raydium_extra_bytes_err = get_idl_parsed_value_given_data( + rd_pid, + &hex::decode("8fbe5adac41e33de1027000000000000b80d0202000000000012").unwrap(), + ) + .unwrap_err() + .to_string(); assert_eq!( - spl_transfer_2.token_mint, - Some("EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string()) - ); - assert_eq!( - spl_transfer_2.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + raydium_extra_bytes_err, + "extra unexpected bytes remaining at the end of instruction call data".to_string() ); } #[test] - fn parse_spl_transfer_using_address_table_lookups_owner() { - // This transaction contains two SPL transfer instructions - // 1. Uses an Address Table look up to represent the Token Mint address - // 2. Uses an Address Table look up to represent the Owner address - // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses - - // ensure that transaction gets parsed without errors - let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04020003130a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); - let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); - let tx_metadata = parsed_tx.transaction_metadata().unwrap(); - - let spl_transfers = tx_metadata.spl_transfers; - - // SPL transfer 1 (Uses an address table lookup token mint address) - let spl_transfer_1 = spl_transfers[0].clone(); + fn test_insufficient_data_cases() { + let kamino_idl_file = "kamino.json"; + // Below is Kamino instruction data with an extra byte "00" added on to the end + // Instruction #2 at this link -- https://solscan.io/tx/2RhYLjmRbKry2tyHKpaTfGYaEXSUmUtBTxWLRZ347nfKVVknBGgYNf5BrcykjVRjcmc8zNwhanLDcQy8ex6SfS3s + // Instruction Name: initializeStrategy + let u64_parse_err = get_idl_parsed_value_given_data( + kamino_idl_file, + &hex::decode("d0779091b23969fc0100000000000000ea0000000000000000000000000000").unwrap(), + ) + .unwrap_err() + .to_string(); assert_eq!( - spl_transfer_1.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); - assert_eq!( - spl_transfer_1.to, - "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() - ); - assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction - assert_eq!( - spl_transfer_1.owner, - "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + u64_parse_err, + "failed to parse IDL argument with error: failed to fill whole buffer" ); - // SPL transfer 2 (Uses an address table lookup for owner address) - let spl_transfer_2 = spl_transfers[1].clone(); - assert_eq!( - spl_transfer_2.from, - "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() - ); + // Instruction #2 at this link -- https://solscan.io/tx/yWdjGMsPP4zNVFpbqXRSVTwZJQogXzqjpV4kFLMH5G7T4Dc6hjrQ8QCkJpKLPi2sq8PyEBgedntRLVervR8Eg6o + // Instruction Name: initializeSharesMetadata + let string_parse_err = get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("030fac72c8008320180000004b616d696e6f20774d2d5553444320285261796469756d29080000006b574d2d555344435800000068747470733a2f2f6170692e6b616d696e6f2e66696e616e63652f6b746f6b656e732f4273334c5757747165435641714b75315032574839674b5a344d4a736b57484e786631453666686f7667597a2f6d65746164").unwrap()).unwrap_err().to_string(); assert_eq!( - spl_transfer_2.to, - "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + string_parse_err, + "failed to parse IDL argument with error: failed to fill whole buffer" ); - assert_eq!( - spl_transfer_2.token_mint, - Some("DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string()) - ); - assert_eq!(spl_transfer_2.owner, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction } - #[cfg(test)] - mod tests { - use super::*; - use std::fs; - - // JUPITER AGGREGATOR V6 TESTS -- IDL PARSING - #[test] - fn test_idl_parsing_jupiter_instruction_1() { - let jup_idl_file = "jupiter_agg_v6.json"; - - // Instruction #3 at this link -- https://solscan.io/tx/JcGkMSRd5iyXH5dar6MnjyrpEhQi5XR9mV2TfQpxLbTZH53geZsToDY4nzbcLGnvV32RvuEe4BPTSTkobV6rjEx - // Instruction Name: route - let _ = get_idl_parsed_value_given_data( - jup_idl_file, - &hex::decode("e517cb977ae3ad2a0200000007640001266401026ca35b0400000000a5465c0400000000000000").unwrap(), - ) - .unwrap(); - - // Instruction #6 at this link -- https://solscan.io/tx/eQqquSewgVCP3jBkKeTstnEGGMuGaT7g7G7uPL9kfFgJk163BYFHhqE3hxNTsaoWWX6gJHRYNr2sSeRtEP3nnA3 - // Instruction Name: shared_accounts_route - let _ = get_idl_parsed_value_given_data( - jup_idl_file, - &hex::decode("c1209b3341d69c810502000000266400014701006401028a28000000000000f405000000000000460014") - .unwrap(), - ) - .unwrap(); - - // Instruction #5 at this link -- https://solscan.io/tx/huK6BK5iUUvGZHHGJZbPrTjQrWgQY7vDQ4umFQTw8he1rvxPV6DH41XcwgEMZWXr9irgGKomxotQGzueCARqfkM - // Instruction Name: set_token_ledger - let _ = get_idl_parsed_value_given_data(jup_idl_file, &hex::decode("e455b9704e4f4d02").unwrap()).unwrap(); - - // Instruction #14 at this link -- https://solscan.io/tx/huK6BK5iUUvGZHHGJZbPrTjQrWgQY7vDQ4umFQTw8he1rvxPV6DH41XcwgEMZWXr9irgGKomxotQGzueCARqfkM - // Instruction Name: shared_account_route_with_token_ledger - let _ = get_idl_parsed_value_given_data( - jup_idl_file, - &hex::decode("e6798f50779f6aaa0402000000076400012f0000640102201fd10200000000080700").unwrap(), - ) - .unwrap(); - - // Instruction #9 at this link -- https://solscan.io/tx/34JeXyn1YgX2VXFeasJwFhp135dWN4xaHJquYUNP6ymgHksvM9wi4GSR6DRXmHwJ2vguB5swuauG9GPP9zEDCMds - // Instruction Name: route_with_token_ledger - let _ = get_idl_parsed_value_given_data( - jup_idl_file, - &hex::decode("96564774a75d0e680300000011006400013d016401021a6402036142950900000000320000").unwrap(), - ) - .unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/DB5hZyNoUSo7JxNmpZkrx3Fz4rUPRLhyfkvBSp2RaqiMnEaJkgV7gUhrUAcKKtoJFzGpmsBQdmUuka2fQHY4WyR - // Instruction Name: shared_accounts_exact_out_route - let _ = get_idl_parsed_value_given_data( - jup_idl_file, - &hex::decode("b0d169a89a7d453e07020000001a6400011a6401028beb033902000000ec0a3e0500000000f40100") - .unwrap(), - ) - .unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/dUQfdooGYYxRRMNYYD3fCJ6aPVCK8aEcHsgZiujZK9yZqaS3zWTfFmygQHWSRdcKYwz19u7HtHqcxvuUrbQBa79 - // Instruction Name: claim_token - let _ = get_idl_parsed_value_given_data(jup_idl_file, &hex::decode("74ce1bbfa613004908").unwrap()).unwrap(); - } - - // APE PRO SMART WALLET PROGRAM -- IDL PARSING TESTS - #[test] - fn test_idl_ape_pro() { - let ape_idl_file = "ape_pro.json"; - - // Instruction #4 at this link -- https://solscan.io/tx/5Yifk8KmHjGWxLP3haiMsyzuUBT7EUoqzHphW5qQoazvAqhzREf3Lwu5mRFEK7o9xHMrLuM1UavDGmRetP8sDKrB - // Instruction Name: preFlashSwapApprove - let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("315e88b6bffd5c280246218496010000cd6e86c4290c162621bd683dcf8de9da5aed6ffcbe1c3e0fb03581739510ba8137e3cba7093f1e888c7726a4f56809f90d38ed4cf1d85e2aae04b51016dee2ec01809698000000000000ca9a3b000000000000000000000000092d00000000ffffffffffffffff").unwrap()).unwrap(); - - // Instruction #4 at this link -- https://solscan.io/tx/4JwtqYCqan3DPSbjGqwxYKzuhSZndSdX5b1JEieJr4AZD2pm1nEzCnGHKRNPRf7NvurvnvNSiECy1jV9hxiPgvy7 - // Instruction Name: postSwap - let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("9fd5b739b38a75a1").unwrap()).unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/4K89QAFV5EiB4AEp344XxKYxB2gdqDEcFEWj8jN6F8teKURY8zwGQbNkGoP1vFN6JvDBo9Z9M6V97qX9RG4xBYh4 - // Instruction Name: withdrawToken - let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("88ebb505656d395121353b84960100009ef7641be0a9b34d7f8a97129c3f5711a7eaa5d35d9b7fc5b09f5746cec2165378babd55434c969c16946f4414ef59e78136f2c3c0a7bf619ecb08b4361d0af301bfa79caf05000000").unwrap()).unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/5BoC44EDb33DAhxmkZrFkQsBnG6ALJ3A5VzYXYkeCvkcWSLEXorZ2X4jeHP8V42kST8tNCxDkTE4cnt1efjSAFSn - // Instruction Name: flashSwapApprove - let _ = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("f5d2c7fb443c609454baaf9492010000ed4f9590d2243e5fe524dcf665c758b4567a5e1e58949f504f1688fcd77fdc3b4a552d0e8caf9499d41c6b013bc1a6f39ec1191b8ba6e97a3bea0e5fc22453750040420f00000000004c38c175510000000000000000000000").unwrap()).unwrap(); - } - - // RaydiumCMPP -- IDL PARSING TESTS - #[test] - fn test_idl_raydium() { - let rd_idl_file = "raydium.json"; - - // Instruction #4 at this link -- https://solscan.io/tx/1FyuW7Pwxip4RZ5BpR3jwnDZQ5z9uUQ4Pa74hBUZw2z1k4eVWgAGBkXptSamNCkWxV58a4aXt4UHWywCERRP54a - // Instruction Name: swapBaseInput - let _ = get_idl_parsed_value_given_data( - rd_idl_file, - &hex::decode("8fbe5adac41e33de1027000000000000b80d020200000000").unwrap(), - ) - .unwrap(); - - // Instruction #5 at this link -- https://solscan.io/tx/5LETS5M435N9tEUynZy2DZX5PToNJSZZ4Cxab89zF9iHk5p6PPpgPFUsKn2aa8GVGyCHukQ7a69tafweYGS3Rk2A - // Instruction Name: withdraw - let _ = get_idl_parsed_value_given_data( - rd_idl_file, - &hex::decode("b712469c946da1229c2fef7dba0200007679e100000000000a90393145d5fa08").unwrap(), - ) - .unwrap(); - - // Instruction #3.1 at this link -- https://solscan.io/tx/4s6ajUvRdsLLorb6w1N9GyJDmmzDksUrSgHHLfdZ4NPjgDe7emGWTFBt2JSWMxq9pq1bxx6bMqfgGGZ68pPb5LjU - // Instruction Name: createAmmConfig - let _ = get_idl_parsed_value_given_data( - rd_idl_file, - &hex::decode("8934edd4d7756c6804008813000000000000c0d4010000000000409c00000000000080d1f00800000000") - .unwrap(), - ) - .unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/5frS5mvdNJvnnLH4VhboGKzGMWTnL1HPuc9jbgpFTRavC7qZjcV92LNQBCPQLUhpsUybjfXqxy9xWgwgjuu4Hy9h - // Instruction Name: collectProtocolFee - let _ = get_idl_parsed_value_given_data( - rd_idl_file, - &hex::decode("8888fcddc2427e59ffffffffffffffffffffffffffffffff").unwrap(), - ) - .unwrap(); - } - - // Kamino Program -- IDL PARSING TESTS - #[test] - fn test_idl_kamino() { - let kamino_idl_file = "kamino.json"; - - // Instruction #3 at this link -- https://solscan.io/tx/2RhYLjmRbKry2tyHKpaTfGYaEXSUmUtBTxWLRZ347nfKVVknBGgYNf5BrcykjVRjcmc8zNwhanLDcQy8ex6SfS3s - // Instruction Name: initializeStrategy - let _ = get_idl_parsed_value_given_data( - kamino_idl_file, - &hex::decode("d0779091b23969fc0100000000000000ea000000000000000000000000000000").unwrap(), - ) - .unwrap(); - - // Instruction #3.1 at this link -- https://solscan.io/tx/2qWFsUVJKkKYz19fzQnf2eaapYEXQXgnvmHvNZksmw7Co89Ez9BTZB7bkBrP6CicjbpwvM1VLmHwshQV58WVUsUF - // Instruction Name: insertCollateralInfo - let _ = get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("1661044ea6bc33beea000000000000000b86be66bc1f98b47d20a3be615a4905a825b826864e2a0f4c948467d33ee7090000000000000000000000000000000000000000000000000000000000000000e2003e00ffffffffe0001400ffffffff774d0000000000000000000000000000000000000000000000000000000000002c010000000000004001000000000000000000000000000000ffffffffffffffff").unwrap()).unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/PuKd8JeLhvSwA8VxN7hkeV5ZHdyj1hk8EoFawjn2X9YVu9GrHza93yPT1AxvMgTFzckkSBCVXhtc4eocwVxx68u - // Instruction Name: flashSwapUnevenVaultsStart - let _ = get_idl_parsed_value_given_data( - kamino_idl_file, - &hex::decode("816fae0c0a3c95c19786991c0000000000").unwrap(), - ) - .unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/5fJXiyDoUxffhPm2riHTugu2zSsx16Rq1ezGv4oomjw6jEUgyrPLG3vrFkGazXgDspQpcFsJ8UapAUuNX8ph6yD8 - // Instruction Name: collectFeesAndRewards - let _ = - get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("71124b08b61f69ba").unwrap()).unwrap(); - - // Instruction #3 at this link -- https://solscan.io/tx/oYVVbND3bbpBuL3eb9jyyET2wWu1nEKxacC6BsRHP4KdsA9WHNjD7tHSEcNJkt4R83NFTquESp2xrhR92DRFCEW - // Instruction Name: invest - let _ = - get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("0df5b467feb67904").unwrap()).unwrap(); - - // Instruction #2 at this link -- https://solscan.io/tx/yWdjGMsPP4zNVFpbqXRSVTwZJQogXzqjpV4kFLMH5G7T4Dc6hjrQ8QCkJpKLPi2sq8PyEBgedntRLVervR8Eg6o - // Instruction Name: initializeSharesMetadata - let _ = get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("030fac72c8008320180000004b616d696e6f20774d2d5553444320285261796469756d29080000006b574d2d555344435800000068747470733a2f2f6170692e6b616d696e6f2e66696e616e63652f6b746f6b656e732f4273334c5757747165435641714b75315032574839674b5a344d4a736b57484e786631453666686f7667597a2f6d65746164617461").unwrap()).unwrap(); - } - - // Meteora Program -- IDL PARSING TESTS - #[test] - fn test_idl_meteora() { - let mtr_idl_file = "meteora.json"; - - // Instruction #3.6 at this link -- https://solscan.io/tx/3Veo71aP5mVi4VT35hYDQRaxqvdkEbcgPmjh9dsBVNK2XLVusrvk3PLzrS36sYsY2h3wL2S7Z76DYW3B13uispv4 - // Instruction Name: swap - let _ = get_idl_parsed_value_given_data( - mtr_idl_file, - &hex::decode("f8c69e91e17587c8d0b49c7a000000000000000000000000").unwrap(), - ) - .unwrap(); - } - - // Test Calculating Default Anchor discriminator across different contracts - #[test] - fn test_anchor_default_discriminator_generation() { - // Test Ape Pro contract instruction - // Check that the instruction name collectFeesAndRewards parses to the correct instruction discriminator - // Reference for the correct instruction discriminator is the first 8 bytes of instruction #3 at this link - https://solscan.io/tx/5fJXiyDoUxffhPm2riHTugu2zSsx16Rq1ezGv4oomjw6jEUgyrPLG3vrFkGazXgDspQpcFsJ8UapAUuNX8ph6yD8 - let inst_name = "collectFeesAndRewards"; - let exp_disc = hex::decode("71124b08b61f69ba").unwrap(); - let calc_disc = idl_parser::compute_default_anchor_discriminator(inst_name).unwrap(); - assert_eq!(exp_disc, calc_disc); - - // Test Orca Whirlpools contract instruction - // Check that the instruction name openPosition parses to the correct instruction discriminator - // Reference for the correct instruction discriminator is the first 8 bytes of instruction #3.5 at this link - https://solscan.io/tx/3TxJ1wMMBpfXucDzv5wbgXY2qx5W6pbxRGwPmkNgAhBRBqZViZNUwBWBzi2UXvsbpPf4rjQj3j2GFLT4gmX9WpKQ - let inst_name = "openPosition"; - let exp_disc = hex::decode("87802f4d0f98f031").unwrap(); - let calc_disc = idl_parser::compute_default_anchor_discriminator(inst_name).unwrap(); - assert_eq!(exp_disc, calc_disc); - - // Test OpenBook instruction - // Check that the instruction name consumeEvents parses to the correct instruction discriminator - // Reference for the correct instruction discriminator is the first 8 bytes of instruction #4.1 at this link - https://solscan.io/tx/sCJp1GLAZfGNVGBZxhCE3ZQ4uyXPy8ui7L63bWF2bSPbEUhEkzPtztzr7G18DaJK64yR88RojTXPwDdvkNeDgzL - let inst_name = "consumeEvents"; - let exp_disc = hex::decode("dd91b1341f2f3fc9").unwrap(); - let calc_disc = idl_parser::compute_default_anchor_discriminator(inst_name).unwrap(); - assert_eq!(exp_disc, calc_disc); - - // Test empty instruction name -- ERROR CASE - let empty_inst_name = ""; - let empty_err_str = idl_parser::compute_default_anchor_discriminator(empty_inst_name) - .unwrap_err() - .to_string(); - assert_eq!( - empty_err_str, - "attempted to compute the default anchor instruction discriminator for an instruction with no name" - .to_string() - ); - } - - // Test cycle detection in defined types resolution - #[test] - fn test_cyclic_types_idl() { - let idl_json_string = fs::read_to_string(IDL_DIRECTORY.to_string() + "cyclic.json").unwrap(); - let cyclic_err_str = idl_parser::decode_idl_data(&idl_json_string).unwrap_err().to_string(); - assert_eq!( - cyclic_err_str, - "defined types cycle check failed. Recursive type found: TypeA".to_string() - ); - } - - // Test type name collision detection - #[test] - fn test_type_names_collision() { - let idl_json_string = fs::read_to_string(IDL_DIRECTORY.to_string() + "collision.json").unwrap(); - let cyclic_err_str = idl_parser::decode_idl_data(&idl_json_string).unwrap_err().to_string(); - assert_eq!( - cyclic_err_str, - "multiple types with the same name detected: TypeA".to_string() - ); - } - - // Test detection of extraneous bytes at the end of an instruction - #[test] - fn test_extraneous_bytes() { - let kamino_idl_file = "kamino.json"; - // Below is Kamino instruction data with an extra byte "00" added on to the end - // Original instruction #3 at this link -- https://solscan.io/tx/oYVVbND3bbpBuL3eb9jyyET2wWu1nEKxacC6BsRHP4KdsA9WHNjD7tHSEcNJkt4R83NFTquESp2xrhR92DRFCEW - let kamino_extra_bytes_err = - get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("0df5b467feb6790400").unwrap()) - .unwrap_err() - .to_string(); - assert_eq!( - kamino_extra_bytes_err, - "extra unexpected bytes remaining at the end of instruction call data".to_string() - ); - - let rd_pid = "raydium.json"; - // Below is Raydium instruction data with some extra bytes "0012" added on to the end - // Original Instruction #4 at this link -- https://solscan.io/tx/1FyuW7Pwxip4RZ5BpR3jwnDZQ5z9uUQ4Pa74hBUZw2z1k4eVWgAGBkXptSamNCkWxV58a4aXt4UHWywCERRP54a - let raydium_extra_bytes_err = get_idl_parsed_value_given_data( - rd_pid, - &hex::decode("8fbe5adac41e33de1027000000000000b80d0202000000000012").unwrap(), - ) - .unwrap_err() - .to_string(); - assert_eq!( - raydium_extra_bytes_err, - "extra unexpected bytes remaining at the end of instruction call data".to_string() - ); - } - - #[test] - fn test_insufficient_data_cases() { - let kamino_idl_file = "kamino.json"; - // Below is Kamino instruction data with an extra byte "00" added on to the end - // Instruction #2 at this link -- https://solscan.io/tx/2RhYLjmRbKry2tyHKpaTfGYaEXSUmUtBTxWLRZ347nfKVVknBGgYNf5BrcykjVRjcmc8zNwhanLDcQy8ex6SfS3s - // Instruction Name: initializeStrategy - let u64_parse_err = get_idl_parsed_value_given_data( - kamino_idl_file, - &hex::decode("d0779091b23969fc0100000000000000ea0000000000000000000000000000").unwrap(), - ) - .unwrap_err().to_string(); - assert_eq!( - u64_parse_err, - "failed to parse IDL argument with error: failed to fill whole buffer" - ); - - // Instruction #2 at this link -- https://solscan.io/tx/yWdjGMsPP4zNVFpbqXRSVTwZJQogXzqjpV4kFLMH5G7T4Dc6hjrQ8QCkJpKLPi2sq8PyEBgedntRLVervR8Eg6o - // Instruction Name: initializeSharesMetadata - let string_parse_err = get_idl_parsed_value_given_data(kamino_idl_file, &hex::decode("030fac72c8008320180000004b616d696e6f20774d2d5553444320285261796469756d29080000006b574d2d555344435800000068747470733a2f2f6170692e6b616d696e6f2e66696e616e63652f6b746f6b656e732f4273334c5757747165435641714b75315032574839674b5a344d4a736b57484e786631453666686f7667597a2f6d65746164").unwrap()).unwrap_err().to_string(); - assert_eq!(string_parse_err, "failed to parse IDL argument with error: failed to fill whole buffer"); - } - - // This test tests ALL the parsed values making sure all values for the below instruction parsed, as provided by instruction - #[allow(clippy::too_many_lines)] - #[test] - fn test_full_jupiter_idl_call() { - let jup_idl_file = "jupiter_agg_v6.json"; - - // Instruction #3 at this link -- https://solscan.io/tx/JcGkMSRd5iyXH5dar6MnjyrpEhQi5XR9mV2TfQpxLbTZH53geZsToDY4nzbcLGnvV32RvuEe4BPTSTkobV6rjEx - // Instruction Name: route - let value_map = get_idl_parsed_value_given_data( - jup_idl_file, - &hex::decode("e517cb977ae3ad2a0200000007640001266401026ca35b0400000000a5465c0400000000000000").unwrap(), + // This test tests ALL the parsed values making sure all values for the below instruction parsed, as provided by instruction + #[allow(clippy::too_many_lines)] + #[test] + fn test_full_jupiter_idl_call() { + let jup_idl_file = "jupiter_agg_v6.json"; + + // Instruction #3 at this link -- https://solscan.io/tx/JcGkMSRd5iyXH5dar6MnjyrpEhQi5XR9mV2TfQpxLbTZH53geZsToDY4nzbcLGnvV32RvuEe4BPTSTkobV6rjEx + // Instruction Name: route + let value_map = get_idl_parsed_value_given_data( + jup_idl_file, + &hex::decode( + "e517cb977ae3ad2a0200000007640001266401026ca35b0400000000a5465c0400000000000000", ) - .unwrap(); - - // Initialize expected map - let expected_map = HashMap::from_iter([ - ("slippage_bps".to_string(), Value::Number(Number::from(0))), - ("in_amount".to_string(), Value::Number(Number::from(73114476))), - ("platform_fee_bps".to_string(), Value::Number(Number::from(0))), - ("quoted_out_amount".to_string(), Value::Number(Number::from(73156261))), - ("route_plan".to_string(), Value::Array(vec![ + .unwrap(), + ) + .unwrap(); + + // Initialize expected map + let expected_map = HashMap::from_iter([ + ("slippage_bps".to_string(), Value::Number(Number::from(0))), + ( + "in_amount".to_string(), + Value::Number(Number::from(73114476)), + ), + ( + "platform_fee_bps".to_string(), + Value::Number(Number::from(0)), + ), + ( + "quoted_out_amount".to_string(), + Value::Number(Number::from(73156261)), + ), + ( + "route_plan".to_string(), + Value::Array(vec![ Value::Object(Map::from_iter([ ("input_index".to_string(), Value::Number(Number::from(0))), ("output_index".to_string(), Value::Number(Number::from(1))), ("percent".to_string(), Value::Number(Number::from(100))), - ("swap".to_string(), Value::Object(Map::from_iter([ - ("Raydium".to_string(), Value::Null) - ]))) + ( + "swap".to_string(), + Value::Object(Map::from_iter([("Raydium".to_string(), Value::Null)])), + ), ])), Value::Object(Map::from_iter([ ("input_index".to_string(), Value::Number(Number::from(1))), ("output_index".to_string(), Value::Number(Number::from(2))), ("percent".to_string(), Value::Number(Number::from(100))), - ("swap".to_string(), Value::Object(Map::from_iter([ - ("MeteoraDlmm".to_string(), Value::Null) - ]))) - ])) - ])) - ]); - - assert_eq!(value_map, expected_map); - } - - // this test tests some of the actual values within the ape pro contract call provided - #[allow(clippy::too_many_lines)] - #[test] - fn test_ape_pro_call_values() { - let ape_idl_file = "ape_pro.json"; - - // Instruction #4 at this link -- https://solscan.io/tx/5Yifk8KmHjGWxLP3haiMsyzuUBT7EUoqzHphW5qQoazvAqhzREf3Lwu5mRFEK7o9xHMrLuM1UavDGmRetP8sDKrB - // Instruction Name: preFlashSwapApprove - let value_map = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("315e88b6bffd5c280246218496010000cd6e86c4290c162621bd683dcf8de9da5aed6ffcbe1c3e0fb03581739510ba8137e3cba7093f1e888c7726a4f56809f90d38ed4cf1d85e2aae04b51016dee2ec01809698000000000000ca9a3b000000000000000000000000092d00000000ffffffffffffffff").unwrap()).unwrap(); - - // initialize espected map - let expected_map = HashMap::from_iter([ - ("useDurableNonce".to_string(), Value::Bool(false)), - ("postSwapIxIndex".to_string(), Value::Number(Number::from(9))), - ("recoveryId".to_string(), Value::Number(Number::from(1))), - ("feeBps".to_string(), Value::Number(Number::from(45))), - ("inputAmount".to_string(), Value::Number(Number::from(1000000000))), - ("priorityFeeLamports".to_string(), Value::Number(Number::from(10000000))), - ("maxOut".to_string(), Value::Number(Number::from(18446744073709551615u64))), - ("minOut".to_string(), Value::Number(Number::from(0))), - ("nonce".to_string(), Value::Number(Number::from(1745973495298u64))), - ("referralShareBps".to_string(), Value::Number(Number::from(0))), - ("signature".to_string(), Value::Array(vec![ - Value::Number(Number::from(205)), - Value::Number(Number::from(110)), - Value::Number(Number::from(134)), - Value::Number(Number::from(196)), - Value::Number(Number::from(41)), - Value::Number(Number::from(12)), - Value::Number(Number::from(22)), - Value::Number(Number::from(38)), - Value::Number(Number::from(33)), - Value::Number(Number::from(189)), - Value::Number(Number::from(104)), - Value::Number(Number::from(61)), - Value::Number(Number::from(207)), - Value::Number(Number::from(141)), - Value::Number(Number::from(233)), - Value::Number(Number::from(218)), - Value::Number(Number::from(90)), - Value::Number(Number::from(237)), - Value::Number(Number::from(111)), - Value::Number(Number::from(252)), - Value::Number(Number::from(190)), - Value::Number(Number::from(28)), - Value::Number(Number::from(62)), - Value::Number(Number::from(15)), - Value::Number(Number::from(176)), - Value::Number(Number::from(53)), - Value::Number(Number::from(129)), - Value::Number(Number::from(115)), - Value::Number(Number::from(149)), - Value::Number(Number::from(16)), - Value::Number(Number::from(186)), - Value::Number(Number::from(129)), - Value::Number(Number::from(55)), - Value::Number(Number::from(227)), - Value::Number(Number::from(203)), - Value::Number(Number::from(167)), - Value::Number(Number::from(9)), - Value::Number(Number::from(63)), - Value::Number(Number::from(30)), - Value::Number(Number::from(136)), - Value::Number(Number::from(140)), - Value::Number(Number::from(119)), - Value::Number(Number::from(38)), - Value::Number(Number::from(164)), - Value::Number(Number::from(245)), - Value::Number(Number::from(104)), - Value::Number(Number::from(9)), - Value::Number(Number::from(249)), - Value::Number(Number::from(13)), - Value::Number(Number::from(56)), - Value::Number(Number::from(237)), - Value::Number(Number::from(76)), - Value::Number(Number::from(241)), - Value::Number(Number::from(216)), - Value::Number(Number::from(94)), - Value::Number(Number::from(42)), - Value::Number(Number::from(174)), - Value::Number(Number::from(4)), - Value::Number(Number::from(181)), - Value::Number(Number::from(16)), - Value::Number(Number::from(22)), - Value::Number(Number::from(222)), - Value::Number(Number::from(226)), - Value::Number(Number::from(236)) - ])) - ]); - - assert_eq!(value_map, expected_map); - } - - #[allow(clippy::cast_possible_truncation)] - // this test tests as many different arg types as possible - #[test] - fn test_different_arg_types() { - let meteora_idl_file = "meteora.json"; - - // Instruction #2 at this link -- https://solscan.io/tx/5N9UR8ojzPwhWWQCwcYhU2ayL4tSjBqHG7uNgbhERYL44TD9qr61uipsugiBZTKYeHEobqPnvqauCcck872hamMD - // Instruction Name: initializePosition - let inst_map_1 = get_idl_parsed_value_given_data( - meteora_idl_file, - &hex::decode("dbc0ea47bebf6650cefbffff45000000").unwrap(), - ) - .unwrap(); - - /* - testing the SIGNED INT types - */ - let lower_bin_id = inst_map_1.get("lowerBinId").unwrap(); - let exp_val = -1_074_i32; - assert_eq!( - lower_bin_id.clone(), - Value::Number(Number::from(exp_val)) - ); - - // Instruction #3 at this link -- https://solscan.io/tx/2wSFx1JvMsdqXgbvKYJu8a1mRGdzBRjpeg7TSsFi5y1xxfEF7nTsc7Uj2ZEfrtdWCaz9ndZRK6NnrCERHtuf5QM1 - // Instruction Name: initializeBinArray - let inst_map_2 = get_idl_parsed_value_given_data( - meteora_idl_file, - &hex::decode("235613b94ed44bd3f5ffffffffffffff").unwrap(), - ) - .unwrap(); - - let index = inst_map_2.get("index").unwrap(); - let exp_val = -11_i64; - assert_eq!( - index.clone(), - Value::Number(Number::from(exp_val)) - ); - - // Instruction #5 at this link -- https://solscan.io/tx/4852PmY5jz4XmqJVAEeF1kHMvkXYgGZqScMCCFDkH4tJAXt2cwh4k1rnvd5nGJFHyXToCMfyYFRc5C59zecewAdu - // Instruction Name: swapWithPriceImpact2 - let inst_map_3 = get_idl_parsed_value_given_data( - meteora_idl_file, - &hex::decode("4a62c0d6b1334b338096980000000000018ffdffffe80300000000").unwrap(), - ) - .unwrap(); - - /* - testing the OPTION type - */ - // active id is an optional field -- it is populated here - let active_id = inst_map_3.get("activeId").unwrap(); - let exp_val = -625_i64; - assert_eq!( - active_id.clone(), - Value::Number(Number::from(exp_val)) - ); - - /* - testing the FLOAT type - */ - let openb_idl_file = "openbook.json"; - - // Instruction #4 at this link -- https://solscan.io/tx/sBXgLUYZaPLHLNPDPsakVcHKYEbmp9YU9WvK4HZ3fXQL8sy7tg3EZpGc6rpnyKY69uABjtutvQQhytmDFvAaWxh - // Instruction Name: CreateMarket - let inst_map_5 = get_idl_parsed_value_given_data(openb_idl_file, &hex::decode("67e261ebc8bcfbfe080000005749462d55534443cdcccc3d010a00000001000000000000001027000000000000000000000000000000000000000000000000000000000000").unwrap()).unwrap(); - - let expected_map = HashMap::from_iter([ - ("takerFee".to_string(), Value::Number(Number::from(0))), - ("timeExpiry".to_string(), Value::Number(Number::from(0))), - ("oracleConfig".to_string(), Value::Object(Map::from_iter([ - ("confFilter".to_string(), Value::Number(Number::from_f64(0.10000000149011612).unwrap())), - ("maxStalenessSlots".to_string(), Value::Number(Number::from(10))) - ]))), - ("makerFee".to_string(), Value::Number(Number::from(0))), - ("quoteLotSize".to_string(), Value::Number(Number::from(1))), - ("baseLotSize".to_string(), Value::Number(Number::from(10000))), - ("name".to_string(), Value::String("WIF-USDC".to_string())) - ]); - - assert_eq!(inst_map_5, expected_map) - - // /* - // NOTE: Currently, call data instances of the instruction below break our parsing, because of a bug (or oversight) in the SDK used to create the instruction call data - // - Contract: Meteora DLMM - // - Instruction Name: initializeCustomizablePermissionlessLbPair2 - - // Specifically: - // - The IDL for this specifies a padding buffer of length 62 -- Reference: https://github.com/MeteoraAg/dlmm-sdk/blob/main/ts-client/src/dlmm/idl.ts#L5634 - // - But the SDK creates one of length 63 -- Reference: https://github.com/MeteoraAg/dlmm-sdk/blob/main/ts-client/src/dlmm/index.ts#L1389 - - // - This leads to our parsing buffer having one extraneous byte left over after parsing all expected arguments. - // - NO TODO'S: At the current time I'm NOT going to add any exception cases here. I believe that block explorers can parse this - // because they have laxer requirements and don't hard fail if extraneous bytes are leftover in transaction call data - // - We should have tighter restrictions than block explorers given security implications and should fail in the case of one-off bugs like this and resort to us/our clients triaging - // the specifics of functions as they come up, with potential for reevaluation if it happens to frequently, which I do not anticipate given testing volume - // - Keeping this test case below commented out as a reference in case we ever need to use it. - // */ - // // // ALT instance of instruction: https://solscan.io/tx/2hsUoPNtouChnkhDMUzYUQMqgyVA4zCuckZyCYwNi8SdAMx1LSE14t2QnSweU4P3GrimMxAaezeN54b2GQAeAmwA - // // // Instruction #1 at this link -- https://solscan.io/tx/4pjxruibNd4apBm7JqzFq8yKXWTxQRTPXNaEJY5zxZtTSULqmi3yp8jmxbbqKEdaHFTRxRUAL3jivzybqsysJyDZ - // // // Instruction Name: initializeCustomizablePermissionlessLbPair2 - // // let inst_map_4 = get_idl_parsed_value_given_data( - // // meteora_idl_file, - // // &hex::decode("f349817e3313f16b34fcffff6400204e01000144e73e68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), - // // ) - // // .unwrap(); - } - - // ******* IDL TESTING HELPER FUNCTION **** - fn get_idl_parsed_value_given_data( - idl_file_name: &str, - data: &[u8], - ) -> Result, Box> { - let idl_json_string = fs::read_to_string(IDL_DIRECTORY.to_string() + idl_file_name).unwrap(); - - // Parse the IDL - let idl = idl_parser::decode_idl_data(&idl_json_string).unwrap(); - - // Find the correct instruction - let instruction = idl_parser::find_instruction_by_discriminator(data, idl.instructions.clone()).unwrap(); - - // parse instruction call data - let parsed_args = idl_parser::parse_data_into_args(data, &instruction, &idl)?; - - // Create ArgMap from parsed args - let mut arg_map: HashMap = HashMap::new(); - for a in parsed_args { - arg_map.insert(a.0, a.1); - } - - Ok(arg_map) + ( + "swap".to_string(), + Value::Object(Map::from_iter([( + "MeteoraDlmm".to_string(), + Value::Null, + )])), + ), + ])), + ]), + ), + ]); + + assert_eq!(value_map, expected_map); + } + + // this test tests some of the actual values within the ape pro contract call provided + #[allow(clippy::too_many_lines)] + #[test] + fn test_ape_pro_call_values() { + let ape_idl_file = "ape_pro.json"; + + // Instruction #4 at this link -- https://solscan.io/tx/5Yifk8KmHjGWxLP3haiMsyzuUBT7EUoqzHphW5qQoazvAqhzREf3Lwu5mRFEK7o9xHMrLuM1UavDGmRetP8sDKrB + // Instruction Name: preFlashSwapApprove + let value_map = get_idl_parsed_value_given_data(ape_idl_file, &hex::decode("315e88b6bffd5c280246218496010000cd6e86c4290c162621bd683dcf8de9da5aed6ffcbe1c3e0fb03581739510ba8137e3cba7093f1e888c7726a4f56809f90d38ed4cf1d85e2aae04b51016dee2ec01809698000000000000ca9a3b000000000000000000000000092d00000000ffffffffffffffff").unwrap()).unwrap(); + + // initialize espected map + let expected_map = HashMap::from_iter([ + ("useDurableNonce".to_string(), Value::Bool(false)), + ( + "postSwapIxIndex".to_string(), + Value::Number(Number::from(9)), + ), + ("recoveryId".to_string(), Value::Number(Number::from(1))), + ("feeBps".to_string(), Value::Number(Number::from(45))), + ( + "inputAmount".to_string(), + Value::Number(Number::from(1000000000)), + ), + ( + "priorityFeeLamports".to_string(), + Value::Number(Number::from(10000000)), + ), + ( + "maxOut".to_string(), + Value::Number(Number::from(18446744073709551615u64)), + ), + ("minOut".to_string(), Value::Number(Number::from(0))), + ( + "nonce".to_string(), + Value::Number(Number::from(1745973495298u64)), + ), + ( + "referralShareBps".to_string(), + Value::Number(Number::from(0)), + ), + ( + "signature".to_string(), + Value::Array(vec![ + Value::Number(Number::from(205)), + Value::Number(Number::from(110)), + Value::Number(Number::from(134)), + Value::Number(Number::from(196)), + Value::Number(Number::from(41)), + Value::Number(Number::from(12)), + Value::Number(Number::from(22)), + Value::Number(Number::from(38)), + Value::Number(Number::from(33)), + Value::Number(Number::from(189)), + Value::Number(Number::from(104)), + Value::Number(Number::from(61)), + Value::Number(Number::from(207)), + Value::Number(Number::from(141)), + Value::Number(Number::from(233)), + Value::Number(Number::from(218)), + Value::Number(Number::from(90)), + Value::Number(Number::from(237)), + Value::Number(Number::from(111)), + Value::Number(Number::from(252)), + Value::Number(Number::from(190)), + Value::Number(Number::from(28)), + Value::Number(Number::from(62)), + Value::Number(Number::from(15)), + Value::Number(Number::from(176)), + Value::Number(Number::from(53)), + Value::Number(Number::from(129)), + Value::Number(Number::from(115)), + Value::Number(Number::from(149)), + Value::Number(Number::from(16)), + Value::Number(Number::from(186)), + Value::Number(Number::from(129)), + Value::Number(Number::from(55)), + Value::Number(Number::from(227)), + Value::Number(Number::from(203)), + Value::Number(Number::from(167)), + Value::Number(Number::from(9)), + Value::Number(Number::from(63)), + Value::Number(Number::from(30)), + Value::Number(Number::from(136)), + Value::Number(Number::from(140)), + Value::Number(Number::from(119)), + Value::Number(Number::from(38)), + Value::Number(Number::from(164)), + Value::Number(Number::from(245)), + Value::Number(Number::from(104)), + Value::Number(Number::from(9)), + Value::Number(Number::from(249)), + Value::Number(Number::from(13)), + Value::Number(Number::from(56)), + Value::Number(Number::from(237)), + Value::Number(Number::from(76)), + Value::Number(Number::from(241)), + Value::Number(Number::from(216)), + Value::Number(Number::from(94)), + Value::Number(Number::from(42)), + Value::Number(Number::from(174)), + Value::Number(Number::from(4)), + Value::Number(Number::from(181)), + Value::Number(Number::from(16)), + Value::Number(Number::from(22)), + Value::Number(Number::from(222)), + Value::Number(Number::from(226)), + Value::Number(Number::from(236)), + ]), + ), + ]); + + assert_eq!(value_map, expected_map); + } + + #[allow(clippy::cast_possible_truncation)] + // this test tests as many different arg types as possible + #[test] + fn test_different_arg_types() { + let meteora_idl_file = "meteora.json"; + + // Instruction #2 at this link -- https://solscan.io/tx/5N9UR8ojzPwhWWQCwcYhU2ayL4tSjBqHG7uNgbhERYL44TD9qr61uipsugiBZTKYeHEobqPnvqauCcck872hamMD + // Instruction Name: initializePosition + let inst_map_1 = get_idl_parsed_value_given_data( + meteora_idl_file, + &hex::decode("dbc0ea47bebf6650cefbffff45000000").unwrap(), + ) + .unwrap(); + + /* + testing the SIGNED INT types + */ + let lower_bin_id = inst_map_1.get("lowerBinId").unwrap(); + let exp_val = -1_074_i32; + assert_eq!(lower_bin_id.clone(), Value::Number(Number::from(exp_val))); + + // Instruction #3 at this link -- https://solscan.io/tx/2wSFx1JvMsdqXgbvKYJu8a1mRGdzBRjpeg7TSsFi5y1xxfEF7nTsc7Uj2ZEfrtdWCaz9ndZRK6NnrCERHtuf5QM1 + // Instruction Name: initializeBinArray + let inst_map_2 = get_idl_parsed_value_given_data( + meteora_idl_file, + &hex::decode("235613b94ed44bd3f5ffffffffffffff").unwrap(), + ) + .unwrap(); + + let index = inst_map_2.get("index").unwrap(); + let exp_val = -11_i64; + assert_eq!(index.clone(), Value::Number(Number::from(exp_val))); + + // Instruction #5 at this link -- https://solscan.io/tx/4852PmY5jz4XmqJVAEeF1kHMvkXYgGZqScMCCFDkH4tJAXt2cwh4k1rnvd5nGJFHyXToCMfyYFRc5C59zecewAdu + // Instruction Name: swapWithPriceImpact2 + let inst_map_3 = get_idl_parsed_value_given_data( + meteora_idl_file, + &hex::decode("4a62c0d6b1334b338096980000000000018ffdffffe80300000000").unwrap(), + ) + .unwrap(); + + /* + testing the OPTION type + */ + // active id is an optional field -- it is populated here + let active_id = inst_map_3.get("activeId").unwrap(); + let exp_val = -625_i64; + assert_eq!(active_id.clone(), Value::Number(Number::from(exp_val))); + + /* + testing the FLOAT type + */ + let openb_idl_file = "openbook.json"; + + // Instruction #4 at this link -- https://solscan.io/tx/sBXgLUYZaPLHLNPDPsakVcHKYEbmp9YU9WvK4HZ3fXQL8sy7tg3EZpGc6rpnyKY69uABjtutvQQhytmDFvAaWxh + // Instruction Name: CreateMarket + let inst_map_5 = get_idl_parsed_value_given_data(openb_idl_file, &hex::decode("67e261ebc8bcfbfe080000005749462d55534443cdcccc3d010a00000001000000000000001027000000000000000000000000000000000000000000000000000000000000").unwrap()).unwrap(); + + let expected_map = HashMap::from_iter([ + ("takerFee".to_string(), Value::Number(Number::from(0))), + ("timeExpiry".to_string(), Value::Number(Number::from(0))), + ( + "oracleConfig".to_string(), + Value::Object(Map::from_iter([ + ( + "confFilter".to_string(), + Value::Number(Number::from_f64(0.10000000149011612).unwrap()), + ), + ( + "maxStalenessSlots".to_string(), + Value::Number(Number::from(10)), + ), + ])), + ), + ("makerFee".to_string(), Value::Number(Number::from(0))), + ("quoteLotSize".to_string(), Value::Number(Number::from(1))), + ( + "baseLotSize".to_string(), + Value::Number(Number::from(10000)), + ), + ("name".to_string(), Value::String("WIF-USDC".to_string())), + ]); + + assert_eq!(inst_map_5, expected_map) + + // /* + // NOTE: Currently, call data instances of the instruction below break our parsing, because of a bug (or oversight) in the SDK used to create the instruction call data + // - Contract: Meteora DLMM + // - Instruction Name: initializeCustomizablePermissionlessLbPair2 + + // Specifically: + // - The IDL for this specifies a padding buffer of length 62 -- Reference: https://github.com/MeteoraAg/dlmm-sdk/blob/main/ts-client/src/dlmm/idl.ts#L5634 + // - But the SDK creates one of length 63 -- Reference: https://github.com/MeteoraAg/dlmm-sdk/blob/main/ts-client/src/dlmm/index.ts#L1389 + + // - This leads to our parsing buffer having one extraneous byte left over after parsing all expected arguments. + // - NO TODO'S: At the current time I'm NOT going to add any exception cases here. I believe that block explorers can parse this + // because they have laxer requirements and don't hard fail if extraneous bytes are leftover in transaction call data + // - We should have tighter restrictions than block explorers given security implications and should fail in the case of one-off bugs like this and resort to us/our clients triaging + // the specifics of functions as they come up, with potential for reevaluation if it happens to frequently, which I do not anticipate given testing volume + // - Keeping this test case below commented out as a reference in case we ever need to use it. + // */ + // // // ALT instance of instruction: https://solscan.io/tx/2hsUoPNtouChnkhDMUzYUQMqgyVA4zCuckZyCYwNi8SdAMx1LSE14t2QnSweU4P3GrimMxAaezeN54b2GQAeAmwA + // // // Instruction #1 at this link -- https://solscan.io/tx/4pjxruibNd4apBm7JqzFq8yKXWTxQRTPXNaEJY5zxZtTSULqmi3yp8jmxbbqKEdaHFTRxRUAL3jivzybqsysJyDZ + // // // Instruction Name: initializeCustomizablePermissionlessLbPair2 + // // let inst_map_4 = get_idl_parsed_value_given_data( + // // meteora_idl_file, + // // &hex::decode("f349817e3313f16b34fcffff6400204e01000144e73e68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), + // // ) + // // .unwrap(); + } + + // ******* IDL TESTING HELPER FUNCTION **** + fn get_idl_parsed_value_given_data( + idl_file_name: &str, + data: &[u8], + ) -> Result, Box> { + let idl_json_string = + fs::read_to_string(TEST_IDL_DIRECTORY.to_string() + idl_file_name).unwrap(); + + // Parse the IDL + let idl = idl_parser::decode_idl_data(&idl_json_string).unwrap(); + + // Find the correct instruction + let instruction = + idl_parser::find_instruction_by_discriminator(data, idl.instructions.clone()).unwrap(); + + // parse instruction call data + let parsed_args = idl_parser::parse_data_into_args(data, &instruction, &idl)?; + + // Create ArgMap from parsed args + let mut arg_map: HashMap = HashMap::new(); + for a in parsed_args { + arg_map.insert(a.0, a.1); } - - #[test] - fn test_instruction_data_memory_allocation_guard_arrays() { - // bug discovered with fuzz testing - - let idl_definition = + + Ok(arg_map) + } + + #[test] + fn test_instruction_data_memory_allocation_guard_arrays() { + // bug discovered with fuzz testing + + let idl_definition = "{\n \"types\": [\n \n ],\n \"instructions\": [\n {\n \"name\": \"cyclciInstruction\",\n \"accounts\": [],\n \"args\": [ {\n \"name\": \"vault\", \"type\":{ \n \"array\": [\"u16\", 20000001000] }\n }\n ]\n }\n ]}"; - let instruction_data = vec![199, 102, 212, 229, 0, 145, 80, 22, 0, 116, 0, 0, 0]; - - let idl = idl_parser::decode_idl_data(idl_definition).unwrap(); - - // will crash with "memory allocation of 640000032000 bytes failed" - let instruction = idl_parser::find_instruction_by_discriminator(&instruction_data, idl.instructions.clone()).unwrap(); - let array_err = idl_parser::parse_data_into_args(&instruction_data, &instruction, &idl).unwrap_err().to_string(); - assert_eq!(array_err, "failed to parse IDL argument with error: memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data"); - } - - #[test] - fn test_instruction_data_memory_allocation_guard_string() { - // bug discovered with fuzz testing - - let malicious_string_idl = r#" + let instruction_data = vec![199, 102, 212, 229, 0, 145, 80, 22, 0, 116, 0, 0, 0]; + + let idl = idl_parser::decode_idl_data(idl_definition).unwrap(); + + // will crash with "memory allocation of 640000032000 bytes failed" + let instruction = idl_parser::find_instruction_by_discriminator( + &instruction_data, + idl.instructions.clone(), + ) + .unwrap(); + let array_err = idl_parser::parse_data_into_args(&instruction_data, &instruction, &idl) + .unwrap_err() + .to_string(); + assert_eq!(array_err, "failed to parse IDL argument with error: memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data"); + } + + #[test] + fn test_instruction_data_memory_allocation_guard_string() { + // bug discovered with fuzz testing + + let malicious_string_idl = r#" { "types": [], "instructions": [ @@ -1700,27 +1858,31 @@ use crate::solana::idl_parser; } ] }"#; - - // Instruction data: [discriminator] + [4-byte length] + [truncated data] - let string_data = vec![ - 77, 67, 244, 160, 33, 24, 166, 14, // Discriminator for 'dangerous_string' - 255, 255, 255, 127, // 2,147,483,647 length (i32::MAX) - 65, // Single 'A' character (data truncated) - ]; - - let idl = idl_parser::decode_idl_data(malicious_string_idl).unwrap(); - - // will crash with "memory allocation failed" - let instruction = idl_parser::find_instruction_by_discriminator(&string_data, idl.instructions.clone()).unwrap(); - let string_err = idl_parser::parse_data_into_args(&string_data, &instruction, &idl).unwrap_err().to_string(); - assert_eq!(string_err, "failed to parse IDL argument with error: memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data"); - } - - #[test] - fn test_instruction_data_memory_allocation_guard_composite() { - // bug discovered with fuzz testing - - let composite_idl = r#" + + // Instruction data: [discriminator] + [4-byte length] + [truncated data] + let string_data = vec![ + 77, 67, 244, 160, 33, 24, 166, 14, // Discriminator for 'dangerous_string' + 255, 255, 255, 127, // 2,147,483,647 length (i32::MAX) + 65, // Single 'A' character (data truncated) + ]; + + let idl = idl_parser::decode_idl_data(malicious_string_idl).unwrap(); + + // will crash with "memory allocation failed" + let instruction = + idl_parser::find_instruction_by_discriminator(&string_data, idl.instructions.clone()) + .unwrap(); + let string_err = idl_parser::parse_data_into_args(&string_data, &instruction, &idl) + .unwrap_err() + .to_string(); + assert_eq!(string_err, "failed to parse IDL argument with error: memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data"); + } + + #[test] + fn test_instruction_data_memory_allocation_guard_composite() { + // bug discovered with fuzz testing + + let composite_idl = r#" { "types": [], "instructions": [ @@ -1740,35 +1902,40 @@ use crate::solana::idl_parser; } ] }"#; - - // Instruction data: [discriminator] + nested lengths - let composite_data = vec![ - 138, 103, 254, 47, 252, 195, 139, 174, // Discriminator - 4, 0, 0, 0, // 4 strings - 255, 255, 255, 127, // String 1 length (2,147,483,647) - 255, 255, 255, 127, // String 2 length - 255, 255, 255, 127, // String 3 length - 255, 255, 255, 127, // String 4 length - 4, 0, 0, 0, // 4 inner vectors - 8, 0, 0, 0, // Each inner vector has 8 elements - 255, 255, 255, 127, // Each element has 2GB size - ]; - - let idl = idl_parser::decode_idl_data(composite_idl).unwrap(); - - // will crash with "memory allocation failed" - let instruction = idl_parser::find_instruction_by_discriminator(&composite_data, idl.instructions.clone()).unwrap(); - let composite_err = idl_parser::parse_data_into_args(&composite_data, &instruction, &idl) - .unwrap_err().to_string(); - assert_eq!(composite_err, "failed to parse IDL argument with error: memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data"); - } - - #[allow(clippy::too_many_lines)] - #[test] - fn test_max_recursive_depth_of_idl() { - // bug discovered with fuzz testing - - let deep_idl = r#" + + // Instruction data: [discriminator] + nested lengths + let composite_data = vec![ + 138, 103, 254, 47, 252, 195, 139, 174, // Discriminator + 4, 0, 0, 0, // 4 strings + 255, 255, 255, 127, // String 1 length (2,147,483,647) + 255, 255, 255, 127, // String 2 length + 255, 255, 255, 127, // String 3 length + 255, 255, 255, 127, // String 4 length + 4, 0, 0, 0, // 4 inner vectors + 8, 0, 0, 0, // Each inner vector has 8 elements + 255, 255, 255, 127, // Each element has 2GB size + ]; + + let idl = idl_parser::decode_idl_data(composite_idl).unwrap(); + + // will crash with "memory allocation failed" + let instruction = idl_parser::find_instruction_by_discriminator( + &composite_data, + idl.instructions.clone(), + ) + .unwrap(); + let composite_err = idl_parser::parse_data_into_args(&composite_data, &instruction, &idl) + .unwrap_err() + .to_string(); + assert_eq!(composite_err, "failed to parse IDL argument with error: memory allocation exceeded maximum allowed budget while parsing IDL call data -- check your uploaded IDL or call data"); + } + + #[allow(clippy::too_many_lines)] + #[test] + fn test_max_recursive_depth_of_idl() { + // bug discovered with fuzz testing + + let deep_idl = r#" { "types": [ { @@ -1925,9 +2092,138 @@ use crate::solana::idl_parser; ] } "#; - - let depth_err = idl_parser::decode_idl_data(deep_idl).unwrap_err().to_string(); - assert_eq!(depth_err, "defined types resolution max depth exceeded on type: L"); - } + + let depth_err = idl_parser::decode_idl_data(deep_idl) + .unwrap_err() + .to_string(); + assert_eq!( + depth_err, + "defined types resolution max depth exceeded on type: L" + ); + } +} + +mod custom_idl_tests { + use super::*; + use crate::solana::parser::parse_transaction; + use crate::solana::structs::ProgramType; + + #[test] + fn test_idl_hash_computation() { + // Test that whitespace is removed and hash is consistent + let idl_with_spaces = r#"{ "instructions": [], "types": [] }"#; + let idl_without_spaces = r#"{"instructions":[],"types":[]}"#; + + let hash1 = idl_parser::compute_idl_hash(idl_with_spaces); + let hash2 = idl_parser::compute_idl_hash(idl_without_spaces); + + assert_eq!(hash1, hash2); + assert_eq!(hash1.len(), 64); // SHA256 produces 64 hex chars + } + + #[test] + fn test_custom_idl_for_unknown_program() { + // Create a minimal valid IDL + let custom_idl = r#"{ + "instructions": [ + { + "name": "testInstruction", + "accounts": [], + "args": [] + } + ], + "types": [] + }"#; + + let unknown_program_id = "UnknownProgram11111111111111111111111111".to_string(); + let mut custom_idls = HashMap::new(); + custom_idls.insert(unknown_program_id.clone(), (custom_idl.to_string(), true)); + + // This should not fail even though program is unknown + let idl_map = + idl_parser::construct_custom_idl_records_map_with_overrides(Some(custom_idls)).unwrap(); + + assert!(idl_map.contains_key(&unknown_program_id)); + let record = &idl_map[&unknown_program_id]; + assert!(record.custom_idl_json.is_some()); + assert!(record.override_builtin); + } + + #[test] + fn test_builtin_idl_without_override() { + // Use Jupiter program as test case + let jupiter_program_id = "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB".to_string(); + + let custom_idl = r#"{ + "instructions": [], + "types": [] + }"#; + + let mut custom_idls = HashMap::new(); + custom_idls.insert(jupiter_program_id.clone(), (custom_idl.to_string(), false)); + + let idl_map = + idl_parser::construct_custom_idl_records_map_with_overrides(Some(custom_idls)).unwrap(); + + let record = &idl_map[&jupiter_program_id]; + assert!(record.custom_idl_json.is_some()); + assert!(!record.override_builtin); // Should not override + assert_eq!(record.program_type, Some(ProgramType::Jupiter)); // Built-in program type still there + } + + #[test] + fn test_builtin_idl_with_override() { + let jupiter_program_id = "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB".to_string(); + + let custom_idl = r#"{ + "instructions": [], + "types": [] + }"#; + + let mut custom_idls = HashMap::new(); + custom_idls.insert(jupiter_program_id.clone(), (custom_idl.to_string(), true)); + + let idl_map = + idl_parser::construct_custom_idl_records_map_with_overrides(Some(custom_idls)).unwrap(); + + let record = &idl_map[&jupiter_program_id]; + assert!(record.custom_idl_json.is_some()); + assert!(record.override_builtin); // Should override + } + + #[test] + fn test_program_type_from_program_id() { + let jupiter_id = "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB"; + let orca_id = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"; + + let jupiter_type = ProgramType::from_program_id(jupiter_id); + assert_eq!(jupiter_type, Some(ProgramType::Jupiter)); + + let orca_type = ProgramType::from_program_id(orca_id); + assert_eq!(orca_type, Some(ProgramType::Orca)); + + let unknown = ProgramType::from_program_id("Unknown111111111111111111111111111111"); + assert_eq!(unknown, None); + } + + #[test] + fn test_program_type_methods() { + let jupiter = ProgramType::Jupiter; + + assert_eq!( + jupiter.program_id(), + "JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB" + ); + assert_eq!(jupiter.file_path(), "jupiter.json"); + assert_eq!(jupiter.program_name(), "Jupiter Swap"); + } + + #[test] + fn test_parse_transaction_without_custom_idl() { + // Test that existing functionality still works + let unsigned_payload = "010001032b162ad640a79029d57fbe5dad39d5741066c4c65b22bd248c8677174c28a4630d42099a5e0aaeaad1d4ede263662787cb3f6291a6ede340c4aa7ca26249dbe3000000000000000000000000000000000000000000000000000000000000000021d594adba2b7fbd34a0383ded05e2ba526e907270d8394b47886805b880e73201020200010c020000006f00000000000000".to_string(); + + let result = parse_transaction(unsigned_payload, false, None); + assert!(result.is_ok()); } - \ No newline at end of file +}