From 7a4d49d1cfb3bec15dcb112b0ed688d676fae641 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Fri, 19 Dec 2025 11:14:27 -0500 Subject: [PATCH 1/2] move some helpful functions to utils --- crates/core/src/surfnet/locker.rs | 2 +- crates/core/src/surfnet/mod.rs | 1 + crates/core/src/surfnet/svm.rs | 104 +++------------------------- crates/core/src/surfnet/utils.rs | 109 ++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 97 deletions(-) create mode 100644 crates/core/src/surfnet/utils.rs diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index 0dcc7d95..8291766c 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -3307,7 +3307,7 @@ mod tests { use super::*; use crate::{ scenarios::registry::PYTH_V2_IDL_CONTENT, - surfnet::{SurfnetSvm, svm::apply_override_to_decoded_account}, + surfnet::{SurfnetSvm, utils::apply_override_to_decoded_account}, }; #[test] diff --git a/crates/core/src/surfnet/mod.rs b/crates/core/src/surfnet/mod.rs index c66a199a..80dd24b7 100644 --- a/crates/core/src/surfnet/mod.rs +++ b/crates/core/src/surfnet/mod.rs @@ -24,6 +24,7 @@ use crate::{ pub mod locker; pub mod remote; pub mod svm; +pub mod utils; pub const FINALIZATION_SLOT_THRESHOLD: u64 = 31; pub const SLOTS_PER_EPOCH: u64 = 432000; diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index a07fa38e..dc0df8db 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -79,10 +79,7 @@ use surfpool_types::{ ComputeUnitsEstimationResult, KeyedProfileResult, UiKeyedProfileResult, UuidOrSignature, }, }; -use txtx_addon_kit::{ - indexmap::IndexMap, - types::types::{AddonJsonConverter, Value}, -}; +use txtx_addon_kit::indexmap::IndexMap; use txtx_addon_network_svm::codec::idl::borsh_encode_value_to_idl_type; use txtx_addon_network_svm_types::subgraph::idl::{ parse_bytes_to_value_with_expected_idl_type_def_ty, @@ -99,96 +96,19 @@ use crate::{ error::{SurfpoolError, SurfpoolResult}, rpc::utils::convert_transaction_metadata_from_canonical, scenarios::TemplateRegistry, - surfnet::{LogsSubscriptionData, locker::is_supported_token_program}, + surfnet::{ + LogsSubscriptionData, + locker::is_supported_token_program, + utils::{ + apply_override_to_decoded_account, find_discriminator, get_txtx_value_json_converters, + }, + }, types::{ GeyserAccountUpdate, MintAccount, SurfnetTransactionStatus, SyntheticBlockhash, TokenAccount, TransactionWithStatusMeta, }, }; -/// Helper function to apply an override to a decoded account value using dot notation -pub fn apply_override_to_decoded_account( - decoded_value: &mut Value, - path: &str, - value: &serde_json::Value, -) -> SurfpoolResult<()> { - let parts: Vec<&str> = path.split('.').collect(); - - if parts.is_empty() { - return Err(SurfpoolError::internal("Empty path provided for override")); - } - - // Navigate to the parent of the target field - let mut current = decoded_value; - for part in &parts[..parts.len() - 1] { - match current { - Value::Object(map) => { - current = map.get_mut(&part.to_string()).ok_or_else(|| { - SurfpoolError::internal(format!( - "Path segment '{}' not found in decoded account", - part - )) - })?; - } - _ => { - return Err(SurfpoolError::internal(format!( - "Cannot navigate through field '{}' - not an object", - part - ))); - } - } - } - - // Set the final field - let final_key = parts[parts.len() - 1]; - match current { - Value::Object(map) => { - // Convert serde_json::Value to txtx Value - let txtx_value = json_to_txtx_value(value)?; - map.insert(final_key.to_string(), txtx_value); - Ok(()) - } - _ => Err(SurfpoolError::internal(format!( - "Cannot set field '{}' - parent is not an object", - final_key - ))), - } -} - -/// Helper function to convert serde_json::Value to txtx Value -fn json_to_txtx_value(json: &serde_json::Value) -> SurfpoolResult { - match json { - serde_json::Value::Null => Ok(Value::Null), - serde_json::Value::Bool(b) => Ok(Value::Bool(*b)), - serde_json::Value::Number(n) => { - if let Some(i) = n.as_i64() { - Ok(Value::Integer(i as i128)) - } else if let Some(u) = n.as_u64() { - Ok(Value::Integer(u as i128)) - } else if let Some(f) = n.as_f64() { - Ok(Value::Float(f)) - } else { - Err(SurfpoolError::internal(format!( - "Unable to convert number: {}", - n - ))) - } - } - serde_json::Value::String(s) => Ok(Value::String(s.clone())), - serde_json::Value::Array(arr) => { - let txtx_arr: Result, _> = arr.iter().map(json_to_txtx_value).collect(); - Ok(Value::Array(Box::new(txtx_arr?))) - } - serde_json::Value::Object(obj) => { - let mut txtx_obj = IndexMap::new(); - for (k, v) in obj.iter() { - txtx_obj.insert(k.clone(), json_to_txtx_value(v)?); - } - Ok(Value::Object(txtx_obj)) - } - } -} - pub type AccountOwner = Pubkey; #[allow(deprecated)] @@ -197,14 +117,6 @@ use solana_sysvar::recent_blockhashes::MAX_ENTRIES; #[allow(deprecated)] pub const MAX_RECENT_BLOCKHASHES_STANDARD: usize = MAX_ENTRIES; -pub fn get_txtx_value_json_converters() -> Vec> { - vec![ - Box::new(move |value: &txtx_addon_kit::types::types::Value| { - txtx_addon_network_svm_types::SvmValue::to_json(value) - }) as AddonJsonConverter<'static>, - ] -} - /// `SurfnetSvm` provides a lightweight Solana Virtual Machine (SVM) for testing and simulation. /// /// It supports a local in-memory blockchain state, diff --git a/crates/core/src/surfnet/utils.rs b/crates/core/src/surfnet/utils.rs new file mode 100644 index 00000000..6a9e8a42 --- /dev/null +++ b/crates/core/src/surfnet/utils.rs @@ -0,0 +1,109 @@ +use txtx_addon_kit::{ + indexmap::IndexMap, + types::types::{AddonJsonConverter, Value}, +}; + +use crate::error::{SurfpoolError, SurfpoolResult}; + +pub fn get_txtx_value_json_converters() -> Vec> { + vec![ + Box::new(move |value: &txtx_addon_kit::types::types::Value| { + txtx_addon_network_svm_types::SvmValue::to_json(value) + }) as AddonJsonConverter<'static>, + ] +} + +/// Helper function to apply an override to a decoded account value using dot notation +pub fn apply_override_to_decoded_account( + decoded_value: &mut Value, + path: &str, + value: &serde_json::Value, +) -> SurfpoolResult<()> { + let parts: Vec<&str> = path.split('.').collect(); + + if parts.is_empty() { + return Err(SurfpoolError::internal("Empty path provided for override")); + } + + // Navigate to the parent of the target field + let mut current = decoded_value; + for part in &parts[..parts.len() - 1] { + match current { + Value::Object(map) => { + current = map.get_mut(&part.to_string()).ok_or_else(|| { + SurfpoolError::internal(format!( + "Path segment '{}' not found in decoded account", + part + )) + })?; + } + _ => { + return Err(SurfpoolError::internal(format!( + "Cannot navigate through field '{}' - not an object", + part + ))); + } + } + } + + // Set the final field + let final_key = parts[parts.len() - 1]; + match current { + Value::Object(map) => { + // Convert serde_json::Value to txtx Value + let txtx_value = json_to_txtx_value(value)?; + map.insert(final_key.to_string(), txtx_value); + Ok(()) + } + _ => Err(SurfpoolError::internal(format!( + "Cannot set field '{}' - parent is not an object", + final_key + ))), + } +} + +/// Helper function to convert serde_json::Value to txtx Value +fn json_to_txtx_value(json: &serde_json::Value) -> SurfpoolResult { + match json { + serde_json::Value::Null => Ok(Value::Null), + serde_json::Value::Bool(b) => Ok(Value::Bool(*b)), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(Value::Integer(i as i128)) + } else if let Some(u) = n.as_u64() { + Ok(Value::Integer(u as i128)) + } else if let Some(f) = n.as_f64() { + Ok(Value::Float(f)) + } else { + Err(SurfpoolError::internal(format!( + "Unable to convert number: {}", + n + ))) + } + } + serde_json::Value::String(s) => Ok(Value::String(s.clone())), + serde_json::Value::Array(arr) => { + let txtx_arr: Result, _> = arr.iter().map(json_to_txtx_value).collect(); + Ok(Value::Array(Box::new(txtx_arr?))) + } + serde_json::Value::Object(obj) => { + let mut txtx_obj = IndexMap::new(); + for (k, v) in obj.iter() { + txtx_obj.insert(k.clone(), json_to_txtx_value(v)?); + } + Ok(Value::Object(txtx_obj)) + } + } +} + +/// Helper function to find if the account data starts with the given IDL discriminator +/// ## Inputs +/// - `account_data`: The raw account data bytes +/// - `idl_discriminator`: The IDL discriminator bytes to match against +/// ## Returns +/// - `true` if the account data starts with the IDL discriminator, `false` otherwise +pub fn find_discriminator(account_data: &[u8], idl_discriminator: &[u8]) -> bool { + let idl_discriminator_len = idl_discriminator.len(); + account_data.len() >= idl_discriminator_len + && account_data[..idl_discriminator_len].eq(idl_discriminator) +} From ebc5a2b65da5fa8408d3728f2fd1af6f6e028389 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Fri, 19 Dec 2025 11:14:57 -0500 Subject: [PATCH 2/2] fix(core): when parsing account data against idl, allow variable length discriminators as dictated by the idl --- crates/core/src/surfnet/svm.rs | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index dc0df8db..03e255d9 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -1594,31 +1594,31 @@ impl SurfnetSvm { idl: &Idl, overrides: &HashMap, ) -> SurfpoolResult> { - // Validate account data size - if account_data.len() < 8 { - return Err(SurfpoolError::invalid_account_data( - account_pubkey, - "Account data too small to be an Anchor account (need at least 8 bytes for discriminator)", - Some("Data length too small"), - )); - } - - // Split discriminator and data - let discriminator = &account_data[..8]; - let serialized_data = &account_data[8..]; - // Find the account type using the discriminator let account_def = idl .accounts .iter() - .find(|acc| acc.discriminator.eq(discriminator)) + .find(|acc| find_discriminator(&account_data, &acc.discriminator)) .ok_or_else(|| { SurfpoolError::internal(format!( "Account with discriminator '{:?}' not found in IDL", - discriminator + &account_data[..8] // assuming 8-byte discriminator for error message only )) })?; + // Validate account data size + if account_data.len() <= account_def.discriminator.len() { + return Err(SurfpoolError::invalid_account_data( + account_pubkey, + "Account data too small to contain discriminator and data".to_string(), + Some("Data length too small"), + )); + } + + // Split discriminator and data + let discriminator = &account_data[..account_def.discriminator.len()]; + let serialized_data = &account_data[account_def.discriminator.len()..]; + // Find the corresponding type definition let account_type = idl .types @@ -1687,7 +1687,7 @@ impl SurfnetSvm { // Reconstruct the account data with discriminator and preserve any trailing bytes let mut new_account_data = - Vec::with_capacity(8 + re_encoded_data.len() + leftover_bytes.len()); + Vec::with_capacity(discriminator.len() + re_encoded_data.len() + leftover_bytes.len()); new_account_data.extend_from_slice(discriminator); new_account_data.extend_from_slice(&re_encoded_data); new_account_data.extend_from_slice(leftover_bytes); @@ -2298,17 +2298,17 @@ impl SurfnetSvm { } }) .collect::>(); + // if we have none in this loop, it means the only IDLs registered for this pubkey are for a // future slot, for some reason. if we have some, we'll try each one in this loop, starting // with the most recent one, to see if the account data can be parsed to the IDL type for idl in &ordered_available_idls { // If we have a valid IDL, use it to parse the account data let data = account.data(); - let discriminator = &data[..8]; if let Some(matching_account) = idl .accounts .iter() - .find(|a| a.discriminator.eq(&discriminator)) + .find(|a| find_discriminator(data, &a.discriminator)) { // If we found a matching account, we can look up the type to parse the account if let Some(account_type) =