From 75f01ca41e99a8e1fc3858152b7ae413c1ee5eea Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Thu, 16 Oct 2025 15:39:17 -0400 Subject: [PATCH 1/3] fix(core): add native_sol account to litesvm state on startup --- Cargo.lock | 23 +++++++++++++++++++++++ Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/surfnet/svm.rs | 22 +++++++++++++++++++--- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42d7df05..847e0036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4901,6 +4901,28 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "litesvm-token" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c643347d7f08566efa47559928b163b53fbef1081272ba9e93e8aa9da651111" +dependencies = [ + "litesvm", + "smallvec", + "solana-account 3.1.0", + "solana-keypair", + "solana-program-option 3.0.0", + "solana-program-pack 3.0.0", + "solana-pubkey 3.0.0", + "solana-rent 3.0.0", + "solana-signer", + "solana-system-interface 2.0.0", + "solana-transaction", + "solana-transaction-error 3.0.0", + "spl-associated-token-account-interface", + "spl-token-interface", +] + [[package]] name = "local-channel" version = "0.1.5" @@ -12110,6 +12132,7 @@ dependencies = [ "jsonrpc-ws-server", "libloading", "litesvm", + "litesvm-token", "log 0.4.28", "reqwest 0.12.23", "serde", diff --git a/Cargo.toml b/Cargo.toml index cafdecf2..adb914db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ juniper_graphql_ws = { version = "0.4.0", default-features = false } lazy_static = "1.5.0" libloading = "0.7.4" litesvm = { version = "0.8.1", features = ["nodejs-internal"] } +litesvm-token = "0.8.1" log = "0.4.27" mime_guess = { version = "2.0.4", default-features = false } mustache = "0.9.0" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 3abdaac7..79dffcb3 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -38,6 +38,7 @@ jsonrpc-pubsub = { workspace = true } jsonrpc-ws-server = { workspace = true } libloading = { workspace = true } litesvm = { workspace = true } +litesvm-token = { workspace = true } log = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 526cd9a0..b941bb23 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -14,6 +14,7 @@ use litesvm::{ FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata, TransactionResult, }, }; +use litesvm_token::create_native_mint; use solana_account::{Account, AccountSharedData, ReadableAccount}; use solana_account_decoder::{ UiAccount, UiAccountData, UiAccountEncoding, UiDataSliceConfig, encode_ui_account, @@ -166,11 +167,26 @@ impl SurfnetSvm { // todo: consider making this configurable via config feature_set.deactivate(&enable_extend_program_checked::id()); - let inner = LiteSVM::new() + let mut inner = LiteSVM::new() .with_feature_set(feature_set.clone()) .with_blockhash_check(false) .with_sigverify(false); + // Add the native mint (SOL) to the SVM + create_native_mint(&mut inner); + let native_mint_account = inner + .get_account(&spl_token_interface::native_mint::ID) + .unwrap(); + let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap(); + + // Load native mint into owned account and token mint indexes + let accounts_by_owner = HashMap::from([( + native_mint_account.owner, + vec![spl_token_interface::native_mint::ID], + )]); + let token_mints = + HashMap::from([(spl_token_interface::native_mint::ID, parsed_mint_account)]); + let mut svm = Self { inner, remote_rpc_url: None, @@ -200,10 +216,10 @@ impl SurfnetSvm { logs_subscriptions: Vec::new(), updated_at: Utc::now().timestamp_millis() as u64, slot_time: DEFAULT_SLOT_TIME_MS, - accounts_by_owner: HashMap::new(), + accounts_by_owner, account_associated_data: HashMap::new(), token_accounts: HashMap::new(), - token_mints: HashMap::new(), + token_mints, token_accounts_by_owner: HashMap::new(), token_accounts_by_delegate: HashMap::new(), token_accounts_by_mint: HashMap::new(), From 9c82c478c3061bc01b54e7b5cffe1bb442ae3851 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Thu, 16 Oct 2025 15:42:30 -0400 Subject: [PATCH 2/3] feat(cli): add scaffold for setup_surfnet iac --- Cargo.toml | 1 + crates/cli/Cargo.toml | 1 + crates/cli/src/cli/mod.rs | 7 +- crates/cli/src/cli/simnet/mod.rs | 12 +- crates/cli/src/scaffold/anchor.rs | 216 +++++++++++++++++++++++++++++- crates/cli/src/scaffold/mod.rs | 26 ++-- crates/cli/src/types/mod.rs | 10 +- 7 files changed, 250 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index adb914db..bb2cd987 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ toml = { version = "0.8.23", default-features = false } tracing = { version = "0.1.41", default-features = false } url = { version = "1.7.2", default-features = false } uuid = "1.15.1" +walkdir = "2.3.3" zip = { version = "0.6", features = ["deflate"], default-features = false } surfpool-core = { path = "crates/core", default-features = false } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 5d94c863..b81e50f7 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -54,6 +54,7 @@ solana-transaction = { workspace = true } tokio = { workspace = true } toml = { workspace = true, optional = true } url = { workspace = true } +walkdir = { workspace = true } surfpool-core = { workspace = true } surfpool-gql = { workspace = true } diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index c6ebb8d0..e91f2f9c 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -224,10 +224,9 @@ pub struct StartSimnet { /// Start surfpool with some CI adequate settings (eg. surfpool start --ci) #[clap(long = "ci", action=ArgAction::SetTrue, default_value = "false")] pub ci: bool, - /// Apply suggested defaults for runbook generation and execution. - /// This includes executing any deployment runbooks, and generating in-memory deployment runbooks if none exist. (eg. surfpool start --autopilot) - #[clap(long = "autopilot", action=ArgAction::SetTrue, default_value = "false")] - pub autopilot: bool, + /// Apply suggested defaults for runbook generation and execution when running as part of an anchor test suite (eg. surfpool start --anchor-compatibility) + #[clap(long = "anchor-compatibility", action=ArgAction::SetTrue, default_value = "false")] + pub anchor_compat: bool, } #[derive(clap::ValueEnum, PartialEq, Clone, Debug)] diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index 81468149..9234742a 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -431,14 +431,14 @@ async fn write_and_execute_iac( .map_err(|e| format!("Failed to detect project framework: {}", e))?; let (progress_tx, progress_rx) = crossbeam::channel::unbounded(); - if let Some((framework, programs)) = deployment { + if let Some((framework, programs, genesis_accounts)) = deployment { // Is infrastructure-as-code (IaC) already setup? let base_location = FileLocation::from_path_string(&cmd.manifest_path)?.get_parent_location()?; let mut txtx_manifest_location = base_location.clone(); txtx_manifest_location.append_path("txtx.yml")?; let txtx_manifest_exists = txtx_manifest_location.exists(); - let do_write_scaffold = !cmd.autopilot && !txtx_manifest_exists; + let do_write_scaffold = !cmd.anchor_compat && !txtx_manifest_exists; if do_write_scaffold { // Scaffold IaC scaffold_iac_layout( @@ -452,12 +452,16 @@ async fn write_and_execute_iac( // If there were existing on-disk runbooks, we'll execute those instead of in-memory ones // If there were no existing runbooks and the user requested autopilot, we'll generate and execute in-memory runbooks // If there were no existing runbooks and the user did not request autopilot, we'll generate and execute on-disk runbooks - let do_execute_in_memory_runbooks = cmd.autopilot && !txtx_manifest_exists; + let do_execute_in_memory_runbooks = cmd.anchor_compat && !txtx_manifest_exists; let mut on_disk_runbook_data = None; let mut in_memory_runbook_data = None; if do_execute_in_memory_runbooks { - in_memory_runbook_data = Some(scaffold_in_memory_iac(&framework, &programs)?); + in_memory_runbook_data = Some(scaffold_in_memory_iac( + &framework, + &programs, + &genesis_accounts, + )?); } else { let runbooks_ids_to_execute = cmd.runbooks.clone(); on_disk_runbook_data = Some((txtx_manifest_location.clone(), runbooks_ids_to_execute)); diff --git a/crates/cli/src/scaffold/anchor.rs b/crates/cli/src/scaffold/anchor.rs index 28a33464..9b27a347 100644 --- a/crates/cli/src/scaffold/anchor.rs +++ b/crates/cli/src/scaffold/anchor.rs @@ -1,18 +1,23 @@ #![allow(dead_code)] -use std::{collections::BTreeMap, str::FromStr}; +use std::{ + collections::{BTreeMap, HashMap}, + path::{Path, PathBuf}, + str::FromStr, +}; use anyhow::{Result, anyhow}; use serde::{Deserialize, Serialize}; use txtx_core::kit::helpers::fs::FileLocation; use url::Url; +use walkdir::WalkDir; use super::ProgramMetadata; -use crate::types::Framework; +use crate::{scaffold::GenesisEntry, types::Framework}; pub fn try_get_programs_from_project( base_location: FileLocation, -) -> Result)>, String> { +) -> Result, Option>)>, String> { let mut manifest_location = base_location.clone(); manifest_location.append_path("Anchor.toml")?; if manifest_location.exists() { @@ -30,8 +35,38 @@ pub fn try_get_programs_from_project( programs.push(ProgramMetadata::new(program_name, &deployment.idl)); } } + let mut genesis_entries = manifest + .test + .as_ref() + .and_then(|test| test.genesis.as_ref()) + .cloned() + .unwrap_or_default(); + if let Some(test_configs) = TestConfig::discover_test_toml(&base_location.expect_path_buf()) + .map_err(|e| { + format!( + "failed to discover Test.toml files in workspace: {}", + e.to_string() + ) + })? + { + for (_, config) in test_configs.test_suite_configs.iter() { + if let Some(test_config) = config.test.as_ref() { + if let Some(genesis) = test_config.genesis.as_ref() { + genesis_entries.extend(genesis.clone()); + } + } + } + } - Ok(Some((Framework::Anchor, programs))) + Ok(Some(( + Framework::Anchor, + programs, + if genesis_entries.is_empty() { + None + } else { + Some(genesis_entries) + }, + ))) } else { Ok(None) } @@ -46,6 +81,7 @@ pub struct AnchorManifest { pub programs: ProgramsConfig, pub scripts: ScriptsConfig, pub workspace: WorkspaceConfig, + pub test: Option, } #[derive(Debug, Deserialize)] @@ -57,6 +93,7 @@ pub struct AnchorManifestFile { // provider: Provider, workspace: Option, scripts: Option, + test: Option, } impl AnchorManifest { @@ -72,6 +109,7 @@ impl AnchorManifest { .programs .map_or(Ok(BTreeMap::new()), |p| deser_programs(p, base_location))?, workspace: cfg.workspace.unwrap_or_default(), + test: cfg.test, }) } } @@ -122,6 +160,170 @@ impl Default for RegistryConfig { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestTomlFile { + pub extends: Option>, + pub test: Option, + pub scripts: Option, +} + +impl TestTomlFile { + fn from_path(path: impl AsRef) -> Result { + let s = std::fs::read_to_string(&path)?; + let parsed_toml: Self = toml::from_str(&s)?; + let mut current_toml = TestTomlFile { + extends: None, + test: None, + scripts: None, + }; + if let Some(bases) = &parsed_toml.extends { + for base in bases { + let mut canonical_base = base.clone(); + canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?; + current_toml.merge(TestTomlFile::from_path(&canonical_base)?); + } + } + current_toml.merge(parsed_toml); + + if let Some(test) = &mut current_toml.test { + if let Some(genesis_programs) = &mut test.genesis { + for entry in genesis_programs { + entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?; + } + } + } + Ok(current_toml) + } +} + +impl From for TestToml { + fn from(value: TestTomlFile) -> Self { + Self { + test: value.test, + scripts: value.scripts.unwrap_or_default(), + } + } +} + +impl TestTomlFile { + fn merge(&mut self, other: Self) { + let mut my_scripts = self.scripts.take(); + match &mut my_scripts { + None => my_scripts = other.scripts, + Some(my_scripts) => { + if let Some(other_scripts) = other.scripts { + for (name, script) in other_scripts { + my_scripts.insert(name, script); + } + } + } + } + + let mut my_test = self.test.take(); + match &mut my_test { + Some(my_test) => { + if let Some(other_test) = other.test { + if let Some(other_genesis) = other_test.genesis { + match &mut my_test.genesis { + Some(my_genesis) => { + for other_entry in other_genesis { + match my_genesis + .iter() + .position(|g| *g.address == other_entry.address) + { + None => my_genesis.push(other_entry), + Some(i) => my_genesis[i] = other_entry, + } + } + } + None => my_test.genesis = Some(other_genesis), + } + } + } + } + None => my_test = other.test, + }; + + // Instantiating a new Self object here ensures that + // this function will fail to compile if new fields get added + // to Self. This is useful as a reminder if they also require merging + *self = Self { + test: my_test, + scripts: my_scripts, + extends: self.extends.take(), + }; + } +} + +fn canonicalize_filepath_from_origin( + file_path: impl AsRef, + origin: impl AsRef, +) -> Result { + use anyhow::Context; + let previous_dir = std::env::current_dir()?; + std::env::set_current_dir(origin.as_ref().parent().unwrap())?; + let result = std::fs::canonicalize(&file_path) + .with_context(|| { + format!( + "Error reading (possibly relative) path: {}. If relative, this is the path that was used as the current path: {}", + &file_path.as_ref().display(), + &origin.as_ref().display() + ) + })? + .display() + .to_string(); + std::env::set_current_dir(previous_dir)?; + Ok(result) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestToml { + #[serde(skip_serializing_if = "Option::is_none")] + pub test: Option, + pub scripts: ScriptsConfig, +} +impl TestToml { + pub fn from_path(p: impl AsRef) -> Result { + TestTomlFile::from_path(&p).map(Into::into).map_err(|e| { + anyhow!( + "Unable to read Test.toml at {}: {}", + p.as_ref().display(), + e + ) + }) + } +} +#[derive(Debug, Clone)] +pub struct TestConfig { + pub test_suite_configs: HashMap, +} +fn is_hidden(entry: &walkdir::DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| (s != "." && (s.starts_with('.') || s.starts_with("./."))) || s == "target") + .unwrap_or(false) +} +impl TestConfig { + pub fn discover_test_toml(root: impl AsRef) -> Result> { + let walker = WalkDir::new(root).into_iter(); + let mut test_suite_configs = HashMap::new(); + for entry in walker.filter_entry(|e| !is_hidden(e)) { + let entry = entry?; + if entry.file_name() == "Test.toml" { + let entry_path = entry.path(); + let test_toml = TestToml::from_path(entry_path)?; + test_suite_configs.insert(entry.path().into(), test_toml); + } + } + + Ok(match test_suite_configs.is_empty() { + true => None, + false => Some(Self { test_suite_configs }), + }) + } +} + #[derive(Debug, Default, Serialize, Deserialize)] pub struct AnchorProgramDeployment { pub address: String, @@ -241,6 +443,12 @@ pub struct WorkspaceConfig { pub types: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestValidatorConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub genesis: Option>, +} + fn deser_programs( programs: BTreeMap>, base_location: &FileLocation, diff --git a/crates/cli/src/scaffold/mod.rs b/crates/cli/src/scaffold/mod.rs index 2068bd9c..b373389b 100644 --- a/crates/cli/src/scaffold/mod.rs +++ b/crates/cli/src/scaffold/mod.rs @@ -6,7 +6,7 @@ use std::{ use dialoguer::{Confirm, Input, MultiSelect, console::Style, theme::ColorfulTheme}; use surfpool_types::{DEFAULT_NETWORK_HOST, DEFAULT_RPC_PORT}; use txtx_addon_network_svm::templates::{ - get_interpolated_addon_template, get_interpolated_devnet_signer_template, + GenesisEntry, get_interpolated_addon_template, get_interpolated_devnet_signer_template, get_interpolated_header_template, get_interpolated_localnet_signer_template, get_interpolated_mainnet_signer_template, }; @@ -30,43 +30,44 @@ pub mod utils; pub async fn detect_program_frameworks( manifest_path: &str, -) -> Result)>, String> { +) -> Result, Option>)>, String> { let manifest_location = FileLocation::from_path_string(manifest_path)?; let base_dir = manifest_location.get_parent_location()?; // Look for Anchor project layout // Note: Poseidon projects generate Anchor.toml files, so they will also be identified here - if let Some((framework, programs)) = anchor::try_get_programs_from_project(base_dir.clone()) - .map_err(|e| format!("Invalid Anchor project: {e}"))? + if let Some((framework, programs, genesis_accounts)) = + anchor::try_get_programs_from_project(base_dir.clone()) + .map_err(|e| format!("Invalid Anchor project: {e}"))? { - return Ok(Some((framework, programs))); + return Ok(Some((framework, programs, genesis_accounts))); } // Look for Steel project layout if let Some((framework, programs)) = steel::try_get_programs_from_project(base_dir.clone()) .map_err(|e| format!("Invalid Steel project: {e}"))? { - return Ok(Some((framework, programs))); + return Ok(Some((framework, programs, None))); } // Look for Typhoon project layout if let Some((framework, programs)) = typhoon::try_get_programs_from_project(base_dir.clone()) .map_err(|e| format!("Invalid Typhoon project: {e}"))? { - return Ok(Some((framework, programs))); + return Ok(Some((framework, programs, None))); } // Look for Pinocchio project layout if let Some((framework, programs)) = pinocchio::try_get_programs_from_project(base_dir.clone()) .map_err(|e| format!("Invalid Pinocchio project: {e}"))? { - return Ok(Some((framework, programs))); + return Ok(Some((framework, programs, None))); } // Look for Native project layout if let Some((framework, programs)) = native::try_get_programs_from_project(base_dir.clone()) .map_err(|e| format!("Invalid Native project: {e}"))? { - return Ok(Some((framework, programs))); + return Ok(Some((framework, programs, None))); } Ok(None) @@ -90,6 +91,7 @@ impl ProgramMetadata { pub fn scaffold_in_memory_iac( framework: &Framework, programs: &Vec, + genesis_accounts: &Option>, ) -> Result<(String, RunbookSources, WorkspaceManifest), String> { let mut deployment_runbook_src: String = String::new(); @@ -119,6 +121,12 @@ pub fn scaffold_in_memory_iac( } } + if let Some(setup_surfnet_iac) = framework + .get_interpolated_setup_surfnet_template(genesis_accounts.as_ref().unwrap_or(&vec![])) + { + deployment_runbook_src.push_str(&setup_surfnet_iac); + } + let runbook_id = "deployment"; let mut manifest = WorkspaceManifest::new("memory".to_string()); let runbook = RunbookMetadata::new(runbook_id, runbook_id, Some("Deploy programs".to_string())); diff --git a/crates/cli/src/types/mod.rs b/crates/cli/src/types/mod.rs index 412e07d2..9d0c22c3 100644 --- a/crates/cli/src/types/mod.rs +++ b/crates/cli/src/types/mod.rs @@ -1,8 +1,8 @@ use txtx_addon_network_svm::templates::{ - get_in_memory_interpolated_anchor_program_deployment_template, + GenesisEntry, get_in_memory_interpolated_anchor_program_deployment_template, get_in_memory_interpolated_native_program_deployment_template, get_interpolated_anchor_program_deployment_template, get_interpolated_anchor_subgraph_template, - get_interpolated_native_program_deployment_template, + get_interpolated_native_program_deployment_template, get_interpolated_setup_surfnet_template, }; #[derive(Debug, Clone)] @@ -59,6 +59,12 @@ impl Framework { Framework::Typhoon => todo!(), } } + pub fn get_interpolated_setup_surfnet_template( + &self, + genesis_accounts: &Vec, + ) -> Option { + get_interpolated_setup_surfnet_template(genesis_accounts) + } } impl std::fmt::Display for Framework { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From 78e258b3a93612cdaaa07e9ca88d32043edb7b3b Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Fri, 17 Oct 2025 10:55:27 -0400 Subject: [PATCH 3/3] fix(cli): update anchor compatibility flag to legacy naming --- crates/cli/src/cli/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index e91f2f9c..92303cbc 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -224,8 +224,8 @@ pub struct StartSimnet { /// Start surfpool with some CI adequate settings (eg. surfpool start --ci) #[clap(long = "ci", action=ArgAction::SetTrue, default_value = "false")] pub ci: bool, - /// Apply suggested defaults for runbook generation and execution when running as part of an anchor test suite (eg. surfpool start --anchor-compatibility) - #[clap(long = "anchor-compatibility", action=ArgAction::SetTrue, default_value = "false")] + /// Apply suggested defaults for runbook generation and execution when running as part of an anchor test suite (eg. surfpool start --legacy-anchor-compatibility) + #[clap(long = "legacy-anchor-compatibility", action=ArgAction::SetTrue, default_value = "false")] pub anchor_compat: bool, }