From a77b07236d2473f4209a1693370033b3d05054fe Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 12:21:35 -0500 Subject: [PATCH 01/12] feat(svm-core/svm-types): add shank IDL support --- Cargo.lock | 68 + addons/svm/core/src/codec/anchor.rs | 14 +- addons/svm/core/src/codec/idl/mod.rs | 401 +++-- addons/svm/core/src/codec/native.rs | 34 +- .../svm/core/src/commands/deploy_program.rs | 9 +- .../setup_surfnet/cheatcode_deploy_program.rs | 28 +- addons/svm/core/src/functions.rs | 8 +- addons/svm/core/src/templates/mod.rs | 8 +- addons/svm/types/Cargo.toml | 1 + addons/svm/types/src/lib.rs | 1 + addons/svm/types/src/subgraph/mod.rs | 1 + addons/svm/types/src/subgraph/shank.rs | 1164 +++++++++++++++ addons/svm/types/src/subgraph/tests.rs | 622 +++++++- .../src/test_fixtures/shank_test_idl.json | 1327 +++++++++++++++++ 14 files changed, 3552 insertions(+), 134 deletions(-) create mode 100644 addons/svm/types/src/subgraph/shank.rs create mode 100644 addons/svm/types/src/test_fixtures/shank_test_idl.json diff --git a/Cargo.lock b/Cargo.lock index df4096690..0a9a74f27 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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a8cee6731ee59f3e24c44aab6c7d3adb365b1f7314da73ea6654aec354e4341" +dependencies = [ + "anyhow", + "cargo_toml", + "heck 0.3.3", + "serde", + "serde_json", + "shank_macro_impl", + "shellexpand", +] + +[[package]] +name = "shank_macro_impl" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c08958b4ba24f8289481c0d8b377c63a8205fc037b59fc80643c491ec1f487" +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..3714d80ec 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -3,22 +3,36 @@ pub mod convert_idl; use std::str::FromStr; use crate::typing::anchor as anchor_lang_idl; +use crate::typing::shank as shank_idl; use crate::typing::SvmValue; use anchor_lang_idl::types::{ - Idl, IdlArrayLen, IdlDefinedFields, IdlGenericArg, IdlInstruction, IdlType, IdlTypeDef, - IdlTypeDefGeneric, IdlTypeDefTy, + Idl as AnchorIdl, IdlArrayLen, IdlDefinedFields, IdlGenericArg, IdlInstruction, IdlType, + IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy, }; use convert_idl::classic_idl_to_anchor_idl; +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::subgraph::shank::{ + borsh_encode_value_to_shank_idl_type, extract_shank_instruction_arg_type, extract_shank_types, +}; use txtx_addon_network_svm_types::I256; use txtx_addon_network_svm_types::U256; +/// Represents the kind of IDL format being used. +#[derive(Debug, Clone)] +pub enum IdlKind { + /// Anchor IDL format (v0.30+) + Anchor(AnchorIdl), + /// Shank IDL format + Shank(ShankIdl), +} + #[derive(Debug, Clone)] pub struct IdlRef { - pub idl: Idl, + pub idl: IdlKind, pub location: Option, } @@ -31,8 +45,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 +63,192 @@ 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 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")) + 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") + }), + IdlKind::Shank(_) => Err(diagnosed_error!( + "get_instruction is not supported for Shank IDL" + )), + } } pub fn get_types(&self) -> Vec { - self.idl.types.clone() + match &self.idl { + IdlKind::Anchor(idl) => idl.types.clone(), + IdlKind::Shank(_) => { + // Shank types are not directly compatible with Anchor IdlTypeDef + vec![] + } + } } /// 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()); - } - - 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) + 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 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()); + } + + // Extract types to our local format for encoding + let idl_types = extract_shank_types(idl) + .map_err(|e| diagnosed_error!("failed to extract IDL types: {}", e))?; + + 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) + } + } } /// Encodes the arguments for a given instruction into a flat byte array. @@ -105,63 +257,132 @@ 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) + 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(vec![]); + } + + 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| { + diagnosed_error!( + "error in argument at position {}: {}", + user_arg_idx + 1, + e + ) + }, + )?; + encoded_args.append(&mut 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(vec![]); + } + + // Extract types to our local format for encoding + let idl_types = extract_shank_types(idl) + .map_err(|e| diagnosed_error!("failed to extract IDL types: {}", e))?; + + 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) + } + } } } -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_string(idl_str: &str) -> Result { + // Try parsing as Anchor IDL first (modern format) + if let Ok(anchor_idl) = serde_json::from_str::(idl_str) { + return Ok(IdlKind::Anchor(anchor_idl)); + } + + // Try parsing as Shank IDL + if let Ok(shank_idl) = serde_json::from_str::(idl_str) { + return Ok(IdlKind::Shank(shank_idl)); + } + + // 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)) + } + Err(e) => Err(diagnosed_error!("invalid idl: {e}")), + } } -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) +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)) + } + Err(e) => Err(diagnosed_error!("invalid idl: {e}")), + } } pub fn borsh_encode_value_to_idl_type( diff --git a/addons/svm/core/src/codec/native.rs b/addons/svm/core/src/codec/native.rs index 6ba6713c7..f867f2ec7 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; @@ -14,7 +12,7 @@ use txtx_addon_kit::{ use crate::typing::SvmValue; -use super::idl::IdlRef; +use super::idl::{IdlKind, IdlRef}; pub struct NativeProgramArtifacts { /// The binary of the native program, stored for a native project at `target/deploy/.so`. @@ -24,8 +22,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 +32,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 +84,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 +100,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 +111,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 +151,9 @@ 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..f11264fb1 100644 --- a/addons/svm/core/src/commands/deploy_program.rs +++ b/addons/svm/core/src/commands/deploy_program.rs @@ -969,7 +969,14 @@ impl CommandImplementation for DeployProgram { .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(); + let value = match idl_ref.kind() { + crate::codec::idl::IdlKind::Anchor(anchor_idl) => { + serde_json::to_value(anchor_idl).unwrap() + } + crate::codec::idl::IdlKind::Shank(shank_idl) => { + serde_json::to_value(shank_idl).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..daeb84c7c 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::SvmValue; use crate::{ - codec::{utils::cheatcode_deploy_program, validate_program_so}, + codec::{idl::IdlKind, 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 } @@ -171,12 +165,16 @@ impl SurfpoolDeployProgram { ) .await?; if let Some(idl) = &program_deployment.idl { + let idl_str = match idl { + IdlKind::Anchor(anchor_idl) => serde_json::to_string(anchor_idl), + IdlKind::Shank(shank_idl) => serde_json::to_string(shank_idl), + } + .map_err(|e| diagnosed_error!("failed to serialize idl for rpc call: {e}"))?; + 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_str]), ) .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..e8d9da8ed 100644 --- a/addons/svm/core/src/templates/mod.rs +++ b/addons/svm/core/src/templates/mod.rs @@ -150,8 +150,12 @@ 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 = idl_ref.as_anchor().ok_or_else(|| { + "expected Anchor IDL for subgraph template, but found a different IDL format".to_string() + })?; 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..66182eb50 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.6" [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/mod.rs b/addons/svm/types/src/subgraph/mod.rs index a6e1bb816..3d522b296 100644 --- a/addons/svm/types/src/subgraph/mod.rs +++ b/addons/svm/types/src/subgraph/mod.rs @@ -22,6 +22,7 @@ use txtx_addon_kit::{ mod event; pub mod idl; mod pda; +pub mod shank; pub mod token_account; pub use event::EventSubgraphSource; diff --git a/addons/svm/types/src/subgraph/shank.rs b/addons/svm/types/src/subgraph/shank.rs new file mode 100644 index 000000000..a02d4b75f --- /dev/null +++ b/addons/svm/types/src/subgraph/shank.rs @@ -0,0 +1,1164 @@ +//! Shank IDL codec functions for converting between Shank IDL types, txtx types, and bytes. +//! +//! This module provides functions similar to those in `idl.rs` but specifically for Shank IDLs. +//! Since shank_idl doesn't export its internal types directly, we define local types that mirror +//! the shank_idl structure for use in function signatures. + +use serde::{Deserialize, Serialize}; +use txtx_addon_kit::{ + indexmap::IndexMap, + types::types::{ObjectDefinition, ObjectProperty, ObjectType, Type, Value}, +}; + +use crate::{SvmValue, SVM_PUBKEY}; +use std::fmt::Display; + +// ============================================================================ +// Local type definitions that mirror shank_idl internal types +// These are needed because shank_idl doesn't export its internal modules +// ============================================================================ + +/// Shank IDL type representation +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ShankIdlType { + Bool, + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + Bytes, + String, + #[serde(rename = "publicKey")] + PublicKey, + #[serde(untagged)] + Option(ShankIdlTypeOption), + #[serde(untagged)] + FixedSizeOption(ShankIdlTypeFixedSizeOption), + #[serde(untagged)] + Vec(ShankIdlTypeVec), + #[serde(untagged)] + Array(ShankIdlTypeArray), + #[serde(untagged)] + Tuple(ShankIdlTypeTuple), + #[serde(untagged)] + Defined(ShankIdlTypeDefined), + #[serde(untagged)] + HashMap(ShankIdlTypeHashMap), + #[serde(untagged)] + BTreeMap(ShankIdlTypeBTreeMap), + #[serde(untagged)] + HashSet(ShankIdlTypeHashSet), + #[serde(untagged)] + BTreeSet(ShankIdlTypeBTreeSet), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeOption { + pub option: Box, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ShankIdlTypeFixedSizeOption { + pub fixed_size_option: ShankIdlTypeFixedSizeOptionInner, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeFixedSizeOptionInner { + pub inner: Box, + #[serde(skip_serializing_if = "Option::is_none")] + pub sentinel: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeVec { + pub vec: Box, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeArray { + pub array: (Box, usize), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeTuple { + pub tuple: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeDefined { + pub defined: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ShankIdlTypeHashMap { + pub hash_map: (Box, Box), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ShankIdlTypeBTreeMap { + pub b_tree_map: (Box, Box), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ShankIdlTypeHashSet { + pub hash_set: Box, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ShankIdlTypeBTreeSet { + pub b_tree_set: Box, +} + +/// Shank IDL field definition +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlField { + pub name: String, + #[serde(rename = "type")] + pub ty: ShankIdlType, +} + +/// Shank IDL type definition +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlTypeDef { + pub name: String, + #[serde(rename = "type")] + pub ty: ShankIdlTypeDefTy, + #[serde(skip_serializing_if = "Option::is_none")] + pub pod_sentinel: Option>, +} + +/// Shank IDL type definition type (struct or enum) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum ShankIdlTypeDefTy { + Struct { fields: Vec }, + Enum { variants: Vec }, +} + +/// Shank IDL enum variant +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlEnumVariant { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option, +} + +/// Shank enum fields (named or tuple) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum ShankEnumFields { + Named(Vec), + Tuple(Vec), +} + +/// Shank IDL constant +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ShankIdlConst { + pub name: String, + #[serde(rename = "type")] + pub ty: ShankIdlType, + pub value: String, +} + +// ============================================================================ +// Helper function to convert from shank_idl::idl::Idl to our local types +// ============================================================================ + +/// Extracts type definitions from a Shank IDL by serializing and deserializing +pub fn extract_shank_types(idl: &shank_idl::idl::Idl) -> Result, String> { + // Serialize the IDL types to JSON and deserialize to our local types + let types_json = serde_json::to_string(&idl.types) + .map_err(|e| format!("failed to serialize IDL types: {}", e))?; + let mut types: Vec = serde_json::from_str(&types_json) + .map_err(|e| format!("failed to deserialize IDL types: {}", e))?; + + // Also include accounts as they can be referenced as types + let accounts_json = serde_json::to_string(&idl.accounts) + .map_err(|e| format!("failed to serialize IDL accounts: {}", e))?; + let accounts: Vec = serde_json::from_str(&accounts_json) + .map_err(|e| format!("failed to deserialize IDL accounts: {}", e))?; + + types.extend(accounts); + Ok(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))?; + + let arg_json = serde_json::to_string(&arg.ty) + .map_err(|e| format!("failed to serialize arg type: {}", e))?; + serde_json::from_str(&arg_json) + .map_err(|e| format!("failed to deserialize arg type: {}", e)) +} + +// ============================================================================ +// Type conversion functions +// ============================================================================ + +/// Converts a Shank IDL type to a txtx Type. +pub fn shank_idl_type_to_txtx_type( + idl_type: &ShankIdlType, + idl_types: &[ShankIdlTypeDef], + _idl_constants: &[ShankIdlConst], +) -> Result { + let res = match idl_type { + ShankIdlType::Bool => Type::bool(), + ShankIdlType::U8 => Type::addon(crate::SVM_U8), + ShankIdlType::U16 => Type::addon(crate::SVM_U16), + ShankIdlType::U32 => Type::addon(crate::SVM_U32), + ShankIdlType::U64 => Type::addon(crate::SVM_U64), + ShankIdlType::U128 => Type::addon(crate::SVM_U128), + ShankIdlType::I8 => Type::addon(crate::SVM_I8), + ShankIdlType::I16 => Type::addon(crate::SVM_I16), + ShankIdlType::I32 => Type::addon(crate::SVM_I32), + ShankIdlType::I64 => Type::addon(crate::SVM_I64), + ShankIdlType::I128 => Type::addon(crate::SVM_I128), + ShankIdlType::Bytes => Type::buffer(), + ShankIdlType::String => Type::string(), + ShankIdlType::PublicKey => Type::addon(SVM_PUBKEY), + ShankIdlType::Option(opt) => { + Type::typed_null(shank_idl_type_to_txtx_type(&opt.option, idl_types, _idl_constants)?) + } + ShankIdlType::FixedSizeOption(opt) => { + Type::typed_null(shank_idl_type_to_txtx_type(&opt.fixed_size_option.inner, idl_types, _idl_constants)?) + } + ShankIdlType::Vec(vec) => { + Type::array(shank_idl_type_to_txtx_type(&vec.vec, idl_types, _idl_constants)?) + } + ShankIdlType::Array(arr) => { + Type::array(shank_idl_type_to_txtx_type(&arr.array.0, idl_types, _idl_constants)?) + } + ShankIdlType::Tuple(tuple) => { + let mut props = vec![]; + for (i, ty) in tuple.tuple.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)) + } + ShankIdlType::Defined(def) => { + let Some(matching_type) = idl_types.iter().find(|t| t.name == def.defined) else { + return Err(format!("unable to find defined type '{}'", def.defined)); + }; + get_expected_type_from_shank_idl_type_def_ty( + &matching_type.ty, + idl_types, + _idl_constants, + )? + } + ShankIdlType::HashMap(map) => { + let value_type = shank_idl_type_to_txtx_type(&map.hash_map.1, idl_types, _idl_constants)?; + Type::array(value_type) + } + ShankIdlType::BTreeMap(map) => { + let value_type = shank_idl_type_to_txtx_type(&map.b_tree_map.1, idl_types, _idl_constants)?; + Type::array(value_type) + } + ShankIdlType::HashSet(set) => { + Type::array(shank_idl_type_to_txtx_type(&set.hash_set, idl_types, _idl_constants)?) + } + ShankIdlType::BTreeSet(set) => { + Type::array(shank_idl_type_to_txtx_type(&set.b_tree_set, 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: &ShankIdlTypeDefTy, + idl_types: &[ShankIdlTypeDef], + idl_constants: &[ShankIdlConst], +) -> Result { + let ty = match idl_type_def_ty { + ShankIdlTypeDefTy::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)) + } + ShankIdlTypeDefTy::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: &ShankEnumFields, + idl_types: &[ShankIdlTypeDef], + idl_constants: &[ShankIdlConst], +) -> Result { + match fields { + ShankEnumFields::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))) + } + ShankEnumFields::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: &ShankIdlTypeDefTy, + idl_types: &[ShankIdlTypeDef], +) -> 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: &ShankIdlTypeDefTy, + idl_types: &[ShankIdlTypeDef], +) -> Result<(Value, &'a [u8]), String> { + match expected_type { + ShankIdlTypeDefTy::Struct { fields } => { + parse_bytes_to_shank_struct_with_leftover_bytes(data, fields, idl_types) + } + ShankIdlTypeDefTy::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: &[ShankIdlField], + idl_types: &[ShankIdlTypeDef], +) -> 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: &ShankIdlEnumVariant, + idl_types: &[ShankIdlTypeDef], +) -> Result<(Value, &'a [u8]), String> { + match &variant.fields { + Some(ShankEnumFields::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(ShankEnumFields::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: &ShankIdlType, + idl_types: &[ShankIdlTypeDef], +) -> 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 { + ShankIdlType::U8 => { + let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("u8"))?; + Ok((SvmValue::u8(v[0]), rest)) + } + ShankIdlType::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, + )) + } + ShankIdlType::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, + )) + } + ShankIdlType::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, + )) + } + ShankIdlType::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, + )) + } + ShankIdlType::I8 => { + let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("i8"))?; + Ok((SvmValue::i8(i8::from_le_bytes([v[0]])), rest)) + } + ShankIdlType::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, + )) + } + ShankIdlType::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, + )) + } + ShankIdlType::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, + )) + } + ShankIdlType::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, + )) + } + ShankIdlType::Bool => { + let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("bool"))?; + Ok((Value::bool(v[0] != 0), rest)) + } + ShankIdlType::PublicKey => { + let (v, rest) = data.split_at_checked(32).ok_or(bytes_err("pubkey"))?; + Ok((SvmValue::pubkey(v.to_vec()), rest)) + } + ShankIdlType::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)) + } + ShankIdlType::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)) + } + ShankIdlType::Option(opt) => { + 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, &opt.option, idl_types) + } + } + ShankIdlType::FixedSizeOption(opt) => { + let inner_size = get_shank_type_size(&opt.fixed_size_option.inner)?; + let (inner_bytes, rest) = data.split_at_checked(inner_size).ok_or(bytes_err("fixed_size_option"))?; + + if let Some(ref sentinel_bytes) = opt.fixed_size_option.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, &opt.fixed_size_option.inner, idl_types)?; + Ok((value, rest)) + } + ShankIdlType::Vec(vec) => { + 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, &vec.vec, idl_types)?; + vec_values.push(value); + remaining_data = rest; + } + + Ok((Value::array(vec_values), remaining_data)) + } + ShankIdlType::Array(arr) => { + let len = arr.array.1; + 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, &arr.array.0, idl_types)?; + vec_values.push(value); + remaining_data = rest; + } + Ok((Value::array(vec_values), remaining_data)) + } + ShankIdlType::Tuple(tuple) => { + let mut values = Vec::with_capacity(tuple.tuple.len()); + let mut remaining_data = data; + for ty in &tuple.tuple { + 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)) + } + ShankIdlType::Defined(def) => { + let matching_type = idl_types + .iter() + .find(|t| t.name == def.defined) + .ok_or(err(&def.defined, &"not found in IDL types"))?; + + parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes( + data, + &matching_type.ty, + idl_types, + ) + } + ShankIdlType::HashMap(map) => { + 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, &map.hash_map.0, idl_types)?; + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(rest, &map.hash_map.1, idl_types)?; + remaining_data = rest; + result_map.insert(key.to_string(), value); + } + + Ok((ObjectType::from_map(result_map).to_value(), remaining_data)) + } + ShankIdlType::BTreeMap(map) => { + 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, &map.b_tree_map.0, idl_types)?; + let (value, rest) = + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(rest, &map.b_tree_map.1, idl_types)?; + remaining_data = rest; + result_map.insert(key.to_string(), value); + } + + Ok((ObjectType::from_map(result_map).to_value(), remaining_data)) + } + ShankIdlType::HashSet(set) => { + 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, &set.hash_set, idl_types)?; + values.push(value); + remaining_data = rest; + } + + Ok((Value::array(values), remaining_data)) + } + ShankIdlType::BTreeSet(set) => { + 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, &set.b_tree_set, 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: &ShankIdlType) -> Result { + match ty { + ShankIdlType::Bool | ShankIdlType::U8 | ShankIdlType::I8 => Ok(1), + ShankIdlType::U16 | ShankIdlType::I16 => Ok(2), + ShankIdlType::U32 | ShankIdlType::I32 => Ok(4), + ShankIdlType::U64 | ShankIdlType::I64 => Ok(8), + ShankIdlType::U128 | ShankIdlType::I128 => Ok(16), + ShankIdlType::PublicKey => Ok(32), + ShankIdlType::Array(arr) => { + let inner_size = get_shank_type_size(&arr.array.0)?; + Ok(inner_size * arr.array.1) + } + _ => 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: &ShankIdlType, + idl_types: &[ShankIdlTypeDef], +) -> 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 { + ShankIdlType::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")), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::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)), + ShankIdlType::Bytes => Ok(value.to_be_bytes().clone()), + ShankIdlType::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")), + ShankIdlType::PublicKey => SvmValue::to_pubkey(value) + .map_err(|_| mismatch_err("pubkey")) + .map(|p| borsh::to_vec(&p))? + .map_err(|e| encode_err("pubkey", &e)), + ShankIdlType::Option(opt) => { + if value.as_null().is_some() { + Ok(vec![0u8]) // None discriminator + } else { + let encoded_inner = borsh_encode_value_to_shank_idl_type(value, &opt.option, idl_types)?; + let mut result = vec![1u8]; // Some discriminator + result.extend(encoded_inner); + Ok(result) + } + } + ShankIdlType::FixedSizeOption(opt) => { + if value.as_null().is_some() { + if let Some(ref sentinel_bytes) = opt.fixed_size_option.sentinel { + Ok(sentinel_bytes.clone()) + } else { + let size = get_shank_type_size(&opt.fixed_size_option.inner)?; + Ok(vec![0u8; size]) + } + } else { + borsh_encode_value_to_shank_idl_type(value, &opt.fixed_size_option.inner, idl_types) + } + } + ShankIdlType::Vec(vec) => match value { + Value::String(_) => { + let bytes = value.get_buffer_bytes_result().map_err(|_| mismatch_err("vec"))?; + borsh_encode_bytes_to_shank_idl_type(&bytes, &vec.vec, 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, &vec.vec, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + _ => Err(mismatch_err("vec")), + }, + ShankIdlType::Array(arr) => { + let array = value.as_array().ok_or(mismatch_err("array"))?; + let expected_len = arr.array.1; + 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, &arr.array.0, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + ShankIdlType::Tuple(tuple) => { + let array = value.as_array().ok_or(mismatch_err("tuple"))?; + if array.len() != tuple.tuple.len() { + return Err(format!( + "invalid value for idl type: expected tuple length of {}, found {}", + tuple.tuple.len(), + array.len() + )); + } + let mut result = vec![]; + for (v, ty) in array.iter().zip(tuple.tuple.iter()) { + let encoded = borsh_encode_value_to_shank_idl_type(v, ty, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + ShankIdlType::Defined(def) => { + let typing = idl_types + .iter() + .find(|t| t.name == def.defined) + .ok_or_else(|| format!("unable to find type definition for {} in idl", def.defined))?; + + borsh_encode_value_to_shank_idl_type_def_ty(value, &typing.ty, idl_types) + } + ShankIdlType::HashMap(map) => { + 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, &map.hash_map.0, idl_types)?; + let encoded_value = borsh_encode_value_to_shank_idl_type(v, &map.hash_map.1, idl_types)?; + result.extend(encoded_key); + result.extend(encoded_value); + } + Ok(result) + } + ShankIdlType::BTreeMap(map) => { + 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, &map.b_tree_map.0, idl_types)?; + let encoded_value = borsh_encode_value_to_shank_idl_type(v, &map.b_tree_map.1, idl_types)?; + result.extend(encoded_key); + result.extend(encoded_value); + } + Ok(result) + } + ShankIdlType::HashSet(set) => { + 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, &set.hash_set, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + ShankIdlType::BTreeSet(set) => { + 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, &set.b_tree_set, idl_types)?; + result.extend(encoded); + } + Ok(result) + } + } +} + +fn borsh_encode_value_to_shank_idl_type_def_ty( + value: &Value, + idl_type_def_ty: &ShankIdlTypeDefTy, + idl_types: &[ShankIdlTypeDef], +) -> Result, String> { + match idl_type_def_ty { + ShankIdlTypeDefTy::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) + } + ShankIdlTypeDefTy::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(ShankEnumFields::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(ShankEnumFields::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: &ShankIdlType, + _idl_types: &[ShankIdlTypeDef], +) -> Result, String> { + match idl_type { + ShankIdlType::U8 => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for u8, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::U16 => { + if bytes.len() != 2 { + return Err(format!("expected 2 bytes for u16, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::U32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for u32, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::U64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for u64, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::U128 => { + if bytes.len() != 16 { + return Err(format!("expected 16 bytes for u128, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::I8 => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for i8, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::I16 => { + if bytes.len() != 2 { + return Err(format!("expected 2 bytes for i16, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::I32 => { + if bytes.len() != 4 { + return Err(format!("expected 4 bytes for i32, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::I64 => { + if bytes.len() != 8 { + return Err(format!("expected 8 bytes for i64, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::I128 => { + if bytes.len() != 16 { + return Err(format!("expected 16 bytes for i128, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::Bool => { + if bytes.len() != 1 { + return Err(format!("expected 1 byte for bool, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::PublicKey => { + if bytes.len() != 32 { + return Err(format!("expected 32 bytes for Pubkey, found {}", bytes.len())); + } + Ok(bytes.to_vec()) + } + ShankIdlType::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)) + } + ShankIdlType::Bytes => Ok(bytes.to_vec()), + ShankIdlType::Vec(vec) => { + match &*vec.vec { + ShankIdlType::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", + vec.vec + )) + } + } + ShankIdlType::Array(arr) => { + match &*arr.array.0 { + ShankIdlType::U8 => { + let expected_len = arr.array.1; + 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 [{:?}; {}]", + arr.array.0, arr.array.1 + )) + } + } + _ => Err(format!("cannot convert raw bytes to {:?}", idl_type)), + } +} diff --git a/addons/svm/types/src/subgraph/tests.rs b/addons/svm/types/src/subgraph/tests.rs index 75a7646cd..3920392c6 100644 --- a/addons/svm/types/src/subgraph/tests.rs +++ b/addons/svm/types/src/subgraph/tests.rs @@ -1,4 +1,16 @@ -use crate::{subgraph::idl::parse_bytes_to_value_with_expected_idl_type_def_ty, SvmValue, SVM_U64}; +use crate::{ + subgraph::{ + idl::parse_bytes_to_value_with_expected_idl_type_def_ty, + 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, ShankIdlConst, ShankIdlType, ShankIdlTypeArray, + ShankIdlTypeDef, ShankIdlTypeDefined, ShankIdlTypeOption, ShankIdlTypeVec, + }, + }, + SvmValue, SVM_PUBKEY, SVM_U64, +}; use super::*; use anchor_lang_idl::types::{ @@ -917,3 +929,611 @@ 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!("../test_fixtures/shank_test_idl.json").to_vec()) + .unwrap(); + pub static ref SHANK_TYPES: Vec = extract_shank_types(&SHANK_IDL).unwrap(); + 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(ShankIdlTypeOption { 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(ShankIdlTypeOption { 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(ShankIdlTypeVec { 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(ShankIdlTypeVec { 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(ShankIdlTypeArray { 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(ShankIdlTypeArray { 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(ShankIdlTypeOption { 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(ShankIdlTypeVec { 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(ShankIdlTypeArray { 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(ShankIdlTypeDefined { 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(ShankIdlTypeOption { 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(ShankIdlTypeOption { 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(ShankIdlTypeOption { 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(ShankIdlTypeVec { 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(ShankIdlTypeArray { 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(ShankIdlTypeDefined { 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(ShankIdlTypeDefined { 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(ShankIdlTypeDefined { 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(ShankIdlTypeDefined { 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(ShankIdlTypeVec { 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(ShankIdlTypeDefined { 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(ShankIdlTypeArray { + 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(ShankIdlTypeDefined { 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).unwrap(); + 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/test_fixtures/shank_test_idl.json b/addons/svm/types/src/test_fixtures/shank_test_idl.json new file mode 100644 index 000000000..5c7155601 --- /dev/null +++ b/addons/svm/types/src/test_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" + } +} From 688df58cbcf9464cf1ba5007ea4e86cf5bfdadb9 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 08:47:14 -0500 Subject: [PATCH 02/12] feat: update IDL handling and discriminator mapping for shank IDLs - Refactor IDL reference handling in deploy_program.rs to use IdlKind. - Enhance shank.rs with functions for discriminator-based account detection, including find_account_discriminator_const and build_discriminator_mapping. - Implement find_account_by_discriminator to identify account types based on discriminator values. - Add tests for new discriminator functionality and ensure correct behavior with various IDL structures. - Bump version numbers for affected crates: txtx-addon-network-svm-types to 0.3.17, txtx-addon-kit to 0.4.12, and txtx-core to 0.4.15. --- addons/svm/core/src/codec/idl/mod.rs | 206 ++--- addons/svm/core/src/codec/mod.rs | 12 +- addons/svm/core/src/codec/native.rs | 3 +- .../svm/core/src/commands/deploy_program.rs | 13 +- addons/svm/types/src/subgraph/shank.rs | 748 +++++++++++++++--- 5 files changed, 775 insertions(+), 207 deletions(-) diff --git a/addons/svm/core/src/codec/idl/mod.rs b/addons/svm/core/src/codec/idl/mod.rs index 3714d80ec..0fe644822 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -10,6 +10,9 @@ use anchor_lang_idl::types::{ IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy, }; use convert_idl::classic_idl_to_anchor_idl; +use log::debug; +use serde::Deserialize; +use serde::Serialize; use shank_idl::idl::Idl as ShankIdl; use solana_pubkey::Pubkey; use std::fmt::Display; @@ -22,13 +25,23 @@ use txtx_addon_network_svm_types::I256; use txtx_addon_network_svm_types::U256; /// Represents the kind of IDL format being used. -#[derive(Debug, Clone)] +#[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)), + } + } +} #[derive(Debug, Clone)] pub struct IdlRef { @@ -94,6 +107,10 @@ impl IdlRef { } } + pub fn idl(&self) -> &IdlKind { + &self.idl + } + pub fn get_program_pubkey(&self) -> Result { let address = match &self.idl { IdlKind::Anchor(idl) => idl.address.clone(), @@ -110,23 +127,17 @@ impl IdlRef { pub fn get_discriminator(&self, instruction_name: &str) -> Result, Diagnostic> { 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") - })?; + 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") - })?; + 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]) } @@ -135,16 +146,14 @@ impl IdlRef { pub fn get_instruction(&self, instruction_name: &str) -> Result<&IdlInstruction, Diagnostic> { match &self.idl { - IdlKind::Anchor(idl) => idl - .instructions - .iter() - .find(|i| i.name == instruction_name) - .ok_or_else(|| { + IdlKind::Anchor(idl) => { + idl.instructions.iter().find(|i| i.name == instruction_name).ok_or_else(|| { diagnosed_error!("instruction '{instruction_name}' not found in IDL") - }), - IdlKind::Shank(_) => Err(diagnosed_error!( - "get_instruction is not supported for Shank IDL" - )), + }) + } + IdlKind::Shank(_) => { + Err(diagnosed_error!("get_instruction is not supported for Shank IDL")) + } } } @@ -167,13 +176,10 @@ impl IdlRef { ) -> Result>, Diagnostic> { 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") - })?; + 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", @@ -192,27 +198,23 @@ impl IdlRef { 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| { + 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") - })?; + 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", @@ -232,18 +234,19 @@ impl IdlRef { 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 - ) - }, - )?; + 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) @@ -259,13 +262,10 @@ impl IdlRef { ) -> Result, Diagnostic> { 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") - })?; + 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", @@ -284,27 +284,23 @@ impl IdlRef { 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| { + 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) } 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") - })?; + 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", @@ -323,18 +319,19 @@ impl IdlRef { 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 - ) - }, - )?; + 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) @@ -345,13 +342,19 @@ impl IdlRef { fn parse_idl_string(idl_str: &str) -> Result { // Try parsing as Anchor IDL first (modern format) - if let Ok(anchor_idl) = serde_json::from_str::(idl_str) { - return Ok(IdlKind::Anchor(anchor_idl)); + 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); + } } // Try parsing as Shank IDL - if let Ok(shank_idl) = serde_json::from_str::(idl_str) { - return Ok(IdlKind::Shank(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); + } } // Try parsing as classic/legacy Anchor IDL and convert to modern format @@ -572,7 +575,9 @@ pub fn borsh_encode_value_to_idl_type( // 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") { + 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!( @@ -595,12 +600,13 @@ pub fn borsh_encode_value_to_idl_type( 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(), - ) - })?; + 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) }; @@ -932,12 +938,10 @@ fn borsh_encode_bytes_to_idl_type( name )) } - IdlType::Generic(name) => { - Err(format!( - "cannot convert raw bytes to generic type '{}'; type must be resolved first", - 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)), } } diff --git a/addons/svm/core/src/codec/mod.rs b/addons/svm/core/src/codec/mod.rs index cf1c7095e..e316f11c0 100644 --- a/addons/svm/core/src/codec/mod.rs +++ b/addons/svm/core/src/codec/mod.rs @@ -7,6 +7,7 @@ pub mod squads; pub mod ui_encode; pub mod utils; +use crate::codec::idl::IdlKind; use crate::codec::ui_encode::get_formatted_transaction_meta_description; use crate::codec::ui_encode::message_to_formatted_tx; use crate::codec::utils::wait_n_slots; @@ -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 f867f2ec7..b81c7cbb2 100644 --- a/addons/svm/core/src/codec/native.rs +++ b/addons/svm/core/src/codec/native.rs @@ -151,8 +151,7 @@ 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_ref = - IdlRef::from_str(idl_str).map_err(|e| diagnosed_error!("{e}"))?; + 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 f11264fb1..466a45959 100644 --- a/addons/svm/core/src/commands/deploy_program.rs +++ b/addons/svm/core/src/commands/deploy_program.rs @@ -35,7 +35,7 @@ use txtx_addon_kit::types::{ConstructDid, Did}; use txtx_addon_kit::uuid::Uuid; use txtx_addon_network_svm_types::{SVM_KEYPAIR, SVM_PUBKEY}; -use crate::codec::idl::IdlRef; +use crate::codec::idl::IdlKind; use crate::codec::send_transaction::send_transaction_background_task; use crate::codec::utils::cheatcode_deploy_program; use crate::codec::{DeploymentTransaction, ProgramArtifacts, UpgradeableProgramDeployer}; @@ -968,15 +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 = match idl_ref.kind() { - crate::codec::idl::IdlKind::Anchor(anchor_idl) => { - serde_json::to_value(anchor_idl).unwrap() - } - crate::codec::idl::IdlKind::Shank(shank_idl) => { - serde_json::to_value(shank_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/types/src/subgraph/shank.rs b/addons/svm/types/src/subgraph/shank.rs index a02d4b75f..b93b5aa58 100644 --- a/addons/svm/types/src/subgraph/shank.rs +++ b/addons/svm/types/src/subgraph/shank.rs @@ -195,18 +195,188 @@ pub fn extract_shank_types(idl: &shank_idl::idl::Idl) -> Result Result { - let instruction = idl.instructions.iter() +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))?; + let arg = instruction.args.get(arg_index).ok_or_else(|| { + format!("argument {} not found in instruction '{}'", arg_index, instruction_name) + })?; let arg_json = serde_json::to_string(&arg.ty) .map_err(|e| format!("failed to serialize arg type: {}", e))?; - serde_json::from_str(&arg_json) - .map_err(|e| format!("failed to deserialize arg type: {}", e)) + serde_json::from_str(&arg_json).map_err(|e| format!("failed to deserialize arg type: {}", e)) +} + +// ============================================================================ +// 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" { + // The value is typically a quoted string like "\"AccountType\"" + // Strip the quotes if present + 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).ok()?; + + // 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 { + ShankIdlTypeDefTy::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: &[ShankIdlField], + enum_type_name: &str, + idl_types: &[ShankIdlTypeDef], +) -> Result { + let mut offset = 0; + for field in fields { + // Check if this field is the discriminator enum type + if let ShankIdlType::Defined(def) = &field.ty { + if def.defined == 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).ok()?; + + // 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 { + ShankIdlTypeDefTy::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 } // ============================================================================ @@ -237,9 +407,11 @@ pub fn shank_idl_type_to_txtx_type( ShankIdlType::Option(opt) => { Type::typed_null(shank_idl_type_to_txtx_type(&opt.option, idl_types, _idl_constants)?) } - ShankIdlType::FixedSizeOption(opt) => { - Type::typed_null(shank_idl_type_to_txtx_type(&opt.fixed_size_option.inner, idl_types, _idl_constants)?) - } + ShankIdlType::FixedSizeOption(opt) => Type::typed_null(shank_idl_type_to_txtx_type( + &opt.fixed_size_option.inner, + idl_types, + _idl_constants, + )?), ShankIdlType::Vec(vec) => { Type::array(shank_idl_type_to_txtx_type(&vec.vec, idl_types, _idl_constants)?) } @@ -272,11 +444,13 @@ pub fn shank_idl_type_to_txtx_type( )? } ShankIdlType::HashMap(map) => { - let value_type = shank_idl_type_to_txtx_type(&map.hash_map.1, idl_types, _idl_constants)?; + let value_type = + shank_idl_type_to_txtx_type(&map.hash_map.1, idl_types, _idl_constants)?; Type::array(value_type) } ShankIdlType::BTreeMap(map) => { - let value_type = shank_idl_type_to_txtx_type(&map.b_tree_map.1, idl_types, _idl_constants)?; + let value_type = + shank_idl_type_to_txtx_type(&map.b_tree_map.1, idl_types, _idl_constants)?; Type::array(value_type) } ShankIdlType::HashSet(set) => { @@ -299,10 +473,10 @@ pub fn get_expected_type_from_shank_idl_type_def_ty( ShankIdlTypeDefTy::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), - )?; + 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, @@ -346,8 +520,7 @@ fn get_expected_type_from_shank_enum_fields( ShankEnumFields::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)?; + let field_type = shank_idl_type_to_txtx_type(&field.ty, idl_types, idl_constants)?; props.push(ObjectProperty { documentation: "".into(), typing: field_type, @@ -387,8 +560,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_def_ty( expected_type: &ShankIdlTypeDefTy, idl_types: &[ShankIdlTypeDef], ) -> Result { - let (value, rest) = - parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes(data, expected_type, idl_types)?; + 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", @@ -435,8 +611,11 @@ fn parse_bytes_to_shank_struct_with_leftover_bytes<'a>( 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)?; + 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); } @@ -467,8 +646,11 @@ fn parse_bytes_to_shank_enum_variant_with_leftover_bytes<'a>( 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)?; + 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); } @@ -495,21 +677,27 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( ShankIdlType::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))?)), + SvmValue::u16(u16::from_le_bytes( + <[u8; 2]>::try_from(v).map_err(|e| err("u16", &e))?, + )), rest, )) } ShankIdlType::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))?)), + SvmValue::u32(u32::from_le_bytes( + <[u8; 4]>::try_from(v).map_err(|e| err("u32", &e))?, + )), rest, )) } ShankIdlType::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))?)), + SvmValue::u64(u64::from_le_bytes( + <[u8; 8]>::try_from(v).map_err(|e| err("u64", &e))?, + )), rest, )) } @@ -529,21 +717,27 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( ShankIdlType::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))?)), + SvmValue::i16(i16::from_le_bytes( + <[u8; 2]>::try_from(v).map_err(|e| err("i16", &e))?, + )), rest, )) } ShankIdlType::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))?)), + SvmValue::i32(i32::from_le_bytes( + <[u8; 4]>::try_from(v).map_err(|e| err("i32", &e))?, + )), rest, )) } ShankIdlType::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))?)), + SvmValue::i64(i64::from_le_bytes( + <[u8; 8]>::try_from(v).map_err(|e| err("i64", &e))?, + )), rest, )) } @@ -569,7 +763,8 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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_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)) } @@ -586,12 +781,17 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( if is_some[0] == 0 { Ok((Value::null(), rest)) } else { - parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(rest, &opt.option, idl_types) + parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + rest, + &opt.option, + idl_types, + ) } } ShankIdlType::FixedSizeOption(opt) => { let inner_size = get_shank_type_size(&opt.fixed_size_option.inner)?; - let (inner_bytes, rest) = data.split_at_checked(inner_size).ok_or(bytes_err("fixed_size_option"))?; + let (inner_bytes, rest) = + data.split_at_checked(inner_size).ok_or(bytes_err("fixed_size_option"))?; if let Some(ref sentinel_bytes) = opt.fixed_size_option.sentinel { if inner_bytes == sentinel_bytes.as_slice() { @@ -599,7 +799,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( } } - let (value, _) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(inner_bytes, &opt.fixed_size_option.inner, idl_types)?; + let (value, _) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + inner_bytes, + &opt.fixed_size_option.inner, + idl_types, + )?; Ok((value, rest)) } ShankIdlType::Vec(vec) => { @@ -612,8 +816,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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, &vec.vec, idl_types)?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &vec.vec, + idl_types, + )?; vec_values.push(value); remaining_data = rest; } @@ -625,8 +832,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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, &arr.array.0, idl_types)?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &arr.array.0, + idl_types, + )?; vec_values.push(value); remaining_data = rest; } @@ -636,8 +846,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( let mut values = Vec::with_capacity(tuple.tuple.len()); let mut remaining_data = data; for ty in &tuple.tuple { - let (value, rest) = - parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(remaining_data, ty, idl_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; } @@ -665,10 +878,16 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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, &map.hash_map.0, idl_types)?; - let (value, rest) = - parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(rest, &map.hash_map.1, idl_types)?; + let (key, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &map.hash_map.0, + idl_types, + )?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + rest, + &map.hash_map.1, + idl_types, + )?; remaining_data = rest; result_map.insert(key.to_string(), value); } @@ -685,10 +904,16 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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, &map.b_tree_map.0, idl_types)?; - let (value, rest) = - parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes(rest, &map.b_tree_map.1, idl_types)?; + let (key, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &map.b_tree_map.0, + idl_types, + )?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + rest, + &map.b_tree_map.1, + idl_types, + )?; remaining_data = rest; result_map.insert(key.to_string(), value); } @@ -705,8 +930,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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, &set.hash_set, idl_types)?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &set.hash_set, + idl_types, + )?; values.push(value); remaining_data = rest; } @@ -723,8 +951,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( 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, &set.b_tree_set, idl_types)?; + let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( + remaining_data, + &set.b_tree_set, + idl_types, + )?; values.push(value); remaining_data = rest; } @@ -736,6 +967,15 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( /// Returns the size in bytes of a Shank IDL type (for fixed-size types). fn get_shank_type_size(ty: &ShankIdlType) -> 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: &ShankIdlType, + idl_types: &[ShankIdlTypeDef], +) -> Result { match ty { ShankIdlType::Bool | ShankIdlType::U8 | ShankIdlType::I8 => Ok(1), ShankIdlType::U16 | ShankIdlType::I16 => Ok(2), @@ -744,9 +984,32 @@ fn get_shank_type_size(ty: &ShankIdlType) -> Result { ShankIdlType::U128 | ShankIdlType::I128 => Ok(16), ShankIdlType::PublicKey => Ok(32), ShankIdlType::Array(arr) => { - let inner_size = get_shank_type_size(&arr.array.0)?; + let inner_size = get_shank_type_size_with_types(&arr.array.0, idl_types)?; Ok(inner_size * arr.array.1) } + ShankIdlType::Defined(def) => { + let type_def = idl_types + .iter() + .find(|t| t.name == def.defined) + .ok_or_else(|| format!("cannot determine fixed size for type {:?}", ty))?; + match &type_def.ty { + ShankIdlTypeDefTy::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: {}", def.defined)) + } + } + ShankIdlTypeDefTy::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)), } } @@ -762,11 +1025,7 @@ pub fn borsh_encode_value_to_shank_idl_type( idl_types: &[ShankIdlTypeDef], ) -> Result, String> { let mismatch_err = |expected: &str| { - format!( - "invalid value for idl type: expected {}, found {:?}", - expected, - value.get_type() - ) + 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) @@ -774,7 +1033,9 @@ pub fn borsh_encode_value_to_shank_idl_type( // 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::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) } @@ -831,7 +1092,8 @@ pub fn borsh_encode_value_to_shank_idl_type( if value.as_null().is_some() { Ok(vec![0u8]) // None discriminator } else { - let encoded_inner = borsh_encode_value_to_shank_idl_type(value, &opt.option, idl_types)?; + let encoded_inner = + borsh_encode_value_to_shank_idl_type(value, &opt.option, idl_types)?; let mut result = vec![1u8]; // Some discriminator result.extend(encoded_inner); Ok(result) @@ -898,10 +1160,9 @@ pub fn borsh_encode_value_to_shank_idl_type( Ok(result) } ShankIdlType::Defined(def) => { - let typing = idl_types - .iter() - .find(|t| t.name == def.defined) - .ok_or_else(|| format!("unable to find type definition for {} in idl", def.defined))?; + let typing = idl_types.iter().find(|t| t.name == def.defined).ok_or_else(|| { + format!("unable to find type definition for {} in idl", def.defined) + })?; borsh_encode_value_to_shank_idl_type_def_ty(value, &typing.ty, idl_types) } @@ -910,8 +1171,10 @@ pub fn borsh_encode_value_to_shank_idl_type( 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, &map.hash_map.0, idl_types)?; - let encoded_value = borsh_encode_value_to_shank_idl_type(v, &map.hash_map.1, idl_types)?; + let encoded_key = + borsh_encode_value_to_shank_idl_type(&key_value, &map.hash_map.0, idl_types)?; + let encoded_value = + borsh_encode_value_to_shank_idl_type(v, &map.hash_map.1, idl_types)?; result.extend(encoded_key); result.extend(encoded_value); } @@ -922,8 +1185,10 @@ pub fn borsh_encode_value_to_shank_idl_type( 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, &map.b_tree_map.0, idl_types)?; - let encoded_value = borsh_encode_value_to_shank_idl_type(v, &map.b_tree_map.1, idl_types)?; + let encoded_key = + borsh_encode_value_to_shank_idl_type(&key_value, &map.b_tree_map.0, idl_types)?; + let encoded_value = + borsh_encode_value_to_shank_idl_type(v, &map.b_tree_map.1, idl_types)?; result.extend(encoded_key); result.extend(encoded_value); } @@ -958,16 +1223,17 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( match idl_type_def_ty { ShankIdlTypeDefTy::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()))?; + 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))?; + 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) @@ -980,21 +1246,21 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( // 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() - })?; + 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() - })?; + let (variant_name, variant_value) = + enum_value.iter().next().ok_or_else(|| "empty object".to_string())?; (variant_name.clone(), variant_value.clone()) }; @@ -1008,21 +1274,22 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( match &expected_variant.fields { Some(ShankEnumFields::Named(fields)) => { - let user_values_map = enum_variant_value.as_object().ok_or_else(|| { - format!("expected object for enum variant 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)?; + let field_encoded = + borsh_encode_value_to_shank_idl_type(user_value, &field.ty, idl_types)?; encoded.extend(field_encoded); } } Some(ShankEnumFields::Tuple(types)) => { - let values = enum_variant_value.as_array().ok_or_else(|| { - format!("expected array for enum tuple variant") - })?; + 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 {}", @@ -1162,3 +1429,300 @@ fn borsh_encode_bytes_to_shank_idl_type( _ => 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); + } +} From e29a65906dac81ffcab2336d2193df66fd51ade8 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 11:38:31 -0500 Subject: [PATCH 03/12] move idl types around --- addons/svm/core/src/codec/idl/mod.rs | 24 ++--------------- addons/svm/core/src/codec/mod.rs | 2 +- addons/svm/core/src/codec/native.rs | 3 ++- .../svm/core/src/commands/deploy_program.rs | 2 +- .../setup_surfnet/cheatcode_deploy_program.rs | 4 +-- addons/svm/core/src/templates/mod.rs | 7 ++--- addons/svm/types/src/subgraph/event.rs | 5 ++-- .../src/subgraph/{idl.rs => idl/anchor.rs} | 0 addons/svm/types/src/subgraph/idl/mod.rs | 26 +++++++++++++++++++ .../svm/types/src/subgraph/{ => idl}/shank.rs | 0 addons/svm/types/src/subgraph/mod.rs | 15 ++++++++--- addons/svm/types/src/subgraph/pda.rs | 2 +- addons/svm/types/src/subgraph/tests.rs | 4 +-- .../svm/types/src/subgraph/token_account.rs | 2 +- 14 files changed, 57 insertions(+), 39 deletions(-) rename addons/svm/types/src/subgraph/{idl.rs => idl/anchor.rs} (100%) create mode 100644 addons/svm/types/src/subgraph/idl/mod.rs rename addons/svm/types/src/subgraph/{ => idl}/shank.rs (100%) diff --git a/addons/svm/core/src/codec/idl/mod.rs b/addons/svm/core/src/codec/idl/mod.rs index 0fe644822..b06045f85 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -11,38 +11,18 @@ use anchor_lang_idl::types::{ }; use convert_idl::classic_idl_to_anchor_idl; use log::debug; -use serde::Deserialize; -use serde::Serialize; 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::subgraph::shank::{ +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; use txtx_addon_network_svm_types::I256; use txtx_addon_network_svm_types::U256; -/// 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)), - } - } -} - #[derive(Debug, Clone)] pub struct IdlRef { pub idl: IdlKind, diff --git a/addons/svm/core/src/codec/mod.rs b/addons/svm/core/src/codec/mod.rs index e316f11c0..a04d29d0b 100644 --- a/addons/svm/core/src/codec/mod.rs +++ b/addons/svm/core/src/codec/mod.rs @@ -7,7 +7,6 @@ pub mod squads; pub mod ui_encode; pub mod utils; -use crate::codec::idl::IdlKind; use crate::codec::ui_encode::get_formatted_transaction_meta_description; use crate::codec::ui_encode::message_to_formatted_tx; use crate::codec::utils::wait_n_slots; @@ -33,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; diff --git a/addons/svm/core/src/codec/native.rs b/addons/svm/core/src/codec/native.rs index b81c7cbb2..fafa2fb1a 100644 --- a/addons/svm/core/src/codec/native.rs +++ b/addons/svm/core/src/codec/native.rs @@ -9,10 +9,11 @@ use txtx_addon_kit::{ types::{ObjectType, Value}, }, }; +use txtx_addon_network_svm_types::subgraph::idl::IdlKind; use crate::typing::SvmValue; -use super::idl::{IdlKind, IdlRef}; +use super::idl::IdlRef; pub struct NativeProgramArtifacts { /// The binary of the native program, stored for a native project at `target/deploy/.so`. diff --git a/addons/svm/core/src/commands/deploy_program.rs b/addons/svm/core/src/commands/deploy_program.rs index 466a45959..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::IdlKind; use crate::codec::send_transaction::send_transaction_background_task; use crate::codec::utils::cheatcode_deploy_program; use crate::codec::{DeploymentTransaction, ProgramArtifacts, UpgradeableProgramDeployer}; 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 daeb84c7c..10742f06e 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 @@ -12,10 +12,10 @@ use txtx_addon_kit::{ AuthorizationContext, }, }; -use txtx_addon_network_svm_types::SvmValue; +use txtx_addon_network_svm_types::{subgraph::idl::IdlKind, SvmValue}; use crate::{ - codec::{idl::IdlKind, utils::cheatcode_deploy_program, validate_program_so}, + codec::{utils::cheatcode_deploy_program, validate_program_so}, constants::DEPLOY_PROGRAM, }; diff --git a/addons/svm/core/src/templates/mod.rs b/addons/svm/core/src/templates/mod.rs index e8d9da8ed..90ea6f845 100644 --- a/addons/svm/core/src/templates/mod.rs +++ b/addons/svm/core/src/templates/mod.rs @@ -153,9 +153,10 @@ pub fn get_interpolated_anchor_subgraph_template( let idl_ref = IdlRef::from_str(idl_str).map_err(|e| format!("failed to parse program idl: {e}"))?; - let idl = idl_ref.as_anchor().ok_or_else(|| { - "expected Anchor IDL for subgraph template, but found a different IDL format".to_string() - })?; + 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/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/idl.rs b/addons/svm/types/src/subgraph/idl/anchor.rs similarity index 100% rename from addons/svm/types/src/subgraph/idl.rs rename to addons/svm/types/src/subgraph/idl/anchor.rs 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/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs similarity index 100% rename from addons/svm/types/src/subgraph/shank.rs rename to addons/svm/types/src/subgraph/idl/shank.rs diff --git a/addons/svm/types/src/subgraph/mod.rs b/addons/svm/types/src/subgraph/mod.rs index 3d522b296..012370db6 100644 --- a/addons/svm/types/src/subgraph/mod.rs +++ b/addons/svm/types/src/subgraph/mod.rs @@ -22,7 +22,6 @@ use txtx_addon_kit::{ mod event; pub mod idl; mod pda; -pub mod shank; pub mod token_account; pub use event::EventSubgraphSource; @@ -30,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, @@ -255,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 3920392c6..029abf7a8 100644 --- a/addons/svm/types/src/subgraph/tests.rs +++ b/addons/svm/types/src/subgraph/tests.rs @@ -1,7 +1,7 @@ use crate::{ subgraph::{ - idl::parse_bytes_to_value_with_expected_idl_type_def_ty, - shank::{ + 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, 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, From 60d565b201bc7c6ba40e46a8022f6ab6ba6b9ad7 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 14:03:25 -0500 Subject: [PATCH 04/12] move anchor helpers to surfpool-types --- addons/svm/core/src/codec/idl/mod.rs | 605 +------------------- addons/svm/types/src/subgraph/idl/anchor.rs | 591 ++++++++++++++++++- 2 files changed, 590 insertions(+), 606 deletions(-) diff --git a/addons/svm/core/src/codec/idl/mod.rs b/addons/svm/core/src/codec/idl/mod.rs index b06045f85..5fd0eeae0 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -4,24 +4,18 @@ use std::str::FromStr; use crate::typing::anchor as anchor_lang_idl; use crate::typing::shank as shank_idl; -use crate::typing::SvmValue; -use anchor_lang_idl::types::{ - Idl as AnchorIdl, IdlArrayLen, IdlDefinedFields, IdlGenericArg, IdlInstruction, IdlType, - IdlTypeDef, IdlTypeDefGeneric, IdlTypeDefTy, -}; +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::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; -use txtx_addon_network_svm_types::I256; -use txtx_addon_network_svm_types::U256; #[derive(Debug, Clone)] pub struct IdlRef { @@ -137,16 +131,6 @@ impl IdlRef { } } - pub fn get_types(&self) -> Vec { - match &self.idl { - IdlKind::Anchor(idl) => idl.types.clone(), - IdlKind::Shank(_) => { - // Shank types are not directly compatible with Anchor IdlTypeDef - vec![] - } - } - } - /// 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( @@ -367,588 +351,3 @@ fn parse_idl_bytes(idl_bytes: &[u8]) -> Result { Err(e) => Err(diagnosed_error!("invalid idl: {e}")), } } - -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/anchor.rs b/addons/svm/types/src/subgraph/idl/anchor.rs index 9931bac75..41b7a1aeb 100644 --- a/addons/svm/types/src/subgraph/idl/anchor.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()) +} From 9ab2041b83efcd307f128a8374e2bb455e102838 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 14:03:31 -0500 Subject: [PATCH 05/12] add todo --- addons/svm/core/src/codec/idl/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/svm/core/src/codec/idl/mod.rs b/addons/svm/core/src/codec/idl/mod.rs index 5fd0eeae0..8bfd7a2bf 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -125,6 +125,8 @@ impl IdlRef { 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")) } From 14039b52b83963e255057056f5c9c2b9a2773e80 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 16:02:19 -0500 Subject: [PATCH 06/12] fix cheatcode_deploy_program bug introduced by claude --- .../commands/setup_surfnet/cheatcode_deploy_program.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 10742f06e..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 @@ -165,16 +165,10 @@ impl SurfpoolDeployProgram { ) .await?; if let Some(idl) = &program_deployment.idl { - let idl_str = match idl { - IdlKind::Anchor(anchor_idl) => serde_json::to_string(anchor_idl), - IdlKind::Shank(shank_idl) => serde_json::to_string(shank_idl), - } - .map_err(|e| diagnosed_error!("failed to serialize idl for rpc call: {e}"))?; - rpc_client .send::( RpcRequest::Custom { method: "surfnet_registerIdl" }, - json!([idl_str]), + json!([idl.to_json_value().unwrap()]), ) .await .map_err(|e| diagnosed_error!("failed to register idl via rpc call: {e}"))?; From a816705f65d95d9a3337f3bc29e92cc30095836e Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 16:07:53 -0500 Subject: [PATCH 07/12] move test fixture --- .../{test_fixtures => subgraph/fixtures}/shank_test_idl.json | 0 addons/svm/types/src/subgraph/tests.rs | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) rename addons/svm/types/src/{test_fixtures => subgraph/fixtures}/shank_test_idl.json (100%) diff --git a/addons/svm/types/src/test_fixtures/shank_test_idl.json b/addons/svm/types/src/subgraph/fixtures/shank_test_idl.json similarity index 100% rename from addons/svm/types/src/test_fixtures/shank_test_idl.json rename to addons/svm/types/src/subgraph/fixtures/shank_test_idl.json diff --git a/addons/svm/types/src/subgraph/tests.rs b/addons/svm/types/src/subgraph/tests.rs index 029abf7a8..ec3115667 100644 --- a/addons/svm/types/src/subgraph/tests.rs +++ b/addons/svm/types/src/subgraph/tests.rs @@ -936,8 +936,7 @@ fn test_bad_data(bad_data: Vec, expected_type: IdlTypeDefTy, expected_err: & lazy_static! { pub static ref SHANK_IDL: shank_idl::idl::Idl = - serde_json::from_slice(&include_bytes!("../test_fixtures/shank_test_idl.json").to_vec()) - .unwrap(); + 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).unwrap(); pub static ref SHANK_CONSTANTS: Vec = vec![]; } From e2a20143ae66b110311095cdc11c97e59f06234f Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 16:20:39 -0500 Subject: [PATCH 08/12] remove irrelevant comment --- addons/svm/types/src/subgraph/idl/shank.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/addons/svm/types/src/subgraph/idl/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs index b93b5aa58..420549aed 100644 --- a/addons/svm/types/src/subgraph/idl/shank.rs +++ b/addons/svm/types/src/subgraph/idl/shank.rs @@ -1,6 +1,5 @@ //! Shank IDL codec functions for converting between Shank IDL types, txtx types, and bytes. //! -//! This module provides functions similar to those in `idl.rs` but specifically for Shank IDLs. //! Since shank_idl doesn't export its internal types directly, we define local types that mirror //! the shank_idl structure for use in function signatures. @@ -305,10 +304,7 @@ fn find_discriminator_field_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 - )) + 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. @@ -333,11 +329,8 @@ pub fn find_account_by_discriminator(idl: &shank_idl::idl::Idl, data: &[u8]) -> 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(); + 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 { @@ -998,7 +991,10 @@ fn get_shank_type_size_with_types( if variants.iter().all(|v| v.fields.is_none()) { Ok(1) } else { - Err(format!("cannot determine fixed size for enum with data: {}", def.defined)) + Err(format!( + "cannot determine fixed size for enum with data: {}", + def.defined + )) } } ShankIdlTypeDefTy::Struct { fields } => { From c656bfd844fda55eef15b9fa0a03a089b9e2f77d Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 16:21:36 -0500 Subject: [PATCH 09/12] add todo with issue pointer --- addons/svm/types/src/subgraph/idl/shank.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/svm/types/src/subgraph/idl/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs index 420549aed..64b176805 100644 --- a/addons/svm/types/src/subgraph/idl/shank.rs +++ b/addons/svm/types/src/subgraph/idl/shank.rs @@ -15,6 +15,8 @@ use std::fmt::Display; // ============================================================================ // Local type definitions that mirror shank_idl internal types // These are needed because shank_idl doesn't export its internal modules +// TODO: Remove local types once shank_idl exposes them publicly +// https://github.com/txtx/txtx/issues/382 // ============================================================================ /// Shank IDL type representation From 329cc0923459ee5c39c79f13eb8ff41bdd4510f6 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 28 Jan 2026 16:47:09 -0500 Subject: [PATCH 10/12] fix comment --- addons/svm/types/src/subgraph/idl/shank.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/svm/types/src/subgraph/idl/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs index 64b176805..16599e7c2 100644 --- a/addons/svm/types/src/subgraph/idl/shank.rs +++ b/addons/svm/types/src/subgraph/idl/shank.rs @@ -236,8 +236,7 @@ pub struct DiscriminatorMapping { pub fn find_account_discriminator_const(idl: &shank_idl::idl::Idl) -> Option { for constant in &idl.constants { if constant.name == "account_discriminator" { - // The value is typically a quoted string like "\"AccountType\"" - // Strip the quotes if present + // 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() From ced16a5a12794700291e63094648817b0df18de2 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Fri, 30 Jan 2026 13:34:05 -0500 Subject: [PATCH 11/12] refactor shank_idl_type_to_txtx_type to handle HashMap and BTreeMap as arbitrary map types --- addons/svm/types/src/subgraph/idl/shank.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/addons/svm/types/src/subgraph/idl/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs index 16599e7c2..b89d453ac 100644 --- a/addons/svm/types/src/subgraph/idl/shank.rs +++ b/addons/svm/types/src/subgraph/idl/shank.rs @@ -437,15 +437,15 @@ pub fn shank_idl_type_to_txtx_type( _idl_constants, )? } - ShankIdlType::HashMap(map) => { - let value_type = - shank_idl_type_to_txtx_type(&map.hash_map.1, idl_types, _idl_constants)?; - Type::array(value_type) - } - ShankIdlType::BTreeMap(map) => { - let value_type = - shank_idl_type_to_txtx_type(&map.b_tree_map.1, idl_types, _idl_constants)?; - Type::array(value_type) + ShankIdlType::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()) + } + ShankIdlType::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()) } ShankIdlType::HashSet(set) => { Type::array(shank_idl_type_to_txtx_type(&set.hash_set, idl_types, _idl_constants)?) From 80784bc1dbf273c8691a872de0ed9c198f6e80af Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Mon, 9 Feb 2026 13:58:39 -0500 Subject: [PATCH 12/12] chore(svm-types/svm-core): update shank import; remove duplicate shank types --- Cargo.lock | 8 +- addons/svm/core/src/codec/idl/mod.rs | 8 +- addons/svm/types/Cargo.toml | 2 +- addons/svm/types/src/subgraph/idl/shank.rs | 621 ++++++++------------- addons/svm/types/src/subgraph/tests.rs | 57 +- 5 files changed, 255 insertions(+), 441 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a9a74f27..2b93aa023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7634,9 +7634,9 @@ dependencies = [ [[package]] name = "shank_idl" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a8cee6731ee59f3e24c44aab6c7d3adb365b1f7314da73ea6654aec354e4341" +checksum = "1862d324b77d15de6cd8d5de859ee70431737f131d34ae670cb4fb0568301d87" dependencies = [ "anyhow", "cargo_toml", @@ -7649,9 +7649,9 @@ dependencies = [ [[package]] name = "shank_macro_impl" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c08958b4ba24f8289481c0d8b377c63a8205fc037b59fc80643c491ec1f487" +checksum = "aab0b29f9717ebb2430ef424627e0fc234c832363955cbac2997c2973743990d" dependencies = [ "anyhow", "proc-macro2", diff --git a/addons/svm/core/src/codec/idl/mod.rs b/addons/svm/core/src/codec/idl/mod.rs index 8bfd7a2bf..c279550a0 100644 --- a/addons/svm/core/src/codec/idl/mod.rs +++ b/addons/svm/core/src/codec/idl/mod.rs @@ -193,9 +193,7 @@ impl IdlRef { return Ok(IndexMap::new()); } - // Extract types to our local format for encoding - let idl_types = extract_shank_types(idl) - .map_err(|e| diagnosed_error!("failed to extract IDL types: {}", e))?; + let idl_types = extract_shank_types(idl); let mut encoded_args = IndexMap::new(); for (user_arg_idx, arg) in args.iter().enumerate() { @@ -279,9 +277,7 @@ impl IdlRef { return Ok(vec![]); } - // Extract types to our local format for encoding - let idl_types = extract_shank_types(idl) - .map_err(|e| diagnosed_error!("failed to extract IDL types: {}", e))?; + let idl_types = extract_shank_types(idl); let mut encoded_args = vec![]; for (user_arg_idx, arg) in args.iter().enumerate() { diff --git a/addons/svm/types/Cargo.toml b/addons/svm/types/Cargo.toml index 66182eb50..1e073fce5 100644 --- a/addons/svm/types/Cargo.toml +++ b/addons/svm/types/Cargo.toml @@ -30,7 +30,7 @@ lazy_static = "1.4.0" serde = "1" serde_json = "1" serde_derive = "1" -shank_idl = "0.4.6" +shank_idl = "0.4.7" [dev-dependencies] test-case = "3.3" diff --git a/addons/svm/types/src/subgraph/idl/shank.rs b/addons/svm/types/src/subgraph/idl/shank.rs index b89d453ac..085f2b0f3 100644 --- a/addons/svm/types/src/subgraph/idl/shank.rs +++ b/addons/svm/types/src/subgraph/idl/shank.rs @@ -1,9 +1,5 @@ //! Shank IDL codec functions for converting between Shank IDL types, txtx types, and bytes. -//! -//! Since shank_idl doesn't export its internal types directly, we define local types that mirror -//! the shank_idl structure for use in function signatures. -use serde::{Deserialize, Serialize}; use txtx_addon_kit::{ indexmap::IndexMap, types::types::{ObjectDefinition, ObjectProperty, ObjectType, Type, Value}, @@ -12,187 +8,21 @@ use txtx_addon_kit::{ use crate::{SvmValue, SVM_PUBKEY}; use std::fmt::Display; -// ============================================================================ -// Local type definitions that mirror shank_idl internal types -// These are needed because shank_idl doesn't export its internal modules -// TODO: Remove local types once shank_idl exposes them publicly -// https://github.com/txtx/txtx/issues/382 -// ============================================================================ - -/// Shank IDL type representation -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum ShankIdlType { - Bool, - U8, - U16, - U32, - U64, - U128, - I8, - I16, - I32, - I64, - I128, - Bytes, - String, - #[serde(rename = "publicKey")] - PublicKey, - #[serde(untagged)] - Option(ShankIdlTypeOption), - #[serde(untagged)] - FixedSizeOption(ShankIdlTypeFixedSizeOption), - #[serde(untagged)] - Vec(ShankIdlTypeVec), - #[serde(untagged)] - Array(ShankIdlTypeArray), - #[serde(untagged)] - Tuple(ShankIdlTypeTuple), - #[serde(untagged)] - Defined(ShankIdlTypeDefined), - #[serde(untagged)] - HashMap(ShankIdlTypeHashMap), - #[serde(untagged)] - BTreeMap(ShankIdlTypeBTreeMap), - #[serde(untagged)] - HashSet(ShankIdlTypeHashSet), - #[serde(untagged)] - BTreeSet(ShankIdlTypeBTreeSet), -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeOption { - pub option: Box, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ShankIdlTypeFixedSizeOption { - pub fixed_size_option: ShankIdlTypeFixedSizeOptionInner, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeFixedSizeOptionInner { - pub inner: Box, - #[serde(skip_serializing_if = "Option::is_none")] - pub sentinel: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeVec { - pub vec: Box, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeArray { - pub array: (Box, usize), -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeTuple { - pub tuple: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeDefined { - pub defined: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ShankIdlTypeHashMap { - pub hash_map: (Box, Box), -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ShankIdlTypeBTreeMap { - pub b_tree_map: (Box, Box), -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ShankIdlTypeHashSet { - pub hash_set: Box, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ShankIdlTypeBTreeSet { - pub b_tree_set: Box, -} - -/// Shank IDL field definition -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlField { - pub name: String, - #[serde(rename = "type")] - pub ty: ShankIdlType, -} - -/// Shank IDL type definition -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlTypeDef { - pub name: String, - #[serde(rename = "type")] - pub ty: ShankIdlTypeDefTy, - #[serde(skip_serializing_if = "Option::is_none")] - pub pod_sentinel: Option>, -} - -/// Shank IDL type definition type (struct or enum) -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(tag = "kind", rename_all = "lowercase")] -pub enum ShankIdlTypeDefTy { - Struct { fields: Vec }, - Enum { variants: Vec }, -} - -/// Shank IDL enum variant -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlEnumVariant { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub fields: Option, -} - -/// Shank enum fields (named or tuple) -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum ShankEnumFields { - Named(Vec), - Tuple(Vec), -} - -/// Shank IDL constant -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ShankIdlConst { - pub name: String, - #[serde(rename = "type")] - pub ty: ShankIdlType, - pub value: String, -} +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 convert from shank_idl::idl::Idl to our local types +// Helper function to extract types from shank_idl::idl::Idl // ============================================================================ -/// Extracts type definitions from a Shank IDL by serializing and deserializing -pub fn extract_shank_types(idl: &shank_idl::idl::Idl) -> Result, String> { - // Serialize the IDL types to JSON and deserialize to our local types - let types_json = serde_json::to_string(&idl.types) - .map_err(|e| format!("failed to serialize IDL types: {}", e))?; - let mut types: Vec = serde_json::from_str(&types_json) - .map_err(|e| format!("failed to deserialize IDL types: {}", e))?; - - // Also include accounts as they can be referenced as types - let accounts_json = serde_json::to_string(&idl.accounts) - .map_err(|e| format!("failed to serialize IDL accounts: {}", e))?; - let accounts: Vec = serde_json::from_str(&accounts_json) - .map_err(|e| format!("failed to deserialize IDL accounts: {}", e))?; - - types.extend(accounts); - Ok(types) +/// 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 @@ -200,7 +30,7 @@ pub fn extract_shank_instruction_arg_type( idl: &shank_idl::idl::Idl, instruction_name: &str, arg_index: usize, -) -> Result { +) -> Result { let instruction = idl .instructions .iter() @@ -211,9 +41,7 @@ pub fn extract_shank_instruction_arg_type( format!("argument {} not found in instruction '{}'", arg_index, instruction_name) })?; - let arg_json = serde_json::to_string(&arg.ty) - .map_err(|e| format!("failed to serialize arg type: {}", e))?; - serde_json::from_str(&arg_json).map_err(|e| format!("failed to deserialize arg type: {}", e)) + Ok(arg.ty.clone()) } // ============================================================================ @@ -255,14 +83,14 @@ pub fn build_discriminator_mapping( idl: &shank_idl::idl::Idl, enum_type_name: &str, ) -> Option { - let types = extract_shank_types(idl).ok()?; + 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 { - ShankIdlTypeDefTy::Enum { variants } => variants, + IdlTypeDefinitionTy::Enum { variants } => variants, _ => return None, }; @@ -289,15 +117,15 @@ pub fn build_discriminator_mapping( /// 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: &[ShankIdlField], + fields: &[IdlField], enum_type_name: &str, - idl_types: &[ShankIdlTypeDef], + idl_types: &[IdlTypeDefinition], ) -> Result { let mut offset = 0; for field in fields { // Check if this field is the discriminator enum type - if let ShankIdlType::Defined(def) = &field.ty { - if def.defined == enum_type_name { + if let IdlType::Defined(name) = &field.ty { + if name == enum_type_name { return Ok(offset); } } @@ -324,7 +152,7 @@ pub fn find_account_by_discriminator(idl: &shank_idl::idl::Idl, data: &[u8]) -> let enum_type_name = find_account_discriminator_const(idl)?; // Get all type definitions (needed for offset calculation) - let types = extract_shank_types(idl).ok()?; + let types = extract_shank_types(idl); // Build the discriminator mapping (variant index -> account name) let mapping = build_discriminator_mapping(idl, &enum_type_name)?; @@ -347,7 +175,7 @@ pub fn find_account_by_discriminator(idl: &shank_idl::idl::Idl, data: &[u8]) -> // Get the struct fields let fields = match &account_type.ty { - ShankIdlTypeDefTy::Struct { fields } => fields, + IdlTypeDefinitionTy::Struct { fields } => fields, _ => continue, }; @@ -379,42 +207,40 @@ pub fn find_account_by_discriminator(idl: &shank_idl::idl::Idl, data: &[u8]) -> /// Converts a Shank IDL type to a txtx Type. pub fn shank_idl_type_to_txtx_type( - idl_type: &ShankIdlType, - idl_types: &[ShankIdlTypeDef], - _idl_constants: &[ShankIdlConst], + idl_type: &IdlType, + idl_types: &[IdlTypeDefinition], + _idl_constants: &[IdlConst], ) -> Result { let res = match idl_type { - ShankIdlType::Bool => Type::bool(), - ShankIdlType::U8 => Type::addon(crate::SVM_U8), - ShankIdlType::U16 => Type::addon(crate::SVM_U16), - ShankIdlType::U32 => Type::addon(crate::SVM_U32), - ShankIdlType::U64 => Type::addon(crate::SVM_U64), - ShankIdlType::U128 => Type::addon(crate::SVM_U128), - ShankIdlType::I8 => Type::addon(crate::SVM_I8), - ShankIdlType::I16 => Type::addon(crate::SVM_I16), - ShankIdlType::I32 => Type::addon(crate::SVM_I32), - ShankIdlType::I64 => Type::addon(crate::SVM_I64), - ShankIdlType::I128 => Type::addon(crate::SVM_I128), - ShankIdlType::Bytes => Type::buffer(), - ShankIdlType::String => Type::string(), - ShankIdlType::PublicKey => Type::addon(SVM_PUBKEY), - ShankIdlType::Option(opt) => { - Type::typed_null(shank_idl_type_to_txtx_type(&opt.option, idl_types, _idl_constants)?) - } - ShankIdlType::FixedSizeOption(opt) => Type::typed_null(shank_idl_type_to_txtx_type( - &opt.fixed_size_option.inner, - idl_types, - _idl_constants, - )?), - ShankIdlType::Vec(vec) => { - Type::array(shank_idl_type_to_txtx_type(&vec.vec, idl_types, _idl_constants)?) - } - ShankIdlType::Array(arr) => { - Type::array(shank_idl_type_to_txtx_type(&arr.array.0, idl_types, _idl_constants)?) - } - ShankIdlType::Tuple(tuple) => { + 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 tuple.tuple.iter().enumerate() { + 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(), @@ -427,9 +253,9 @@ pub fn shank_idl_type_to_txtx_type( } Type::object(ObjectDefinition::tuple(props)) } - ShankIdlType::Defined(def) => { - let Some(matching_type) = idl_types.iter().find(|t| t.name == def.defined) else { - return Err(format!("unable to find defined type '{}'", def.defined)); + 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, @@ -437,21 +263,21 @@ pub fn shank_idl_type_to_txtx_type( _idl_constants, )? } - ShankIdlType::HashMap(_) => { + 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()) } - ShankIdlType::BTreeMap(_) => { + 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()) } - ShankIdlType::HashSet(set) => { - Type::array(shank_idl_type_to_txtx_type(&set.hash_set, idl_types, _idl_constants)?) + IdlType::HashSet(inner) => { + Type::array(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) } - ShankIdlType::BTreeSet(set) => { - Type::array(shank_idl_type_to_txtx_type(&set.b_tree_set, idl_types, _idl_constants)?) + IdlType::BTreeSet(inner) => { + Type::array(shank_idl_type_to_txtx_type(inner, idl_types, _idl_constants)?) } }; Ok(res) @@ -459,12 +285,12 @@ pub fn shank_idl_type_to_txtx_type( /// 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: &ShankIdlTypeDefTy, - idl_types: &[ShankIdlTypeDef], - idl_constants: &[ShankIdlConst], + idl_type_def_ty: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], + idl_constants: &[IdlConst], ) -> Result { let ty = match idl_type_def_ty { - ShankIdlTypeDefTy::Struct { fields } => { + 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) @@ -482,7 +308,7 @@ pub fn get_expected_type_from_shank_idl_type_def_ty( } Type::object(ObjectDefinition::strict(props)) } - ShankIdlTypeDefTy::Enum { variants } => { + IdlTypeDefinitionTy::Enum { variants } => { let mut props = vec![]; for variant in variants { let variant_type = if let Some(ref fields) = variant.fields { @@ -506,12 +332,12 @@ pub fn get_expected_type_from_shank_idl_type_def_ty( } fn get_expected_type_from_shank_enum_fields( - fields: &ShankEnumFields, - idl_types: &[ShankIdlTypeDef], - idl_constants: &[ShankIdlConst], + fields: &EnumFields, + idl_types: &[IdlTypeDefinition], + idl_constants: &[IdlConst], ) -> Result { match fields { - ShankEnumFields::Named(idl_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)?; @@ -526,7 +352,7 @@ fn get_expected_type_from_shank_enum_fields( } Ok(Type::object(ObjectDefinition::strict(props))) } - ShankEnumFields::Tuple(types) => { + 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)?; @@ -551,8 +377,8 @@ fn get_expected_type_from_shank_enum_fields( /// 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: &ShankIdlTypeDefTy, - idl_types: &[ShankIdlTypeDef], + expected_type: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], ) -> Result { let (value, rest) = parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes( data, @@ -571,14 +397,14 @@ pub fn parse_bytes_to_value_with_shank_idl_type_def_ty( /// 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: &ShankIdlTypeDefTy, - idl_types: &[ShankIdlTypeDef], + expected_type: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], ) -> Result<(Value, &'a [u8]), String> { match expected_type { - ShankIdlTypeDefTy::Struct { fields } => { + IdlTypeDefinitionTy::Struct { fields } => { parse_bytes_to_shank_struct_with_leftover_bytes(data, fields, idl_types) } - ShankIdlTypeDefTy::Enum { variants } => { + 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; @@ -599,8 +425,8 @@ pub fn parse_bytes_to_value_with_shank_idl_type_def_ty_with_leftover_bytes<'a>( fn parse_bytes_to_shank_struct_with_leftover_bytes<'a>( data: &'a [u8], - fields: &[ShankIdlField], - idl_types: &[ShankIdlTypeDef], + fields: &[IdlField], + idl_types: &[IdlTypeDefinition], ) -> Result<(Value, &'a [u8]), String> { let mut map = IndexMap::new(); let mut remaining_data = data; @@ -618,11 +444,11 @@ fn parse_bytes_to_shank_struct_with_leftover_bytes<'a>( fn parse_bytes_to_shank_enum_variant_with_leftover_bytes<'a>( data: &'a [u8], - variant: &ShankIdlEnumVariant, - idl_types: &[ShankIdlTypeDef], + variant: &IdlEnumVariant, + idl_types: &[IdlTypeDefinition], ) -> Result<(Value, &'a [u8]), String> { match &variant.fields { - Some(ShankEnumFields::Named(fields)) => { + Some(EnumFields::Named(fields)) => { let mut map = IndexMap::new(); let mut remaining_data = data; for field in fields { @@ -636,7 +462,7 @@ fn parse_bytes_to_shank_enum_variant_with_leftover_bytes<'a>( } Ok((ObjectType::from_map(map).to_value(), remaining_data)) } - Some(ShankEnumFields::Tuple(types)) => { + Some(EnumFields::Tuple(types)) => { let mut values = Vec::with_capacity(types.len()); let mut remaining_data = data; for ty in types { @@ -657,18 +483,18 @@ fn parse_bytes_to_shank_enum_variant_with_leftover_bytes<'a>( /// 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: &ShankIdlType, - idl_types: &[ShankIdlTypeDef], + 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 { - ShankIdlType::U8 => { + IdlType::U8 => { let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("u8"))?; Ok((SvmValue::u8(v[0]), rest)) } - ShankIdlType::U16 => { + IdlType::U16 => { let (v, rest) = data.split_at_checked(2).ok_or(bytes_err("u16"))?; Ok(( SvmValue::u16(u16::from_le_bytes( @@ -677,7 +503,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::U32 => { + IdlType::U32 => { let (v, rest) = data.split_at_checked(4).ok_or(bytes_err("u32"))?; Ok(( SvmValue::u32(u32::from_le_bytes( @@ -686,7 +512,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::U64 => { + IdlType::U64 => { let (v, rest) = data.split_at_checked(8).ok_or(bytes_err("u64"))?; Ok(( SvmValue::u64(u64::from_le_bytes( @@ -695,7 +521,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::U128 => { + IdlType::U128 => { let (v, rest) = data.split_at_checked(16).ok_or(bytes_err("u128"))?; Ok(( SvmValue::u128(u128::from_le_bytes( @@ -704,11 +530,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::I8 => { + 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)) } - ShankIdlType::I16 => { + IdlType::I16 => { let (v, rest) = data.split_at_checked(2).ok_or(bytes_err("i16"))?; Ok(( SvmValue::i16(i16::from_le_bytes( @@ -717,7 +543,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::I32 => { + IdlType::I32 => { let (v, rest) = data.split_at_checked(4).ok_or(bytes_err("i32"))?; Ok(( SvmValue::i32(i32::from_le_bytes( @@ -726,7 +552,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::I64 => { + IdlType::I64 => { let (v, rest) = data.split_at_checked(8).ok_or(bytes_err("i64"))?; Ok(( SvmValue::i64(i64::from_le_bytes( @@ -735,7 +561,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::I128 => { + IdlType::I128 => { let (v, rest) = data.split_at_checked(16).ok_or(bytes_err("i128"))?; Ok(( SvmValue::i128(i128::from_le_bytes( @@ -744,15 +570,15 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( rest, )) } - ShankIdlType::Bool => { + IdlType::Bool => { let (v, rest) = data.split_at_checked(1).ok_or(bytes_err("bool"))?; Ok((Value::bool(v[0] != 0), rest)) } - ShankIdlType::PublicKey => { + IdlType::PublicKey => { let (v, rest) = data.split_at_checked(32).ok_or(bytes_err("pubkey"))?; Ok((SvmValue::pubkey(v.to_vec()), rest)) } - ShankIdlType::String => { + 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))?, @@ -762,7 +588,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( let string_value = String::from_utf8_lossy(string_bytes).to_string(); Ok((Value::string(string_value), rest)) } - ShankIdlType::Bytes => { + 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))?, @@ -770,24 +596,22 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( let (vec_bytes, rest) = rest.split_at_checked(vec_len).ok_or(bytes_err("bytes"))?; Ok((Value::buffer(vec_bytes.to_vec()), rest)) } - ShankIdlType::Option(opt) => { + 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, - &opt.option, - idl_types, + rest, inner, idl_types, ) } } - ShankIdlType::FixedSizeOption(opt) => { - let inner_size = get_shank_type_size(&opt.fixed_size_option.inner)?; + 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) = opt.fixed_size_option.sentinel { + if let Some(ref sentinel_bytes) = sentinel { if inner_bytes == sentinel_bytes.as_slice() { return Ok((Value::null(), rest)); } @@ -795,12 +619,12 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( let (value, _) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( inner_bytes, - &opt.fixed_size_option.inner, + inner, idl_types, )?; Ok((value, rest)) } - ShankIdlType::Vec(vec) => { + 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))?, @@ -812,7 +636,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( for _ in 0..vec_len { let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, - &vec.vec, + inner, idl_types, )?; vec_values.push(value); @@ -821,14 +645,13 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( Ok((Value::array(vec_values), remaining_data)) } - ShankIdlType::Array(arr) => { - let len = arr.array.1; - let mut vec_values = Vec::with_capacity(len); + IdlType::Array(inner, len) => { + let mut vec_values = Vec::with_capacity(*len); let mut remaining_data = data; - for _ in 0..len { + for _ in 0..*len { let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, - &arr.array.0, + inner, idl_types, )?; vec_values.push(value); @@ -836,10 +659,10 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( } Ok((Value::array(vec_values), remaining_data)) } - ShankIdlType::Tuple(tuple) => { - let mut values = Vec::with_capacity(tuple.tuple.len()); + IdlType::Tuple(types) => { + let mut values = Vec::with_capacity(types.len()); let mut remaining_data = data; - for ty in &tuple.tuple { + for ty in types { let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, ty, @@ -850,11 +673,11 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( } Ok((Value::array(values), remaining_data)) } - ShankIdlType::Defined(def) => { + IdlType::Defined(name) => { let matching_type = idl_types .iter() - .find(|t| t.name == def.defined) - .ok_or(err(&def.defined, &"not found in IDL types"))?; + .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, @@ -862,7 +685,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( idl_types, ) } - ShankIdlType::HashMap(map) => { + 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))?, @@ -874,12 +697,12 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( for _ in 0..map_len { let (key, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, - &map.hash_map.0, + key_type, idl_types, )?; let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( rest, - &map.hash_map.1, + value_type, idl_types, )?; remaining_data = rest; @@ -888,7 +711,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( Ok((ObjectType::from_map(result_map).to_value(), remaining_data)) } - ShankIdlType::BTreeMap(map) => { + 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))?, @@ -900,12 +723,12 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( for _ in 0..map_len { let (key, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, - &map.b_tree_map.0, + key_type, idl_types, )?; let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( rest, - &map.b_tree_map.1, + value_type, idl_types, )?; remaining_data = rest; @@ -914,7 +737,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( Ok((ObjectType::from_map(result_map).to_value(), remaining_data)) } - ShankIdlType::HashSet(set) => { + 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))?, @@ -926,7 +749,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( for _ in 0..set_len { let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, - &set.hash_set, + inner, idl_types, )?; values.push(value); @@ -935,7 +758,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( Ok((Value::array(values), remaining_data)) } - ShankIdlType::BTreeSet(set) => { + 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))?, @@ -947,7 +770,7 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( for _ in 0..set_len { let (value, rest) = parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes( remaining_data, - &set.b_tree_set, + inner, idl_types, )?; values.push(value); @@ -960,45 +783,45 @@ pub fn parse_bytes_to_value_with_shank_idl_type_with_leftover_bytes<'a>( } /// Returns the size in bytes of a Shank IDL type (for fixed-size types). -fn get_shank_type_size(ty: &ShankIdlType) -> Result { +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: &ShankIdlType, - idl_types: &[ShankIdlTypeDef], + ty: &IdlType, + idl_types: &[IdlTypeDefinition], ) -> Result { match ty { - ShankIdlType::Bool | ShankIdlType::U8 | ShankIdlType::I8 => Ok(1), - ShankIdlType::U16 | ShankIdlType::I16 => Ok(2), - ShankIdlType::U32 | ShankIdlType::I32 => Ok(4), - ShankIdlType::U64 | ShankIdlType::I64 => Ok(8), - ShankIdlType::U128 | ShankIdlType::I128 => Ok(16), - ShankIdlType::PublicKey => Ok(32), - ShankIdlType::Array(arr) => { - let inner_size = get_shank_type_size_with_types(&arr.array.0, idl_types)?; - Ok(inner_size * arr.array.1) - } - ShankIdlType::Defined(def) => { + 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 == def.defined) + .find(|t| t.name == *name) .ok_or_else(|| format!("cannot determine fixed size for type {:?}", ty))?; match &type_def.ty { - ShankIdlTypeDefTy::Enum { variants } => { + 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: {}", - def.defined + name )) } } - ShankIdlTypeDefTy::Struct { fields } => { + IdlTypeDefinitionTy::Struct { fields } => { let mut size = 0; for field in fields { size += get_shank_type_size_with_types(&field.ty, idl_types)?; @@ -1018,8 +841,8 @@ fn get_shank_type_size_with_types( /// Encodes a txtx Value to bytes using a Shank IDL type. pub fn borsh_encode_value_to_shank_idl_type( value: &Value, - idl_type: &ShankIdlType, - idl_types: &[ShankIdlTypeDef], + 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()) @@ -1040,93 +863,92 @@ pub fn borsh_encode_value_to_shank_idl_type( } match idl_type { - ShankIdlType::Bool => value + 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")), - ShankIdlType::U8 => SvmValue::to_number::(value) + 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)), - ShankIdlType::U16 => SvmValue::to_number::(value) + 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)), - ShankIdlType::U32 => SvmValue::to_number::(value) + 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)), - ShankIdlType::U64 => SvmValue::to_number::(value) + 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)), - ShankIdlType::U128 => SvmValue::to_number::(value) + 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)), - ShankIdlType::I8 => SvmValue::to_number::(value) + 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)), - ShankIdlType::I16 => SvmValue::to_number::(value) + 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)), - ShankIdlType::I32 => SvmValue::to_number::(value) + 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)), - ShankIdlType::I64 => SvmValue::to_number::(value) + 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)), - ShankIdlType::I128 => SvmValue::to_number::(value) + 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)), - ShankIdlType::Bytes => Ok(value.to_be_bytes().clone()), - ShankIdlType::String => value + 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")), - ShankIdlType::PublicKey => SvmValue::to_pubkey(value) + IdlType::PublicKey => SvmValue::to_pubkey(value) .map_err(|_| mismatch_err("pubkey")) .map(|p| borsh::to_vec(&p))? .map_err(|e| encode_err("pubkey", &e)), - ShankIdlType::Option(opt) => { + 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, &opt.option, idl_types)?; + borsh_encode_value_to_shank_idl_type(value, inner, idl_types)?; let mut result = vec![1u8]; // Some discriminator result.extend(encoded_inner); Ok(result) } } - ShankIdlType::FixedSizeOption(opt) => { + IdlType::FixedSizeOption { inner, sentinel } => { if value.as_null().is_some() { - if let Some(ref sentinel_bytes) = opt.fixed_size_option.sentinel { + if let Some(ref sentinel_bytes) = sentinel { Ok(sentinel_bytes.clone()) } else { - let size = get_shank_type_size(&opt.fixed_size_option.inner)?; + let size = get_shank_type_size(inner)?; Ok(vec![0u8; size]) } } else { - borsh_encode_value_to_shank_idl_type(value, &opt.fixed_size_option.inner, idl_types) + borsh_encode_value_to_shank_idl_type(value, inner, idl_types) } } - ShankIdlType::Vec(vec) => match value { + 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, &vec.vec, idl_types) + 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, &vec.vec, idl_types)?; + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; result.extend(encoded); } Ok(result) } _ => Err(mismatch_err("vec")), }, - ShankIdlType::Array(arr) => { + IdlType::Array(inner, expected_len) => { let array = value.as_array().ok_or(mismatch_err("array"))?; - let expected_len = arr.array.1; - if array.len() != expected_len { + if array.len() != *expected_len { return Err(format!( "invalid value for idl type: expected array length of {}, found {}", expected_len, @@ -1135,76 +957,76 @@ pub fn borsh_encode_value_to_shank_idl_type( } let mut result = vec![]; for v in array.iter() { - let encoded = borsh_encode_value_to_shank_idl_type(v, &arr.array.0, idl_types)?; + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; result.extend(encoded); } Ok(result) } - ShankIdlType::Tuple(tuple) => { + IdlType::Tuple(types) => { let array = value.as_array().ok_or(mismatch_err("tuple"))?; - if array.len() != tuple.tuple.len() { + if array.len() != types.len() { return Err(format!( "invalid value for idl type: expected tuple length of {}, found {}", - tuple.tuple.len(), + types.len(), array.len() )); } let mut result = vec![]; - for (v, ty) in array.iter().zip(tuple.tuple.iter()) { + 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) } - ShankIdlType::Defined(def) => { - let typing = idl_types.iter().find(|t| t.name == def.defined).ok_or_else(|| { - format!("unable to find type definition for {} in idl", def.defined) + 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) } - ShankIdlType::HashMap(map) => { + 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, &map.hash_map.0, idl_types)?; + borsh_encode_value_to_shank_idl_type(&key_value, key_type, idl_types)?; let encoded_value = - borsh_encode_value_to_shank_idl_type(v, &map.hash_map.1, idl_types)?; + borsh_encode_value_to_shank_idl_type(v, value_type, idl_types)?; result.extend(encoded_key); result.extend(encoded_value); } Ok(result) } - ShankIdlType::BTreeMap(map) => { + 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, &map.b_tree_map.0, idl_types)?; + borsh_encode_value_to_shank_idl_type(&key_value, key_type, idl_types)?; let encoded_value = - borsh_encode_value_to_shank_idl_type(v, &map.b_tree_map.1, idl_types)?; + borsh_encode_value_to_shank_idl_type(v, value_type, idl_types)?; result.extend(encoded_key); result.extend(encoded_value); } Ok(result) } - ShankIdlType::HashSet(set) => { + 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, &set.hash_set, idl_types)?; + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; result.extend(encoded); } Ok(result) } - ShankIdlType::BTreeSet(set) => { + 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, &set.b_tree_set, idl_types)?; + let encoded = borsh_encode_value_to_shank_idl_type(v, inner, idl_types)?; result.extend(encoded); } Ok(result) @@ -1214,11 +1036,11 @@ pub fn borsh_encode_value_to_shank_idl_type( fn borsh_encode_value_to_shank_idl_type_def_ty( value: &Value, - idl_type_def_ty: &ShankIdlTypeDefTy, - idl_types: &[ShankIdlTypeDef], + idl_type_def_ty: &IdlTypeDefinitionTy, + idl_types: &[IdlTypeDefinition], ) -> Result, String> { match idl_type_def_ty { - ShankIdlTypeDefTy::Struct { fields } => { + 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()) @@ -1235,7 +1057,7 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( } Ok(encoded_fields) } - ShankIdlTypeDefTy::Enum { variants } => { + IdlTypeDefinitionTy::Enum { variants } => { let enum_value = value .as_object() .ok_or_else(|| format!("expected object for enum, found {:?}", value.get_type()))?; @@ -1270,7 +1092,7 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( let mut encoded = vec![variant_index as u8]; match &expected_variant.fields { - Some(ShankEnumFields::Named(fields)) => { + Some(EnumFields::Named(fields)) => { let user_values_map = enum_variant_value .as_object() .ok_or_else(|| format!("expected object for enum variant fields"))?; @@ -1283,7 +1105,7 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( encoded.extend(field_encoded); } } - Some(ShankEnumFields::Tuple(types)) => { + Some(EnumFields::Tuple(types)) => { let values = enum_variant_value .as_array() .ok_or_else(|| format!("expected array for enum tuple variant"))?; @@ -1311,104 +1133,103 @@ fn borsh_encode_value_to_shank_idl_type_def_ty( fn borsh_encode_bytes_to_shank_idl_type( bytes: &[u8], - idl_type: &ShankIdlType, - _idl_types: &[ShankIdlTypeDef], + idl_type: &IdlType, + _idl_types: &[IdlTypeDefinition], ) -> Result, String> { match idl_type { - ShankIdlType::U8 => { + IdlType::U8 => { if bytes.len() != 1 { return Err(format!("expected 1 byte for u8, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::U16 => { + IdlType::U16 => { if bytes.len() != 2 { return Err(format!("expected 2 bytes for u16, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::U32 => { + IdlType::U32 => { if bytes.len() != 4 { return Err(format!("expected 4 bytes for u32, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::U64 => { + IdlType::U64 => { if bytes.len() != 8 { return Err(format!("expected 8 bytes for u64, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::U128 => { + IdlType::U128 => { if bytes.len() != 16 { return Err(format!("expected 16 bytes for u128, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::I8 => { + IdlType::I8 => { if bytes.len() != 1 { return Err(format!("expected 1 byte for i8, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::I16 => { + IdlType::I16 => { if bytes.len() != 2 { return Err(format!("expected 2 bytes for i16, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::I32 => { + IdlType::I32 => { if bytes.len() != 4 { return Err(format!("expected 4 bytes for i32, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::I64 => { + IdlType::I64 => { if bytes.len() != 8 { return Err(format!("expected 8 bytes for i64, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::I128 => { + IdlType::I128 => { if bytes.len() != 16 { return Err(format!("expected 16 bytes for i128, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::Bool => { + IdlType::Bool => { if bytes.len() != 1 { return Err(format!("expected 1 byte for bool, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::PublicKey => { + IdlType::PublicKey => { if bytes.len() != 32 { return Err(format!("expected 32 bytes for Pubkey, found {}", bytes.len())); } Ok(bytes.to_vec()) } - ShankIdlType::String => { + 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)) } - ShankIdlType::Bytes => Ok(bytes.to_vec()), - ShankIdlType::Vec(vec) => { - match &*vec.vec { - ShankIdlType::U8 => { + 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", - vec.vec + inner )) } } - ShankIdlType::Array(arr) => { - match &*arr.array.0 { - ShankIdlType::U8 => { - let expected_len = arr.array.1; - if bytes.len() != expected_len { + IdlType::Array(inner, expected_len) => { + match &**inner { + IdlType::U8 => { + if bytes.len() != *expected_len { return Err(format!( "expected {} bytes for array, found {}", expected_len, @@ -1419,7 +1240,7 @@ fn borsh_encode_bytes_to_shank_idl_type( } _ => Err(format!( "cannot convert raw bytes to [{:?}; {}]", - arr.array.0, arr.array.1 + inner, expected_len )) } } diff --git a/addons/svm/types/src/subgraph/tests.rs b/addons/svm/types/src/subgraph/tests.rs index ec3115667..3aa817c40 100644 --- a/addons/svm/types/src/subgraph/tests.rs +++ b/addons/svm/types/src/subgraph/tests.rs @@ -5,8 +5,7 @@ use crate::{ 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, ShankIdlConst, ShankIdlType, ShankIdlTypeArray, - ShankIdlTypeDef, ShankIdlTypeDefined, ShankIdlTypeOption, ShankIdlTypeVec, + shank_idl_type_to_txtx_type, IdlConst, IdlType as ShankIdlType, IdlTypeDefinition, }, }, SvmValue, SVM_PUBKEY, SVM_U64, @@ -937,8 +936,8 @@ fn test_bad_data(bad_data: Vec, expected_type: IdlTypeDefTy, expected_err: & 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).unwrap(); - pub static ref SHANK_CONSTANTS: Vec = vec![]; + pub static ref SHANK_TYPES: Vec = extract_shank_types(&SHANK_IDL); + pub static ref SHANK_CONSTANTS: Vec = vec![]; } // -------------------------------------------------------------------------- @@ -1015,7 +1014,7 @@ fn test_shank_decode_bytes() { #[test] fn test_shank_decode_option_none() { let data = borsh::to_vec(&None::).unwrap(); - let opt_type = ShankIdlType::Option(ShankIdlTypeOption { option: Box::new(ShankIdlType::U64) }); + 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(); @@ -1026,7 +1025,7 @@ fn test_shank_decode_option_none() { #[test] fn test_shank_decode_option_some() { let data = borsh::to_vec(&Some(42u64)).unwrap(); - let opt_type = ShankIdlType::Option(ShankIdlTypeOption { option: Box::new(ShankIdlType::U64) }); + 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(); @@ -1041,7 +1040,7 @@ fn test_shank_decode_option_some() { #[test] fn test_shank_decode_vec_u64() { let data = borsh::to_vec(&vec![1u64, 2u64, 3u64]).unwrap(); - let vec_type = ShankIdlType::Vec(ShankIdlTypeVec { vec: Box::new(ShankIdlType::U64) }); + 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(); @@ -1052,7 +1051,7 @@ fn test_shank_decode_vec_u64() { #[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(ShankIdlTypeVec { vec: Box::new(ShankIdlType::String) }); + 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(); @@ -1071,7 +1070,7 @@ fn test_shank_decode_vec_string() { fn test_shank_decode_fixed_array_u8() { let data: [u8; 4] = [1, 2, 3, 4]; let arr_type = - ShankIdlType::Array(ShankIdlTypeArray { array: (Box::new(ShankIdlType::U8), 4) }); + 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(); @@ -1086,7 +1085,7 @@ fn test_shank_decode_fixed_array_u8() { fn test_shank_decode_pubkey_like_array() { let pubkey_bytes = [42u8; 32]; let arr_type = - ShankIdlType::Array(ShankIdlTypeArray { array: (Box::new(ShankIdlType::U8), 32) }); + 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, @@ -1260,14 +1259,14 @@ fn test_shank_idl_type_to_txtx_type(idl_type: ShankIdlType, expected: Type) { #[test] fn test_shank_option_type_to_txtx_type() { - let opt_type = ShankIdlType::Option(ShankIdlTypeOption { option: Box::new(ShankIdlType::U64) }); + 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(ShankIdlTypeVec { vec: Box::new(ShankIdlType::String) }); + 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())); } @@ -1275,14 +1274,14 @@ fn test_shank_vec_type_to_txtx_type() { #[test] fn test_shank_array_type_to_txtx_type() { let arr_type = - ShankIdlType::Array(ShankIdlTypeArray { array: (Box::new(ShankIdlType::U8), 32) }); + 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(ShankIdlTypeDefined { defined: "Point".to_string() }); + 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()); @@ -1314,7 +1313,7 @@ fn test_shank_encode_string() { #[test] fn test_shank_encode_option_none() { let value = Value::null(); - let opt_type = ShankIdlType::Option(ShankIdlTypeOption { option: Box::new(ShankIdlType::U64) }); + 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()); } @@ -1325,7 +1324,7 @@ fn test_shank_encode_option_some_bool() { // Addon values go through a different path (borsh_encode_bytes_to_shank_idl_type) let value = Value::bool(true); let opt_type = - ShankIdlType::Option(ShankIdlTypeOption { option: Box::new(ShankIdlType::Bool) }); + 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 @@ -1337,7 +1336,7 @@ fn test_shank_encode_option_some_bool() { fn test_shank_encode_option_some_string() { let value = Value::string("test".to_string()); let opt_type = - ShankIdlType::Option(ShankIdlTypeOption { option: Box::new(ShankIdlType::String) }); + 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()); @@ -1347,7 +1346,7 @@ fn test_shank_encode_option_some_string() { #[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(ShankIdlTypeVec { vec: Box::new(ShankIdlType::U64) }); + 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); @@ -1358,7 +1357,7 @@ 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(ShankIdlTypeArray { array: (Box::new(ShankIdlType::U8), 4) }); + 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]); } @@ -1366,7 +1365,7 @@ fn test_shank_encode_fixed_array() { #[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(ShankIdlTypeDefined { defined: "Point".to_string() }); + 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)] @@ -1382,7 +1381,7 @@ fn test_shank_encode_struct() { 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(ShankIdlTypeDefined { defined: "Status".to_string() }); + 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]); @@ -1397,7 +1396,7 @@ fn test_shank_encode_struct_enum() { )]) .to_value(); let def_type = - ShankIdlType::Defined(ShankIdlTypeDefined { defined: "StructVariantEnum".to_string() }); + ShankIdlType::Defined("StructVariantEnum".to_string()); let result = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES).unwrap(); #[derive(BorshSerialize)] @@ -1443,7 +1442,7 @@ fn test_shank_round_trip_primitives() { 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(ShankIdlTypeDefined { defined: "Point".to_string() }); + let def_type = ShankIdlType::Defined("Point".to_string()); let encoded = borsh_encode_value_to_shank_idl_type(&value, &def_type, &SHANK_TYPES).unwrap(); @@ -1462,7 +1461,7 @@ fn test_shank_round_trip_vec() { Value::string("second".to_string()), Value::string("third".to_string()), ]); - let vec_type = ShankIdlType::Vec(ShankIdlTypeVec { vec: Box::new(ShankIdlType::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) = @@ -1493,7 +1492,7 @@ fn test_shank_decode_not_enough_bytes() { fn test_shank_decode_undefined_type() { let data = vec![0u8; 8]; let def_type = - ShankIdlType::Defined(ShankIdlTypeDefined { defined: "NonExistentType".to_string() }); + 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()); @@ -1503,9 +1502,7 @@ fn test_shank_decode_undefined_type() { #[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(ShankIdlTypeArray { - array: (Box::new(ShankIdlType::U8), 4), // Expects 4 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")); @@ -1514,7 +1511,7 @@ fn test_shank_encode_wrong_array_length() { #[test] fn test_shank_encode_invalid_enum_variant() { let value = ObjectType::from([("InvalidVariant", Value::null())]).to_value(); - let def_type = ShankIdlType::Defined(ShankIdlTypeDefined { defined: "Status".to_string() }); + 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")); @@ -1526,7 +1523,7 @@ fn test_shank_encode_invalid_enum_variant() { #[test] fn test_extract_shank_types_from_idl() { - let types = extract_shank_types(&SHANK_IDL).unwrap(); + let types = extract_shank_types(&SHANK_IDL); assert!(!types.is_empty()); // Verify some expected types exist