Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/core/src/surfnet/locker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/surfnet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
140 changes: 26 additions & 114 deletions crates/core/src/surfnet/svm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Value> {
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<Vec<Value>, _> = 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)]
Expand All @@ -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<AddonJsonConverter<'static>> {
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,
Expand Down Expand Up @@ -1682,31 +1594,31 @@ impl SurfnetSvm {
idl: &Idl,
overrides: &HashMap<String, serde_json::Value>,
) -> SurfpoolResult<Vec<u8>> {
// 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
Expand Down Expand Up @@ -1775,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);
Expand Down Expand Up @@ -2386,17 +2298,17 @@ impl SurfnetSvm {
}
})
.collect::<Vec<_>>();

// 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) =
Expand Down
109 changes: 109 additions & 0 deletions crates/core/src/surfnet/utils.rs
Original file line number Diff line number Diff line change
@@ -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<AddonJsonConverter<'static>> {
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<Value> {
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<Vec<Value>, _> = 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)
}