diff --git a/Cargo.lock b/Cargo.lock index df4096690..2b93aa023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2002,6 +2002,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cargo_toml" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" +dependencies = [ + "serde", + "toml 0.8.19", +] + [[package]] name = "cast" version = "0.3.0" @@ -2886,6 +2896,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -2914,6 +2933,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -7602,12 +7632,49 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "shank_idl" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1862d324b77d15de6cd8d5de859ee70431737f131d34ae670cb4fb0568301d87" +dependencies = [ + "anyhow", + "cargo_toml", + "heck 0.3.3", + "serde", + "serde_json", + "shank_macro_impl", + "shellexpand", +] + +[[package]] +name = "shank_macro_impl" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab0b29f9717ebb2430ef424627e0fc234c832363955cbac2997c2973743990d" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "shell-words" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs 4.0.0", +] + [[package]] name = "shlex" version = "1.3.0" @@ -10771,6 +10838,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "shank_idl", "solana-blake3-hasher", "solana-clock", "solana-epoch-info", diff --git a/addons/svm/core/src/codec/anchor.rs b/addons/svm/core/src/codec/anchor.rs index 752f12cf7..2f8915a93 100644 --- a/addons/svm/core/src/codec/anchor.rs +++ b/addons/svm/core/src/codec/anchor.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, str::FromStr}; +use std::path::PathBuf; use crate::{codec::validate_program_so, typing::anchor::types as anchor_types}; @@ -87,14 +87,22 @@ impl AnchorProgramArtifacts { None }; - let program_id = Pubkey::from_str(&idl_ref.idl.address).map_err(|e| { + let program_id = idl_ref.get_program_pubkey().map_err(|e| { format!( "invalid anchor program idl at location {}: {}", &idl_path.to_str().unwrap_or(""), e ) })?; - Ok(Self { idl: idl_ref.idl, bin, keypair, program_id }) + + let idl = idl_ref.as_anchor().ok_or_else(|| { + format!( + "expected Anchor IDL at location {}, but found a different IDL format", + &idl_path.to_str().unwrap_or("") + ) + })?.clone(); + + Ok(Self { idl, bin, keypair, program_id }) } pub fn to_value(&self) -> Result { diff --git a/addons/svm/core/src/codec/idl/mod.rs b/addons/svm/core/src/codec/idl/mod.rs index 6332e8a4a..c279550a0 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -3,22 +3,23 @@ pub mod convert_idl; use std::str::FromStr; use crate::typing::anchor as anchor_lang_idl; -use crate::typing::SvmValue; -use anchor_lang_idl::types::{ - Idl, IdlArrayLen, IdlDefinedFields, IdlGenericArg, IdlInstruction, IdlType, IdlTypeDef, - IdlTypeDefGeneric, IdlTypeDefTy, -}; +use crate::typing::shank as shank_idl; +use anchor_lang_idl::types::{Idl as AnchorIdl, IdlInstruction}; use convert_idl::classic_idl_to_anchor_idl; +use log::debug; +use shank_idl::idl::Idl as ShankIdl; use solana_pubkey::Pubkey; -use std::fmt::Display; use txtx_addon_kit::types::diagnostics::Diagnostic; use txtx_addon_kit::{helpers::fs::FileLocation, indexmap::IndexMap, types::types::Value}; -use txtx_addon_network_svm_types::I256; -use txtx_addon_network_svm_types::U256; +use txtx_addon_network_svm_types::subgraph::idl::anchor::borsh_encode_value_to_idl_type; +use txtx_addon_network_svm_types::subgraph::idl::shank::{ + borsh_encode_value_to_shank_idl_type, extract_shank_instruction_arg_type, extract_shank_types, +}; +use txtx_addon_network_svm_types::subgraph::idl::IdlKind; #[derive(Debug, Clone)] pub struct IdlRef { - pub idl: Idl, + pub idl: IdlKind, pub location: Option, } @@ -31,8 +32,12 @@ impl IdlRef { Ok(Self { idl, location: Some(location) }) } - pub fn from_idl(idl: Idl) -> Self { - Self { idl, location: None } + pub fn from_anchor_idl(idl: AnchorIdl) -> Self { + Self { idl: IdlKind::Anchor(idl), location: None } + } + + pub fn from_shank_idl(idl: ShankIdl) -> Self { + Self { idl: IdlKind::Shank(idl), location: None } } pub fn from_bytes(bytes: &[u8]) -> Result { @@ -45,58 +50,172 @@ impl IdlRef { Ok(Self { idl, location: None }) } + /// Returns the IDL kind (Anchor or Shank) + pub fn kind(&self) -> &IdlKind { + &self.idl + } + + /// Returns true if this is an Anchor IDL + pub fn is_anchor(&self) -> bool { + matches!(self.idl, IdlKind::Anchor(_)) + } + + /// Returns true if this is a Shank IDL + pub fn is_shank(&self) -> bool { + matches!(self.idl, IdlKind::Shank(_)) + } + + /// Returns a reference to the Anchor IDL if this is an Anchor IDL + pub fn as_anchor(&self) -> Option<&AnchorIdl> { + match &self.idl { + IdlKind::Anchor(idl) => Some(idl), + IdlKind::Shank(_) => None, + } + } + + /// Returns a reference to the Shank IDL if this is a Shank IDL + pub fn as_shank(&self) -> Option<&ShankIdl> { + match &self.idl { + IdlKind::Anchor(_) => None, + IdlKind::Shank(idl) => Some(idl), + } + } + + pub fn idl(&self) -> &IdlKind { + &self.idl + } + pub fn get_program_pubkey(&self) -> Result { - Pubkey::from_str(&self.idl.address) + let address = match &self.idl { + IdlKind::Anchor(idl) => idl.address.clone(), + IdlKind::Shank(idl) => idl + .metadata + .address + .clone() + .ok_or_else(|| diagnosed_error!("Shank IDL is missing program address"))?, + }; + Pubkey::from_str(&address) .map_err(|e| diagnosed_error!("invalid pubkey in program IDL: {e}")) } pub fn get_discriminator(&self, instruction_name: &str) -> Result, Diagnostic> { - self.get_instruction(instruction_name).map(|i| i.discriminator.clone()) + match &self.idl { + IdlKind::Anchor(idl) => { + let instruction = + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else( + || diagnosed_error!("instruction '{instruction_name}' not found in IDL"), + )?; + Ok(instruction.discriminator.clone()) + } + IdlKind::Shank(idl) => { + let instruction = + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else( + || diagnosed_error!("instruction '{instruction_name}' not found in IDL"), + )?; + // Shank uses a single u8 discriminant + Ok(vec![instruction.discriminant.value]) + } + } } pub fn get_instruction(&self, instruction_name: &str) -> Result<&IdlInstruction, Diagnostic> { - self.idl - .instructions - .iter() - .find(|i| i.name == instruction_name) - .ok_or_else(|| diagnosed_error!("instruction '{instruction_name}' not found in IDL")) - } - - pub fn get_types(&self) -> Vec { - self.idl.types.clone() + match &self.idl { + IdlKind::Anchor(idl) => { + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else(|| { + diagnosed_error!("instruction '{instruction_name}' not found in IDL") + }) + } + // TODO: Once we have internal types from shank (https://github.com/txtx/txtx/issues/382) + // we should expand this to return a Shank instruction as well. + IdlKind::Shank(_) => { + Err(diagnosed_error!("get_instruction is not supported for Shank IDL")) + } + } } /// Encodes the arguments for a given instruction into a map of argument names to byte arrays. + /// Note: This method currently only supports Anchor IDLs. pub fn get_encoded_args_map( &self, instruction_name: &str, args: Vec, ) -> Result>, Diagnostic> { - let instruction = self.get_instruction(instruction_name)?; - if args.len() != instruction.args.len() { - return Err(diagnosed_error!( - "{} arguments provided for instruction {}, which expects {} arguments", - args.len(), - instruction_name, - instruction.args.len() - )); - } - if args.is_empty() { - return Ok(IndexMap::new()); - } + match &self.idl { + IdlKind::Anchor(idl) => { + let instruction = + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else( + || diagnosed_error!("instruction '{instruction_name}' not found in IDL"), + )?; + if args.len() != instruction.args.len() { + return Err(diagnosed_error!( + "{} arguments provided for instruction {}, which expects {} arguments", + args.len(), + instruction_name, + instruction.args.len() + )); + } + if args.is_empty() { + return Ok(IndexMap::new()); + } + + let idl_types = idl.types.clone(); - let idl_types = self.get_types(); + let mut encoded_args = IndexMap::new(); + for (user_arg_idx, arg) in args.iter().enumerate() { + let idl_arg = instruction.args.get(user_arg_idx).unwrap(); + let encoded_arg = + borsh_encode_value_to_idl_type(arg, &idl_arg.ty, &idl_types, None) + .map_err(|e| { + diagnosed_error!( + "error in argument at position {}: {}", + user_arg_idx + 1, + e + ) + })?; + encoded_args.insert(idl_arg.name.clone(), encoded_arg); + } + Ok(encoded_args) + } + IdlKind::Shank(idl) => { + let instruction = + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else( + || diagnosed_error!("instruction '{instruction_name}' not found in IDL"), + )?; + if args.len() != instruction.args.len() { + return Err(diagnosed_error!( + "{} arguments provided for instruction {}, which expects {} arguments", + args.len(), + instruction_name, + instruction.args.len() + )); + } + if args.is_empty() { + return Ok(IndexMap::new()); + } - let mut encoded_args = IndexMap::new(); - for (user_arg_idx, arg) in args.iter().enumerate() { - let idl_arg = instruction.args.get(user_arg_idx).unwrap(); - let encoded_arg = borsh_encode_value_to_idl_type(arg, &idl_arg.ty, &idl_types, None) - .map_err(|e| { - diagnosed_error!("error in argument at position {}: {}", user_arg_idx + 1, e) - })?; - encoded_args.insert(idl_arg.name.clone(), encoded_arg); + let idl_types = extract_shank_types(idl); + + let mut encoded_args = IndexMap::new(); + for (user_arg_idx, arg) in args.iter().enumerate() { + let idl_arg = instruction.args.get(user_arg_idx).unwrap(); + let arg_type = + extract_shank_instruction_arg_type(idl, instruction_name, user_arg_idx) + .map_err(|e| diagnosed_error!("failed to extract arg type: {}", e))?; + let encoded_arg = borsh_encode_value_to_shank_idl_type( + arg, &arg_type, &idl_types, + ) + .map_err(|e| { + diagnosed_error!( + "error in argument at position {}: {}", + user_arg_idx + 1, + e + ) + })?; + encoded_args.insert(idl_arg.name.clone(), encoded_arg); + } + Ok(encoded_args) + } } - Ok(encoded_args) } /// Encodes the arguments for a given instruction into a flat byte array. @@ -105,645 +224,128 @@ impl IdlRef { instruction_name: &str, args: Vec, ) -> Result, Diagnostic> { - let instruction = self.get_instruction(instruction_name)?; - if args.len() != instruction.args.len() { - return Err(diagnosed_error!( - "{} arguments provided for instruction {}, which expects {} arguments", - args.len(), - instruction_name, - instruction.args.len() - )); - } - if args.is_empty() { - return Ok(vec![]); - } - - let idl_types = self.get_types(); - - let mut encoded_args = vec![]; - for (user_arg_idx, arg) in args.iter().enumerate() { - let idl_arg = instruction.args.get(user_arg_idx).unwrap(); - let mut encoded_arg = borsh_encode_value_to_idl_type( - arg, - &idl_arg.ty, - &idl_types, - None, - ) - .map_err(|e| { - diagnosed_error!("error in argument at position {}: {}", user_arg_idx + 1, e) - })?; - encoded_args.append(&mut encoded_arg); - } - Ok(encoded_args) - } -} - -fn parse_idl_string(idl_str: &str) -> Result { - let idl = match serde_json::from_str(&idl_str) { - Ok(anchor_idl) => anchor_idl, - Err(e) => match serde_json::from_str(&idl_str) { - Ok(classic_idl) => classic_idl_to_anchor_idl(classic_idl)?, - Err(_) => { - return Err(diagnosed_error!("invalid idl: {e}")); - } - }, - }; - Ok(idl) -} - -fn parse_idl_bytes(idl_bytes: &[u8]) -> Result { - let idl = match serde_json::from_slice(&idl_bytes) { - Ok(anchor_idl) => anchor_idl, - Err(e) => match serde_json::from_slice(&idl_bytes) { - Ok(classic_idl) => classic_idl_to_anchor_idl(classic_idl)?, - Err(_) => { - return Err(diagnosed_error!("invalid idl: {e}")); - } - }, - }; - Ok(idl) -} - -pub fn borsh_encode_value_to_idl_type( - value: &Value, - idl_type: &IdlType, - idl_types: &Vec, - defined_parent_context: Option<&IdlType>, -) -> Result, String> { - let mismatch_err = |expected: &str| { - format!( - "invalid value for idl type: expected {}, found {}", - expected, - value.get_type().to_string() - ) - }; - let encode_err = |expected: &str, e: &dyn Display| { - format!("unable to encode value ({}) as borsh {}: {}", value.to_string(), expected, e) - }; - - match value { - Value::Buffer(bytes) => return borsh_encode_bytes_to_idl_type(bytes, idl_type, idl_types), - Value::Addon(addon_data) => { - return borsh_encode_bytes_to_idl_type(&addon_data.bytes, idl_type, idl_types) - } - _ => {} - } - - match idl_type { - IdlType::Bool => value - .as_bool() - .and_then(|b| Some(borsh::to_vec(&b).map_err(|e| encode_err("bool", &e)))) - .transpose()? - .ok_or(mismatch_err("bool")), - IdlType::U8 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("u8", &e)), - IdlType::U16 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("u16", &e)), - IdlType::U32 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("u32", &e)), - IdlType::U64 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("u64", &e)), - IdlType::U128 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("u128", &e)), - IdlType::U256 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num.0).map_err(|e| e.to_string())) - .map_err(|e| encode_err("u256", &e)), - IdlType::I8 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("i8", &e)), - IdlType::I16 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("i16", &e)), - IdlType::I32 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("i32", &e)), - IdlType::I64 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("i64", &e)), - IdlType::I128 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("i128", &e)), - IdlType::I256 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num.0).map_err(|e| e.to_string())) - .map_err(|e| encode_err("i256", &e)), - IdlType::F32 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("f32", &e)), - IdlType::F64 => SvmValue::to_number::(value) - .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) - .map_err(|e| encode_err("f64", &e)), - IdlType::Bytes => Ok(value.to_be_bytes().clone()), - IdlType::String => value - .as_string() - .and_then(|s| Some(borsh::to_vec(&s).map_err(|e| encode_err("string", &e)))) - .transpose()? - .ok_or(mismatch_err("string")), - IdlType::Pubkey => SvmValue::to_pubkey(value) - .map_err(|_| mismatch_err("pubkey")) - .map(|p| borsh::to_vec(&p))? - .map_err(|e| encode_err("pubkey", &e)), - IdlType::Option(idl_type) => { - if let Some(_) = value.as_null() { - borsh::to_vec(&None::).map_err(|e| encode_err("Optional", &e)) - } else { - let encoded_arg = borsh_encode_value_to_idl_type(value, idl_type, idl_types, None)?; - borsh::to_vec(&Some(encoded_arg)).map_err(|e| encode_err("Optional", &e)) - } - } - IdlType::Vec(idl_type) => match value { - Value::String(_) => { - let bytes = value.get_buffer_bytes_result().map_err(|_| mismatch_err("vec"))?; - borsh_encode_bytes_to_idl_type(&bytes, idl_type, idl_types) - } - Value::Array(vec) => vec - .iter() - .map(|v| borsh_encode_value_to_idl_type(v, idl_type, idl_types, None)) - .collect::, _>>() - .map(|v| v.into_iter().flatten().collect::>()), - _ => Err(mismatch_err("vec")), - }, - IdlType::Array(idl_type, idl_array_len) => { - let expected_length = match idl_array_len { - IdlArrayLen::Generic(generic_len) => { - let Some(&IdlType::Defined { - name: defined_parent_name, - generics: defined_parent_generics, - }) = defined_parent_context.as_ref() - else { - return Err(format!( - "generic array length does not contain parent type name" - )); - }; - - let type_def_generics = &idl_types - .iter() - .find(|t| t.name.eq(defined_parent_name)) - .ok_or_else(|| { - format!( - "unable to find type definition for {} in idl", - defined_parent_name - ) - })? - .generics; - - let IdlType::Defined { name, .. } = parse_generic_expected_type( - &IdlType::Generic(generic_len.to_string()), - &type_def_generics, - &defined_parent_generics, - )? - else { - return Err(format!("unable to parse generic array length")); - }; - &name - .parse::() - .map_err(|e| format!("unable to parse generic array length: {}", e))? + match &self.idl { + IdlKind::Anchor(idl) => { + let instruction = + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else( + || diagnosed_error!("instruction '{instruction_name}' not found in IDL"), + )?; + if args.len() != instruction.args.len() { + return Err(diagnosed_error!( + "{} arguments provided for instruction {}, which expects {} arguments", + args.len(), + instruction_name, + instruction.args.len() + )); } - IdlArrayLen::Value(len) => len, - }; - let array = value - .as_array() - .map(|a| { - if expected_length != &a.len() { - return Err(format!( - "invalid value for idl type: expected array length of {}, found {}", - expected_length, - a.len() - )); - } - a.iter() - .map(|v| borsh_encode_value_to_idl_type(v, idl_type, idl_types, None)) - .collect::, _>>() - }) - .transpose()? - .map(|v| v.into_iter().flatten().collect::>()) - .ok_or(mismatch_err("vec")); - array - } - IdlType::Defined { name, generics } => { - let typing = idl_types - .iter() - .find(|t| &t.name == name) - .ok_or_else(|| format!("unable to find type definition for {} in idl", name))?; - let fields = match &typing.ty { - IdlTypeDefTy::Struct { fields } => { - if let Some(idl_defined_fields) = fields { - borsh_encode_value_to_idl_defined_fields( - idl_defined_fields, - value, - idl_type, - idl_types, - generics, - &typing.generics, - ) - .map_err(|e| format!("unable to encode value as borsh struct: {}", e))? - } else { - vec![] - } + if args.is_empty() { + return Ok(vec![]); } - IdlTypeDefTy::Enum { variants } => { - let enum_value = value.as_object().ok_or(mismatch_err("object"))?; - - // Handle two enum formats: - // 1. {"variant": "VariantName", "value": ...} (explicit format) - // 2. {"VariantName": ...} (decoded format from parse_bytes_to_value) - let (enum_variant, enum_variant_value) = if let Some(variant_field) = enum_value.get("variant") { - // Format 1: explicit variant field - let variant_name = variant_field.as_string().ok_or_else(|| { - format!( - "unable to encode value ({}) as borsh enum: expected variant field to be a string", - value.to_string(), - ) - })?; - let variant_value = enum_value.get("value").ok_or_else(|| { - format!( - "unable to encode value ({}) as borsh enum: missing 'value' field", - value.to_string(), - ) - })?; - (variant_name, variant_value) - } else { - // Format 2: variant name as object key - if enum_value.len() != 1 { - return Err(format!( - "unable to encode value ({}) as borsh enum: expected exactly one field (the variant name)", - value.to_string(), - )); - } - let (variant_name, variant_value) = enum_value.iter().next().ok_or_else(|| { - format!( - "unable to encode value ({}) as borsh enum: empty object", - value.to_string(), - ) - })?; - (variant_name.as_str(), variant_value) - }; - - let (variant_index, expected_variant) = variants - .iter() - .enumerate() - .find(|(_, v)| v.name.eq(enum_variant)) - .ok_or_else(|| { - format!( - "unable to encode value ({}) as borsh enum: unknown variant {}", - value.to_string(), - enum_variant - ) - })?; - - let mut encoded = vec![variant_index as u8]; - - let type_def_generics = idl_types - .iter() - .find(|t| &t.name == name) - .map(|t| t.generics.clone()) - .unwrap_or_default(); - - match &expected_variant.fields { - Some(idl_defined_fields) => { - let mut encoded_fields = borsh_encode_value_to_idl_defined_fields( - &idl_defined_fields, - enum_variant_value, - idl_type, - idl_types, - &vec![], - &type_def_generics, - ) + + let idl_types = idl.types.clone(); + + let mut encoded_args = vec![]; + for (user_arg_idx, arg) in args.iter().enumerate() { + let idl_arg = instruction.args.get(user_arg_idx).unwrap(); + let mut encoded_arg = + borsh_encode_value_to_idl_type(arg, &idl_arg.ty, &idl_types, None) .map_err(|e| { - format!("unable to encode value as borsh struct: {}", e) + diagnosed_error!( + "error in argument at position {}: {}", + user_arg_idx + 1, + e + ) })?; - - encoded.append(&mut encoded_fields); - encoded - } - None => encoded, - } + encoded_args.append(&mut encoded_arg); } - IdlTypeDefTy::Type { alias } => { - borsh_encode_value_to_idl_type(value, &alias, idl_types, Some(idl_type))? + Ok(encoded_args) + } + IdlKind::Shank(idl) => { + let instruction = + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else( + || diagnosed_error!("instruction '{instruction_name}' not found in IDL"), + )?; + if args.len() != instruction.args.len() { + return Err(diagnosed_error!( + "{} arguments provided for instruction {}, which expects {} arguments", + args.len(), + instruction_name, + instruction.args.len() + )); } - }; - Ok(fields) - } - IdlType::Generic(generic) => { - let idl_generic = idl_types.iter().find_map(|t| { - t.generics.iter().find_map(|g| { - let is_match = match g { - IdlTypeDefGeneric::Type { name } => name == generic, - IdlTypeDefGeneric::Const { name, .. } => name == generic, - }; - if is_match { - Some(g) - } else { - None - } - }) - }); - let Some(idl_generic) = idl_generic else { - return Err(format!("unable to find generic {} in idl", generic)); - }; - match idl_generic { - IdlTypeDefGeneric::Type { name } => { - let ty = IdlType::from_str(name) - .map_err(|e| format!("invalid generic type: {e}"))?; - borsh_encode_value_to_idl_type(value, &ty, idl_types, None) + if args.is_empty() { + return Ok(vec![]); } - IdlTypeDefGeneric::Const { ty, .. } => { - let ty = - IdlType::from_str(ty).map_err(|e| format!("invalid generic type: {e}"))?; - borsh_encode_value_to_idl_type(value, &ty, idl_types, None) + + let idl_types = extract_shank_types(idl); + + let mut encoded_args = vec![]; + for (user_arg_idx, arg) in args.iter().enumerate() { + let arg_type = + extract_shank_instruction_arg_type(idl, instruction_name, user_arg_idx) + .map_err(|e| diagnosed_error!("failed to extract arg type: {}", e))?; + let mut encoded_arg = borsh_encode_value_to_shank_idl_type( + arg, &arg_type, &idl_types, + ) + .map_err(|e| { + diagnosed_error!( + "error in argument at position {}: {}", + user_arg_idx + 1, + e + ) + })?; + encoded_args.append(&mut encoded_arg); } + Ok(encoded_args) } } - t => return Err(format!("IDL type {:?} is not yet supported", t)), } } -fn borsh_encode_value_to_idl_defined_fields( - idl_defined_fields: &IdlDefinedFields, - value: &Value, - idl_type: &IdlType, - idl_types: &Vec, - generics: &Vec, - type_def_generics: &Vec, -) -> Result, String> { - let mismatch_err = |expected: &str| { - format!( - "invalid value for idl type: expected {}, found {}", - expected, - value.get_type().to_string() - ) - }; - let encode_err = |expected: &str, e| { - format!("unable to encode value ({}) as borsh {}: {}", value.to_string(), expected, e) - }; - let mut encoded_fields = vec![]; - match idl_defined_fields { - IdlDefinedFields::Named(expected_fields) => { - let mut user_values_map = value.as_object().ok_or(mismatch_err("object"))?.clone(); - for field in expected_fields { - let user_value = user_values_map - .swap_remove(&field.name) - .ok_or_else(|| format!("missing field '{}' in object", field.name))?; - - let ty = parse_generic_expected_type(&field.ty, &type_def_generics, generics)?; - - let mut encoded_field = - borsh_encode_value_to_idl_type(&user_value, &ty, idl_types, Some(idl_type)) - .map_err(|e| format!("failed to encode field '{}': {}", field.name, e))?; - encoded_fields.append(&mut encoded_field); - } - if !user_values_map.is_empty() { - return Err(format!( - "extra fields found in object: {}", - user_values_map.keys().map(|s| s.as_str()).collect::>().join(", ") - )); - } +fn parse_idl_string(idl_str: &str) -> Result { + // Try parsing as Anchor IDL first (modern format) + match serde_json::from_str::(idl_str) { + Ok(anchor_idl) => return Ok(IdlKind::Anchor(anchor_idl)), + Err(e) => { + debug!("Failed to parse as Anchor IDL: {}", e); } - IdlDefinedFields::Tuple(expected_tuple_types) => { - let user_values = value.as_array().ok_or(mismatch_err("array"))?; - let mut encoded_tuple_fields = vec![]; - - if user_values.len() != expected_tuple_types.len() { - return Err(format!( - "invalid value for idl type: expected tuple length of {}, found {}", - expected_tuple_types.len(), - user_values.len() - )); - } - for (i, expected_type) in expected_tuple_types.iter().enumerate() { - let user_value = user_values - .get(i) - .ok_or_else(|| format!("missing field value in {} index of array", i))?; - - let ty = parse_generic_expected_type(expected_type, &type_def_generics, generics)?; + } - let encoded_field = - borsh_encode_value_to_idl_type(user_value, &ty, idl_types, Some(idl_type)) - .map_err(|e| format!("failed to encode field #{}: {}", i + 1, e))?; - encoded_tuple_fields.push(encoded_field); - } - encoded_fields.append( - &mut borsh::to_vec(&encoded_tuple_fields).map_err(|e| encode_err("tuple", e))?, - ); + // Try parsing as Shank IDL + match serde_json::from_str::(idl_str) { + Ok(shank_idl) => return Ok(IdlKind::Shank(shank_idl)), + Err(e) => { + debug!("Failed to parse as Shank IDL: {}", e); } } - Ok(encoded_fields) -} -fn borsh_encode_bytes_to_idl_type( - bytes: &Vec, - idl_type: &IdlType, - idl_types: &Vec, -) -> Result, String> { - match idl_type { - // Primitive numeric types - deserialize from bytes - IdlType::U8 => { - if bytes.len() != 1 { - return Err(format!("expected 1 byte for u8, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::U16 => { - if bytes.len() != 2 { - return Err(format!("expected 2 bytes for u16, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::U32 => { - if bytes.len() != 4 { - return Err(format!("expected 4 bytes for u32, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::U64 => { - if bytes.len() != 8 { - return Err(format!("expected 8 bytes for u64, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::U128 => { - if bytes.len() != 16 { - return Err(format!("expected 16 bytes for u128, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::U256 => { - if bytes.len() != 32 { - return Err(format!("expected 32 bytes for u256, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::I8 => { - if bytes.len() != 1 { - return Err(format!("expected 1 byte for i8, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::I16 => { - if bytes.len() != 2 { - return Err(format!("expected 2 bytes for i16, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::I32 => { - if bytes.len() != 4 { - return Err(format!("expected 4 bytes for i32, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::I64 => { - if bytes.len() != 8 { - return Err(format!("expected 8 bytes for i64, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::I128 => { - if bytes.len() != 16 { - return Err(format!("expected 16 bytes for i128, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::I256 => { - if bytes.len() != 32 { - return Err(format!("expected 32 bytes for i256, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::F32 => { - if bytes.len() != 4 { - return Err(format!("expected 4 bytes for f32, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::F64 => { - if bytes.len() != 8 { - return Err(format!("expected 8 bytes for f64, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::Bool => { - if bytes.len() != 1 { - return Err(format!("expected 1 byte for bool, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::Pubkey => { - if bytes.len() != 32 { - return Err(format!("expected 32 bytes for Pubkey, found {}", bytes.len())); - } - Ok(bytes.clone()) - } - IdlType::String => { - // Assume bytes are UTF-8 encoded string, encode as borsh string - let s = std::str::from_utf8(bytes) - .map_err(|e| format!("invalid UTF-8 for string: {}", e))?; - borsh::to_vec(&s).map_err(|e| format!("failed to encode string: {}", e)) - } - IdlType::Bytes => { - // Return raw bytes as-is - Ok(bytes.clone()) - } - IdlType::Vec(inner_type) => { - // Encode as vector - each element from inner type - match &**inner_type { - IdlType::U8 => { - // Vec - encode as borsh vector - borsh::to_vec(bytes).map_err(|e| format!("failed to encode Vec: {}", e)) - } - _ => { - // For other types, try to split bytes and encode each element - Err(format!( - "cannot convert raw bytes to Vec<{:?}>; bytes can only be directly converted to Vec", - inner_type - )) - } - } + // Try parsing as classic/legacy Anchor IDL and convert to modern format + match serde_json::from_str(idl_str) { + Ok(classic_idl) => { + let anchor_idl = classic_idl_to_anchor_idl(classic_idl)?; + Ok(IdlKind::Anchor(anchor_idl)) } - IdlType::Array(inner_type, array_len) => { - let expected_len = match array_len { - IdlArrayLen::Value(len) => *len, - IdlArrayLen::Generic(_) => { - return Err(format!("cannot determine array length from generic")); - } - }; - - match &**inner_type { - IdlType::U8 => { - // [u8; N] - validate length and return bytes - if bytes.len() != expected_len { - return Err(format!( - "expected {} bytes for array, found {}", - expected_len, - bytes.len() - )); - } - Ok(bytes.clone()) - } - _ => { - // For other types, would need to know element size - Err(format!( - "cannot convert raw bytes to [{:?}; {}]; bytes can only be directly converted to [u8; N]", - inner_type, expected_len - )) - } - } - } - IdlType::Option(inner_type) => { - // If bytes are empty, encode as None - if bytes.is_empty() { - borsh::to_vec(&None::).map_err(|e| format!("failed to encode None: {}", e)) - } else { - // Otherwise encode as Some with inner bytes - let inner_encoded = borsh_encode_bytes_to_idl_type(bytes, inner_type, idl_types)?; - borsh::to_vec(&Some(inner_encoded)) - .map_err(|e| format!("failed to encode Option: {}", e)) - } - } - IdlType::Defined { name, .. } => { - // For defined types, we can't directly convert from bytes without knowing the structure - Err(format!( - "cannot convert raw bytes to defined type '{}'; use structured value instead", - name - )) - } - IdlType::Generic(name) => { - Err(format!( - "cannot convert raw bytes to generic type '{}'; type must be resolved first", - name - )) - } - t => Err(format!("IDL type {:?} is not yet supported for bytes encoding", t)), + Err(e) => Err(diagnosed_error!("invalid idl: {e}")), } } -fn parse_generic_expected_type( - expected_type: &IdlType, - type_def_generics: &Vec, - generic_args: &Vec, -) -> Result { - let ty = if let IdlType::Generic(generic) = &expected_type { - let Some(generic_pos) = type_def_generics.iter().position(|g| match g { - IdlTypeDefGeneric::Type { name } => name.eq(generic), - IdlTypeDefGeneric::Const { name, .. } => name.eq(generic), - }) else { - return Err(format!("unable to find generic {} in idl", generic)); - }; - let generic = generic_args - .get(generic_pos) - .ok_or(format!("unable to find generic {} in idl", generic))?; - match generic { - IdlGenericArg::Type { ty } => ty, - IdlGenericArg::Const { value } => { - &IdlType::from_str(value).map_err(|e| format!("invalid generic type: {e}"))? - } +fn parse_idl_bytes(idl_bytes: &[u8]) -> Result { + // Try parsing as Anchor IDL first (modern format) + if let Ok(anchor_idl) = serde_json::from_slice::(idl_bytes) { + return Ok(IdlKind::Anchor(anchor_idl)); + } + + // Try parsing as Shank IDL + if let Ok(shank_idl) = serde_json::from_slice::(idl_bytes) { + return Ok(IdlKind::Shank(shank_idl)); + } + + // Try parsing as classic/legacy Anchor IDL and convert to modern format + match serde_json::from_slice(idl_bytes) { + Ok(classic_idl) => { + let anchor_idl = classic_idl_to_anchor_idl(classic_idl)?; + Ok(IdlKind::Anchor(anchor_idl)) } - } else { - &expected_type - }; - Ok(ty.clone()) + Err(e) => Err(diagnosed_error!("invalid idl: {e}")), + } } diff --git a/addons/svm/core/src/codec/mod.rs b/addons/svm/core/src/codec/mod.rs index cf1c7095e..a04d29d0b 100644 --- a/addons/svm/core/src/codec/mod.rs +++ b/addons/svm/core/src/codec/mod.rs @@ -32,6 +32,7 @@ use solana_signer::Signer; use solana_system_interface::instruction as system_instruction; use solana_system_interface::MAX_PERMITTED_DATA_LENGTH; use txtx_addon_kit::types::frontend::LogDispatcher; +use txtx_addon_network_svm_types::subgraph::idl::IdlKind; use crate::typing::DeploymentTransactionType; use solana_loader_v3_interface::instruction as bpf_loader_upgradeable; use solana_instruction::Instruction; @@ -1711,9 +1712,16 @@ impl ProgramArtifacts { } pub fn idl(&self) -> Result, Diagnostic> { match self { - ProgramArtifacts::Native(_) => Ok(None), + ProgramArtifacts::Native(artifacts) => artifacts + .idl + .as_ref() + .map(|idl| { + serde_json::to_string(idl) + .map_err(|e| diagnosed_error!("invalid native idl: {e}")) + }) + .transpose(), ProgramArtifacts::Anchor(artifacts) => Some( - serde_json::to_string(&artifacts.idl) + serde_json::to_string(&IdlKind::Anchor(artifacts.idl.clone())) .map_err(|e| diagnosed_error!("invalid anchor idl: {e}")), ) .transpose(), diff --git a/addons/svm/core/src/codec/native.rs b/addons/svm/core/src/codec/native.rs index 6ba6713c7..fafa2fb1a 100644 --- a/addons/svm/core/src/codec/native.rs +++ b/addons/svm/core/src/codec/native.rs @@ -1,6 +1,4 @@ -use std::str::FromStr; - -use crate::{codec::validate_program_so, typing::anchor::types as anchor_types}; +use crate::codec::validate_program_so; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; @@ -11,6 +9,7 @@ use txtx_addon_kit::{ types::{ObjectType, Value}, }, }; +use txtx_addon_network_svm_types::subgraph::idl::IdlKind; use crate::typing::SvmValue; @@ -24,8 +23,8 @@ pub struct NativeProgramArtifacts { pub keypair: Option, /// The program pubkey of the native program. pub program_id: Pubkey, - /// The IDL of the program, if provided. IDLs are converted to anchor-style IDLs. - pub idl: Option, + /// The IDL of the program, if provided. Can be either Anchor or Shank format. + pub idl: Option, } impl NativeProgramArtifacts { @@ -34,15 +33,15 @@ impl NativeProgramArtifacts { idl_path: FileLocation, bin_path: FileLocation, ) -> Result { - let some_idl = if idl_path.exists() { + let some_idl_ref = if idl_path.exists() { let idl_str = idl_path.read_content_as_utf8().map_err(|e| { diagnosed_error!("invalid idl location {}: {}", &idl_path.to_string(), e) })?; - let idl = IdlRef::from_str(&idl_str).map_err(|e| { + let idl_ref = IdlRef::from_str(&idl_str).map_err(|e| { diagnosed_error!("invalid idl at location {}: {}", &idl_path.to_string(), e) })?; - Some(idl.idl) + Some(idl_ref) } else { None }; @@ -86,8 +85,8 @@ impl NativeProgramArtifacts { None }; - let program_id = match (keypair.as_ref(), some_idl.as_ref()) { - (_, Some(idl)) => Pubkey::from_str(&idl.address).map_err(|e| { + let program_id = match (keypair.as_ref(), some_idl_ref.as_ref()) { + (_, Some(idl_ref)) => idl_ref.get_program_pubkey().map_err(|e| { diagnosed_error!( "invalid program id in idl at location {}: {}", &idl_path.to_string(), @@ -102,6 +101,7 @@ impl NativeProgramArtifacts { } }; + let some_idl = some_idl_ref.map(|idl_ref| idl_ref.idl); Ok(NativeProgramArtifacts { bin, keypair, program_id, idl: some_idl }) } @@ -112,8 +112,11 @@ impl NativeProgramArtifacts { ("framework", Value::string("native".to_string())), ]); if let Some(idl) = &self.idl { - let idl_str = serde_json::to_string_pretty(&idl) - .map_err(|e| diagnosed_error!("invalid idl: {e}"))?; + let idl_str = match idl { + IdlKind::Anchor(anchor_idl) => serde_json::to_string_pretty(anchor_idl), + IdlKind::Shank(shank_idl) => serde_json::to_string_pretty(shank_idl), + } + .map_err(|e| diagnosed_error!("invalid idl: {e}"))?; obj.insert("idl", Value::string(idl_str)); }; @@ -149,9 +152,8 @@ impl NativeProgramArtifacts { let idl_str = idl_value.as_string().ok_or(diagnosed_error!( "native program artifacts value had invalid idl data: expected string" ))?; - let idl: anchor_types::Idl = - serde_json::from_str(idl_str).map_err(|e| diagnosed_error!("{e}"))?; - Some(idl) + let idl_ref = IdlRef::from_str(idl_str).map_err(|e| diagnosed_error!("{e}"))?; + Some(idl_ref.idl) } else { None }; diff --git a/addons/svm/core/src/commands/deploy_program.rs b/addons/svm/core/src/commands/deploy_program.rs index 5bf345346..113962943 100644 --- a/addons/svm/core/src/commands/deploy_program.rs +++ b/addons/svm/core/src/commands/deploy_program.rs @@ -33,9 +33,9 @@ use txtx_addon_kit::types::types::{ }; use txtx_addon_kit::types::{ConstructDid, Did}; use txtx_addon_kit::uuid::Uuid; +use txtx_addon_network_svm_types::subgraph::idl::IdlKind; use txtx_addon_network_svm_types::{SVM_KEYPAIR, SVM_PUBKEY}; -use crate::codec::idl::IdlRef; use crate::codec::send_transaction::send_transaction_background_task; use crate::codec::utils::cheatcode_deploy_program; use crate::codec::{DeploymentTransaction, ProgramArtifacts, UpgradeableProgramDeployer}; @@ -968,8 +968,8 @@ impl CommandImplementation for DeployProgram { .get_scoped_value(&nested_construct_did.to_string(), PROGRAM_IDL) .and_then(|v| v.as_string()) { - if let Ok(idl_ref) = IdlRef::from_str(idl) { - let value = serde_json::to_value(&idl_ref.idl).unwrap(); + if let Ok(idl_kind) = serde_json::from_str::(idl) { + let value = idl_kind.to_json_value().unwrap(); let params = serde_json::to_value(&vec![value]).unwrap(); let router = cloud_service_context diff --git a/addons/svm/core/src/commands/setup_surfnet/cheatcode_deploy_program.rs b/addons/svm/core/src/commands/setup_surfnet/cheatcode_deploy_program.rs index 5ca37d619..a2ad77e2b 100644 --- a/addons/svm/core/src/commands/setup_surfnet/cheatcode_deploy_program.rs +++ b/addons/svm/core/src/commands/setup_surfnet/cheatcode_deploy_program.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -use serde::{Deserialize, Serialize}; use serde_json::json; use solana_client::nonblocking::rpc_client::RpcClient; use solana_client::rpc_request::RpcRequest; @@ -13,24 +12,19 @@ use txtx_addon_kit::{ AuthorizationContext, }, }; -use txtx_addon_network_svm_types::{anchor::types::Idl, SvmValue}; +use txtx_addon_network_svm_types::{subgraph::idl::IdlKind, SvmValue}; use crate::{ codec::{utils::cheatcode_deploy_program, validate_program_so}, constants::DEPLOY_PROGRAM, }; -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Default)] pub struct SurfpoolDeployProgram { - #[serde(skip)] pub program_id: Pubkey, - #[serde(skip)] pub binary: Vec, - #[serde(skip)] pub authority: Option, - #[serde(skip)] - pub idl: Option, + pub idl: Option, } impl SurfpoolDeployProgram { @@ -92,10 +86,10 @@ impl SurfpoolDeployProgram { diagnosed_error!("invalid idl location {}: {}", &idl_path.to_string(), e) })?; - let idl = crate::codec::idl::IdlRef::from_str(&idl_str).map_err(|e| { + let idl_ref = crate::codec::idl::IdlRef::from_str(&idl_str).map_err(|e| { diagnosed_error!("invalid idl at location {}: {}", &idl_path.to_string(), e) })?; - Some(idl.idl) + Some(idl_ref.idl) } else { None } @@ -174,9 +168,7 @@ impl SurfpoolDeployProgram { rpc_client .send::( RpcRequest::Custom { method: "surfnet_registerIdl" }, - json!([serde_json::to_string(idl).map_err(|e| { - diagnosed_error!("failed to serialize idl for rpc call: {e}") - })?]), + json!([idl.to_json_value().unwrap()]), ) .await .map_err(|e| diagnosed_error!("failed to register idl via rpc call: {e}"))?; diff --git a/addons/svm/core/src/functions.rs b/addons/svm/core/src/functions.rs index 1b33f1201..90686eb2c 100644 --- a/addons/svm/core/src/functions.rs +++ b/addons/svm/core/src/functions.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::{codec::utils::get_seeds_from_value, typing::anchor::types::Idl}; +use crate::codec::utils::get_seeds_from_value; use crate::constants::{DEFAULT_NATIVE_TARGET_PATH, DEFAULT_SHANK_IDL_PATH}; use solana_pubkey::Pubkey; @@ -490,13 +490,9 @@ impl FunctionImplementation for GetInstructionDataFromIdl { let arguments = args.get(2).and_then(|a| Some(a.as_array().unwrap().to_vec())).unwrap_or(vec![]); - // let idl: Idl = serde_json::from_slice(&idl_bytes) - // .map_err(|e| to_diag(fn_spec, format!("invalid idl: {e}")))?; - let idl: Idl = serde_json::from_str(idl_str) + let idl_ref = IdlRef::from_str(idl_str) .map_err(|e| to_diag(fn_spec, format!("invalid idl: {e}")))?; - let idl_ref = IdlRef::from_idl(idl); - let mut data = idl_ref.get_discriminator(&instruction_name).map_err(|e| to_diag(fn_spec, e))?; let mut encoded_args = idl_ref diff --git a/addons/svm/core/src/templates/mod.rs b/addons/svm/core/src/templates/mod.rs index f17e4e677..90ea6f845 100644 --- a/addons/svm/core/src/templates/mod.rs +++ b/addons/svm/core/src/templates/mod.rs @@ -150,8 +150,13 @@ pub fn get_interpolated_anchor_subgraph_template( program_name: &str, idl_str: &str, ) -> Result, String> { - let idl = - IdlRef::from_str(idl_str).map_err(|e| format!("failed to parse program idl: {e}"))?.idl; + let idl_ref = + IdlRef::from_str(idl_str).map_err(|e| format!("failed to parse program idl: {e}"))?; + + let idl = match idl_ref.as_anchor() { + Some(idl) => idl, + None => return Ok(None), + }; let subgraph_runbook = if idl.events.is_empty() { None diff --git a/addons/svm/types/Cargo.toml b/addons/svm/types/Cargo.toml index e67ccbae0..1e073fce5 100644 --- a/addons/svm/types/Cargo.toml +++ b/addons/svm/types/Cargo.toml @@ -30,6 +30,7 @@ lazy_static = "1.4.0" serde = "1" serde_json = "1" serde_derive = "1" +shank_idl = "0.4.7" [dev-dependencies] test-case = "3.3" diff --git a/addons/svm/types/src/lib.rs b/addons/svm/types/src/lib.rs index 11378a7a8..baba6ce22 100644 --- a/addons/svm/types/src/lib.rs +++ b/addons/svm/types/src/lib.rs @@ -23,6 +23,7 @@ use txtx_addon_kit::{ }; pub use anchor_lang_idl as anchor; +pub use shank_idl as shank; pub const SVM_TRANSACTION: &str = "svm::transaction"; pub const SVM_INSTRUCTION: &str = "svm::instruction"; diff --git a/addons/svm/types/src/subgraph/event.rs b/addons/svm/types/src/subgraph/event.rs index f4b445cff..7508dbec7 100644 --- a/addons/svm/types/src/subgraph/event.rs +++ b/addons/svm/types/src/subgraph/event.rs @@ -11,8 +11,9 @@ use txtx_addon_kit::{ }; use crate::subgraph::{ - idl::parse_bytes_to_value_with_expected_idl_type_def_ty, IntrinsicField, SubgraphRequest, - SubgraphSourceType, SLOT_INTRINSIC_FIELD, TRANSACTION_SIGNATURE_INTRINSIC_FIELD, + idl::anchor::parse_bytes_to_value_with_expected_idl_type_def_ty, IntrinsicField, + SubgraphRequest, SubgraphSourceType, SLOT_INTRINSIC_FIELD, + TRANSACTION_SIGNATURE_INTRINSIC_FIELD, }; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/addons/svm/types/src/subgraph/fixtures/shank_test_idl.json b/addons/svm/types/src/subgraph/fixtures/shank_test_idl.json new file mode 100644 index 000000000..5c7155601 --- /dev/null +++ b/addons/svm/types/src/subgraph/fixtures/shank_test_idl.json @@ -0,0 +1,1327 @@ +{ + "version": "0.1.0", + "name": "swap_program", + "instructions": [ + { + "name": "ProcessPrimitives", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer account" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account to initialize" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "System program" + ] + } + ], + "args": [ + { + "name": "primitives", + "type": { + "defined": "AllPrimitives" + } + } + ], + "discriminant": { + "type": "u8", + "value": 0 + } + }, + { + "name": "ProcessStrings", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "dataAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Data account" + ] + } + ], + "args": [ + { + "name": "strings", + "type": { + "defined": "StringContainer" + } + } + ], + "discriminant": { + "type": "u8", + "value": 1 + } + }, + { + "name": "ProcessBytes", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "dataAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Data account" + ] + } + ], + "args": [ + { + "name": "bytes", + "type": { + "defined": "BytesContainer" + } + } + ], + "discriminant": { + "type": "u8", + "value": 2 + } + }, + { + "name": "ProcessPubkeys", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "pubkeys", + "type": { + "defined": "PubkeyContainer" + } + } + ], + "discriminant": { + "type": "u8", + "value": 3 + } + }, + { + "name": "ProcessOptions", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true, + "docs": [ + "Owner" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "options", + "type": { + "defined": "OptionPrimitives" + } + }, + { + "name": "optionString", + "type": { + "defined": "OptionString" + } + }, + { + "name": "optionNested", + "type": { + "defined": "OptionNested" + } + } + ], + "discriminant": { + "type": "u8", + "value": 4 + } + }, + { + "name": "ProcessVecs", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "vecs", + "type": { + "defined": "VecPrimitives" + } + }, + { + "name": "vecStrings", + "type": { + "defined": "VecStrings" + } + }, + { + "name": "vecNested", + "type": { + "defined": "VecNested" + } + } + ], + "discriminant": { + "type": "u8", + "value": 5 + } + }, + { + "name": "ProcessArrays", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "arrays", + "type": { + "defined": "ArrayContainer" + } + }, + { + "name": "boolArray", + "type": { + "defined": "BoolArray" + } + } + ], + "discriminant": { + "type": "u8", + "value": 6 + } + }, + { + "name": "ProcessEnums", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "status", + "type": { + "defined": "Status" + } + }, + { + "name": "priority", + "type": { + "defined": "Priority" + } + }, + { + "name": "singleValue", + "type": { + "defined": "SingleValueEnum" + } + }, + { + "name": "structVariant", + "type": { + "defined": "StructVariantEnum" + } + }, + { + "name": "mixed", + "type": { + "defined": "MixedEnum" + } + } + ], + "discriminant": { + "type": "u8", + "value": 7 + } + }, + { + "name": "ProcessNested", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "nested", + "type": { + "defined": "NestedStructs" + } + }, + { + "name": "complex", + "type": { + "defined": "ComplexContainer" + } + } + ], + "discriminant": { + "type": "u8", + "value": 8 + } + }, + { + "name": "ProcessAccountState", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Target account" + ] + } + ], + "args": [ + { + "name": "state", + "type": { + "defined": "TestAccountState" + } + } + ], + "discriminant": { + "type": "u8", + "value": 9 + } + }, + { + "name": "ProcessDirectPrimitives", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account" + ] + } + ], + "args": [ + { + "name": "boolVal", + "type": "bool" + }, + { + "name": "u8Val", + "type": "u8" + }, + { + "name": "u16Val", + "type": "u16" + }, + { + "name": "u32Val", + "type": "u32" + }, + { + "name": "u64Val", + "type": "u64" + }, + { + "name": "u128Val", + "type": "u128" + }, + { + "name": "i8Val", + "type": "i8" + }, + { + "name": "i16Val", + "type": "i16" + }, + { + "name": "i32Val", + "type": "i32" + }, + { + "name": "i64Val", + "type": "i64" + }, + { + "name": "i128Val", + "type": "i128" + } + ], + "discriminant": { + "type": "u8", + "value": 10 + } + }, + { + "name": "ProcessDirectString", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account" + ] + } + ], + "args": [ + { + "name": "value", + "type": "string" + } + ], + "discriminant": { + "type": "u8", + "value": 11 + } + }, + { + "name": "ProcessDirectVec", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account" + ] + } + ], + "args": [ + { + "name": "values", + "type": { + "vec": "u64" + } + } + ], + "discriminant": { + "type": "u8", + "value": 12 + } + }, + { + "name": "ProcessDirectOption", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account" + ] + } + ], + "args": [ + { + "name": "maybeValue", + "type": { + "option": "u64" + } + } + ], + "discriminant": { + "type": "u8", + "value": 13 + } + }, + { + "name": "ProcessDirectArray", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account" + ] + } + ], + "args": [ + { + "name": "fixedData", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ], + "discriminant": { + "type": "u8", + "value": 14 + } + }, + { + "name": "ProcessInstructionData", + "accounts": [ + { + "name": "authority", + "isMut": true, + "isSigner": true, + "docs": [ + "Authority" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "docs": [ + "Account" + ] + } + ], + "args": [ + { + "name": "simple", + "type": { + "defined": "SimpleInstructionData" + } + }, + { + "name": "complex", + "type": { + "defined": "ComplexInstructionData" + } + } + ], + "discriminant": { + "type": "u8", + "value": 15 + } + } + ], + "types": [ + { + "name": "AllPrimitives", + "type": { + "kind": "struct", + "fields": [ + { + "name": "boolField", + "type": "bool" + }, + { + "name": "u8Field", + "type": "u8" + }, + { + "name": "u16Field", + "type": "u16" + }, + { + "name": "u32Field", + "type": "u32" + }, + { + "name": "u64Field", + "type": "u64" + }, + { + "name": "u128Field", + "type": "u128" + }, + { + "name": "i8Field", + "type": "i8" + }, + { + "name": "i16Field", + "type": "i16" + }, + { + "name": "i32Field", + "type": "i32" + }, + { + "name": "i64Field", + "type": "i64" + }, + { + "name": "i128Field", + "type": "i128" + } + ] + } + }, + { + "name": "StringContainer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "label", + "type": "string" + }, + { + "name": "description", + "type": "string" + } + ] + } + }, + { + "name": "BytesContainer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "data", + "type": "bytes" + }, + { + "name": "rawBytes", + "type": "bytes" + } + ] + } + }, + { + "name": "PubkeyContainer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "authority", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "destination", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "OptionPrimitives", + "type": { + "kind": "struct", + "fields": [ + { + "name": "optionalU8", + "type": { + "option": "u8" + } + }, + { + "name": "optionalU64", + "type": { + "option": "u64" + } + }, + { + "name": "optionalU128", + "type": { + "option": "u128" + } + }, + { + "name": "optionalBool", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "OptionString", + "type": { + "kind": "struct", + "fields": [ + { + "name": "optionalString", + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "Point", + "type": { + "kind": "struct", + "fields": [ + { + "name": "x", + "type": "i32" + }, + { + "name": "y", + "type": "i32" + } + ] + } + }, + { + "name": "OptionNested", + "type": { + "kind": "struct", + "fields": [ + { + "name": "optionalPoint", + "type": { + "option": { + "defined": "Point" + } + } + } + ] + } + }, + { + "name": "VecPrimitives", + "type": { + "kind": "struct", + "fields": [ + { + "name": "u8Vec", + "type": "bytes" + }, + { + "name": "u64Vec", + "type": { + "vec": "u64" + } + }, + { + "name": "boolVec", + "type": { + "vec": "bool" + } + } + ] + } + }, + { + "name": "VecStrings", + "type": { + "kind": "struct", + "fields": [ + { + "name": "strings", + "type": { + "vec": "string" + } + } + ] + } + }, + { + "name": "VecNested", + "type": { + "kind": "struct", + "fields": [ + { + "name": "points", + "type": { + "vec": { + "defined": "Point" + } + } + } + ] + } + }, + { + "name": "ArrayContainer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "smallArray", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "mediumArray", + "type": { + "array": [ + "u32", + 8 + ] + } + }, + { + "name": "largeArray", + "type": { + "array": [ + "u64", + 16 + ] + } + }, + { + "name": "pubkeyLike", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "BoolArray", + "type": { + "kind": "struct", + "fields": [ + { + "name": "flags", + "type": { + "array": [ + "bool", + 8 + ] + } + } + ] + } + }, + { + "name": "NestedStructs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "point", + "type": { + "defined": "Point" + } + }, + { + "name": "primitives", + "type": { + "defined": "AllPrimitives" + } + }, + { + "name": "status", + "type": { + "defined": "Status" + } + } + ] + } + }, + { + "name": "ComplexContainer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "type": "u64" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "point", + "type": { + "defined": "Point" + } + }, + { + "name": "status", + "type": { + "defined": "Status" + } + }, + { + "name": "optionalPoint", + "type": { + "option": { + "defined": "Point" + } + } + }, + { + "name": "points", + "type": { + "vec": { + "defined": "Point" + } + } + } + ] + } + }, + { + "name": "TestAccountState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "discriminator", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "authority", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "balance", + "type": "u64" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "status", + "type": { + "defined": "Status" + } + }, + { + "name": "lastUpdated", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "SimpleInstructionData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "count", + "type": "u32" + }, + { + "name": "flag", + "type": "bool" + } + ] + } + }, + { + "name": "ComplexInstructionData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amounts", + "type": { + "vec": "u64" + } + }, + { + "name": "label", + "type": "string" + }, + { + "name": "optionalPoint", + "type": { + "option": { + "defined": "Point" + } + } + }, + { + "name": "data", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "SupportDex", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Pump" + }, + { + "name": "PumpAmm" + }, + { + "name": "RaydiumAmm" + }, + { + "name": "RaydiumCP" + }, + { + "name": "RaydiumCLMM" + }, + { + "name": "DLMM" + }, + { + "name": "WhirlPool" + } + ] + } + }, + { + "name": "Status", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Inactive" + }, + { + "name": "Active" + }, + { + "name": "Paused" + }, + { + "name": "Completed" + } + ] + } + }, + { + "name": "Priority", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Low" + }, + { + "name": "Medium" + }, + { + "name": "High" + }, + { + "name": "Critical" + } + ] + } + }, + { + "name": "SingleValueEnum", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Empty" + }, + { + "name": "WithU64", + "fields": [ + "u64" + ] + }, + { + "name": "WithU128", + "fields": [ + "u128" + ] + }, + { + "name": "WithBool", + "fields": [ + "bool" + ] + } + ] + } + }, + { + "name": "StructVariantEnum", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "WithAmount", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "WithCoordinates", + "fields": [ + { + "name": "x", + "type": "i32" + }, + { + "name": "y", + "type": "i32" + } + ] + }, + { + "name": "WithDetails", + "fields": [ + { + "name": "id", + "type": "u32" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "active", + "type": "bool" + } + ] + } + ] + } + }, + { + "name": "MixedEnum", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Unit" + }, + { + "name": "Data", + "fields": [ + { + "name": "value", + "type": "u64" + }, + { + "name": "flag", + "type": "bool" + } + ] + }, + { + "name": "Complex", + "fields": [ + { + "name": "point", + "type": { + "defined": "Point" + } + }, + { + "name": "label", + "type": "string" + } + ] + } + ] + } + } + ], + "metadata": { + "origin": "shank", + "address": "D7Nv2Yt9i7r1xSGgTZo9zGHgZ8wwiAX13nFodBXdpox4" + } +} diff --git a/addons/svm/types/src/subgraph/idl.rs b/addons/svm/types/src/subgraph/idl/anchor.rs similarity index 50% rename from addons/svm/types/src/subgraph/idl.rs rename to addons/svm/types/src/subgraph/idl/anchor.rs index 9931bac75..41b7a1aeb 100644 --- a/addons/svm/types/src/subgraph/idl.rs +++ b/addons/svm/types/src/subgraph/idl/anchor.rs @@ -1,6 +1,6 @@ use anchor_lang_idl::types::{ - IdlConst, IdlDefinedFields, IdlGenericArg, IdlInstruction, IdlInstructionAccountItem, IdlType, - IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy, + IdlArrayLen, IdlConst, IdlDefinedFields, IdlGenericArg, IdlInstruction, + IdlInstructionAccountItem, IdlType, IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy, }; use solana_pubkey::Pubkey; use txtx_addon_kit::{ @@ -8,7 +8,7 @@ use txtx_addon_kit::{ types::types::{ObjectDefinition, ObjectProperty, ObjectType, Type, Value}, }; -use crate::{SvmValue, SVM_PUBKEY}; +use crate::{SvmValue, I256, SVM_PUBKEY, U256}; use std::{fmt::Display, str::FromStr}; pub fn get_expected_type_from_idl_defined_fields( @@ -681,3 +681,588 @@ pub fn match_idl_accounts( .map(|(name, &index)| (name, message_account_keys[index as usize], index as usize)) .collect() } + +pub fn borsh_encode_value_to_idl_type( + value: &Value, + idl_type: &IdlType, + idl_types: &Vec, + defined_parent_context: Option<&IdlType>, +) -> Result, String> { + let mismatch_err = |expected: &str| { + format!( + "invalid value for idl type: expected {}, found {}", + expected, + value.get_type().to_string() + ) + }; + let encode_err = |expected: &str, e: &dyn Display| { + format!("unable to encode value ({}) as borsh {}: {}", value.to_string(), expected, e) + }; + + match value { + Value::Buffer(bytes) => return borsh_encode_bytes_to_idl_type(bytes, idl_type, idl_types), + Value::Addon(addon_data) => { + return borsh_encode_bytes_to_idl_type(&addon_data.bytes, idl_type, idl_types) + } + _ => {} + } + + match idl_type { + IdlType::Bool => value + .as_bool() + .and_then(|b| Some(borsh::to_vec(&b).map_err(|e| encode_err("bool", &e)))) + .transpose()? + .ok_or(mismatch_err("bool")), + IdlType::U8 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u8", &e)), + IdlType::U16 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u16", &e)), + IdlType::U32 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u32", &e)), + IdlType::U64 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u64", &e)), + IdlType::U128 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u128", &e)), + IdlType::U256 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num.0).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u256", &e)), + IdlType::I8 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i8", &e)), + IdlType::I16 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i16", &e)), + IdlType::I32 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i32", &e)), + IdlType::I64 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i64", &e)), + IdlType::I128 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i128", &e)), + IdlType::I256 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num.0).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i256", &e)), + IdlType::F32 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("f32", &e)), + IdlType::F64 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("f64", &e)), + IdlType::Bytes => Ok(value.to_be_bytes().clone()), + IdlType::String => value + .as_string() + .and_then(|s| Some(borsh::to_vec(&s).map_err(|e| encode_err("string", &e)))) + .transpose()? + .ok_or(mismatch_err("string")), + IdlType::Pubkey => SvmValue::to_pubkey(value) + .map_err(|_| mismatch_err("pubkey")) + .map(|p| borsh::to_vec(&p))? + .map_err(|e| encode_err("pubkey", &e)), + IdlType::Option(idl_type) => { + if let Some(_) = value.as_null() { + borsh::to_vec(&None::).map_err(|e| encode_err("Optional", &e)) + } else { + let encoded_arg = borsh_encode_value_to_idl_type(value, idl_type, idl_types, None)?; + borsh::to_vec(&Some(encoded_arg)).map_err(|e| encode_err("Optional", &e)) + } + } + IdlType::Vec(idl_type) => match value { + Value::String(_) => { + let bytes = value.get_buffer_bytes_result().map_err(|_| mismatch_err("vec"))?; + borsh_encode_bytes_to_idl_type(&bytes, idl_type, idl_types) + } + Value::Array(vec) => vec + .iter() + .map(|v| borsh_encode_value_to_idl_type(v, idl_type, idl_types, None)) + .collect::, _>>() + .map(|v| v.into_iter().flatten().collect::>()), + _ => Err(mismatch_err("vec")), + }, + IdlType::Array(idl_type, idl_array_len) => { + let expected_length = match idl_array_len { + IdlArrayLen::Generic(generic_len) => { + let Some(&IdlType::Defined { + name: defined_parent_name, + generics: defined_parent_generics, + }) = defined_parent_context.as_ref() + else { + return Err(format!( + "generic array length does not contain parent type name" + )); + }; + + let type_def_generics = &idl_types + .iter() + .find(|t| t.name.eq(defined_parent_name)) + .ok_or_else(|| { + format!( + "unable to find type definition for {} in idl", + defined_parent_name + ) + })? + .generics; + + let IdlType::Defined { name, .. } = parse_generic_expected_type( + &IdlType::Generic(generic_len.to_string()), + &type_def_generics, + &defined_parent_generics, + )? + else { + return Err(format!("unable to parse generic array length")); + }; + &name + .parse::() + .map_err(|e| format!("unable to parse generic array length: {}", e))? + } + IdlArrayLen::Value(len) => len, + }; + let array = value + .as_array() + .map(|a| { + if expected_length != &a.len() { + return Err(format!( + "invalid value for idl type: expected array length of {}, found {}", + expected_length, + a.len() + )); + } + a.iter() + .map(|v| borsh_encode_value_to_idl_type(v, idl_type, idl_types, None)) + .collect::, _>>() + }) + .transpose()? + .map(|v| v.into_iter().flatten().collect::>()) + .ok_or(mismatch_err("vec")); + array + } + IdlType::Defined { name, generics } => { + let typing = idl_types + .iter() + .find(|t| &t.name == name) + .ok_or_else(|| format!("unable to find type definition for {} in idl", name))?; + let fields = match &typing.ty { + IdlTypeDefTy::Struct { fields } => { + if let Some(idl_defined_fields) = fields { + borsh_encode_value_to_idl_defined_fields( + idl_defined_fields, + value, + idl_type, + idl_types, + generics, + &typing.generics, + ) + .map_err(|e| format!("unable to encode value as borsh struct: {}", e))? + } else { + vec![] + } + } + IdlTypeDefTy::Enum { variants } => { + let enum_value = value.as_object().ok_or(mismatch_err("object"))?; + + // Handle two enum formats: + // 1. {"variant": "VariantName", "value": ...} (explicit format) + // 2. {"VariantName": ...} (decoded format from parse_bytes_to_value) + let (enum_variant, enum_variant_value) = if let Some(variant_field) = + enum_value.get("variant") + { + // Format 1: explicit variant field + let variant_name = variant_field.as_string().ok_or_else(|| { + format!( + "unable to encode value ({}) as borsh enum: expected variant field to be a string", + value.to_string(), + ) + })?; + let variant_value = enum_value.get("value").ok_or_else(|| { + format!( + "unable to encode value ({}) as borsh enum: missing 'value' field", + value.to_string(), + ) + })?; + (variant_name, variant_value) + } else { + // Format 2: variant name as object key + if enum_value.len() != 1 { + return Err(format!( + "unable to encode value ({}) as borsh enum: expected exactly one field (the variant name)", + value.to_string(), + )); + } + let (variant_name, variant_value) = + enum_value.iter().next().ok_or_else(|| { + format!( + "unable to encode value ({}) as borsh enum: empty object", + value.to_string(), + ) + })?; + (variant_name.as_str(), variant_value) + }; + + let (variant_index, expected_variant) = variants + .iter() + .enumerate() + .find(|(_, v)| v.name.eq(enum_variant)) + .ok_or_else(|| { + format!( + "unable to encode value ({}) as borsh enum: unknown variant {}", + value.to_string(), + enum_variant + ) + })?; + + let mut encoded = vec![variant_index as u8]; + + let type_def_generics = idl_types + .iter() + .find(|t| &t.name == name) + .map(|t| t.generics.clone()) + .unwrap_or_default(); + + match &expected_variant.fields { + Some(idl_defined_fields) => { + let mut encoded_fields = borsh_encode_value_to_idl_defined_fields( + &idl_defined_fields, + enum_variant_value, + idl_type, + idl_types, + &vec![], + &type_def_generics, + ) + .map_err(|e| { + format!("unable to encode value as borsh struct: {}", e) + })?; + + encoded.append(&mut encoded_fields); + encoded + } + None => encoded, + } + } + IdlTypeDefTy::Type { alias } => { + borsh_encode_value_to_idl_type(value, &alias, idl_types, Some(idl_type))? + } + }; + Ok(fields) + } + IdlType::Generic(generic) => { + let idl_generic = idl_types.iter().find_map(|t| { + t.generics.iter().find_map(|g| { + let is_match = match g { + IdlTypeDefGeneric::Type { name } => name == generic, + IdlTypeDefGeneric::Const { name, .. } => name == generic, + }; + if is_match { + Some(g) + } else { + None + } + }) + }); + let Some(idl_generic) = idl_generic else { + return Err(format!("unable to find generic {} in idl", generic)); + }; + match idl_generic { + IdlTypeDefGeneric::Type { name } => { + let ty = IdlType::from_str(name) + .map_err(|e| format!("invalid generic type: {e}"))?; + borsh_encode_value_to_idl_type(value, &ty, idl_types, None) + } + IdlTypeDefGeneric::Const { ty, .. } => { + let ty = + IdlType::from_str(ty).map_err(|e| format!("invalid generic type: {e}"))?; + borsh_encode_value_to_idl_type(value, &ty, idl_types, None) + } + } + } + t => return Err(format!("IDL type {:?} is not yet supported", t)), + } +} + +fn borsh_encode_value_to_idl_defined_fields( + idl_defined_fields: &IdlDefinedFields, + value: &Value, + idl_type: &IdlType, + idl_types: &Vec, + generics: &Vec, + type_def_generics: &Vec, +) -> Result, String> { + let mismatch_err = |expected: &str| { + format!( + "invalid value for idl type: expected {}, found {}", + expected, + value.get_type().to_string() + ) + }; + let encode_err = |expected: &str, e| { + format!("unable to encode value ({}) as borsh {}: {}", value.to_string(), expected, e) + }; + let mut encoded_fields = vec![]; + match idl_defined_fields { + IdlDefinedFields::Named(expected_fields) => { + let mut user_values_map = value.as_object().ok_or(mismatch_err("object"))?.clone(); + for field in expected_fields { + let user_value = user_values_map + .swap_remove(&field.name) + .ok_or_else(|| format!("missing field '{}' in object", field.name))?; + + let ty = parse_generic_expected_type(&field.ty, &type_def_generics, generics)?; + + let mut encoded_field = + borsh_encode_value_to_idl_type(&user_value, &ty, idl_types, Some(idl_type)) + .map_err(|e| format!("failed to encode field '{}': {}", field.name, e))?; + encoded_fields.append(&mut encoded_field); + } + if !user_values_map.is_empty() { + return Err(format!( + "extra fields found in object: {}", + user_values_map.keys().map(|s| s.as_str()).collect::>().join(", ") + )); + } + } + IdlDefinedFields::Tuple(expected_tuple_types) => { + let user_values = value.as_array().ok_or(mismatch_err("array"))?; + let mut encoded_tuple_fields = vec![]; + + if user_values.len() != expected_tuple_types.len() { + return Err(format!( + "invalid value for idl type: expected tuple length of {}, found {}", + expected_tuple_types.len(), + user_values.len() + )); + } + for (i, expected_type) in expected_tuple_types.iter().enumerate() { + let user_value = user_values + .get(i) + .ok_or_else(|| format!("missing field value in {} index of array", i))?; + + let ty = parse_generic_expected_type(expected_type, &type_def_generics, generics)?; + + let encoded_field = + borsh_encode_value_to_idl_type(user_value, &ty, idl_types, Some(idl_type)) + .map_err(|e| format!("failed to encode field #{}: {}", i + 1, e))?; + encoded_tuple_fields.push(encoded_field); + } + encoded_fields.append( + &mut borsh::to_vec(&encoded_tuple_fields).map_err(|e| encode_err("tuple", e))?, + ); + } + } + Ok(encoded_fields) +} + +fn borsh_encode_bytes_to_idl_type( + bytes: &Vec, + idl_type: &IdlType, + idl_types: &Vec, +) -> Result, String> { + match idl_type { + // Primitive numeric types - deserialize from bytes + IdlType::U8 => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for u8, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::U16 => { + if bytes.len() != 2 { + return Err(format!("expected 2 bytes for u16, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::U32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for u32, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::U64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for u64, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::U128 => { + if bytes.len() != 16 { + return Err(format!("expected 16 bytes for u128, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::U256 => { + if bytes.len() != 32 { + return Err(format!("expected 32 bytes for u256, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::I8 => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for i8, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::I16 => { + if bytes.len() != 2 { + return Err(format!("expected 2 bytes for i16, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::I32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for i32, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::I64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for i64, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::I128 => { + if bytes.len() != 16 { + return Err(format!("expected 16 bytes for i128, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::I256 => { + if bytes.len() != 32 { + return Err(format!("expected 32 bytes for i256, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::F32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for f32, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::F64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for f64, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::Bool => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for bool, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::Pubkey => { + if bytes.len() != 32 { + return Err(format!("expected 32 bytes for Pubkey, found {}", bytes.len())); + } + Ok(bytes.clone()) + } + IdlType::String => { + // Assume bytes are UTF-8 encoded string, encode as borsh string + let s = std::str::from_utf8(bytes) + .map_err(|e| format!("invalid UTF-8 for string: {}", e))?; + borsh::to_vec(&s).map_err(|e| format!("failed to encode string: {}", e)) + } + IdlType::Bytes => { + // Return raw bytes as-is + Ok(bytes.clone()) + } + IdlType::Vec(inner_type) => { + // Encode as vector - each element from inner type + match &**inner_type { + IdlType::U8 => { + // Vec - encode as borsh vector + borsh::to_vec(bytes).map_err(|e| format!("failed to encode Vec: {}", e)) + } + _ => { + // For other types, try to split bytes and encode each element + Err(format!( + "cannot convert raw bytes to Vec<{:?}>; bytes can only be directly converted to Vec", + inner_type + )) + } + } + } + IdlType::Array(inner_type, array_len) => { + let expected_len = match array_len { + IdlArrayLen::Value(len) => *len, + IdlArrayLen::Generic(_) => { + return Err(format!("cannot determine array length from generic")); + } + }; + + match &**inner_type { + IdlType::U8 => { + // [u8; N] - validate length and return bytes + if bytes.len() != expected_len { + return Err(format!( + "expected {} bytes for array, found {}", + expected_len, + bytes.len() + )); + } + Ok(bytes.clone()) + } + _ => { + // For other types, would need to know element size + Err(format!( + "cannot convert raw bytes to [{:?}; {}]; bytes can only be directly converted to [u8; N]", + inner_type, expected_len + )) + } + } + } + IdlType::Option(inner_type) => { + // If bytes are empty, encode as None + if bytes.is_empty() { + borsh::to_vec(&None::).map_err(|e| format!("failed to encode None: {}", e)) + } else { + // Otherwise encode as Some with inner bytes + let inner_encoded = borsh_encode_bytes_to_idl_type(bytes, inner_type, idl_types)?; + borsh::to_vec(&Some(inner_encoded)) + .map_err(|e| format!("failed to encode Option: {}", e)) + } + } + IdlType::Defined { name, .. } => { + // For defined types, we can't directly convert from bytes without knowing the structure + Err(format!( + "cannot convert raw bytes to defined type '{}'; use structured value instead", + name + )) + } + IdlType::Generic(name) => Err(format!( + "cannot convert raw bytes to generic type '{}'; type must be resolved first", + name + )), + t => Err(format!("IDL type {:?} is not yet supported for bytes encoding", t)), + } +} + +fn parse_generic_expected_type( + expected_type: &IdlType, + type_def_generics: &Vec, + generic_args: &Vec, +) -> Result { + let ty = if let IdlType::Generic(generic) = &expected_type { + let Some(generic_pos) = type_def_generics.iter().position(|g| match g { + IdlTypeDefGeneric::Type { name } => name.eq(generic), + IdlTypeDefGeneric::Const { name, .. } => name.eq(generic), + }) else { + return Err(format!("unable to find generic {} in idl", generic)); + }; + let generic = generic_args + .get(generic_pos) + .ok_or(format!("unable to find generic {} in idl", generic))?; + match generic { + IdlGenericArg::Type { ty } => ty, + IdlGenericArg::Const { value } => { + &IdlType::from_str(value).map_err(|e| format!("invalid generic type: {e}"))? + } + } + } else { + &expected_type + }; + Ok(ty.clone()) +} diff --git a/addons/svm/types/src/subgraph/idl/mod.rs b/addons/svm/types/src/subgraph/idl/mod.rs new file mode 100644 index 000000000..e2560142f --- /dev/null +++ b/addons/svm/types/src/subgraph/idl/mod.rs @@ -0,0 +1,26 @@ +use anchor_lang_idl::types::Idl as AnchorIdl; +use serde::{Deserialize, Serialize}; +use shank_idl::idl::Idl as ShankIdl; +use txtx_addon_kit::types::diagnostics::Diagnostic; + +pub mod anchor; +pub mod shank; + +/// Represents the kind of IDL format being used. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IdlKind { + /// Anchor IDL format (v0.30+) + Anchor(AnchorIdl), + /// Shank IDL format + Shank(ShankIdl), +} +impl IdlKind { + pub fn to_json_value(&self) -> Result { + match self { + IdlKind::Anchor(idl) => serde_json::to_value(idl) + .map_err(|e| diagnosed_error!("failed to serialize Anchor IDL: {}", e)), + IdlKind::Shank(idl) => serde_json::to_value(idl) + .map_err(|e| diagnosed_error!("failed to serialize Shank IDL: {}", e)), + } + } +} diff --git a/addons/svm/types/src/subgraph/idl/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs new file mode 100644 index 000000000..085f2b0f3 --- /dev/null +++ b/addons/svm/types/src/subgraph/idl/shank.rs @@ -0,0 +1,1546 @@ +//! Shank IDL codec functions for converting between Shank IDL types, txtx types, and bytes. + +use txtx_addon_kit::{ + indexmap::IndexMap, + types::types::{ObjectDefinition, ObjectProperty, ObjectType, Type, Value}, +}; + +use crate::{SvmValue, SVM_PUBKEY}; +use std::fmt::Display; + +pub use shank_idl::idl::IdlConst; +pub use shank_idl::idl_field::IdlField; +pub use shank_idl::idl_type::IdlType; +pub use shank_idl::idl_type_definition::{IdlTypeDefinition, IdlTypeDefinitionTy}; +pub use shank_idl::idl_variant::{EnumFields, IdlEnumVariant}; + +// ============================================================================ +// Helper function to extract types from shank_idl::idl::Idl +// ============================================================================ + +/// Extracts type definitions from a Shank IDL +pub fn extract_shank_types(idl: &shank_idl::idl::Idl) -> Vec { + let mut types = idl.types.clone(); + types.extend(idl.accounts.clone()); + types +} + +/// Extracts instruction argument type from a Shank IDL instruction +pub fn extract_shank_instruction_arg_type( + idl: &shank_idl::idl::Idl, + instruction_name: &str, + arg_index: usize, +) -> Result { + let instruction = idl + .instructions + .iter() + .find(|i| i.name == instruction_name) + .ok_or_else(|| format!("instruction '{}' not found", instruction_name))?; + + let arg = instruction.args.get(arg_index).ok_or_else(|| { + format!("argument {} not found in instruction '{}'", arg_index, instruction_name) + })?; + + Ok(arg.ty.clone()) +} + +// ============================================================================ +// Discriminator-based account detection +// ============================================================================ + +use std::collections::BTreeMap; + +/// Mapping from discriminator byte value to account type name +#[derive(Debug, Clone)] +pub struct DiscriminatorMapping { + /// The name of the enum type used as discriminator + pub enum_type_name: String, + /// Maps variant index (discriminator byte) to account name + pub variant_to_account: BTreeMap, +} + +/// Find the account_discriminator constant in a Shank IDL. +/// Returns the enum type name it points to, if found. +pub fn find_account_discriminator_const(idl: &shank_idl::idl::Idl) -> Option { + for constant in &idl.constants { + if constant.name == "account_discriminator" { + // Strip the quotes if encoded like "\"AccountType\"" + let value = constant.value.trim(); + return Some(if value.starts_with('"') && value.ends_with('"') { + value[1..value.len() - 1].to_string() + } else { + value.to_string() + }); + } + } + None +} + +/// Build a mapping from discriminator values to account names. +/// This looks for an enum type with the given name and maps its variant indices +/// to account names that match the variant names. +pub fn build_discriminator_mapping( + idl: &shank_idl::idl::Idl, + enum_type_name: &str, +) -> Option { + let types = extract_shank_types(idl); + + // Find the enum type + let enum_type = types.iter().find(|t| t.name == enum_type_name)?; + + // Make sure it's an enum + let variants = match &enum_type.ty { + IdlTypeDefinitionTy::Enum { variants } => variants, + _ => return None, + }; + + // Build a set of account names for quick lookup + let account_names: std::collections::HashSet = + idl.accounts.iter().map(|a| a.name.clone()).collect(); + + // Map variant index to account name (if the account exists) + let mut variant_to_account = BTreeMap::new(); + for (index, variant) in variants.iter().enumerate() { + // Check if there's an account with the same name as the variant + if account_names.contains(&variant.name) { + variant_to_account.insert(index as u8, variant.name.clone()); + } + } + + if variant_to_account.is_empty() { + return None; + } + + Some(DiscriminatorMapping { enum_type_name: enum_type_name.to_string(), variant_to_account }) +} + +/// Find the byte offset of a discriminator field within a struct's fields. +/// Returns the offset in bytes where the discriminator enum field is located. +fn find_discriminator_field_offset( + fields: &[IdlField], + enum_type_name: &str, + idl_types: &[IdlTypeDefinition], +) -> Result { + let mut offset = 0; + for field in fields { + // Check if this field is the discriminator enum type + if let IdlType::Defined(name) = &field.ty { + if name == enum_type_name { + return Ok(offset); + } + } + // Add this field's size to the offset + let field_size = get_shank_type_size_with_types(&field.ty, idl_types)?; + offset += field_size; + } + Err(format!("discriminator field of type '{}' not found in struct", enum_type_name)) +} + +/// Find the account type by reading the discriminator byte from the data. +/// +/// This function locates the discriminator field within each candidate account struct +/// and reads the discriminator byte at the correct offset. This handles cases where +/// the discriminator is not the first field (e.g., `struct Pool { authority: Pubkey, account_type: AccountType }`). +/// +/// Returns the account name if a matching discriminator is found. +pub fn find_account_by_discriminator(idl: &shank_idl::idl::Idl, data: &[u8]) -> Option { + if data.is_empty() { + return None; + } + + // Get the discriminator enum name from the IDL constant + let enum_type_name = find_account_discriminator_const(idl)?; + + // Get all type definitions (needed for offset calculation) + let types = extract_shank_types(idl); + + // Build the discriminator mapping (variant index -> account name) + let mapping = build_discriminator_mapping(idl, &enum_type_name)?; + + // Build reverse mapping (account name -> expected variant index) + let account_to_variant: BTreeMap = + mapping.variant_to_account.iter().map(|(idx, name)| (name.clone(), *idx)).collect(); + + // Try each account that participates in the discriminator pattern + for account in &idl.accounts { + // Check if this account is in our discriminator mapping + let Some(&expected_variant) = account_to_variant.get(&account.name) else { + continue; + }; + + // Find this account's type definition to get its fields + let Some(account_type) = types.iter().find(|t| t.name == account.name) else { + continue; + }; + + // Get the struct fields + let fields = match &account_type.ty { + IdlTypeDefinitionTy::Struct { fields } => fields, + _ => continue, + }; + + // Calculate the offset of the discriminator field + let Ok(offset) = find_discriminator_field_offset(fields, &enum_type_name, &types) else { + continue; + }; + + // Check if we have enough data to read at this offset + if offset >= data.len() { + continue; + } + + // Read the discriminator byte at the calculated offset + let discriminator = data[offset]; + + // Check if this matches the expected variant for this account + if discriminator == expected_variant { + return Some(account.name.clone()); + } + } + + None +} + +// ============================================================================ +// Type conversion functions +// ============================================================================ + +/// Converts a Shank IDL type to a txtx Type. +pub fn shank_idl_type_to_txtx_type( + idl_type: &IdlType, + idl_types: &[IdlTypeDefinition], + _idl_constants: &[IdlConst], +) -> Result { + let res = match idl_type { + IdlType::Bool => Type::bool(), + IdlType::U8 => Type::addon(crate::SVM_U8), + IdlType::U16 => Type::addon(crate::SVM_U16), + IdlType::U32 => Type::addon(crate::SVM_U32), + IdlType::U64 => Type::addon(crate::SVM_U64), + IdlType::U128 => Type::addon(crate::SVM_U128), + IdlType::I8 => Type::addon(crate::SVM_I8), + IdlType::I16 => Type::addon(crate::SVM_I16), + IdlType::I32 => Type::addon(crate::SVM_I32), + IdlType::I64 => Type::addon(crate::SVM_I64), + IdlType::I128 => Type::addon(crate::SVM_I128), + IdlType::Bytes => Type::buffer(), + IdlType::String => Type::string(), + IdlType::PublicKey => Type::addon(SVM_PUBKEY), + IdlType::Option(inner) => { + Type::typed_null(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) + } + IdlType::FixedSizeOption { inner, .. } => { + Type::typed_null(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) + } + IdlType::Vec(inner) => { + Type::array(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) + } + IdlType::Array(inner, _) => { + Type::array(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) + } + IdlType::Tuple(types) => { + let mut props = vec![]; + for (i, ty) in types.iter().enumerate() { + let inner_type = shank_idl_type_to_txtx_type(ty, idl_types, _idl_constants)?; + props.push(ObjectProperty { + documentation: "".into(), + typing: inner_type, + optional: false, + tainting: false, + name: format!("field_{}", i), + internal: false, + }); + } + Type::object(ObjectDefinition::tuple(props)) + } + IdlType::Defined(name) => { + let Some(matching_type) = idl_types.iter().find(|t| t.name == *name) else { + return Err(format!("unable to find defined type '{}'", name)); + }; + get_expected_type_from_shank_idl_type_def_ty( + &matching_type.ty, + idl_types, + _idl_constants, + )? + } + IdlType::HashMap(_, _) => { + // HashMap is parsed as an object with string keys (key.to_string()) + // Use arbitrary map type since we can't express Map in txtx types + Type::map(ObjectDefinition::arbitrary()) + } + IdlType::BTreeMap(_, _) => { + // BTreeMap is parsed as an object with string keys (key.to_string()) + // Use arbitrary map type since we can't express Map in txtx types + Type::map(ObjectDefinition::arbitrary()) + } + IdlType::HashSet(inner) => { + Type::array(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) + } + IdlType::BTreeSet(inner) => { + Type::array(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) + } + }; + Ok(res) +} + +/// Converts a Shank IDL type definition to a txtx Type. +pub fn get_expected_type_from_shank_idl_type_def_ty( + idl_type_def_ty: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], + idl_constants: &[IdlConst], +) -> Result { + let ty = match idl_type_def_ty { + IdlTypeDefinitionTy::Struct { fields } => { + let mut props = vec![]; + for field in fields { + let field_type = shank_idl_type_to_txtx_type(&field.ty, idl_types, idl_constants) + .map_err(|e| { + format!("could not determine expected type for field '{}': {e}", field.name) + })?; + props.push(ObjectProperty { + documentation: "".into(), + typing: field_type, + optional: false, + tainting: false, + name: field.name.clone(), + internal: false, + }); + } + Type::object(ObjectDefinition::strict(props)) + } + IdlTypeDefinitionTy::Enum { variants } => { + let mut props = vec![]; + for variant in variants { + let variant_type = if let Some(ref fields) = variant.fields { + get_expected_type_from_shank_enum_fields(fields, idl_types, idl_constants)? + } else { + Type::null() + }; + props.push(ObjectProperty { + documentation: "".into(), + typing: variant_type, + optional: false, + tainting: false, + name: variant.name.clone(), + internal: false, + }); + } + Type::object(ObjectDefinition::enum_type(props)) + } + }; + Ok(ty) +} + +fn get_expected_type_from_shank_enum_fields( + fields: &EnumFields, + idl_types: &[IdlTypeDefinition], + idl_constants: &[IdlConst], +) -> Result { + match fields { + EnumFields::Named(idl_fields) => { + let mut props = vec![]; + for field in idl_fields { + let field_type = shank_idl_type_to_txtx_type(&field.ty, idl_types, idl_constants)?; + props.push(ObjectProperty { + documentation: "".into(), + typing: field_type, + optional: false, + tainting: false, + name: field.name.clone(), + internal: false, + }); + } + Ok(Type::object(ObjectDefinition::strict(props))) + } + EnumFields::Tuple(types) => { + let mut props = vec![]; + for (i, ty) in types.iter().enumerate() { + let inner_type = shank_idl_type_to_txtx_type(ty, idl_types, idl_constants)?; + props.push(ObjectProperty { + documentation: "".into(), + typing: inner_type, + optional: false, + tainting: false, + name: format!("field_{}", i), + internal: false, + }); + } + Ok(Type::object(ObjectDefinition::tuple(props))) + } + } +} + +// ============================================================================ +// Byte parsing functions (bytes -> Value) +// ============================================================================ + +/// Parses bytes to a Value using a Shank IDL type definition, consuming all bytes. +pub fn parse_bytes_to_value_with_shank_idl_type_def_ty( + data: &[u8], + expected_type: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], +) -> Result { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes( + data, + expected_type, + idl_types, + )?; + if !rest.is_empty() && rest.iter().any(|&byte| byte != 0) { + return Err(format!( + "expected no leftover bytes after parsing type, but found {} bytes of non-zero data", + rest.len() + )); + } + Ok(value) +} + +/// Parses bytes to a Value using a Shank IDL type definition, returning leftover bytes. +pub fn parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes<'a>( + data: &'a [u8], + expected_type: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], +) -> Result<(Value, &'a [u8]), String> { + match expected_type { + IdlTypeDefinitionTy::Struct { fields } => { + parse_bytes_to_shank_struct_with_leftover_bytes(data, fields, idl_types) + } + IdlTypeDefinitionTy::Enum { variants } => { + let (variant, rest) = + data.split_at_checked(1).ok_or("not enough bytes to decode enum variant index")?; + let variant_index = variant[0] as usize; + if variant_index >= variants.len() { + return Err(format!( + "invalid enum variant index: {} for enum with {} variants", + variant_index, + variants.len() + )); + } + let variant = &variants[variant_index]; + let (value, rest) = + parse_bytes_to_shank_enum_variant_with_leftover_bytes(rest, variant, idl_types)?; + Ok((ObjectType::from([(&variant.name, value)]).to_value(), rest)) + } + } +} + +fn parse_bytes_to_shank_struct_with_leftover_bytes<'a>( + data: &'a [u8], + fields: &[IdlField], + idl_types: &[IdlTypeDefinition], +) -> Result<(Value, &'a [u8]), String> { + let mut map = IndexMap::new(); + let mut remaining_data = data; + for field in fields { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &field.ty, + idl_types, + )?; + remaining_data = rest; + map.insert(field.name.clone(), value); + } + Ok((ObjectType::from_map(map).to_value(), remaining_data)) +} + +fn parse_bytes_to_shank_enum_variant_with_leftover_bytes<'a>( + data: &'a [u8], + variant: &IdlEnumVariant, + idl_types: &[IdlTypeDefinition], +) -> Result<(Value, &'a [u8]), String> { + match &variant.fields { + Some(EnumFields::Named(fields)) => { + let mut map = IndexMap::new(); + let mut remaining_data = data; + for field in fields { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &field.ty, + idl_types, + )?; + remaining_data = rest; + map.insert(field.name.clone(), value); + } + Ok((ObjectType::from_map(map).to_value(), remaining_data)) + } + Some(EnumFields::Tuple(types)) => { + let mut values = Vec::with_capacity(types.len()); + let mut remaining_data = data; + for ty in types { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + ty, + idl_types, + )?; + remaining_data = rest; + values.push(value); + } + Ok((Value::array(values), remaining_data)) + } + None => Ok((Value::null(), data)), + } +} + +/// Parses bytes to a Value using a Shank IDL type, returning leftover bytes. +pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( + data: &'a [u8], + expected_type: &IdlType, + idl_types: &[IdlTypeDefinition], +) -> Result<(Value, &'a [u8]), String> { + let err = |ty: &str, e: &dyn Display| format!("unable to decode {ty}: {e}"); + let bytes_err = |ty: &str| err(ty, &"not enough bytes"); + + match expected_type { + IdlType::U8 => { + let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("u8"))?; + Ok((SvmValue::u8(v[0]), rest)) + } + IdlType::U16 => { + let (v, rest) = data.split_at_checked(2).ok_or(bytes_err("u16"))?; + Ok(( + SvmValue::u16(u16::from_le_bytes( + <[u8; 2]>::try_from(v).map_err(|e| err("u16", &e))?, + )), + rest, + )) + } + IdlType::U32 => { + let (v, rest) = data.split_at_checked(4).ok_or(bytes_err("u32"))?; + Ok(( + SvmValue::u32(u32::from_le_bytes( + <[u8; 4]>::try_from(v).map_err(|e| err("u32", &e))?, + )), + rest, + )) + } + IdlType::U64 => { + let (v, rest) = data.split_at_checked(8).ok_or(bytes_err("u64"))?; + Ok(( + SvmValue::u64(u64::from_le_bytes( + <[u8; 8]>::try_from(v).map_err(|e| err("u64", &e))?, + )), + rest, + )) + } + IdlType::U128 => { + let (v, rest) = data.split_at_checked(16).ok_or(bytes_err("u128"))?; + Ok(( + SvmValue::u128(u128::from_le_bytes( + <[u8; 16]>::try_from(v).map_err(|e| err("u128", &e))?, + )), + rest, + )) + } + IdlType::I8 => { + let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("i8"))?; + Ok((SvmValue::i8(i8::from_le_bytes([v[0]])), rest)) + } + IdlType::I16 => { + let (v, rest) = data.split_at_checked(2).ok_or(bytes_err("i16"))?; + Ok(( + SvmValue::i16(i16::from_le_bytes( + <[u8; 2]>::try_from(v).map_err(|e| err("i16", &e))?, + )), + rest, + )) + } + IdlType::I32 => { + let (v, rest) = data.split_at_checked(4).ok_or(bytes_err("i32"))?; + Ok(( + SvmValue::i32(i32::from_le_bytes( + <[u8; 4]>::try_from(v).map_err(|e| err("i32", &e))?, + )), + rest, + )) + } + IdlType::I64 => { + let (v, rest) = data.split_at_checked(8).ok_or(bytes_err("i64"))?; + Ok(( + SvmValue::i64(i64::from_le_bytes( + <[u8; 8]>::try_from(v).map_err(|e| err("i64", &e))?, + )), + rest, + )) + } + IdlType::I128 => { + let (v, rest) = data.split_at_checked(16).ok_or(bytes_err("i128"))?; + Ok(( + SvmValue::i128(i128::from_le_bytes( + <[u8; 16]>::try_from(v).map_err(|e| err("i128", &e))?, + )), + rest, + )) + } + IdlType::Bool => { + let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("bool"))?; + Ok((Value::bool(v[0] != 0), rest)) + } + IdlType::PublicKey => { + let (v, rest) = data.split_at_checked(32).ok_or(bytes_err("pubkey"))?; + Ok((SvmValue::pubkey(v.to_vec()), rest)) + } + IdlType::String => { + let (string_len, rest) = data.split_at_checked(4).ok_or(bytes_err("string length"))?; + let string_len = u32::from_le_bytes( + <[u8; 4]>::try_from(string_len).map_err(|e| err("string length", &e))?, + ) as usize; + let (string_bytes, rest) = + rest.split_at_checked(string_len).ok_or(bytes_err("string"))?; + let string_value = String::from_utf8_lossy(string_bytes).to_string(); + Ok((Value::string(string_value), rest)) + } + IdlType::Bytes => { + let (vec_len, rest) = data.split_at_checked(4).ok_or(bytes_err("bytes length"))?; + let vec_len = u32::from_le_bytes( + <[u8; 4]>::try_from(vec_len).map_err(|e| err("bytes length", &e))?, + ) as usize; + let (vec_bytes, rest) = rest.split_at_checked(vec_len).ok_or(bytes_err("bytes"))?; + Ok((Value::buffer(vec_bytes.to_vec()), rest)) + } + IdlType::Option(inner) => { + let (is_some, rest) = data.split_at_checked(1).ok_or(bytes_err("option"))?; + if is_some[0] == 0 { + Ok((Value::null(), rest)) + } else { + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + rest, inner, idl_types, + ) + } + } + IdlType::FixedSizeOption { inner, sentinel } => { + let inner_size = get_shank_type_size(inner)?; + let (inner_bytes, rest) = + data.split_at_checked(inner_size).ok_or(bytes_err("fixed_size_option"))?; + + if let Some(ref sentinel_bytes) = sentinel { + if inner_bytes == sentinel_bytes.as_slice() { + return Ok((Value::null(), rest)); + } + } + + let (value, _) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + inner_bytes, + inner, + idl_types, + )?; + Ok((value, rest)) + } + IdlType::Vec(inner) => { + let (vec_len, rest) = data.split_at_checked(4).ok_or(bytes_err("vec length"))?; + let vec_len = u32::from_le_bytes( + <[u8; 4]>::try_from(vec_len).map_err(|e| err("vec length", &e))?, + ) as usize; + + let mut vec_values = Vec::with_capacity(vec_len); + let mut remaining_data = rest; + + for _ in 0..vec_len { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + inner, + idl_types, + )?; + vec_values.push(value); + remaining_data = rest; + } + + Ok((Value::array(vec_values), remaining_data)) + } + IdlType::Array(inner, len) => { + let mut vec_values = Vec::with_capacity(*len); + let mut remaining_data = data; + for _ in 0..*len { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + inner, + idl_types, + )?; + vec_values.push(value); + remaining_data = rest; + } + Ok((Value::array(vec_values), remaining_data)) + } + IdlType::Tuple(types) => { + let mut values = Vec::with_capacity(types.len()); + let mut remaining_data = data; + for ty in types { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + ty, + idl_types, + )?; + values.push(value); + remaining_data = rest; + } + Ok((Value::array(values), remaining_data)) + } + IdlType::Defined(name) => { + let matching_type = idl_types + .iter() + .find(|t| t.name == *name) + .ok_or(err(name, &"not found in IDL types"))?; + + parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes( + data, + &matching_type.ty, + idl_types, + ) + } + IdlType::HashMap(key_type, value_type) => { + let (map_len, rest) = data.split_at_checked(4).ok_or(bytes_err("hashmap length"))?; + let map_len = u32::from_le_bytes( + <[u8; 4]>::try_from(map_len).map_err(|e| err("hashmap length", &e))?, + ) as usize; + + let mut result_map = IndexMap::new(); + let mut remaining_data = rest; + + for _ in 0..map_len { + let (key, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + key_type, + idl_types, + )?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + rest, + value_type, + idl_types, + )?; + remaining_data = rest; + result_map.insert(key.to_string(), value); + } + + Ok((ObjectType::from_map(result_map).to_value(), remaining_data)) + } + IdlType::BTreeMap(key_type, value_type) => { + let (map_len, rest) = data.split_at_checked(4).ok_or(bytes_err("btreemap length"))?; + let map_len = u32::from_le_bytes( + <[u8; 4]>::try_from(map_len).map_err(|e| err("btreemap length", &e))?, + ) as usize; + + let mut result_map = IndexMap::new(); + let mut remaining_data = rest; + + for _ in 0..map_len { + let (key, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + key_type, + idl_types, + )?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + rest, + value_type, + idl_types, + )?; + remaining_data = rest; + result_map.insert(key.to_string(), value); + } + + Ok((ObjectType::from_map(result_map).to_value(), remaining_data)) + } + IdlType::HashSet(inner) => { + let (set_len, rest) = data.split_at_checked(4).ok_or(bytes_err("hashset length"))?; + let set_len = u32::from_le_bytes( + <[u8; 4]>::try_from(set_len).map_err(|e| err("hashset length", &e))?, + ) as usize; + + let mut values = Vec::with_capacity(set_len); + let mut remaining_data = rest; + + for _ in 0..set_len { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + inner, + idl_types, + )?; + values.push(value); + remaining_data = rest; + } + + Ok((Value::array(values), remaining_data)) + } + IdlType::BTreeSet(inner) => { + let (set_len, rest) = data.split_at_checked(4).ok_or(bytes_err("btreeset length"))?; + let set_len = u32::from_le_bytes( + <[u8; 4]>::try_from(set_len).map_err(|e| err("btreeset length", &e))?, + ) as usize; + + let mut values = Vec::with_capacity(set_len); + let mut remaining_data = rest; + + for _ in 0..set_len { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + inner, + idl_types, + )?; + values.push(value); + remaining_data = rest; + } + + Ok((Value::array(values), remaining_data)) + } + } +} + +/// Returns the size in bytes of a Shank IDL type (for fixed-size types). +fn get_shank_type_size(ty: &IdlType) -> Result { + get_shank_type_size_with_types(ty, &[]) +} + +/// Returns the size in bytes of a Shank IDL type, with access to type definitions +/// for resolving defined types. +fn get_shank_type_size_with_types( + ty: &IdlType, + idl_types: &[IdlTypeDefinition], +) -> Result { + match ty { + IdlType::Bool | IdlType::U8 | IdlType::I8 => Ok(1), + IdlType::U16 | IdlType::I16 => Ok(2), + IdlType::U32 | IdlType::I32 => Ok(4), + IdlType::U64 | IdlType::I64 => Ok(8), + IdlType::U128 | IdlType::I128 => Ok(16), + IdlType::PublicKey => Ok(32), + IdlType::Array(inner, size) => { + let inner_size = get_shank_type_size_with_types(inner, idl_types)?; + Ok(inner_size * size) + } + IdlType::Defined(name) => { + let type_def = idl_types + .iter() + .find(|t| t.name == *name) + .ok_or_else(|| format!("cannot determine fixed size for type {:?}", ty))?; + match &type_def.ty { + IdlTypeDefinitionTy::Enum { variants } => { + // Simple enum without data fields = 1 byte discriminator + if variants.iter().all(|v| v.fields.is_none()) { + Ok(1) + } else { + Err(format!( + "cannot determine fixed size for enum with data: {}", + name + )) + } + } + IdlTypeDefinitionTy::Struct { fields } => { + let mut size = 0; + for field in fields { + size += get_shank_type_size_with_types(&field.ty, idl_types)?; + } + Ok(size) + } + } + } + _ => Err(format!("cannot determine fixed size for type {:?}", ty)), + } +} + +// ============================================================================ +// Encoding functions (Value -> bytes) +// ============================================================================ + +/// Encodes a txtx Value to bytes using a Shank IDL type. +pub fn borsh_encode_value_to_shank_idl_type( + value: &Value, + idl_type: &IdlType, + idl_types: &[IdlTypeDefinition], +) -> Result, String> { + let mismatch_err = |expected: &str| { + format!("invalid value for idl type: expected {}, found {:?}", expected, value.get_type()) + }; + let encode_err = |expected: &str, e: &dyn Display| { + format!("unable to encode value ({}) as borsh {}: {}", value.to_string(), expected, e) + }; + + // Handle Buffer and Addon values by encoding their bytes directly + match value { + Value::Buffer(bytes) => { + return borsh_encode_bytes_to_shank_idl_type(bytes, idl_type, idl_types) + } + Value::Addon(addon_data) => { + return borsh_encode_bytes_to_shank_idl_type(&addon_data.bytes, idl_type, idl_types) + } + _ => {} + } + + match idl_type { + IdlType::Bool => value + .as_bool() + .and_then(|b| Some(borsh::to_vec(&b).map_err(|e| encode_err("bool", &e)))) + .transpose()? + .ok_or(mismatch_err("bool")), + IdlType::U8 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u8", &e)), + IdlType::U16 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u16", &e)), + IdlType::U32 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u32", &e)), + IdlType::U64 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u64", &e)), + IdlType::U128 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("u128", &e)), + IdlType::I8 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i8", &e)), + IdlType::I16 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i16", &e)), + IdlType::I32 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i32", &e)), + IdlType::I64 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i64", &e)), + IdlType::I128 => SvmValue::to_number::(value) + .and_then(|num| borsh::to_vec(&num).map_err(|e| e.to_string())) + .map_err(|e| encode_err("i128", &e)), + IdlType::Bytes => Ok(value.to_be_bytes().clone()), + IdlType::String => value + .as_string() + .and_then(|s| Some(borsh::to_vec(&s).map_err(|e| encode_err("string", &e)))) + .transpose()? + .ok_or(mismatch_err("string")), + IdlType::PublicKey => SvmValue::to_pubkey(value) + .map_err(|_| mismatch_err("pubkey")) + .map(|p| borsh::to_vec(&p))? + .map_err(|e| encode_err("pubkey", &e)), + IdlType::Option(inner) => { + if value.as_null().is_some() { + Ok(vec![0u8]) // None discriminator + } else { + let encoded_inner = + borsh_encode_value_to_shank_idl_type(value, inner, idl_types)?; + let mut result = vec![1u8]; // Some discriminator + result.extend(encoded_inner); + Ok(result) + } + } + IdlType::FixedSizeOption { inner, sentinel } => { + if value.as_null().is_some() { + if let Some(ref sentinel_bytes) = sentinel { + Ok(sentinel_bytes.clone()) + } else { + let size = get_shank_type_size(inner)?; + Ok(vec![0u8; size]) + } + } else { + borsh_encode_value_to_shank_idl_type(value, inner, idl_types) + } + } + IdlType::Vec(inner) => match value { + Value::String(_) => { + let bytes = value.get_buffer_bytes_result().map_err(|_| mismatch_err("vec"))?; + borsh_encode_bytes_to_shank_idl_type(&bytes, inner, idl_types) + } + Value::Array(arr) => { + let mut result = (arr.len() as u32).to_le_bytes().to_vec(); + for v in arr.iter() { + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + _ => Err(mismatch_err("vec")), + }, + IdlType::Array(inner, expected_len) => { + let array = value.as_array().ok_or(mismatch_err("array"))?; + if array.len() != *expected_len { + return Err(format!( + "invalid value for idl type: expected array length of {}, found {}", + expected_len, + array.len() + )); + } + let mut result = vec![]; + for v in array.iter() { + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + IdlType::Tuple(types) => { + let array = value.as_array().ok_or(mismatch_err("tuple"))?; + if array.len() != types.len() { + return Err(format!( + "invalid value for idl type: expected tuple length of {}, found {}", + types.len(), + array.len() + )); + } + let mut result = vec![]; + for (v, ty) in array.iter().zip(types.iter()) { + let encoded = borsh_encode_value_to_shank_idl_type(v, ty, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + IdlType::Defined(name) => { + let typing = idl_types.iter().find(|t| t.name == *name).ok_or_else(|| { + format!("unable to find type definition for {} in idl", name) + })?; + + borsh_encode_value_to_shank_idl_type_def_ty(value, &typing.ty, idl_types) + } + IdlType::HashMap(key_type, value_type) => { + let obj = value.as_object().ok_or(mismatch_err("hashmap"))?; + let mut result = (obj.len() as u32).to_le_bytes().to_vec(); + for (k, v) in obj.iter() { + let key_value = Value::string(k.clone()); + let encoded_key = + borsh_encode_value_to_shank_idl_type(&key_value, key_type, idl_types)?; + let encoded_value = + borsh_encode_value_to_shank_idl_type(v, value_type, idl_types)?; + result.extend(encoded_key); + result.extend(encoded_value); + } + Ok(result) + } + IdlType::BTreeMap(key_type, value_type) => { + let obj = value.as_object().ok_or(mismatch_err("btreemap"))?; + let mut result = (obj.len() as u32).to_le_bytes().to_vec(); + for (k, v) in obj.iter() { + let key_value = Value::string(k.clone()); + let encoded_key = + borsh_encode_value_to_shank_idl_type(&key_value, key_type, idl_types)?; + let encoded_value = + borsh_encode_value_to_shank_idl_type(v, value_type, idl_types)?; + result.extend(encoded_key); + result.extend(encoded_value); + } + Ok(result) + } + IdlType::HashSet(inner) => { + let array = value.as_array().ok_or(mismatch_err("hashset"))?; + let mut result = (array.len() as u32).to_le_bytes().to_vec(); + for v in array.iter() { + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + IdlType::BTreeSet(inner) => { + let array = value.as_array().ok_or(mismatch_err("btreeset"))?; + let mut result = (array.len() as u32).to_le_bytes().to_vec(); + for v in array.iter() { + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + } +} + +fn borsh_encode_value_to_shank_idl_type_def_ty( + value: &Value, + idl_type_def_ty: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], +) -> Result, String> { + match idl_type_def_ty { + IdlTypeDefinitionTy::Struct { fields } => { + let mut encoded_fields = vec![]; + let user_values_map = value.as_object().ok_or_else(|| { + format!("expected object for struct, found {:?}", value.get_type()) + })?; + + for field in fields { + let user_value = user_values_map + .get(&field.name) + .ok_or_else(|| format!("missing field '{}' in object", field.name))?; + let encoded = + borsh_encode_value_to_shank_idl_type(user_value, &field.ty, idl_types) + .map_err(|e| format!("failed to encode field '{}': {}", field.name, e))?; + encoded_fields.extend(encoded); + } + Ok(encoded_fields) + } + IdlTypeDefinitionTy::Enum { variants } => { + let enum_value = value + .as_object() + .ok_or_else(|| format!("expected object for enum, found {:?}", value.get_type()))?; + + // Handle two enum formats: + // 1. {"variant": "VariantName", "value": ...} (explicit format) + // 2. {"VariantName": ...} (decoded format from parse_bytes_to_value) + let (enum_variant_name, enum_variant_value) = if let Some(variant_field) = + enum_value.get("variant") + { + let variant_name = variant_field + .as_string() + .ok_or_else(|| "expected variant field to be a string".to_string())?; + let variant_value = + enum_value.get("value").ok_or_else(|| "missing 'value' field".to_string())?; + (variant_name.to_string(), variant_value.clone()) + } else { + if enum_value.len() != 1 { + return Err("expected exactly one field (the variant name)".to_string()); + } + let (variant_name, variant_value) = + enum_value.iter().next().ok_or_else(|| "empty object".to_string())?; + (variant_name.clone(), variant_value.clone()) + }; + + let (variant_index, expected_variant) = variants + .iter() + .enumerate() + .find(|(_, v)| v.name == enum_variant_name) + .ok_or_else(|| format!("unknown variant {}", enum_variant_name))?; + + let mut encoded = vec![variant_index as u8]; + + match &expected_variant.fields { + Some(EnumFields::Named(fields)) => { + let user_values_map = enum_variant_value + .as_object() + .ok_or_else(|| format!("expected object for enum variant fields"))?; + for field in fields { + let user_value = user_values_map.get(&field.name).ok_or_else(|| { + format!("missing field '{}' in enum variant", field.name) + })?; + let field_encoded = + borsh_encode_value_to_shank_idl_type(user_value, &field.ty, idl_types)?; + encoded.extend(field_encoded); + } + } + Some(EnumFields::Tuple(types)) => { + let values = enum_variant_value + .as_array() + .ok_or_else(|| format!("expected array for enum tuple variant"))?; + if values.len() != types.len() { + return Err(format!( + "expected {} tuple fields, found {}", + types.len(), + values.len() + )); + } + for (v, ty) in values.iter().zip(types.iter()) { + let field_encoded = borsh_encode_value_to_shank_idl_type(v, ty, idl_types)?; + encoded.extend(field_encoded); + } + } + None => { + // Unit variant, no additional data + } + } + + Ok(encoded) + } + } +} + +fn borsh_encode_bytes_to_shank_idl_type( + bytes: &[u8], + idl_type: &IdlType, + _idl_types: &[IdlTypeDefinition], +) -> Result, String> { + match idl_type { + IdlType::U8 => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for u8, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::U16 => { + if bytes.len() != 2 { + return Err(format!("expected 2 bytes for u16, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::U32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for u32, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::U64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for u64, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::U128 => { + if bytes.len() != 16 { + return Err(format!("expected 16 bytes for u128, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::I8 => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for i8, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::I16 => { + if bytes.len() != 2 { + return Err(format!("expected 2 bytes for i16, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::I32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for i32, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::I64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for i64, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::I128 => { + if bytes.len() != 16 { + return Err(format!("expected 16 bytes for i128, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::Bool => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for bool, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::PublicKey => { + if bytes.len() != 32 { + return Err(format!("expected 32 bytes for Pubkey, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + IdlType::String => { + let s = std::str::from_utf8(bytes) + .map_err(|e| format!("invalid UTF-8 for string: {}", e))?; + borsh::to_vec(&s).map_err(|e| format!("failed to encode string: {}", e)) + } + IdlType::Bytes => Ok(bytes.to_vec()), + IdlType::Vec(inner) => { + match &**inner { + IdlType::U8 => { + borsh::to_vec(bytes).map_err(|e| format!("failed to encode Vec: {}", e)) + } + _ => Err(format!( + "cannot convert raw bytes to Vec<{:?}>; bytes can only be directly converted to Vec", + inner + )) + } + } + IdlType::Array(inner, expected_len) => { + match &**inner { + IdlType::U8 => { + if bytes.len() != *expected_len { + return Err(format!( + "expected {} bytes for array, found {}", + expected_len, + bytes.len() + )); + } + Ok(bytes.to_vec()) + } + _ => Err(format!( + "cannot convert raw bytes to [{:?}; {}]", + inner, expected_len + )) + } + } + _ => Err(format!("cannot convert raw bytes to {:?}", idl_type)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_idl_with_discriminator() -> shank_idl::idl::Idl { + let idl_json = r#"{ + "name": "test_program", + "version": "0.1.0", + "metadata": { + "origin": "shank", + "address": "TestProgram11111111111111111111111111111111" + }, + "constants": [ + { + "name": "account_discriminator", + "type": "string", + "value": "\"AccountType\"" + } + ], + "types": [ + { + "name": "AccountType", + "type": { + "kind": "enum", + "variants": [ + { "name": "Account1" }, + { "name": "Account2" }, + { "name": "Account3" } + ] + } + }, + { + "name": "Account1", + "type": { + "kind": "struct", + "fields": [ + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "data", "type": "u64" } + ] + } + }, + { + "name": "Account2", + "type": { + "kind": "struct", + "fields": [ + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "value", "type": "u32" } + ] + } + } + ], + "accounts": [ + { + "name": "Account1", + "type": { + "kind": "struct", + "fields": [ + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "data", "type": "u64" } + ] + } + }, + { + "name": "Account2", + "type": { + "kind": "struct", + "fields": [ + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "value", "type": "u32" } + ] + } + } + ], + "instructions": [] + }"#; + + serde_json::from_str(idl_json).expect("Failed to parse test IDL") + } + + fn create_test_idl_without_discriminator() -> shank_idl::idl::Idl { + let idl_json = r#"{ + "name": "test_program", + "version": "0.1.0", + "metadata": { + "origin": "shank", + "address": "TestProgram11111111111111111111111111111111" + }, + "constants": [], + "types": [ + { + "name": "SimpleAccount", + "type": { + "kind": "struct", + "fields": [ + { "name": "data", "type": "u64" } + ] + } + } + ], + "accounts": [ + { + "name": "SimpleAccount", + "type": { + "kind": "struct", + "fields": [ + { "name": "data", "type": "u64" } + ] + } + } + ], + "instructions": [] + }"#; + + serde_json::from_str(idl_json).expect("Failed to parse test IDL") + } + + #[test] + fn test_find_account_discriminator_const() { + let idl = create_test_idl_with_discriminator(); + let result = find_account_discriminator_const(&idl); + assert_eq!(result, Some("AccountType".to_string())); + } + + #[test] + fn test_find_account_discriminator_const_not_found() { + let idl = create_test_idl_without_discriminator(); + let result = find_account_discriminator_const(&idl); + assert_eq!(result, None); + } + + #[test] + fn test_build_discriminator_mapping() { + let idl = create_test_idl_with_discriminator(); + let mapping = build_discriminator_mapping(&idl, "AccountType"); + + assert!(mapping.is_some()); + let mapping = mapping.unwrap(); + + assert_eq!(mapping.enum_type_name, "AccountType"); + // Variant 0 = Account1, Variant 1 = Account2 + // Account3 is in the enum but has no matching account, so it's not in the mapping + assert_eq!(mapping.variant_to_account.get(&0), Some(&"Account1".to_string())); + assert_eq!(mapping.variant_to_account.get(&1), Some(&"Account2".to_string())); + assert_eq!(mapping.variant_to_account.get(&2), None); // No Account3 account + } + + #[test] + fn test_find_account_by_discriminator() { + let idl = create_test_idl_with_discriminator(); + + // Data with discriminator 0 (Account1): [0, <8 bytes of u64>] + let data_account1 = [0u8, 42, 0, 0, 0, 0, 0, 0, 0]; // discriminator 0, data = 42 + let result = find_account_by_discriminator(&idl, &data_account1); + assert_eq!(result, Some("Account1".to_string())); + + // Data with discriminator 1 (Account2): [1, <4 bytes of u32>] + let data_account2 = [1u8, 100, 0, 0, 0]; // discriminator 1, value = 100 + let result = find_account_by_discriminator(&idl, &data_account2); + assert_eq!(result, Some("Account2".to_string())); + + // Data with discriminator 2 (Account3) - no matching account + let data_account3 = [2u8, 0, 0, 0, 0]; + let result = find_account_by_discriminator(&idl, &data_account3); + assert_eq!(result, None); + } + + #[test] + fn test_find_account_by_discriminator_no_const() { + let idl = create_test_idl_without_discriminator(); + + let data = [0u8, 42, 0, 0, 0, 0, 0, 0, 0]; + let result = find_account_by_discriminator(&idl, &data); + assert_eq!(result, None); // No discriminator constant, so returns None + } + + #[test] + fn test_find_account_by_discriminator_empty_data() { + let idl = create_test_idl_with_discriminator(); + + let result = find_account_by_discriminator(&idl, &[]); + assert_eq!(result, None); + } + + fn create_test_idl_with_discriminator_at_offset() -> shank_idl::idl::Idl { + // IDL where discriminator is NOT the first field + // Account structure: { authority: Pubkey (32 bytes), account_type: AccountType (1 byte), ... } + let idl_json = r#"{ + "name": "test_program", + "version": "0.1.0", + "metadata": { + "origin": "shank", + "address": "TestProgram11111111111111111111111111111111" + }, + "constants": [ + { + "name": "account_discriminator", + "type": "string", + "value": "\"AccountType\"" + } + ], + "types": [ + { + "name": "AccountType", + "type": { + "kind": "enum", + "variants": [ + { "name": "Pool" }, + { "name": "Position" } + ] + } + }, + { + "name": "Pool", + "type": { + "kind": "struct", + "fields": [ + { "name": "authority", "type": "publicKey" }, + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "liquidity", "type": "u64" } + ] + } + }, + { + "name": "Position", + "type": { + "kind": "struct", + "fields": [ + { "name": "owner", "type": "publicKey" }, + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "amount", "type": "u64" } + ] + } + } + ], + "accounts": [ + { + "name": "Pool", + "type": { + "kind": "struct", + "fields": [ + { "name": "authority", "type": "publicKey" }, + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "liquidity", "type": "u64" } + ] + } + }, + { + "name": "Position", + "type": { + "kind": "struct", + "fields": [ + { "name": "owner", "type": "publicKey" }, + { "name": "account_type", "type": { "defined": "AccountType" } }, + { "name": "amount", "type": "u64" } + ] + } + } + ], + "instructions": [] + }"#; + + serde_json::from_str(idl_json).expect("Failed to parse test IDL") + } + + #[test] + fn test_find_account_by_discriminator_at_offset() { + let idl = create_test_idl_with_discriminator_at_offset(); + + // Pool account: 32 bytes pubkey + 1 byte discriminator (0) + 8 bytes u64 + // Discriminator is at byte offset 32 + let mut pool_data = vec![0u8; 41]; // 32 + 1 + 8 + pool_data[32] = 0; // AccountType::Pool variant index + let result = find_account_by_discriminator(&idl, &pool_data); + assert_eq!(result, Some("Pool".to_string())); + + // Position account: 32 bytes pubkey + 1 byte discriminator (1) + 8 bytes u64 + let mut position_data = vec![0u8; 41]; + position_data[32] = 1; // AccountType::Position variant index + let result = find_account_by_discriminator(&idl, &position_data); + assert_eq!(result, Some("Position".to_string())); + } + + #[test] + fn test_find_account_by_discriminator_at_offset_wrong_discriminator() { + let idl = create_test_idl_with_discriminator_at_offset(); + + // Data with discriminator at wrong position (first byte instead of byte 32) + // This should not match because we read from the correct offset + let mut data = vec![0u8; 41]; + data[0] = 0; // Wrong position - this is the pubkey, not the discriminator + data[32] = 99; // Invalid discriminator value at correct position + let result = find_account_by_discriminator(&idl, &data); + assert_eq!(result, None); + } +} diff --git a/addons/svm/types/src/subgraph/mod.rs b/addons/svm/types/src/subgraph/mod.rs index a6e1bb816..012370db6 100644 --- a/addons/svm/types/src/subgraph/mod.rs +++ b/addons/svm/types/src/subgraph/mod.rs @@ -29,7 +29,10 @@ pub use pda::PdaSubgraphSource; use crate::{ subgraph::{ - idl::{get_expected_type_from_idl_type_def_ty, idl_type_to_txtx_type}, + idl::{ + anchor::{get_expected_type_from_idl_type_def_ty, idl_type_to_txtx_type}, + IdlKind, + }, token_account::TokenAccountSubgraphSource, }, SvmValue, SVM_F64, SVM_PUBKEY, SVM_SIGNATURE, SVM_U8, @@ -254,9 +257,16 @@ impl SubgraphRequestV0 { construct_did: &ConstructDid, values: &ValueStore, ) -> Result { - let idl = serde_json::from_str(idl_str) + let idl_kind = serde_json::from_str::(idl_str) .map_err(|e| diagnosed_error!("could not deserialize IDL: {e}"))?; + let idl = match idl_kind { + IdlKind::Anchor(idl) => idl, + IdlKind::Shank(_) => { + return Err(diagnosed_error!("Shank IDL not supported for subgraphs")) + } + }; + let (data_source, defined_field_values, intrinsic_field_values) = IndexedSubgraphSourceType::parse_values(values, &idl)?; diff --git a/addons/svm/types/src/subgraph/pda.rs b/addons/svm/types/src/subgraph/pda.rs index 3cb9f3ebc..e4dc9a862 100644 --- a/addons/svm/types/src/subgraph/pda.rs +++ b/addons/svm/types/src/subgraph/pda.rs @@ -12,7 +12,7 @@ use txtx_addon_kit::{ use crate::subgraph::{ find_idl_instruction_account, - idl::{match_idl_accounts, parse_bytes_to_value_with_expected_idl_type_def_ty}, + idl::anchor::{match_idl_accounts, parse_bytes_to_value_with_expected_idl_type_def_ty}, IntrinsicField, SubgraphRequest, SubgraphSourceType, LAMPORTS_INTRINSIC_FIELD, OWNER_INTRINSIC_FIELD, PUBKEY_INTRINSIC_FIELD, SLOT_INTRINSIC_FIELD, }; diff --git a/addons/svm/types/src/subgraph/tests.rs b/addons/svm/types/src/subgraph/tests.rs index 75a7646cd..3aa817c40 100644 --- a/addons/svm/types/src/subgraph/tests.rs +++ b/addons/svm/types/src/subgraph/tests.rs @@ -1,4 +1,15 @@ -use crate::{subgraph::idl::parse_bytes_to_value_with_expected_idl_type_def_ty, SvmValue, SVM_U64}; +use crate::{ + subgraph::{ + idl::anchor::parse_bytes_to_value_with_expected_idl_type_def_ty, + idl::shank::{ + borsh_encode_value_to_shank_idl_type, extract_shank_types, + parse_bytes_to_value_with_shank_idl_type_def_ty, + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes, + shank_idl_type_to_txtx_type, IdlConst, IdlType as ShankIdlType, IdlTypeDefinition, + }, + }, + SvmValue, SVM_PUBKEY, SVM_U64, +}; use super::*; use anchor_lang_idl::types::{ @@ -917,3 +928,608 @@ fn test_bad_data(bad_data: Vec, expected_type: IdlTypeDefTy, expected_err: & .unwrap_err(); assert_eq!(actual_err, expected_err); } + +// ============================================================================ +// Shank IDL codec tests +// ============================================================================ + +lazy_static! { + pub static ref SHANK_IDL: shank_idl::idl::Idl = + serde_json::from_slice(&include_bytes!("./fixtures/shank_test_idl.json").to_vec()).unwrap(); + pub static ref SHANK_TYPES: Vec = extract_shank_types(&SHANK_IDL); + pub static ref SHANK_CONSTANTS: Vec = vec![]; +} + +// -------------------------------------------------------------------------- +// Shank primitive type decoding tests +// -------------------------------------------------------------------------- + +#[test_case(vec![1], ShankIdlType::Bool, Value::bool(true); "shank bool true")] +#[test_case(vec![0], ShankIdlType::Bool, Value::bool(false); "shank bool false")] +#[test_case(vec![255], ShankIdlType::U8, SvmValue::u8(255); "shank u8 max")] +#[test_case(vec![0], ShankIdlType::U8, SvmValue::u8(0); "shank u8 min")] +#[test_case(vec![255, 255], ShankIdlType::U16, SvmValue::u16(u16::MAX); "shank u16 max")] +#[test_case(vec![0, 0], ShankIdlType::U16, SvmValue::u16(0); "shank u16 min")] +#[test_case(vec![255, 255, 255, 255], ShankIdlType::U32, SvmValue::u32(u32::MAX); "shank u32 max")] +#[test_case(vec![0, 0, 0, 0], ShankIdlType::U32, SvmValue::u32(0); "shank u32 min")] +#[test_case(vec![255, 255, 255, 255, 255, 255, 255, 255], ShankIdlType::U64, SvmValue::u64(u64::MAX); "shank u64 max")] +#[test_case(vec![0, 0, 0, 0, 0, 0, 0, 0], ShankIdlType::U64, SvmValue::u64(0); "shank u64 min")] +#[test_case(vec![255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], ShankIdlType::U128, SvmValue::u128(u128::MAX); "shank u128 max")] +#[test_case(vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ShankIdlType::U128, SvmValue::u128(0); "shank u128 min")] +#[test_case(vec![127], ShankIdlType::I8, SvmValue::i8(127); "shank i8 max")] +#[test_case(vec![128], ShankIdlType::I8, SvmValue::i8(-128); "shank i8 min")] +#[test_case(vec![255, 127], ShankIdlType::I16, SvmValue::i16(i16::MAX); "shank i16 max")] +#[test_case(vec![0, 128], ShankIdlType::I16, SvmValue::i16(i16::MIN); "shank i16 min")] +#[test_case(vec![255, 255, 255, 127], ShankIdlType::I32, SvmValue::i32(i32::MAX); "shank i32 max")] +#[test_case(vec![0, 0, 0, 128], ShankIdlType::I32, SvmValue::i32(i32::MIN); "shank i32 min")] +#[test_case(vec![255, 255, 255, 255, 255, 255, 255, 127], ShankIdlType::I64, SvmValue::i64(i64::MAX); "shank i64 max")] +#[test_case(vec![0, 0, 0, 0, 0, 0, 0, 128], ShankIdlType::I64, SvmValue::i64(i64::MIN); "shank i64 min")] +#[test_case(vec![255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127], ShankIdlType::I128, SvmValue::i128(i128::MAX); "shank i128 max")] +#[test_case(vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128], ShankIdlType::I128, SvmValue::i128(i128::MIN); "shank i128 min")] +fn test_shank_decode_primitives(data: Vec, expected_type: ShankIdlType, expected_value: Value) { + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + &data, + &expected_type, + &vec![], + ) + .unwrap(); + assert!(rest.is_empty(), "expected no leftover bytes"); + assert_eq!(value, expected_value); +} + +// -------------------------------------------------------------------------- +// Shank string and bytes decoding tests +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_string() { + let data = borsh::to_vec(&"hello world".to_string()).unwrap(); + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + &data, + &ShankIdlType::String, + &vec![], + ) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!(value, Value::string("hello world".to_string())); +} + +#[test] +fn test_shank_decode_bytes() { + let data = borsh::to_vec(&b"hello world".to_vec()).unwrap(); + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + &data, + &ShankIdlType::Bytes, + &vec![], + ) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!(value, Value::buffer(b"hello world".to_vec())); +} + +// -------------------------------------------------------------------------- +// Shank option decoding tests +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_option_none() { + let data = borsh::to_vec(&None::).unwrap(); + let opt_type = ShankIdlType::Option(Box::new(ShankIdlType::U64)); + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&data, &opt_type, &vec![]) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!(value, Value::null()); +} + +#[test] +fn test_shank_decode_option_some() { + let data = borsh::to_vec(&Some(42u64)).unwrap(); + let opt_type = ShankIdlType::Option(Box::new(ShankIdlType::U64)); + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&data, &opt_type, &vec![]) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!(value, SvmValue::u64(42)); +} + +// -------------------------------------------------------------------------- +// Shank vec decoding tests +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_vec_u64() { + let data = borsh::to_vec(&vec![1u64, 2u64, 3u64]).unwrap(); + let vec_type = ShankIdlType::Vec(Box::new(ShankIdlType::U64)); + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&data, &vec_type, &vec![]) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!(value, Value::array(vec![SvmValue::u64(1), SvmValue::u64(2), SvmValue::u64(3)])); +} + +#[test] +fn test_shank_decode_vec_string() { + let data = borsh::to_vec(&vec!["hello".to_string(), "world".to_string()]).unwrap(); + let vec_type = ShankIdlType::Vec(Box::new(ShankIdlType::String)); + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&data, &vec_type, &vec![]) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!( + value, + Value::array(vec![Value::string("hello".to_string()), Value::string("world".to_string())]) + ); +} + +// -------------------------------------------------------------------------- +// Shank array decoding tests +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_fixed_array_u8() { + let data: [u8; 4] = [1, 2, 3, 4]; + let arr_type = + ShankIdlType::Array(Box::new(ShankIdlType::U8), 4); + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&data, &arr_type, &vec![]) + .unwrap(); + assert!(rest.is_empty()); + assert_eq!( + value, + Value::array(vec![SvmValue::u8(1), SvmValue::u8(2), SvmValue::u8(3), SvmValue::u8(4)]) + ); +} + +#[test] +fn test_shank_decode_pubkey_like_array() { + let pubkey_bytes = [42u8; 32]; + let arr_type = + ShankIdlType::Array(Box::new(ShankIdlType::U8), 32); + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + &pubkey_bytes, + &arr_type, + &vec![], + ) + .unwrap(); + assert!(rest.is_empty()); + let expected = Value::array(std::iter::repeat(SvmValue::u8(42)).take(32).collect()); + assert_eq!(value, expected); +} + +// -------------------------------------------------------------------------- +// Shank struct decoding tests using test fixture +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_point_struct() { + #[derive(BorshSerialize, BorshDeserialize)] + struct Point { + x: i32, + y: i32, + } + + let point = Point { x: 10, y: -20 }; + let data = borsh::to_vec(&point).unwrap(); + + let point_type = SHANK_TYPES.iter().find(|t| t.name == "Point").unwrap(); + let value = + parse_bytes_to_value_with_shank_idl_type_def_ty(&data, &point_type.ty, &SHANK_TYPES) + .unwrap(); + + assert_eq!( + value, + ObjectType::from([("x", SvmValue::i32(10)), ("y", SvmValue::i32(-20))]).to_value() + ); +} + +#[test] +fn test_shank_decode_all_primitives_struct() { + #[derive(BorshSerialize, BorshDeserialize)] + struct AllPrimitives { + bool_field: bool, + u8_field: u8, + u16_field: u16, + u32_field: u32, + u64_field: u64, + u128_field: u128, + i8_field: i8, + i16_field: i16, + i32_field: i32, + i64_field: i64, + i128_field: i128, + } + + let prims = AllPrimitives { + bool_field: true, + u8_field: 255, + u16_field: 1000, + u32_field: 100000, + u64_field: 1000000000, + u128_field: 1000000000000000000, + i8_field: -100, + i16_field: -1000, + i32_field: -100000, + i64_field: -1000000000, + i128_field: -1000000000000000000, + }; + let data = borsh::to_vec(&prims).unwrap(); + + let prims_type = SHANK_TYPES.iter().find(|t| t.name == "AllPrimitives").unwrap(); + let value = + parse_bytes_to_value_with_shank_idl_type_def_ty(&data, &prims_type.ty, &SHANK_TYPES) + .unwrap(); + + let expected = ObjectType::from([ + ("boolField", Value::bool(true)), + ("u8Field", SvmValue::u8(255)), + ("u16Field", SvmValue::u16(1000)), + ("u32Field", SvmValue::u32(100000)), + ("u64Field", SvmValue::u64(1000000000)), + ("u128Field", SvmValue::u128(1000000000000000000)), + ("i8Field", SvmValue::i8(-100)), + ("i16Field", SvmValue::i16(-1000)), + ("i32Field", SvmValue::i32(-100000)), + ("i64Field", SvmValue::i64(-1000000000)), + ("i128Field", SvmValue::i128(-1000000000000000000)), + ]) + .to_value(); + assert_eq!(value, expected); +} + +// -------------------------------------------------------------------------- +// Shank enum decoding tests +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_unit_enum_variant() { + // Status enum: Inactive = 0, Active = 1, Paused = 2, Completed = 3 + let data = vec![1u8]; // Active variant + let status_type = SHANK_TYPES.iter().find(|t| t.name == "Status").unwrap(); + let value = + parse_bytes_to_value_with_shank_idl_type_def_ty(&data, &status_type.ty, &SHANK_TYPES) + .unwrap(); + assert_eq!(value, ObjectType::from([("Active", Value::null())]).to_value()); +} + +#[test] +fn test_shank_decode_tuple_enum_variant() { + // SingleValueEnum: Empty = 0, WithU64 = 1, WithU128 = 2, WithBool = 3 + let mut data = vec![1u8]; // WithU64 variant + data.extend(42u64.to_le_bytes()); // value + + let enum_type = SHANK_TYPES.iter().find(|t| t.name == "SingleValueEnum").unwrap(); + let value = parse_bytes_to_value_with_shank_idl_type_def_ty(&data, &enum_type.ty, &SHANK_TYPES) + .unwrap(); + assert_eq!( + value, + ObjectType::from([("WithU64", Value::array(vec![SvmValue::u64(42)]))]).to_value() + ); +} + +#[test] +fn test_shank_decode_struct_enum_variant() { + // StructVariantEnum: None = 0, WithAmount = 1, WithCoordinates = 2, WithDetails = 3 + #[derive(BorshSerialize)] + enum TestEnum { + #[allow(dead_code)] + None, + WithAmount { + amount: u64, + }, + } + + let data = borsh::to_vec(&TestEnum::WithAmount { amount: 12345 }).unwrap(); + let enum_type = SHANK_TYPES.iter().find(|t| t.name == "StructVariantEnum").unwrap(); + let value = parse_bytes_to_value_with_shank_idl_type_def_ty(&data, &enum_type.ty, &SHANK_TYPES) + .unwrap(); + + assert_eq!( + value, + ObjectType::from([( + "WithAmount", + ObjectType::from([("amount", SvmValue::u64(12345))]).to_value() + )]) + .to_value() + ); +} + +// -------------------------------------------------------------------------- +// Shank type conversion tests (IDL type -> txtx Type) +// -------------------------------------------------------------------------- + +#[test_case(ShankIdlType::Bool, Type::bool(); "shank type bool")] +#[test_case(ShankIdlType::U8, Type::addon(crate::SVM_U8); "shank type u8")] +#[test_case(ShankIdlType::U16, Type::addon(crate::SVM_U16); "shank type u16")] +#[test_case(ShankIdlType::U32, Type::addon(crate::SVM_U32); "shank type u32")] +#[test_case(ShankIdlType::U64, Type::addon(crate::SVM_U64); "shank type u64")] +#[test_case(ShankIdlType::U128, Type::addon(crate::SVM_U128); "shank type u128")] +#[test_case(ShankIdlType::I8, Type::addon(crate::SVM_I8); "shank type i8")] +#[test_case(ShankIdlType::I16, Type::addon(crate::SVM_I16); "shank type i16")] +#[test_case(ShankIdlType::I32, Type::addon(crate::SVM_I32); "shank type i32")] +#[test_case(ShankIdlType::I64, Type::addon(crate::SVM_I64); "shank type i64")] +#[test_case(ShankIdlType::I128, Type::addon(crate::SVM_I128); "shank type i128")] +#[test_case(ShankIdlType::Bytes, Type::buffer(); "shank type bytes")] +#[test_case(ShankIdlType::String, Type::string(); "shank type string")] +#[test_case(ShankIdlType::PublicKey, Type::addon(SVM_PUBKEY); "shank type pubkey")] +fn test_shank_idl_type_to_txtx_type(idl_type: ShankIdlType, expected: Type) { + let result = shank_idl_type_to_txtx_type(&idl_type, &vec![], &vec![]).unwrap(); + assert_eq!(result, expected); +} + +#[test] +fn test_shank_option_type_to_txtx_type() { + let opt_type = ShankIdlType::Option(Box::new(ShankIdlType::U64)); + let result = shank_idl_type_to_txtx_type(&opt_type, &vec![], &vec![]).unwrap(); + assert_eq!(result, Type::typed_null(Type::addon(crate::SVM_U64))); +} + +#[test] +fn test_shank_vec_type_to_txtx_type() { + let vec_type = ShankIdlType::Vec(Box::new(ShankIdlType::String)); + let result = shank_idl_type_to_txtx_type(&vec_type, &vec![], &vec![]).unwrap(); + assert_eq!(result, Type::array(Type::string())); +} + +#[test] +fn test_shank_array_type_to_txtx_type() { + let arr_type = + ShankIdlType::Array(Box::new(ShankIdlType::U8), 32); + let result = shank_idl_type_to_txtx_type(&arr_type, &vec![], &vec![]).unwrap(); + assert_eq!(result, Type::array(Type::addon(crate::SVM_U8))); +} + +#[test] +fn test_shank_defined_type_to_txtx_type() { + let def_type = ShankIdlType::Defined("Point".to_string()); + let result = shank_idl_type_to_txtx_type(&def_type, &SHANK_TYPES, &vec![]).unwrap(); + // Point should have x: i32, y: i32 + assert!(result.as_object().is_some()); +} + +// -------------------------------------------------------------------------- +// Shank encoding tests (Value -> bytes) +// -------------------------------------------------------------------------- + +#[test_case(Value::bool(true), ShankIdlType::Bool, vec![1]; "shank encode bool true")] +#[test_case(Value::bool(false), ShankIdlType::Bool, vec![0]; "shank encode bool false")] +#[test_case(SvmValue::u8(255), ShankIdlType::U8, vec![255]; "shank encode u8 max")] +#[test_case(SvmValue::u64(1000), ShankIdlType::U64, 1000u64.to_le_bytes().to_vec(); "shank encode u64")] +#[test_case(SvmValue::i32(-100), ShankIdlType::I32, (-100i32).to_le_bytes().to_vec(); "shank encode i32 negative")] +fn test_shank_encode_primitives(value: Value, idl_type: ShankIdlType, expected_bytes: Vec) { + let result = borsh_encode_value_to_shank_idl_type(&value, &idl_type, &vec![]).unwrap(); + assert_eq!(result, expected_bytes); +} + +#[test] +fn test_shank_encode_string() { + let value = Value::string("hello".to_string()); + let result = + borsh_encode_value_to_shank_idl_type(&value, &ShankIdlType::String, &vec![]).unwrap(); + let expected = borsh::to_vec(&"hello".to_string()).unwrap(); + assert_eq!(result, expected); +} + +#[test] +fn test_shank_encode_option_none() { + let value = Value::null(); + let opt_type = ShankIdlType::Option(Box::new(ShankIdlType::U64)); + let result = borsh_encode_value_to_shank_idl_type(&value, &opt_type, &vec![]).unwrap(); + assert_eq!(result, borsh::to_vec(&None::).unwrap()); +} + +#[test] +fn test_shank_encode_option_some_bool() { + // Use a primitive Value (bool) instead of an addon (u64) to test Option encoding + // Addon values go through a different path (borsh_encode_bytes_to_shank_idl_type) + let value = Value::bool(true); + let opt_type = + ShankIdlType::Option(Box::new(ShankIdlType::Bool)); + let result = borsh_encode_value_to_shank_idl_type(&value, &opt_type, &vec![]).unwrap(); + // Option encoding: 1 byte discriminator + inner value + let mut expected = vec![1u8]; // Some + expected.extend(borsh::to_vec(&true).unwrap()); + assert_eq!(result, expected); +} + +#[test] +fn test_shank_encode_option_some_string() { + let value = Value::string("test".to_string()); + let opt_type = + ShankIdlType::Option(Box::new(ShankIdlType::String)); + let result = borsh_encode_value_to_shank_idl_type(&value, &opt_type, &vec![]).unwrap(); + let mut expected = vec![1u8]; // Some + expected.extend(borsh::to_vec(&"test".to_string()).unwrap()); + assert_eq!(result, expected); +} + +#[test] +fn test_shank_encode_vec() { + let value = Value::array(vec![SvmValue::u64(1), SvmValue::u64(2), SvmValue::u64(3)]); + let vec_type = ShankIdlType::Vec(Box::new(ShankIdlType::U64)); + let result = borsh_encode_value_to_shank_idl_type(&value, &vec_type, &vec![]).unwrap(); + let expected = borsh::to_vec(&vec![1u64, 2u64, 3u64]).unwrap(); + assert_eq!(result, expected); +} + +#[test] +fn test_shank_encode_fixed_array() { + let value = + Value::array(vec![SvmValue::u8(1), SvmValue::u8(2), SvmValue::u8(3), SvmValue::u8(4)]); + let arr_type = + ShankIdlType::Array(Box::new(ShankIdlType::U8), 4); + let result = borsh_encode_value_to_shank_idl_type(&value, &arr_type, &vec![]).unwrap(); + assert_eq!(result, vec![1, 2, 3, 4]); +} + +#[test] +fn test_shank_encode_struct() { + let value = ObjectType::from([("x", SvmValue::i32(10)), ("y", SvmValue::i32(-20))]).to_value(); + let def_type = ShankIdlType::Defined("Point".to_string()); + let result = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES).unwrap(); + + #[derive(BorshSerialize)] + struct Point { + x: i32, + y: i32, + } + let expected = borsh::to_vec(&Point { x: 10, y: -20 }).unwrap(); + assert_eq!(result, expected); +} + +#[test] +fn test_shank_encode_unit_enum() { + // {"Active": null} for Status enum + let value = ObjectType::from([("Active", Value::null())]).to_value(); + let def_type = ShankIdlType::Defined("Status".to_string()); + let result = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES).unwrap(); + // Active is variant index 1 (Inactive=0, Active=1, Paused=2, Completed=3) + assert_eq!(result, vec![1]); +} + +#[test] +fn test_shank_encode_struct_enum() { + // {"WithAmount": {"amount": 12345}} for StructVariantEnum + let value = ObjectType::from([( + "WithAmount", + ObjectType::from([("amount", SvmValue::u64(12345))]).to_value(), + )]) + .to_value(); + let def_type = + ShankIdlType::Defined("StructVariantEnum".to_string()); + let result = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES).unwrap(); + + #[derive(BorshSerialize)] + enum TestEnum { + #[allow(dead_code)] + None, + WithAmount { + amount: u64, + }, + } + let expected = borsh::to_vec(&TestEnum::WithAmount { amount: 12345 }).unwrap(); + assert_eq!(result, expected); +} + +// -------------------------------------------------------------------------- +// Shank round-trip tests (encode -> decode) +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_round_trip_primitives() { + let test_cases: Vec<(Value, ShankIdlType)> = vec![ + (Value::bool(true), ShankIdlType::Bool), + (SvmValue::u8(42), ShankIdlType::U8), + (SvmValue::u64(123456789), ShankIdlType::U64), + (SvmValue::i128(-999999999999), ShankIdlType::I128), + (Value::string("test string".to_string()), ShankIdlType::String), + ]; + + for (value, idl_type) in test_cases { + let encoded = borsh_encode_value_to_shank_idl_type(&value, &idl_type, &vec![]).unwrap(); + let (decoded, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + &encoded, + &idl_type, + &vec![], + ) + .unwrap(); + assert!(rest.is_empty(), "round trip should consume all bytes"); + assert_eq!(decoded, value, "round trip failed for {:?}", idl_type); + } +} + +#[test] +fn test_shank_round_trip_point_struct() { + let value = + ObjectType::from([("x", SvmValue::i32(100)), ("y", SvmValue::i32(-200))]).to_value(); + let def_type = ShankIdlType::Defined("Point".to_string()); + + let encoded = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES).unwrap(); + + let point_type = SHANK_TYPES.iter().find(|t| t.name == "Point").unwrap(); + let decoded = + parse_bytes_to_value_with_shank_idl_type_def_ty(&encoded, &point_type.ty, &SHANK_TYPES) + .unwrap(); + + assert_eq!(decoded, value); +} + +#[test] +fn test_shank_round_trip_vec() { + let value = Value::array(vec![ + Value::string("first".to_string()), + Value::string("second".to_string()), + Value::string("third".to_string()), + ]); + let vec_type = ShankIdlType::Vec(Box::new(ShankIdlType::String)); + + let encoded = borsh_encode_value_to_shank_idl_type(&value, &vec_type, &vec![]).unwrap(); + let (decoded, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&encoded, &vec_type, &vec![]) + .unwrap(); + + assert!(rest.is_empty()); + assert_eq!(decoded, value); +} + +// -------------------------------------------------------------------------- +// Shank error handling tests +// -------------------------------------------------------------------------- + +#[test] +fn test_shank_decode_not_enough_bytes() { + let data = vec![1u8]; // Only 1 byte, but u64 needs 8 + let result = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + &data, + &ShankIdlType::U64, + &vec![], + ); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not enough bytes")); +} + +#[test] +fn test_shank_decode_undefined_type() { + let data = vec![0u8; 8]; + let def_type = + ShankIdlType::Defined("NonExistentType".to_string()); + let result = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(&data, &def_type, &vec![]); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not found")); +} + +#[test] +fn test_shank_encode_wrong_array_length() { + let value = Value::array(vec![SvmValue::u8(1), SvmValue::u8(2)]); // Only 2 elements + let arr_type = ShankIdlType::Array(Box::new(ShankIdlType::U8), 4); // Expects 4 elements + let result = borsh_encode_value_to_shank_idl_type(&value, &arr_type, &vec![]); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("array length")); +} + +#[test] +fn test_shank_encode_invalid_enum_variant() { + let value = ObjectType::from([("InvalidVariant", Value::null())]).to_value(); + let def_type = ShankIdlType::Defined("Status".to_string()); + let result = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("unknown variant")); +} + +// -------------------------------------------------------------------------- +// Shank IDL extraction tests +// -------------------------------------------------------------------------- + +#[test] +fn test_extract_shank_types_from_idl() { + let types = extract_shank_types(&SHANK_IDL); + assert!(!types.is_empty()); + + // Verify some expected types exist + let type_names: Vec<&str> = types.iter().map(|t| t.name.as_str()).collect(); + assert!(type_names.contains(&"Point")); + assert!(type_names.contains(&"AllPrimitives")); + assert!(type_names.contains(&"Status")); + assert!(type_names.contains(&"StringContainer")); +} diff --git a/addons/svm/types/src/subgraph/token_account.rs b/addons/svm/types/src/subgraph/token_account.rs index f838bfda1..ec453247d 100644 --- a/addons/svm/types/src/subgraph/token_account.rs +++ b/addons/svm/types/src/subgraph/token_account.rs @@ -13,7 +13,7 @@ use txtx_addon_kit::{ }; use crate::subgraph::{ - find_idl_instruction_account, idl::match_idl_accounts, IntrinsicField, SubgraphRequest, + find_idl_instruction_account, idl::anchor::match_idl_accounts, IntrinsicField, SubgraphRequest, SubgraphSourceType, LAMPORTS_INTRINSIC_FIELD, OWNER_INTRINSIC_FIELD, PUBKEY_INTRINSIC_FIELD, SLOT_INTRINSIC_FIELD, TOKEN_AMOUNT_INTRINSIC_FIELD, TOKEN_DECIMALS_INTRINSIC_FIELD, TOKEN_MINT_INTRINSIC_FIELD, TOKEN_PROGRAM_INTRINSIC_FIELD, TOKEN_UI_AMOUNT_INTRINSIC_FIELD,