diff --git a/Cargo.lock b/Cargo.lock index e79baec2..f40883fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,20 +288,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "agave-feature-set" -version = "2.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b7122392ed81e9b4569c3c960a557afb9f735719e0bcc9b2c19d0345568f5f" -dependencies = [ - "ahash 0.8.12", - "solana-epoch-schedule 2.2.1", - "solana-hash 2.3.0", - "solana-pubkey 2.4.0", - "solana-sha256-hasher 2.2.1", - "solana-svm-feature-set 2.3.10", -] - [[package]] name = "agave-feature-set" version = "3.0.6" @@ -313,7 +299,7 @@ dependencies = [ "solana-hash 3.0.0", "solana-pubkey 3.0.0", "solana-sha256-hasher 3.0.0", - "solana-svm-feature-set 3.0.6", + "solana-svm-feature-set", ] [[package]] @@ -356,7 +342,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c10f2547dcb3f20f45e90dbff5b520fbacd52c4231bd2ce1bfb09198b3672530" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "bincode", "digest 0.10.7", "ed25519-dalek 1.0.1", @@ -378,7 +364,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a81224b65697d8e5a7d3bcc0f7d00107247c47b1769bfc0d1874b2b731ea33" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "solana-pubkey 3.0.0", "solana-sdk-ids 3.0.0", ] @@ -415,7 +401,7 @@ dependencies = [ "solana-stable-layout 3.0.0", "solana-stake-interface 2.0.1", "solana-svm-callback", - "solana-svm-feature-set 3.0.6", + "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-measure", "solana-svm-timings", @@ -4842,7 +4828,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22c52e9daf4680aee15e84c877808d94bbd4f3f66cdd32e0ba059d930d581e4" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "agave-precompiles", "agave-reserved-account-keys", "agave-syscalls", @@ -7970,7 +7956,7 @@ dependencies = [ "solana-pubkey 3.0.0", "solana-sbpf", "solana-sdk-ids 3.0.0", - "solana-svm-feature-set 3.0.6", + "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-measure", "solana-svm-type-overrides", @@ -8003,7 +7989,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f6b9e156a48d4c18779b79ae5ec600f59b6c5ac9de5198137801a5f0a888bd3" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", "solana-hash 3.0.0", @@ -8024,7 +8010,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "553a373e7488c8f0fddc1c3076475126c834e091f92b4921c0ca773517207e8f" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "ahash 0.8.12", "log 0.4.28", "solana-bpf-loader-program", @@ -8253,7 +8239,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f542c69beb51a61818bb19bf8159bc293432c14a8b3f9a408bcbf95526d03104" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "log 0.4.28", "solana-borsh 3.0.0", "solana-builtins-default-costs", @@ -8334,7 +8320,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d03b50d1f8b09163d5c583e560501c4dd6463ac02642beda9801d51ec61cfd76" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "ahash 0.8.12", "log 0.4.28", "solana-bincode 3.0.0", @@ -8605,7 +8591,7 @@ dependencies = [ "solana-system-interface 2.0.0", "solana-system-transaction", "solana-transaction", - "solana-version 3.0.6", + "solana-version", "spl-memo-interface", "thiserror 2.0.16", "tokio", @@ -8655,7 +8641,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05a591e8cc59cc9d564d86b91ac7bfb8e4bcb219b5e20f393b1706248be9a992" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "solana-fee-structure", "solana-svm-transaction", ] @@ -8759,7 +8745,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "576df413e3a929ce8b02bed0b27ce7048cd07a7219629d94aa3912aece570c90" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "agave-low-pass-filter", "arrayvec", "assert_matches", @@ -8815,7 +8801,7 @@ dependencies = [ "solana-time-utils", "solana-tpu-client", "solana-transaction", - "solana-version 3.0.6", + "solana-version", "solana-vote", "solana-vote-program", "static_assertions", @@ -9060,7 +9046,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88cba7acada1922890803cda5527f907b3fe020b7fc1f4bf931e2f3a4a905026" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "agave-reserved-account-keys", "anyhow", "assert_matches", @@ -9376,9 +9362,9 @@ dependencies = [ [[package]] name = "solana-native-token" -version = "2.2.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307fb2f78060995979e9b4f68f833623565ed4e55d3725f100454ce78a99a1a3" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" [[package]] name = "solana-native-token" @@ -9632,7 +9618,7 @@ dependencies = [ "solana-loader-v4-interface 2.2.1", "solana-message 2.4.0", "solana-msg 2.2.1", - "solana-native-token 2.2.2", + "solana-native-token 2.3.0", "solana-nonce 2.2.1", "solana-program-entrypoint 2.3.0", "solana-program-error 2.2.2", @@ -9788,7 +9774,7 @@ dependencies = [ "solana-slot-hashes 3.0.0", "solana-stake-interface 2.0.1", "solana-svm-callback", - "solana-svm-feature-set 3.0.6", + "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-measure", "solana-svm-timings", @@ -9989,7 +9975,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83066186e6f3f1fb494020b6f32b3d2e807988d624ec45d80173364a9da40844" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "base64 0.22.1", "bincode", "bs58", @@ -10058,7 +10044,7 @@ dependencies = [ "solana-transaction-error 3.0.0", "solana-transaction-status", "solana-validator-exit", - "solana-version 3.0.6", + "solana-version", "solana-vote", "solana-vote-program", "spl-generic-token", @@ -10105,7 +10091,7 @@ dependencies = [ "solana-transaction", "solana-transaction-error 3.0.0", "solana-transaction-status-client-types", - "solana-version 3.0.6", + "solana-version", "solana-vote-interface 3.0.0", "tokio", ] @@ -10170,7 +10156,7 @@ dependencies = [ "solana-pubkey 3.0.0", "solana-transaction-error 3.0.0", "solana-transaction-status-client-types", - "solana-version 3.0.6", + "solana-version", "spl-generic-token", "thiserror 2.0.16", ] @@ -10181,7 +10167,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99e9a969516a6d9e95f9f55f33e03a62d0d435d09ddf9342f01ebdfb135b1058" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "agave-precompiles", "agave-reserved-account-keys", "agave-syscalls", @@ -10295,7 +10281,7 @@ dependencies = [ "solana-transaction-error 3.0.0", "solana-transaction-status-client-types", "solana-unified-scheduler-logic", - "solana-version 3.0.6", + "solana-version", "solana-vote", "solana-vote-interface 3.0.0", "solana-vote-program", @@ -10742,7 +10728,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d9984ab7bbdf9bcea650fb31f3e196e54b7a2cef9e376edb5a754ea9743d13" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "bincode", "log 0.4.28", "solana-account 3.1.0", @@ -10911,7 +10897,7 @@ dependencies = [ "solana-rent 3.0.0", "solana-sdk-ids 3.0.0", "solana-svm-callback", - "solana-svm-feature-set 3.0.6", + "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-measure", "solana-svm-timings", @@ -10937,12 +10923,6 @@ dependencies = [ "solana-pubkey 3.0.0", ] -[[package]] -name = "solana-svm-feature-set" -version = "2.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c343731bf4a594a615c2aa32a63a0f42f39581e7975114ed825133e30ab68346" - [[package]] name = "solana-svm-feature-set" version = "3.0.6" @@ -11422,28 +11402,13 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d2face763df5afeaa9509b9019968860e69cc1531ec8b4a2e6c7b702204d5a" -[[package]] -name = "solana-version" -version = "2.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb7e1261069a748647abc07f611a5ced461390447aa1fe083eb733796e038b" -dependencies = [ - "agave-feature-set 2.3.10", - "rand 0.8.5", - "semver", - "serde", - "serde_derive", - "solana-sanitize 2.2.1", - "solana-serde-varint 2.2.2", -] - [[package]] name = "solana-version" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e657494d07daa44808f358034aa47df2e541fc0489de22dc2747764cf3341a5" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "rand 0.8.5", "semver", "serde", @@ -11536,7 +11501,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f8f49d7266aba25ea3005b7450f09dcef7418336b0f01d6531a62b22e4617" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "bincode", "log 0.4.28", "num-derive", @@ -11569,7 +11534,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2eab3cefc7a3dc06210c419fdc9da9e19a57f4198a349bfab1c56ae5f5d6278" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "bytemuck", "num-derive", "num-traits", @@ -11623,7 +11588,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3175e35635af1d7227cba9e99358538d0b69af6c127bc8beb572e51cd44e3c6d" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "bytemuck", "num-derive", "num-traits", @@ -12101,14 +12066,13 @@ dependencies = [ "txtx-gql", "txtx-supervisor-ui", "url 1.7.2", - "walkdir", ] [[package]] name = "surfpool-core" version = "0.11.0" dependencies = [ - "agave-feature-set 3.0.6", + "agave-feature-set", "agave-geyser-plugin-interface", "agave-reserved-account-keys", "base64 0.22.1", @@ -12173,7 +12137,7 @@ dependencies = [ "solana-transaction", "solana-transaction-error 3.0.0", "solana-transaction-status", - "solana-version 2.3.10", + "solana-version", "spl-associated-token-account-interface", "spl-token-2022-interface", "spl-token-interface", @@ -13087,9 +13051,9 @@ dependencies = [ [[package]] name = "txtx-addon-network-svm" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c851f2bd4854af1dc0cf999fe5bff1d486ded12957f8d61a71934e1d0de210c" +checksum = "add854320b3aa54e1cd3320bd607ba279796dda9a45990917046d7f56ac5cf1b" dependencies = [ "async-recursion", "bincode", @@ -13102,6 +13066,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-account 3.1.0", + "solana-account-decoder-client-types", "solana-client", "solana-clock 3.0.0", "solana-commitment-config", @@ -13130,9 +13095,9 @@ dependencies = [ [[package]] name = "txtx-addon-network-svm-types" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82cd2c762e396e0776fe29e611cc2b0e7fc54ac9b0e03d81d673cf5c1a755ee" +checksum = "0b8c8776e6a1429951aa2b538b81b87aaaf6716c1b137499039b7d2d7b860f3e" dependencies = [ "anchor-lang-idl", "borsh 1.5.7", diff --git a/Cargo.toml b/Cargo.toml index 751ba90f..10616e3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ solana-transaction = { version = "3.0.0", default-features = false } solana-transaction-context = { version = "3.0.0", default-features = false } solana-transaction-error = { version = "3.0.0", default-features = false } solana-transaction-status = { version = "3.0.0", default-features = false } -solana-version = { version = "2.3.7", default-features = false } +solana-version = { version = "3.0.0", default-features = false } spl-associated-token-account-interface = { version = "2.0.0", default-features = false } spl-token-2022-interface = { version = "2.0.0", default-features = false } spl-token-interface = { version = "2.0.0", default-features = false } @@ -138,7 +138,6 @@ 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 } @@ -150,8 +149,8 @@ surfpool-subgraph = { path = "crates/subgraph", default-features = false } surfpool-types = { path = "crates/types", default-features = false } txtx-addon-kit = "0.4.10" -txtx-addon-network-svm = { version = "0.3.13" } -txtx-addon-network-svm-types = { version = "0.3.12" } +txtx-addon-network-svm = { version = "0.3.14" } +txtx-addon-network-svm-types = { version = "0.3.13" } txtx-cloud = { version = "0.1.13", features = [ "clap", "toml", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index b81e50f7..5d94c863 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -54,7 +54,6 @@ 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 92303cbc..5a6f12fa 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -227,6 +227,9 @@ pub struct StartSimnet { /// 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, + /// Path to the Test.toml test suite files to load (eg. surfpool start --anchor-test-config-path ./path/to/Test.toml) + #[arg(long = "anchor-test-config-path")] + pub anchor_test_config_paths: Vec, } #[derive(clap::ValueEnum, PartialEq, Clone, Debug)] @@ -321,18 +324,7 @@ impl StartSimnet { pub fn simnet_config(&self, airdrop_addresses: Vec) -> SimnetConfig { let remote_rpc_url = if !self.offline { - Some(match &self.network { - Some(NetworkType::Mainnet) => DEFAULT_RPC_URL.to_string(), - Some(NetworkType::Devnet) => DEVNET_RPC_URL.to_string(), - Some(NetworkType::Testnet) => TESTNET_RPC_URL.to_string(), - None => match self.rpc_url { - Some(ref rpc_url) => rpc_url.clone(), - None => match env::var("SURFPOOL_DATASOURCE_RPC_URL") { - Ok(value) => value, - _ => DEFAULT_RPC_URL.to_string(), - }, - }, - }) + Some(self.datasource_rpc_url()) } else { None }; @@ -355,6 +347,21 @@ impl StartSimnet { } } + pub fn datasource_rpc_url(&self) -> String { + match self.network { + Some(NetworkType::Mainnet) => DEFAULT_RPC_URL.to_string(), + Some(NetworkType::Devnet) => DEVNET_RPC_URL.to_string(), + Some(NetworkType::Testnet) => TESTNET_RPC_URL.to_string(), + None => match self.rpc_url { + Some(ref rpc_url) => rpc_url.clone(), + None => match env::var("SURFPOOL_DATASOURCE_RPC_URL") { + Ok(value) => value, + _ => DEFAULT_RPC_URL.to_string(), + }, + }, + } + } + pub fn subgraph_config(&self) -> SubgraphConfig { SubgraphConfig {} } diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index 20551e38..1cee8e06 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -36,7 +36,10 @@ use super::{Context, ExecuteRunbook, StartSimnet}; use crate::{ http::start_subgraph_and_explorer_server, runbook::{execute_in_memory_runbook, execute_on_disk_runbook, handle_log_event}, - scaffold::{detect_program_frameworks, scaffold_iac_layout, scaffold_in_memory_iac}, + scaffold::{ + ProgramFrameworkData, detect_program_frameworks, scaffold_iac_layout, + scaffold_in_memory_iac, + }, tui::{self, simnet::DisplayedUrl}, }; @@ -161,9 +164,10 @@ pub async fn handle_start_local_surfnet_command( let _ = simnet_events_tx.send(event); } + let simnet_commands_tx_copy = simnet_commands_tx.clone(); let mut deploy_progress_rx = vec![]; if !cmd.no_deploy { - match write_and_execute_iac(&cmd, &simnet_events_tx).await { + match write_and_execute_iac(&cmd, &simnet_events_tx, &simnet_commands_tx_copy).await { Ok(rx) => deploy_progress_rx.push(rx), Err(e) => { let _ = simnet_events_tx.send(SimnetEvent::warn(format!( @@ -432,14 +436,38 @@ fn log_events( async fn write_and_execute_iac( cmd: &StartSimnet, simnet_events_tx: &Sender, + simnet_commands_tx: &Sender, ) -> Result, String> { // Are we in a project directory? - let deployment = detect_program_frameworks(&cmd.manifest_path) + let deployment = detect_program_frameworks(&cmd.manifest_path, &cmd.anchor_test_config_paths) .await .map_err(|e| format!("Failed to detect project framework: {}", e))?; let (progress_tx, progress_rx) = crossbeam::channel::unbounded(); - if let Some((framework, programs, genesis_accounts)) = deployment { + if let Some(ProgramFrameworkData { + framework, + programs, + genesis_accounts, + accounts, + accounts_dir, + clones, + }) = deployment + { + if let Some(clones) = clones.as_ref() { + if !clones.is_empty() { + let _ = simnet_commands_tx.try_send(SimnetCommand::FetchRemoteAccounts( + clones + .iter() + .map(|c| { + c.parse() + .map_err(|e| format!("Failed to parse clone address {}: {}", c, e)) + }) + .collect::, _>>()?, + cmd.datasource_rpc_url(), + )); + } + } + // Is infrastructure-as-code (IaC) already setup? let base_location = FileLocation::from_path_string(&cmd.manifest_path)?.get_parent_location()?; @@ -469,6 +497,8 @@ async fn write_and_execute_iac( &framework, &programs, &genesis_accounts, + &accounts, + &accounts_dir, )?); } else { let runbooks_ids_to_execute = cmd.runbooks.clone(); diff --git a/crates/cli/src/scaffold/anchor.rs b/crates/cli/src/scaffold/anchor.rs index 9b27a347..0203d45f 100644 --- a/crates/cli/src/scaffold/anchor.rs +++ b/crates/cli/src/scaffold/anchor.rs @@ -8,16 +8,20 @@ use std::{ use anyhow::{Result, anyhow}; use serde::{Deserialize, Serialize}; +use txtx_addon_network_svm::templates::{AccountDirEntry, AccountEntry}; use txtx_core::kit::helpers::fs::FileLocation; use url::Url; -use walkdir::WalkDir; use super::ProgramMetadata; -use crate::{scaffold::GenesisEntry, types::Framework}; +use crate::{ + scaffold::{GenesisEntry, ProgramFrameworkData}, + types::Framework, +}; pub fn try_get_programs_from_project( base_location: FileLocation, -) -> Result, Option>)>, String> { + test_suite_paths: &Vec, +) -> Result, String> { let mut manifest_location = base_location.clone(); manifest_location.append_path("Anchor.toml")?; if manifest_location.exists() { @@ -41,24 +45,75 @@ pub fn try_get_programs_from_project( .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() - ) - })? - { + + let mut accounts: Vec = vec![]; + + let mut accounts_dirs = manifest + .test + .as_ref() + .and_then(|test| test.validator.as_ref()) + .and_then(|validator| validator.account_dir.as_ref()) + .cloned() + .unwrap_or_default(); + + let mut clones = manifest + .test + .as_ref() + .and_then(|test| test.validator.as_ref()) + .and_then(|validator| validator.clone.as_ref()) + .map(|clones| { + clones + .iter() + .map(|c| c.address.clone()) + .collect::>() + }) + .unwrap_or_default(); + + if let Some(test_configs) = TestConfig::discover_test_toml( + test_suite_paths.iter().map(|s| PathBuf::from(s)).collect(), + ) + .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()); } + if let Some(validator) = test_config.validator.as_ref() { + if let Some(accounts_cfg) = validator.account.as_ref() { + for account in accounts_cfg { + if !accounts.iter().any(|a| a.filename == account.filename) { + accounts.push(account.clone()); + } + } + } + if let Some(accounts_dirs_cfg) = validator.account_dir.as_ref() { + for account_dir in accounts_dirs_cfg { + if !accounts_dirs + .iter() + .any(|a| a.directory == account_dir.directory) + { + accounts_dirs.push(account_dir.clone()); + } + } + } + if let Some(clone_cfg) = validator.clone.as_ref() { + for clone_entry in clone_cfg { + if !clones.iter().any(|c| c == &clone_entry.address) { + clones.push(clone_entry.address.clone()); + } + } + } + } } } } - Ok(Some(( + Ok(Some(ProgramFrameworkData::new( Framework::Anchor, programs, if genesis_entries.is_empty() { @@ -66,6 +121,21 @@ pub fn try_get_programs_from_project( } else { Some(genesis_entries) }, + if accounts.is_empty() { + None + } else { + Some(accounts) + }, + if accounts_dirs.is_empty() { + None + } else { + Some(accounts_dirs) + }, + if clones.is_empty() { + None + } else { + Some(clones) + }, ))) } else { Ok(None) @@ -191,6 +261,13 @@ impl TestTomlFile { entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?; } } + if let Some(validator) = &mut test.validator { + if let Some(accounts) = &mut validator.account { + for entry in accounts { + entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?; + } + } + } } Ok(current_toml) } @@ -239,6 +316,17 @@ impl TestTomlFile { None => my_test.genesis = Some(other_genesis), } } + let mut my_validator = my_test.validator.take(); + match &mut my_validator { + None => my_validator = other_test.validator, + Some(my_validator) => { + if let Some(other_validator) = other_test.validator { + my_validator.merge(other_validator) + } + } + } + + my_test.validator = my_validator; } } None => my_test = other.test, @@ -297,24 +385,14 @@ impl TestToml { 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(); + pub fn discover_test_toml(test_paths: Vec) -> Result> { 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); - } + + for path in test_paths.into_iter() { + let test_toml = TestToml::from_path(path.clone())?; + test_suite_configs.insert(path, test_toml); } Ok(match test_suite_configs.is_empty() { @@ -447,6 +525,85 @@ pub struct WorkspaceConfig { pub struct TestValidatorConfig { #[serde(skip_serializing_if = "Option::is_none")] pub genesis: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub validator: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CloneEntry { + // Base58 pubkey string. + pub address: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidatorConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub account: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub account_dir: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub clone: Option>, +} + +impl ValidatorConfig { + fn merge(&mut self, other: Self) { + *self = Self { + account: match self.account.take() { + None => other.account, + Some(mut entries) => match other.account { + None => Some(entries), + Some(other_entries) => { + for other_entry in other_entries { + match entries + .iter() + .position(|my_entry| *my_entry.address == other_entry.address) + { + None => entries.push(other_entry), + Some(i) => entries[i] = other_entry, + }; + } + Some(entries) + } + }, + }, + account_dir: match self.account_dir.take() { + None => other.account_dir, + Some(mut entries) => match other.account_dir { + None => Some(entries), + Some(other_entries) => { + for other_entry in other_entries { + match entries + .iter() + .position(|my_entry| *my_entry.directory == other_entry.directory) + { + None => entries.push(other_entry), + Some(i) => entries[i] = other_entry, + }; + } + Some(entries) + } + }, + }, + clone: match self.clone.take() { + None => other.clone, + Some(mut entries) => match other.clone { + None => Some(entries), + Some(other_entries) => { + for other_entry in other_entries { + match entries + .iter() + .position(|my_entry| *my_entry.address == other_entry.address) + { + None => entries.push(other_entry), + Some(i) => entries[i] = other_entry, + }; + } + Some(entries) + } + }, + }, + }; + } } fn deser_programs( diff --git a/crates/cli/src/scaffold/mod.rs b/crates/cli/src/scaffold/mod.rs index b373389b..8ff8e119 100644 --- a/crates/cli/src/scaffold/mod.rs +++ b/crates/cli/src/scaffold/mod.rs @@ -6,9 +6,9 @@ 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::{ - GenesisEntry, get_interpolated_addon_template, get_interpolated_devnet_signer_template, - get_interpolated_header_template, get_interpolated_localnet_signer_template, - get_interpolated_mainnet_signer_template, + AccountDirEntry, AccountEntry, GenesisEntry, get_interpolated_addon_template, + get_interpolated_devnet_signer_template, get_interpolated_header_template, + get_interpolated_localnet_signer_template, get_interpolated_mainnet_signer_template, }; use txtx_core::{ kit::{helpers::fs::FileLocation, indexmap::indexmap}, @@ -28,46 +28,86 @@ mod steel; mod typhoon; pub mod utils; +pub struct ProgramFrameworkData { + pub framework: Framework, + pub programs: Vec, + pub genesis_accounts: Option>, + pub accounts: Option>, + pub accounts_dir: Option>, + pub clones: Option>, +} + +impl ProgramFrameworkData { + pub fn new( + framework: Framework, + programs: Vec, + genesis_accounts: Option>, + accounts: Option>, + accounts_dir: Option>, + clones: Option>, + ) -> Self { + Self { + framework, + programs, + genesis_accounts, + accounts, + accounts_dir, + clones, + } + } + + pub fn partial(framework: Framework, programs: Vec) -> Self { + Self { + framework, + programs, + genesis_accounts: None, + accounts: None, + accounts_dir: None, + clones: None, + } + } +} + pub async fn detect_program_frameworks( manifest_path: &str, -) -> Result, Option>)>, String> { + test_paths: &Vec, +) -> Result, 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, genesis_accounts)) = - anchor::try_get_programs_from_project(base_dir.clone()) - .map_err(|e| format!("Invalid Anchor project: {e}"))? + if let Some(res) = anchor::try_get_programs_from_project(base_dir.clone(), test_paths) + .map_err(|e| format!("Invalid Anchor project: {e}"))? { - return Ok(Some((framework, programs, genesis_accounts))); + return Ok(Some(res)); } // 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, None))); + return Ok(Some(ProgramFrameworkData::partial(framework, programs))); } // 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, None))); + return Ok(Some(ProgramFrameworkData::partial(framework, programs))); } // 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, None))); + return Ok(Some(ProgramFrameworkData::partial(framework, programs))); } // 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, None))); + return Ok(Some(ProgramFrameworkData::partial(framework, programs))); } Ok(None) @@ -92,6 +132,8 @@ pub fn scaffold_in_memory_iac( framework: &Framework, programs: &Vec, genesis_accounts: &Option>, + accounts: &Option>, + accounts_dir: &Option>, ) -> Result<(String, RunbookSources, WorkspaceManifest), String> { let mut deployment_runbook_src: String = String::new(); @@ -121,9 +163,11 @@ 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![])) - { + if let Some(setup_surfnet_iac) = framework.get_interpolated_setup_surfnet_template( + genesis_accounts.as_ref().unwrap_or(&vec![]), + accounts.as_ref().unwrap_or(&vec![]), + accounts_dir.as_ref().unwrap_or(&vec![]), + ) { deployment_runbook_src.push_str(&setup_surfnet_iac); } diff --git a/crates/cli/src/types/mod.rs b/crates/cli/src/types/mod.rs index 9d0c22c3..aae397ba 100644 --- a/crates/cli/src/types/mod.rs +++ b/crates/cli/src/types/mod.rs @@ -1,5 +1,6 @@ use txtx_addon_network_svm::templates::{ - GenesisEntry, get_in_memory_interpolated_anchor_program_deployment_template, + AccountDirEntry, AccountEntry, 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_setup_surfnet_template, @@ -62,8 +63,10 @@ impl Framework { pub fn get_interpolated_setup_surfnet_template( &self, genesis_accounts: &Vec, + accounts: &Vec, + accounts_dir: &Vec, ) -> Option { - get_interpolated_setup_surfnet_template(genesis_accounts) + get_interpolated_setup_surfnet_template(genesis_accounts, accounts, accounts_dir) } } impl std::fmt::Display for Framework { diff --git a/crates/core/src/rpc/full.rs b/crates/core/src/rpc/full.rs index b5b720ef..72a524bb 100644 --- a/crates/core/src/rpc/full.rs +++ b/crates/core/src/rpc/full.rs @@ -19,7 +19,7 @@ use solana_client::{ RpcSimulateTransactionResult, }, }; -use solana_clock::{MAX_RECENT_BLOCKHASHES, Slot, UnixTimestamp}; +use solana_clock::{Slot, UnixTimestamp}; use solana_commitment_config::{CommitmentConfig, CommitmentLevel}; use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_message::{VersionedMessage, compiled_instruction::CompiledInstruction}; @@ -44,7 +44,10 @@ use crate::{ SURFPOOL_IDENTITY_PUBKEY, error::{SurfpoolError, SurfpoolResult}, rpc::utils::{adjust_default_transaction_config, get_default_transaction_config}, - surfnet::{FINALIZATION_SLOT_THRESHOLD, GetTransactionResult, locker::SvmAccessContext}, + surfnet::{ + FINALIZATION_SLOT_THRESHOLD, GetTransactionResult, locker::SvmAccessContext, + svm::MAX_RECENT_BLOCKHASHES_EXTERNAL, + }, types::{SurfnetTransactionStatus, surfpool_tx_metadata_to_litesvm_tx_metadata}, }; @@ -2142,7 +2145,8 @@ impl Full for SurfpoolFullRpc { .get_latest_blockhash(&commitment) .unwrap_or_else(|| svm_locker.latest_absolute_blockhash()); - let last_valid_block_height = committed_latest_slot + MAX_RECENT_BLOCKHASHES as u64; + let last_valid_block_height = + committed_latest_slot + MAX_RECENT_BLOCKHASHES_EXTERNAL as u64; Ok(RpcResponse { context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()), value: RpcBlockhash { @@ -3233,7 +3237,8 @@ mod tests { .context .svm_locker .get_slot_for_commitment(&commitment); - let expected_last_valid_block_height = committed_slot + MAX_RECENT_BLOCKHASHES as u64; + let expected_last_valid_block_height = + committed_slot + MAX_RECENT_BLOCKHASHES_EXTERNAL as u64; assert_eq!( res.value.blockhash, @@ -3269,7 +3274,8 @@ mod tests { .context .svm_locker .get_slot_for_commitment(&commitment); - let expected_last_valid_block_height = committed_slot + MAX_RECENT_BLOCKHASHES as u64; + let expected_last_valid_block_height = + committed_slot + MAX_RECENT_BLOCKHASHES_EXTERNAL as u64; assert_eq!( res.value.blockhash, @@ -3305,7 +3311,8 @@ mod tests { .context .svm_locker .get_slot_for_commitment(&commitment); - let expected_last_valid_block_height = committed_slot + MAX_RECENT_BLOCKHASHES as u64; + let expected_last_valid_block_height = + committed_slot + MAX_RECENT_BLOCKHASHES_EXTERNAL as u64; assert_eq!( res.value.blockhash, diff --git a/crates/core/src/rpc/ws.rs b/crates/core/src/rpc/ws.rs index a3792c5d..c0a2ef08 100644 --- a/crates/core/src/rpc/ws.rs +++ b/crates/core/src/rpc/ws.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, RwLock, atomic}, }; +use crossbeam_channel::TryRecvError; use jsonrpc_core::{Error, ErrorCode, Result}; use jsonrpc_derive::rpc; use jsonrpc_pubsub::{ @@ -744,10 +745,22 @@ impl Rpc for SurfpoolWsRpc { &SignatureSubscriptionType::Commitment(CommitmentLevel::Processed), Some(TransactionConfirmationStatus::Processed), ) + | ( + &SignatureSubscriptionType::Commitment(CommitmentLevel::Processed), + Some(TransactionConfirmationStatus::Confirmed), + ) + | ( + &SignatureSubscriptionType::Commitment(CommitmentLevel::Processed), + Some(TransactionConfirmationStatus::Finalized), + ) | ( &SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed), Some(TransactionConfirmationStatus::Confirmed), ) + | ( + &SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed), + Some(TransactionConfirmationStatus::Finalized), + ) | ( &SignatureSubscriptionType::Commitment(CommitmentLevel::Finalized), Some(TransactionConfirmationStatus::Finalized), @@ -773,17 +786,33 @@ impl Rpc for SurfpoolWsRpc { svm_locker.subscribe_for_signature_updates(&signature, subscription_type.clone()); loop { - let Ok((slot, some_err)) = rx.try_recv() else { - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - continue; + let (slot, some_err) = match rx.try_recv() { + Ok(msg) => msg, + Err(e) => { + match e { + TryRecvError::Empty => { + // no update yet, continue + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + continue; + } + TryRecvError::Disconnected => { + warn!( + "Signature subscription channel closed for sub id {:?}", + sub_id + ); + // channel closed, exit loop + break; + } + } + } }; - let Ok(guard) = active.read() else { + let Ok(mut guard) = active.write() else { log::error!("Failed to acquire read lock on signature_subscription_map"); break; }; - let Some(sink) = guard.get(&sub_id) else { + let Some(sink) = guard.remove(&sub_id) else { log::error!("Failed to get sink for subscription ID"); break; }; @@ -803,6 +832,10 @@ impl Rpc for SurfpoolWsRpc { })), }; + if guard.is_empty() { + break; + } + if let Err(e) = res { log::error!("Failed to notify client about account update: {e}"); break; diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index 403add3d..8bb2f0d3 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -24,6 +24,7 @@ use jsonrpc_http_server::{DomainsValidation, ServerBuilder}; use jsonrpc_pubsub::{PubSubHandler, Session}; use jsonrpc_ws_server::{RequestContext, ServerBuilder as WsServerBuilder}; use libloading::{Library, Symbol}; +use solana_commitment_config::CommitmentConfig; #[cfg(feature = "geyser_plugin")] use solana_geyser_plugin_manager::geyser_plugin_manager::{ GeyserPluginManager, LoadedGeyserPlugin, @@ -231,6 +232,19 @@ pub async fn start_block_production_runloop( SimnetCommand::CompleteRunbookExecution(runbook_id, error) => { svm_locker.complete_runbook_execution(runbook_id, error); } + SimnetCommand::FetchRemoteAccounts(pubkeys, remote_url) => { + let remote_client = SurfnetRemoteClient::new_unsafe(&remote_url); + if let Some(remote_client) = remote_client { + match svm_locker.get_multiple_accounts_with_remote_fallback(&remote_client, &pubkeys, CommitmentConfig::confirmed()).await { + Ok(account_updates) => { + svm_locker.write_multiple_account_updates(&account_updates.inner); + } + Err(e) => { + svm_locker.simnet_events_tx().try_send(SimnetEvent::error(format!("Failed to fetch remote accounts {:?}: {}", pubkeys, e))).ok(); + } + }; + } + } } }, } diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index b941bb23..32db15f0 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -33,6 +33,7 @@ use solana_genesis_config::GenesisConfig; use solana_hash::Hash; use solana_inflation::Inflation; use solana_keypair::Keypair; +use solana_loader_v3_interface::state::UpgradeableLoaderState; use solana_message::{Message, VersionedMessage, v0::LoadedAddresses}; use solana_program_option::COption; use solana_pubkey::Pubkey; @@ -78,6 +79,14 @@ use crate::{ pub type AccountOwner = Pubkey; +#[allow(deprecated)] +use solana_sysvar::recent_blockhashes::MAX_ENTRIES; + +#[allow(deprecated)] +pub const MAX_RECENT_BLOCKHASHES_INTERNAL: usize = MAX_ENTRIES; +pub const MAX_RECENT_BLOCKHASHES_EXTERNAL: usize = 500; +pub const MAX_BLOCKHASH_TIME: usize = 30 * 1000; + pub fn get_txtx_value_json_converters() -> Vec> { vec![ Box::new(move |value: &txtx_addon_kit::types::types::Value| { @@ -147,6 +156,7 @@ pub struct SurfnetSvm { pub max_profiles: usize, pub runbook_executions: Vec, pub streamed_accounts: HashMap, + pub recent_blockhashes: VecDeque<(SyntheticBlockhash, i64)>, } pub const FEATURE: Feature = Feature { @@ -236,6 +246,7 @@ impl SurfnetSvm { max_profiles: DEFAULT_PROFILING_MAP_CAPACITY, runbook_executions: Vec::new(), streamed_accounts: HashMap::new(), + recent_blockhashes: VecDeque::new(), }; // Generate the initial synthetic blockhash @@ -436,10 +447,12 @@ impl SurfnetSvm { #[allow(deprecated)] fn new_blockhash(&mut self) -> BlockIdentifier { use solana_slot_hashes::SlotHashes; - use solana_sysvar::recent_blockhashes::{IterItem, MAX_ENTRIES, RecentBlockhashes}; + use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes}; // Backup the current block hashes let recent_blockhashes_backup = self.inner.get_sysvar::(); - let num_blockhashes_expected = recent_blockhashes_backup.len().min(MAX_ENTRIES); + let num_blockhashes_expected = recent_blockhashes_backup + .len() + .min(MAX_RECENT_BLOCKHASHES_INTERNAL); // Invalidate the current block hash. // LiteSVM bug / feature: calling this method empties `sysvar::()` self.inner.expire_blockhash(); @@ -451,6 +464,7 @@ impl SurfnetSvm { .expect("Latest blockhash not found"); let new_synthetic_blockhash = SyntheticBlockhash::new(self.chain_tip.index); + let new_synthetic_blockhash_str = new_synthetic_blockhash.to_string(); recent_blockhashes.push(IterItem( 0, @@ -460,7 +474,7 @@ impl SurfnetSvm { // Append the previous blockhashes, ignoring the first one for (index, entry) in recent_blockhashes_backup.iter().enumerate() { - if recent_blockhashes.len() >= MAX_ENTRIES { + if recent_blockhashes.len() >= MAX_RECENT_BLOCKHASHES_INTERNAL { break; } recent_blockhashes.push(IterItem( @@ -480,9 +494,16 @@ impl SurfnetSvm { ); self.inner.set_sysvar(&SlotHashes::new(&slot_hashes)); + let now = Utc::now().timestamp_millis(); + self.recent_blockhashes + .push_front((new_synthetic_blockhash, now)); + self.recent_blockhashes.retain_mut(|(_, timestamp)| { + now.saturating_sub(*timestamp) <= MAX_BLOCKHASH_TIME as i64 + }); + BlockIdentifier::new( self.chain_tip.index + 1, - new_synthetic_blockhash.to_string().as_str(), + new_synthetic_blockhash_str.as_str(), ) } @@ -494,11 +515,9 @@ impl SurfnetSvm { /// # Returns /// `true` if the blockhash is recent, `false` otherwise. pub fn check_blockhash_is_recent(&self, recent_blockhash: &Hash) -> bool { - #[allow(deprecated)] - self.inner - .get_sysvar::() + self.recent_blockhashes .iter() - .any(|entry| entry.blockhash == *recent_blockhash) + .any(|(h, _)| h.hash() == recent_blockhash) } /// Sets an account in the local SVM state and notifies listeners. @@ -753,7 +772,6 @@ impl SurfnetSvm { )); return Err(FailedTransactionMetadata { err, meta }); } - self.inner.set_blockhash_check(false); match self.inner.send_transaction(tx.clone()) { Ok(res) => Ok(res), @@ -946,9 +964,58 @@ impl SurfnetSvm { /// # Arguments /// * `account_update` - The account update result to process. pub fn write_account_update(&mut self, account_update: GetAccountResult) { + let init_programdata_account = |program_account: &Account| { + if !program_account.executable { + return None; + } + if !program_account + .owner + .eq(&solana_sdk_ids::bpf_loader_upgradeable::id()) + { + return None; + } + let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = bincode::deserialize::(&program_account.data) + else { + return None; + }; + + let programdata_state = UpgradeableLoaderState::ProgramData { + upgrade_authority_address: Some(system_program::id()), + slot: self.get_latest_absolute_slot(), + }; + let mut data = bincode::serialize(&programdata_state).unwrap(); + + data.extend_from_slice(&include_bytes!("../tests/assets/minimum_program.so").to_vec()); + let lamports = self.inner.minimum_balance_for_rent_exemption(data.len()); + Some(( + programdata_address, + Account { + lamports, + data, + owner: solana_sdk_ids::bpf_loader_upgradeable::id(), + executable: false, + rent_epoch: 0, + }, + )) + }; match account_update { GetAccountResult::FoundAccount(pubkey, account, do_update_account) => { if do_update_account { + if let Some((programdata_address, programdata_account)) = + init_programdata_account(&account) + { + if self.get_account(&programdata_address).is_none() { + if let Err(e) = + self.set_account(&programdata_address, programdata_account) + { + let _ = self + .simnet_events_tx + .send(SimnetEvent::error(e.to_string())); + } + } + } if let Err(e) = self.set_account(&pubkey, account.clone()) { let _ = self .simnet_events_tx @@ -956,8 +1023,26 @@ impl SurfnetSvm { } } } - GetAccountResult::FoundProgramAccount((pubkey, account), (_, None)) - | GetAccountResult::FoundTokenAccount((pubkey, account), (_, None)) => { + GetAccountResult::FoundProgramAccount((pubkey, account), (_, None)) => { + if let Some((programdata_address, programdata_account)) = + init_programdata_account(&account) + { + if self.get_account(&programdata_address).is_none() { + if let Err(e) = self.set_account(&programdata_address, programdata_account) + { + let _ = self + .simnet_events_tx + .send(SimnetEvent::error(e.to_string())); + } + } + } + if let Err(e) = self.set_account(&pubkey, account.clone()) { + let _ = self + .simnet_events_tx + .send(SimnetEvent::error(e.to_string())); + } + } + GetAccountResult::FoundTokenAccount((pubkey, account), (_, None)) => { if let Err(e) = self.set_account(&pubkey, account.clone()) { let _ = self .simnet_events_tx @@ -1998,7 +2083,7 @@ mod tests { } } - fn expect_error_event(events_rx: &Receiver, expected_error: &str) -> bool { + fn _expect_error_event(events_rx: &Receiver, expected_error: &str) -> bool { match events_rx.recv() { Ok(event) => match event { SimnetEvent::ErrorLog(_, err) => { @@ -2100,11 +2185,30 @@ mod tests { } } - // GetAccountResult::FoundProgramAccount with no program account fails + // GetAccountResult::FoundProgramAccount with no program account inserts a default programdata account { let (program_address, program_account, program_data_address, _) = create_program_accounts(); + let mut data = bincode::serialize( + &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData { + slot: svm.get_latest_absolute_slot(), + upgrade_authority_address: Some(system_program::id()), + }, + ) + .unwrap(); + + let mut bin = include_bytes!("../tests/assets/minimum_program.so").to_vec(); + data.append(&mut bin); // push our binary after the state data + let lamports = svm.inner.minimum_balance_for_rent_exemption(data.len()); + let default_program_data_account = Account { + lamports, + data, + owner: solana_sdk_ids::bpf_loader_upgradeable::ID, + executable: false, + rent_epoch: 0, + }; + let index_before = svm.inner.accounts_db().clone().inner; let found_program_account_update = GetAccountResult::FoundProgramAccount( (program_address, program_account.clone()), @@ -2112,18 +2216,26 @@ mod tests { ); svm.write_account_update(found_program_account_update); - if !expect_error_event( + if !expect_account_update_event( &events_rx, - &format!( - "Internal error: \"Failed to set account {}: An account required by the instruction is missing\"", - program_address - ), + &svm, + &program_data_address, + &default_program_data_account, ) { panic!( - "Expected error event not received after inserting program account with no program data account" + "Expected account update event not received after inserting default program data account" ); } - assert_eq!(svm.inner.accounts_db().clone().inner, index_before); + + if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) { + panic!( + "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey" + ); + } + assert_eq!( + svm.inner.accounts_db().clone().inner.len(), + index_before.len() + 2 + ); } // GetAccountResult::FoundProgramAccount with program account + program data account inserts two accounts diff --git a/crates/core/src/tests/assets/minimum_program.so b/crates/core/src/tests/assets/minimum_program.so new file mode 100755 index 00000000..8d3ef457 Binary files /dev/null and b/crates/core/src/tests/assets/minimum_program.so differ diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 196b5b06..c0819b37 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -968,6 +968,7 @@ impl TransactionLoadedAddresses { } } +#[derive(Debug, Clone)] pub struct SyntheticBlockhash(Hash); impl SyntheticBlockhash { pub const PREFIX: &str = "SURFNETxSAFEHASHx"; diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index a155ab90..8a2f6e0d 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -491,6 +491,7 @@ pub enum SimnetCommand { Terminate(Option<(Hash, String)>), StartRunbookExecution(String), CompleteRunbookExecution(String, Option>), + FetchRemoteAccounts(Vec, String), } #[derive(Debug)]