diff --git a/Cargo.lock b/Cargo.lock index 9eb4375c..8ecd8fc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,18 +2133,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dotenvy_macro" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" -dependencies = [ - "dotenvy", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "downcast" version = "0.11.0" @@ -10135,7 +10123,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "surfpool-cli" -version = "0.1.11" +version = "0.1.12" dependencies = [ "actix-cors", "actix-web", @@ -10179,7 +10167,7 @@ dependencies = [ [[package]] name = "surfpool-core" -version = "0.1.11" +version = "0.1.12" dependencies = [ "agave-geyser-plugin-interface", "base64 0.22.1", @@ -10203,9 +10191,9 @@ dependencies = [ "log 0.4.25", "regex", "serde", - "serde_bytes", "serde_derive", "serde_json", + "serde_with", "solana-account", "solana-account-decoder", "solana-accounts-db", @@ -10232,6 +10220,7 @@ dependencies = [ "solana-transaction-status", "solana-version", "solana-vote-program", + "spl-associated-token-account", "spl-token", "spl-token-2022 7.0.0", "surfpool-subgraph", @@ -10247,7 +10236,7 @@ dependencies = [ [[package]] name = "surfpool-gql" -version = "0.1.11" +version = "0.1.12" dependencies = [ "async-stream", "convert_case 0.7.1", @@ -10267,7 +10256,7 @@ dependencies = [ [[package]] name = "surfpool-subgraph" -version = "0.1.11" +version = "0.1.12" dependencies = [ "agave-geyser-plugin-interface", "bincode", @@ -10284,7 +10273,7 @@ dependencies = [ [[package]] name = "surfpool-types" -version = "0.1.11" +version = "0.1.12" dependencies = [ "chrono", "crossbeam-channel", @@ -11093,13 +11082,12 @@ dependencies = [ [[package]] name = "txtx-addon-kit" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7971c840d33e2bf59136b3ce6777cffc5dce01fa4350fc8b585874251bea7db7" +checksum = "898b215ceacd1d605147409ac46f9afe707411042a04f1c6fc8ccc24e2cd89fb" dependencies = [ "crossbeam-channel", "dirs 5.0.1", - "dotenvy_macro", "futures 0.3.31", "getrandom 0.2.15", "hcl-edit", @@ -11128,9 +11116,9 @@ dependencies = [ [[package]] name = "txtx-addon-network-svm" -version = "0.1.12" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e250698d84d23fefca6908e27e2be1b133f04bf5392a15a487895461085de627" +checksum = "cd4548f48336fde111da1e18eebf0626c99dccef4d87d9a4734625b561860a5c" dependencies = [ "async-recursion", "bincode", @@ -11145,6 +11133,7 @@ dependencies = [ "solana_idl", "spl-associated-token-account", "spl-token", + "spl-token-2022 7.0.0", "tiny-bip39", "txtx-addon-kit", "txtx-addon-network-svm-types", @@ -11152,9 +11141,9 @@ dependencies = [ [[package]] name = "txtx-addon-network-svm-types" -version = "0.1.1" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d713ff26a74caf15a2c81b44ad1c6fc61f9fe66526e6f9dced05a639670467" +checksum = "74f68dc655224ffca97d1aa0574c8c5b4d872c44ed3b8d776cf0522ec248ec81" dependencies = [ "anchor-lang-idl", "lazy_static", @@ -11176,9 +11165,9 @@ dependencies = [ [[package]] name = "txtx-core" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41c9c8d4a9bda8ccd08aa9dabe80bad07dbb7fb5bcd01e14d9f6ea12996330" +checksum = "ceebe0f7b4293bfe2d680a485f127ae6a6d6621701637c7bc9bee47e08467ea5" dependencies = [ "base64 0.22.1", "better-debug", diff --git a/Cargo.toml b/Cargo.toml index 62238a88..63c17095 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.1.11" +version = "0.1.12" edition = "2021" description = "Surfpool is the best place to train before surfing Solana." license = "Apache-2.0" @@ -24,6 +24,7 @@ surfpool-core = { path = "crates/core", default-features = false } surfpool-gql = { path = "crates/gql", default-features = false } surfpool-subgraph = { path = "crates/subgraph", default-features = false } surfpool-types = { path = "crates/types", default-features = false } +# litesvm = { path = "../litesvm/crates/litesvm", features = ["nodejs-internal"] } litesvm = { version = "0.6.0", features = ["nodejs-internal"] } solana-sdk = "2.2.1" solana-program = "2.2.1" @@ -62,9 +63,10 @@ solana-svm = "2.2.1" solana-program-runtime = "2.2.1" agave-geyser-plugin-interface = "2.2.1" solana-streamer = "2.2.1" +spl-associated-token-account = "6.0.0" ipc-channel = "0.19.0" serde = "1.0.217" -serde_bytes = "0.11.17" +serde_with = "3" serde_derive = "1.0.217" # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 serde_json = "1.0.135" # txtx-addon-kit = { path = "../txtx/crates/txtx-addon-kit", features = ["wasm"]} @@ -73,10 +75,10 @@ serde_json = "1.0.135" # txtx-addon-network-svm-types = { package = "txtx-addon-network-svm-types", path = "../txtx/addons/svm/types" } # txtx-gql = { path = "../txtx/crates/txtx-gql" } # txtx-supervisor-ui = { path = "../txtx/crates/txtx-supervisor-ui", default-features = false, features = ["bin_build"] } -txtx-addon-kit = { version = "0.2.7", features = ["wasm"] } -txtx-core = { version = "0.2.8" } -txtx-addon-network-svm = { version = "0.1.12" } -txtx-addon-network-svm-types = { version = "0.1.1" } +txtx-addon-kit = { version = "0.2.9", features = ["wasm"] } +txtx-core = { version = "0.2.12" } +txtx-addon-network-svm = { version = "0.1.15" } +txtx-addon-network-svm-types = { version = "0.1.14" } txtx-gql = { version = "0.2.5" } txtx-supervisor-ui = { version = "0.1.3", default-features = false, features = ["crates_build"]} bincode = "1.3.3" diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 9c98256b..52d2f10d 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -129,7 +129,7 @@ impl StartSimnet { let mut airdrop_addresses = vec![]; let mut errors = vec![]; for address in self.airdrop_addresses.iter() { - match Pubkey::from_str(&address).map_err(|e| e.to_string()) { + match Pubkey::from_str(address).map_err(|e| e.to_string()) { Ok(pubkey) => { airdrop_addresses.push(pubkey); } @@ -148,7 +148,7 @@ impl StartSimnet { format!( "{}{}", dirs::home_dir().unwrap().display(), - keypair_path[1..].to_string() + &keypair_path[1..] ) } else { keypair_path.clone() @@ -183,7 +183,7 @@ impl StartSimnet { remote_rpc_url: self.rpc_url.clone(), slot_time: self.slot_time, runloop_trigger_mode: surfpool_types::RunloopTriggerMode::Clock, - airdrop_addresses: airdrop_addresses, + airdrop_addresses, airdrop_token_amount: self.airdrop_token_amount, } } @@ -196,7 +196,7 @@ impl StartSimnet { let mut plugin_config_path = self .plugin_config_path .iter() - .map(|f| PathBuf::from(f)) + .map(PathBuf::from) .collect::>(); if plugin_config_path.is_empty() { diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index 73c08e6c..c7b08198 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -61,7 +61,7 @@ pub async fn handle_start_simnet_command(cmd: &StartSimnet, ctx: &Context) -> Re config.clone(), subgraph_events_tx.clone(), subgraph_commands_rx, - &ctx, + ctx, ) .await { @@ -150,7 +150,7 @@ pub async fn handle_start_simnet_command(cmd: &StartSimnet, ctx: &Context) -> Re .map_err(|e| format!("{}", e))?; } if let Some(explorer_handle) = explorer_handle { - let _ = explorer_handle.stop(true); + let _ = explorer_handle.stop(true).await; } Ok(()) } @@ -324,7 +324,7 @@ async fn write_and_execute_iac( let (progress_tx, progress_rx) = crossbeam::channel::unbounded(); - if let Some((_framework, programs)) = deployment { + if let Some((framework, programs)) = deployment { // Is infrastructure-as-code (IaC) already setup? let base_location = FileLocation::from_path_string(&cmd.manifest_path)?.get_parent_location()?; @@ -332,7 +332,7 @@ async fn write_and_execute_iac( txtx_manifest_location.append_path("txtx.yml")?; if !txtx_manifest_location.exists() { // Scaffold IaC - scaffold_iac_layout(programs, &base_location)?; + scaffold_iac_layout(&framework, programs, &base_location)?; } let mut futures = vec![]; diff --git a/crates/cli/src/http/mod.rs b/crates/cli/src/http/mod.rs index 17a95feb..78af7feb 100644 --- a/crates/cli/src/http/mod.rs +++ b/crates/cli/src/http/mod.rs @@ -134,7 +134,7 @@ async fn post_graphql( .ok_or(actix_web::error::ErrorInternalServerError( "Missing expected schema", ))?; - graphql_handler(&schema, &context, req, payload).await + graphql_handler(schema, &context, req, payload).await } async fn get_graphql( @@ -154,7 +154,7 @@ async fn get_graphql( .ok_or(actix_web::error::ErrorInternalServerError( "Missing expected schema", ))?; - graphql_handler(&schema, &context, req, payload).await + graphql_handler(schema, &context, req, payload).await } async fn subscriptions( @@ -232,12 +232,12 @@ fn start_subgraph_runloop( format!("{err_ctx}: Failed to acquire write lock on subgraph name lookup") })?; lookup.insert( - subgraph_uuid.clone(), + subgraph_uuid, subgraph_name.to_case(Case::Camel), ); entries_store.insert( subgraph_name.to_case(Case::Camel), - (subgraph_uuid.clone(), vec![]), + (subgraph_uuid, vec![]), ); let _ = sender.send("http://127.0.0.1:8900/gql/console".into()); } diff --git a/crates/cli/src/runbook/mod.rs b/crates/cli/src/runbook/mod.rs index 55adac60..53bc3c6f 100644 --- a/crates/cli/src/runbook/mod.rs +++ b/crates/cli/src/runbook/mod.rs @@ -29,12 +29,9 @@ use crate::cli::ExecuteRunbook; pub fn get_addon_by_namespace(namespace: &str) -> Option> { let available_addons: Vec> = vec![Box::new(StdAddon::new()), Box::new(SvmNetworkAddon::new())]; - for addon in available_addons.into_iter() { - if namespace.starts_with(&format!("{}", addon.get_namespace())) { - return Some(addon); - } - } - None + available_addons + .into_iter() + .find(|addon| namespace.starts_with(&addon.get_namespace().to_string())) } pub fn load_workspace_manifest_from_manifest_path( @@ -209,7 +206,7 @@ pub async fn execute_runbook( .await?; ctrlc::set_handler(move || { - let _ = kill_supervised_execution_tx.send(true).unwrap(); + kill_supervised_execution_tx.send(true).unwrap(); }) .expect("Error setting Ctrl-C handler"); @@ -388,17 +385,14 @@ pub async fn configure_supervised_execution( let _ = hiro_system_kit::thread_named("Kill Runloops Thread") .spawn(move || { let future = async { - match kill_loops_rx.recv() { - Ok(_) => { - let _ = block_tx.send(BlockEvent::Exit); - #[cfg(feature = "supervisor_ui")] - let _ = relayer_channel_tx.send(RelayerChannelEvent::Exit); - #[cfg(feature = "supervisor_ui")] - if let Some(handle) = web_ui_handle { - let _ = handle.stop(true).await; - } + if kill_loops_rx.recv().is_ok() { + let _ = block_tx.send(BlockEvent::Exit); + #[cfg(feature = "supervisor_ui")] + let _ = relayer_channel_tx.send(RelayerChannelEvent::Exit); + #[cfg(feature = "supervisor_ui")] + if let Some(handle) = web_ui_handle { + let _ = handle.stop(true).await; } - Err(_) => {} }; }; @@ -475,7 +469,7 @@ pub fn display_snapshot_diffing( println!("\n{}", yellow!("Runbook Recovery Plan")); println!("The previous runbook execution was interrupted before completion, causing the following actions to be aborted:"); - for (_i, (change, _impacted)) in synthesized_changes.iter().enumerate() { + for (change, _impacted) in synthesized_changes.iter() { match change { SynthesizedChange::Edition(_, _) => {} SynthesizedChange::FormerFailure(_construct_to_run, command_name) => { @@ -512,8 +506,8 @@ fn log_actions_to_execute( let _ = simnet_events_tx.send(SimnetEvent::info(msg)); let documentation_missing = black!(""); for (context, actions) in actions_to_execute.iter() { - let _ = simnet_events_tx.send(SimnetEvent::info(format!("{}", context))); - for (action_name, documentation) in actions.into_iter() { + let _ = simnet_events_tx.send(SimnetEvent::info(context.to_string())); + for (action_name, documentation) in actions.iter() { let _ = simnet_events_tx.send(SimnetEvent::info(format!( "- {}: {}", action_name, @@ -576,8 +570,8 @@ fn process_runbook_execution_output( ) { if let Err(diags) = execution_result { let _ = simnet_events_tx.send(SimnetEvent::warn("Runbook execution aborted")); - log_diagnostic_lines(diags, &simnet_events_tx); - write_runbook_transient_state(runbook, runbook_state_location, &simnet_events_tx); + log_diagnostic_lines(diags, simnet_events_tx); + write_runbook_transient_state(runbook, runbook_state_location, simnet_events_tx); } else { let runbook_outputs = runbook.collect_formatted_outputs(); if !runbook_outputs.is_empty() { @@ -596,7 +590,7 @@ fn process_runbook_execution_output( Ok(output_location) => { let _ = simnet_events_tx.send(SimnetEvent::info(format!( "Outputs written to {}", - output_location.to_string() + output_location ))); } Err(e) => { @@ -617,6 +611,6 @@ fn process_runbook_execution_output( )); } } - write_runbook_state(runbook, runbook_state_location, &simnet_events_tx); + write_runbook_state(runbook, runbook_state_location, simnet_events_tx); } } diff --git a/crates/cli/src/scaffold/anchor.rs b/crates/cli/src/scaffold/anchor.rs index da8831ec..9ef29256 100644 --- a/crates/cli/src/scaffold/anchor.rs +++ b/crates/cli/src/scaffold/anchor.rs @@ -27,7 +27,7 @@ pub fn try_get_programs_from_project( target_location.append_path("target")?; if let Some((_, deployments)) = manifest.programs.iter().next() { for (program_name, deployment) in deployments.iter() { - programs.push(ProgramMetadata::new(&program_name, &deployment.idl)); + programs.push(ProgramMetadata::new(program_name, &deployment.idl)); } } @@ -150,7 +150,7 @@ impl AnchorProgramDeployment { serde_json::Value::String(address) => Ok(AnchorProgramDeployment { address: address.clone(), path: None, - idl: idl, + idl, }), serde_json::Value::Object(_) => { @@ -158,15 +158,13 @@ impl AnchorProgramDeployment { .map_err(|_| anyhow!("Unable to read Anchor.toml"))?; Ok(AnchorProgramDeployment { address: dep.address, - idl: idl, + idl, path: dep.path, }) } - _ => { - return Err(anyhow!( - "Invalid type for program definition in Anchor.toml" - )) - } + _ => Err(anyhow!( + "Invalid type for program definition in Anchor.toml" + )), } } } @@ -254,7 +252,7 @@ fn deser_programs( .map(|(name, program_id)| { Ok(( name.clone(), - AnchorProgramDeployment::new(name, program_id, &base_location)?, + AnchorProgramDeployment::new(name, program_id, base_location)?, )) }) .collect::>>()?; diff --git a/crates/cli/src/scaffold/mod.rs b/crates/cli/src/scaffold/mod.rs index 2bbfeec5..2a5231a2 100644 --- a/crates/cli/src/scaffold/mod.rs +++ b/crates/cli/src/scaffold/mod.rs @@ -4,8 +4,7 @@ use std::{ fs::{self, File}, }; use txtx_addon_network_svm::templates::{ - get_interpolated_addon_template, get_interpolated_anchor_program_deployment_template, - get_interpolated_anchor_subgraph_template, get_interpolated_devnet_signer_template, + get_interpolated_addon_template, get_interpolated_devnet_signer_template, get_interpolated_header_template, get_interpolated_localnet_signer_template, get_interpolated_mainnet_signer_template, }; @@ -19,8 +18,10 @@ use crate::types::Framework; mod anchor; mod native; +mod pinocchio; mod steel; mod typhoon; +pub mod utils; pub async fn detect_program_frameworks( manifest_path: &str, @@ -28,19 +29,13 @@ pub async fn detect_program_frameworks( 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}"))? { return Ok(Some((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))); - } - // 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}"))? @@ -55,6 +50,20 @@ pub async fn detect_program_frameworks( return Ok(Some((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))); + } + + // 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))); + } + Ok(None) } @@ -74,6 +83,7 @@ impl ProgramMetadata { } pub fn scaffold_iac_layout( + framework: &Framework, programs: Vec, base_location: &FileLocation, ) -> Result<(), String> { @@ -124,7 +134,7 @@ pub fn scaffold_iac_layout( }; let mut deployment_runbook_src: String = String::new(); - let mut subgraph_runbook_src: String = String::new(); + let mut subgraph_runbook_src: Option = None; deployment_runbook_src.push_str(&get_interpolated_header_template(&format!( "Manage {} deployment through Crypto Infrastructure as Code", manifest.name @@ -154,18 +164,15 @@ pub fn scaffold_iac_layout( )); for program_metadata in selected_programs.iter() { - deployment_runbook_src.push_str(&get_interpolated_anchor_program_deployment_template( - &program_metadata.name, - )); - - subgraph_runbook_src.push_str( - &get_interpolated_anchor_subgraph_template( - &program_metadata.name, - &program_metadata.idl.as_ref().unwrap(), - ) - .map_err(|e| format!("failed to generate subgraph infrastructure as code: {}", e))?, + deployment_runbook_src.push_str( + &framework.get_interpolated_program_deployment_template(&program_metadata.name), ); + subgraph_runbook_src = framework.get_interpolated_subgraph_template( + &program_metadata.name, + program_metadata.idl.as_ref(), + )?; + // Configure initialize instruction // let args = vec![ // Value::string("hellosol".into()), @@ -176,7 +183,7 @@ pub fn scaffold_iac_layout( let runbook_name = "deployment"; let description = Some("Deploy programs".to_string()); - let location = format!("runbooks/deployment"); + let location = "runbooks/deployment".to_string(); let runbook = RunbookMetadata { location, @@ -209,7 +216,7 @@ pub fn scaffold_iac_layout( let _ = File::create(manifest_location.to_string()).map_err(|e| { format!( "Failed to create Runbook manifest {}: {e}", - manifest_location.to_string() + manifest_location ) })?; println!("{} {}", green!("Created manifest"), manifest_name); @@ -238,7 +245,7 @@ pub fn scaffold_iac_layout( let mut manifest_file = File::create(manifest_location.to_string()).map_err(|e| { format!( "Failed to create Runbook manifest file {}: {e}", - manifest_location.to_string() + manifest_location ) })?; @@ -253,11 +260,10 @@ pub fn scaffold_iac_layout( match runbook_file_location.exists() { true => {} false => { - fs::create_dir_all(&runbook_file_location.to_string()).map_err(|e| { + fs::create_dir_all(runbook_file_location.to_string()).map_err(|e| { format!( "unable to create parent directory {}\n{}", - runbook_file_location.to_string(), - e + runbook_file_location, e ) })?; } @@ -269,10 +275,7 @@ pub fn scaffold_iac_layout( true => {} false => { let mut readme_file = File::create(readme_file_path.to_string()).map_err(|e| { - format!( - "Failed to create Runbook README {}: {e}", - readme_file_path.to_string() - ) + format!("Failed to create Runbook README {}: {e}", readme_file_path) })?; let readme_file_data = build_manifest_data(&manifest); let template = mustache::compile_str(TXTX_README_TEMPLATE) @@ -284,15 +287,14 @@ pub fn scaffold_iac_layout( } } - runbook_file_location.append_path(&format!("deployment"))?; + runbook_file_location.append_path("deployment")?; match runbook_file_location.exists() { true => {} false => { - fs::create_dir_all(&runbook_file_location.to_string()).map_err(|e| { + fs::create_dir_all(runbook_file_location.to_string()).map_err(|e| { format!( "unable to create parent directory {}\n{}", - runbook_file_location.to_string(), - e + runbook_file_location, e ) })?; } @@ -300,7 +302,7 @@ pub fn scaffold_iac_layout( // Create runbook let runbook_folder_location = runbook_file_location.clone(); - runbook_file_location.append_path(&format!("main.tx"))?; + runbook_file_location.append_path("main.tx")?; match runbook_file_location.exists() { true => { // return Err(format!( @@ -320,28 +322,31 @@ pub fn scaffold_iac_layout( "{} {}", green!("Created file"), runbook_file_location - .get_relative_path_from_base(&base_location) + .get_relative_path_from_base(base_location) .map_err(|e| format!("Invalid Runbook file location: {e}"))? ); - let mut base_dir = runbook_folder_location.clone(); - base_dir.append_path(&format!("subgraphs.localnet.tx"))?; - let _ = File::create(base_dir.to_string()) - .map_err(|e| format!("Failed to create Runbook subgraph file: {e}"))?; - base_dir - .write_content(subgraph_runbook_src.as_bytes()) - .map_err(|e| format!("Failed to write data to Runbook subgraph file: {e}"))?; - println!( - "{} {}", - green!("Created file"), + // write subgraph.tx + if let Some(subgraph_runbook_src) = subgraph_runbook_src { + let mut base_dir = runbook_folder_location.clone(); + base_dir.append_path("subgraphs.localnet.tx")?; + let _ = File::create(base_dir.to_string()) + .map_err(|e| format!("Failed to create Runbook subgraph file: {e}"))?; base_dir - .get_relative_path_from_base(&base_location) - .map_err(|e| format!("Invalid Runbook file location: {e}"))? - ); + .write_content(subgraph_runbook_src.as_bytes()) + .map_err(|e| format!("Failed to write data to Runbook subgraph file: {e}"))?; + println!( + "{} {}", + green!("Created file"), + base_dir + .get_relative_path_from_base(base_location) + .map_err(|e| format!("Invalid Runbook file location: {e}"))? + ); + } // Create local signer let mut base_dir = runbook_folder_location.clone(); - base_dir.append_path(&format!("signers.localnet.tx"))?; + base_dir.append_path("signers.localnet.tx")?; let _ = File::create(base_dir.to_string()) .map_err(|e| format!("Failed to create Runbook signer file: {e}"))?; base_dir @@ -351,13 +356,13 @@ pub fn scaffold_iac_layout( "{} {}", green!("Created file"), base_dir - .get_relative_path_from_base(&base_location) + .get_relative_path_from_base(base_location) .map_err(|e| format!("Invalid Runbook file location: {e}"))? ); // Create devnet signer let mut base_dir = runbook_folder_location.clone(); - base_dir.append_path(&format!("signers.devnet.tx"))?; + base_dir.append_path("signers.devnet.tx")?; let _ = File::create(base_dir.to_string()) .map_err(|e| format!("Failed to create Runbook signer file: {e}"))?; base_dir @@ -367,13 +372,13 @@ pub fn scaffold_iac_layout( "{} {}", green!("Created file"), base_dir - .get_relative_path_from_base(&base_location) + .get_relative_path_from_base(base_location) .map_err(|e| format!("Invalid Runbook file location: {e}"))? ); // Create mainnet signer let mut base_dir = runbook_folder_location.clone(); - base_dir.append_path(&format!("signers.mainnet.tx"))?; + base_dir.append_path("signers.mainnet.tx")?; let _ = File::create(base_dir.to_string()) .map_err(|e| format!("Failed to create Runbook signer file: {e}"))?; base_dir @@ -383,7 +388,7 @@ pub fn scaffold_iac_layout( "{} {}", green!("Created file"), base_dir - .get_relative_path_from_base(&base_location) + .get_relative_path_from_base(base_location) .map_err(|e| format!("Invalid Runbook file location: {e}"))? ); } diff --git a/crates/cli/src/scaffold/native.rs b/crates/cli/src/scaffold/native.rs index 4bff5226..6e24a7a9 100644 --- a/crates/cli/src/scaffold/native.rs +++ b/crates/cli/src/scaffold/native.rs @@ -1,10 +1,41 @@ use crate::types::Framework; +use anyhow::{anyhow, Result}; use txtx_core::kit::helpers::fs::FileLocation; -use super::ProgramMetadata; +use super::{ + utils::{get_program_metadata_from_manifest_with_dep, CargoManifestFile}, + ProgramMetadata, +}; +/// This function attempts to load a program from a native project. +/// It looks for a `Cargo.toml` file in the specified base location. +/// If the `Cargo.toml` has a package with the `solana-program` dependency, +/// it is considered a native project. pub fn try_get_programs_from_project( - _base_location: FileLocation, -) -> Result)>, String> { - Ok(None) + base_location: FileLocation, +) -> Result)>> { + let mut manifest_location = base_location.clone(); + manifest_location + .append_path("Cargo.toml") + .map_err(|e| anyhow!("{e}"))?; + if manifest_location.exists() { + let manifest = manifest_location + .read_content_as_utf8() + .map_err(|e| anyhow!("{e}"))?; + let manifest = CargoManifestFile::from_manifest_str(&manifest) + .map_err(|e| anyhow!("unable to read Cargo.toml: {}", e))?; + + let Some(program_metadata) = get_program_metadata_from_manifest_with_dep( + "solana-program", + &base_location, + &manifest, + )? + else { + return Ok(None); + }; + + Ok(Some((Framework::Native, vec![program_metadata]))) + } else { + Ok(None) + } } diff --git a/crates/cli/src/scaffold/pinocchio.rs b/crates/cli/src/scaffold/pinocchio.rs new file mode 100644 index 00000000..7e5874ee --- /dev/null +++ b/crates/cli/src/scaffold/pinocchio.rs @@ -0,0 +1,39 @@ +use crate::types::Framework; +use anyhow::{anyhow, Result}; + +use txtx_core::kit::helpers::fs::FileLocation; + +use super::{ + utils::{get_program_metadata_from_manifest_with_dep, CargoManifestFile}, + ProgramMetadata, +}; + +/// This function attempts to load a program from a native project. +/// It looks for a `Cargo.toml` file in the specified base location. +/// If the `Cargo.toml` has a package with the `pinocchio` dependency, +/// it is considered a native project. +pub fn try_get_programs_from_project( + base_location: FileLocation, +) -> Result)>> { + let mut manifest_location = base_location.clone(); + manifest_location + .append_path("Cargo.toml") + .map_err(|e| anyhow!("{e}"))?; + if manifest_location.exists() { + let manifest = manifest_location + .read_content_as_utf8() + .map_err(|e| anyhow!("{e}"))?; + let manifest = CargoManifestFile::from_manifest_str(&manifest) + .map_err(|e| anyhow!("unable to read Cargo.toml: {}", e))?; + + let Some(program_metadata) = + get_program_metadata_from_manifest_with_dep("pinocchio", &base_location, &manifest)? + else { + return Ok(None); + }; + + Ok(Some((Framework::Pinocchio, vec![program_metadata]))) + } else { + Ok(None) + } +} diff --git a/crates/cli/src/scaffold/steel.rs b/crates/cli/src/scaffold/steel.rs index 4bff5226..b7105358 100644 --- a/crates/cli/src/scaffold/steel.rs +++ b/crates/cli/src/scaffold/steel.rs @@ -1,10 +1,39 @@ use crate::types::Framework; +use anyhow::{anyhow, Result}; + use txtx_core::kit::helpers::fs::FileLocation; -use super::ProgramMetadata; +use super::{ + utils::{get_program_metadata_from_manifest_with_dep, CargoManifestFile}, + ProgramMetadata, +}; +/// This function attempts to load a program from a native project. +/// It looks for a `Cargo.toml` file in the specified base location. +/// If the `Cargo.toml` has a package with the `steel` dependency, +/// it is considered a native project. pub fn try_get_programs_from_project( - _base_location: FileLocation, -) -> Result)>, String> { - Ok(None) + base_location: FileLocation, +) -> Result)>> { + let mut manifest_location = base_location.clone(); + manifest_location + .append_path("Cargo.toml") + .map_err(|e| anyhow!("{e}"))?; + if manifest_location.exists() { + let manifest = manifest_location + .read_content_as_utf8() + .map_err(|e| anyhow!("{e}"))?; + let manifest = CargoManifestFile::from_manifest_str(&manifest) + .map_err(|e| anyhow!("unable to read Cargo.toml: {}", e))?; + + let Some(program_metadata) = + get_program_metadata_from_manifest_with_dep("steel", &base_location, &manifest)? + else { + return Ok(None); + }; + + Ok(Some((Framework::Steel, vec![program_metadata]))) + } else { + Ok(None) + } } diff --git a/crates/cli/src/scaffold/utils.rs b/crates/cli/src/scaffold/utils.rs new file mode 100644 index 00000000..2de09569 --- /dev/null +++ b/crates/cli/src/scaffold/utils.rs @@ -0,0 +1,140 @@ +use std::collections::{BTreeMap, HashMap}; + +use anyhow::{anyhow, Result}; +use convert_case::{Case, Casing}; +use serde::Deserialize; +use txtx_addon_network_svm::codec::idl::IdlRef; +use txtx_gql::kit::helpers::fs::FileLocation; + +use super::ProgramMetadata; + +pub fn get_program_metadata_from_manifest_with_dep( + dependency_indicator: &str, + base_location: &FileLocation, + manifest: &CargoManifestFile, +) -> Result> { + let Some(manifest) = + manifest.get_manifest_with_dependency(dependency_indicator, base_location)? + else { + return Ok(None); + }; + + let Some(package) = manifest.package else { + return Ok(None); + }; + + let program_name = package.name.to_case(Case::Snake); + + let mut potential_idl_path = base_location.clone(); + let _ = potential_idl_path.append_path(&format!("idl/{program_name}.json")); + let idl = if potential_idl_path.exists() { + let idl_content = potential_idl_path + .read_content() + .map_err(|e| anyhow!("failed to read program idl: {e}"))?; + + let idl_ref = IdlRef::from_bytes(&idl_content) + .map_err(|e| anyhow!("failed to convert idl to anchor-style idl: {e}"))?; + + let idl = serde_json::to_string_pretty(&idl_ref.idl) + .map_err(|e| anyhow!("failed to serialize idl: {e}"))?; + Some(idl) + } else { + None + }; + + Ok(Some(ProgramMetadata::new(&program_name, &idl))) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CargoManifestFile { + pub package: Option, + pub dependencies: Option>, + pub workspace: Option, +} + +impl CargoManifestFile { + pub fn from_manifest_str(manifest: &str) -> Result { + let manifest: CargoManifestFile = + toml::from_str(manifest).map_err(|e| format!("failed to parse Cargo.toml: {}", e))?; + Ok(manifest) + } + + pub fn get_manifest_with_dependency( + &self, + name: &str, + base_location: &FileLocation, + ) -> Result> { + if let Some(deps) = &self.dependencies { + if deps.get(name).is_some() { + return Ok(Some(self.clone())); + } + } + if let Some(workspace) = self.workspace.as_ref() { + for member_manifest in workspace.get_member_cargo_manifests(base_location)? { + if let Some(manifest) = + member_manifest.get_manifest_with_dependency(name, base_location)? + { + return Ok(Some(manifest)); + } + } + } + Ok(None) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Package { + pub name: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Workspace { + pub members: Vec, + + #[serde(rename = "workspace.dependencies")] + #[allow(dead_code)] + pub workspace_dependencies: Option>, +} + +impl Workspace { + pub fn get_member_cargo_manifests( + &self, + base_location: &FileLocation, + ) -> Result> { + let mut member_manifests = vec![]; + for member in &self.members { + let mut member_location = base_location.clone(); + member_location + .append_path(member) + .map_err(|e| anyhow!("failed to append path: {}", e))?; + member_location + .append_path("Cargo.toml") + .map_err(|e| anyhow!("failed to append path: {}", e))?; + if member_location.exists() { + let manifest = member_location + .read_content_as_utf8() + .map_err(|e| anyhow!("{e}"))?; + let manifest = CargoManifestFile::from_manifest_str(&manifest) + .map_err(|e| anyhow!("unable to read Cargo.toml: {}", e))?; + member_manifests.push(manifest); + } + } + Ok(member_manifests) + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +#[allow(dead_code)] +pub enum Dependency { + Version(String), + Detailed(DependencyDetail), +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Deserialize)] +pub struct DependencyDetail { + pub version: Option, + pub features: Option>, + pub path: Option, +} diff --git a/crates/cli/src/tui/simnet.rs b/crates/cli/src/tui/simnet.rs index 29cab414..81001e63 100644 --- a/crates/cli/src/tui/simnet.rs +++ b/crates/cli/src/tui/simnet.rs @@ -115,8 +115,8 @@ impl App { deploy_progress_rx, status_bar_message: None, remote_rpc_url: remote_rpc_url.to_string(), - local_rpc_url: format!("http://{}", local_rpc_url.to_string()), - breaker: breaker, + local_rpc_url: format!("http://{}", local_rpc_url), + breaker, } } @@ -295,8 +295,8 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( Err(_) => break, }, i => match oper.recv(&app.deploy_progress_rx[i - 1]) { - Ok(event) => match event { - BlockEvent::UpdateProgressBarStatus(update) => { + Ok(event) => { + if let BlockEvent::UpdateProgressBarStatus(update) = event { match update.new_status.status_color { ProgressBarStatusColor::Yellow => { app.status_bar_message = Some(format!( @@ -330,8 +330,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( } }; } - _ => {} - }, + } Err(_) => { deployment_completed = true; } @@ -341,59 +340,51 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( terminal.draw(|f| ui(f, &mut app))?; if event::poll(Duration::from_millis(3))? { - match event::read()? { - Event::Key(key_event) => { - if key_event.kind == KeyEventKind::Press { - use KeyCode::*; - if key_event.modifiers == KeyModifiers::CONTROL - && key_event.code == Char('c') - { - return Ok(()); + if let Event::Key(key_event) = event::read()? { + if key_event.kind == KeyEventKind::Press { + use KeyCode::*; + if key_event.modifiers == KeyModifiers::CONTROL && key_event.code == Char('c') { + return Ok(()); + } + match key_event.code { + Char('q') | Esc => return Ok(()), + Down => app.next(), + Up => app.previous(), + Char('f') | Char('j') => { + // Break Solana + let sender = app.breaker.as_ref().unwrap(); + let instruction = system_instruction::transfer( + &sender.pubkey(), + &Pubkey::new_unique(), + 100, + ); + let message = Message::new(&[instruction], Some(&sender.pubkey())); + let _ = tx.send((message, sender.insecure_clone())); + } + Char(' ') => { + let _ = app + .simnet_commands_tx + .send(SimnetCommand::UpdateClock(ClockCommand::Toggle)); } - match key_event.code { - Char('q') | Esc => return Ok(()), - Down => app.next(), - Up => app.previous(), - Char('f') | Char('j') => { - // Break Solana - let sender = app.breaker.as_ref().unwrap(); - let instruction = system_instruction::transfer( - &sender.pubkey(), - &Pubkey::new_unique(), - 100, - ); - let message = - Message::new(&vec![instruction], Some(&sender.pubkey())); - let _ = tx.send((message, sender.insecure_clone())); - } - Char(' ') => { - let _ = app - .simnet_commands_tx - .send(SimnetCommand::UpdateClock(ClockCommand::Toggle)); - } - Tab => { - let _ = app.simnet_commands_tx.send(SimnetCommand::SlotForward); - } - Char('t') => { - let _ = - app.simnet_commands_tx - .send(SimnetCommand::UpdateRunloopMode( - RunloopTriggerMode::Transaction, - )); - } - Char('c') => { - let _ = - app.simnet_commands_tx - .send(SimnetCommand::UpdateRunloopMode( - RunloopTriggerMode::Clock, - )); - } - _ => {} + Tab => { + let _ = app.simnet_commands_tx.send(SimnetCommand::SlotForward); + } + Char('t') => { + let _ = app + .simnet_commands_tx + .send(SimnetCommand::UpdateRunloopMode( + RunloopTriggerMode::Transaction, + )); + } + Char('c') => { + let _ = app + .simnet_commands_tx + .send(SimnetCommand::UpdateRunloopMode(RunloopTriggerMode::Clock)); } + _ => {} } } - _ => {} } } } @@ -413,7 +404,7 @@ fn ui(f: &mut Frame, app: &mut App) { .fg(app.colors.secondary) .bg(app.colors.background); let chrome = Block::default() - .style(default_style.clone()) + .style(default_style) .borders(Borders::ALL) .border_style(default_style) .border_type(BorderType::Plain); @@ -470,7 +461,7 @@ fn render_epoch(f: &mut Frame, app: &mut App, area: Rect) { let default_style = Style::new().fg(app.colors.gray); let separator = Block::default() - .style(default_style.clone()) + .style(default_style) .borders(Borders::LEFT) .border_style(default_style) .border_type(BorderType::Plain); diff --git a/crates/cli/src/types/mod.rs b/crates/cli/src/types/mod.rs index 38d75c86..248fdfd5 100644 --- a/crates/cli/src/types/mod.rs +++ b/crates/cli/src/types/mod.rs @@ -1,8 +1,71 @@ -#[allow(dead_code)] +use txtx_addon_network_svm::templates::{ + get_interpolated_anchor_program_deployment_template, get_interpolated_anchor_subgraph_template, + get_interpolated_native_program_deployment_template, +}; + #[derive(Debug, Clone)] pub enum Framework { Anchor, Native, Steel, Typhoon, + Pinocchio, +} + +impl Framework { + pub fn get_interpolated_program_deployment_template(&self, program_name: &str) -> String { + match self { + Framework::Anchor => get_interpolated_anchor_program_deployment_template(program_name), + Framework::Typhoon => todo!(), + Framework::Native | Framework::Steel | Framework::Pinocchio => { + get_interpolated_native_program_deployment_template(program_name) + } + } + } + pub fn get_interpolated_subgraph_template( + &self, + program_name: &str, + idl: Option<&String>, + ) -> Result, String> { + let Some(idl) = idl else { + return Ok(None); + }; + + match self { + Framework::Anchor | Framework::Native | Framework::Pinocchio => { + let some_template = get_interpolated_anchor_subgraph_template(program_name, idl) + .map_err(|e| { + format!("failed to generate subgraph infrastructure as code: {}", e) + })?; + Ok(some_template) + } + Framework::Steel => todo!(), + Framework::Typhoon => todo!(), + } + } +} +impl std::fmt::Display for Framework { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Framework::Anchor => "anchor", + Framework::Native => "native", + Framework::Steel => "steel", + Framework::Typhoon => "typhoon", + Framework::Pinocchio => "pinocchio", + }; + write!(f, "{}", s) + } +} +impl std::str::FromStr for Framework { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "anchor" => Ok(Framework::Anchor), + "native" => Ok(Framework::Native), + "steel" => Ok(Framework::Steel), + "typhoon" => Ok(Framework::Typhoon), + _ => Err(format!("Unknown framework: {}", s)), + } + } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5a4c325c..363b85ec 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -20,7 +20,7 @@ bincode = { workspace = true } crossbeam-channel = "0.5.14" log = "0.4.22" serde = { workspace = true } -serde_bytes ={ workspace = true } +serde_with ={ workspace = true } serde_derive = { workspace = true } # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 serde_json = { workspace = true } itertools = "0.14.0" @@ -67,6 +67,7 @@ solana-poh = { workspace = true } solana-svm = { workspace = true } solana-program-runtime = { workspace = true } solana-streamer = { workspace = true } +spl-associated-token-account = { workspace = true } spl-token-2022 = "7.0.0" spl-token = "7.0.0" zstd = "0.13.2" diff --git a/crates/core/src/rpc/minimal.rs b/crates/core/src/rpc/minimal.rs index 59c160ac..cb2cc155 100644 --- a/crates/core/src/rpc/minimal.rs +++ b/crates/core/src/rpc/minimal.rs @@ -8,7 +8,7 @@ use solana_client::{ RpcLeaderScheduleConfigWrapper, }, rpc_response::{ - RpcIdentity, RpcLeaderSchedule, RpcResponseContext, RpcSnapshotSlotInfo, RpcVersionInfo, + RpcIdentity, RpcLeaderSchedule, RpcResponseContext, RpcSnapshotSlotInfo, RpcVoteAccountStatus, }, }; @@ -17,6 +17,18 @@ use solana_sdk::{ clock::{Clock, Slot}, epoch_info::EpochInfo, }; +const SURFPOOL_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct SurfpoolRpcVersionInfo { + /// The current version of surfpool + pub surfpool_version: String, + /// The current version of solana-core + pub solana_core: String, + /// first 4 bytes of the FeatureSet identifier + pub feature_set: Option, +} #[rpc] pub trait Minimal { @@ -67,7 +79,7 @@ pub trait Minimal { ) -> Result; #[rpc(meta, name = "getVersion")] - fn get_version(&self, meta: Self::Metadata) -> Result; + fn get_version(&self, meta: Self::Metadata) -> Result; // TODO: Refactor `agave-validator wait-for-restart-window` to not require this method, so // it can be removed from rpc_minimal @@ -196,9 +208,11 @@ impl Minimal for SurfpoolMinimalRpc { Ok(state_reader.transactions_processed as u64) } - fn get_version(&self, _: Self::Metadata) -> Result { + fn get_version(&self, _: Self::Metadata) -> Result { let version = solana_version::Version::default(); - Ok(RpcVersionInfo { + + Ok(SurfpoolRpcVersionInfo { + surfpool_version: format!("surfpool_v{}", SURFPOOL_VERSION), solana_core: version.to_string(), feature_set: Some(version.feature_set), }) @@ -308,6 +322,10 @@ mod tests { let result = setup.rpc.get_version(Some(setup.context)).unwrap(); assert!(!result.solana_core.is_empty()); assert!(result.feature_set.is_some()); + assert_eq!( + result.surfpool_version, + format!("surfpool_v{}", SURFPOOL_VERSION) + ); } #[test] diff --git a/crates/core/src/rpc/mod.rs b/crates/core/src/rpc/mod.rs index 1bf5ea13..a3336a0e 100644 --- a/crates/core/src/rpc/mod.rs +++ b/crates/core/src/rpc/mod.rs @@ -31,6 +31,7 @@ pub struct RunloopContext { pub id: Hash, pub state: Arc>, pub simnet_commands_tx: Sender, + pub simnet_events_tx: Sender, pub plugin_manager_commands_tx: Sender, } @@ -79,12 +80,13 @@ use crate::types::GlobalState; use crate::PluginManagerCommand; use jsonrpc_core::futures::FutureExt; use std::future::Future; -use surfpool_types::{types::RpcConfig, SimnetCommand}; +use surfpool_types::{types::RpcConfig, SimnetCommand, SimnetEvent}; #[derive(Clone)] pub struct SurfpoolMiddleware { pub context: Arc>, pub simnet_commands_tx: Sender, + pub simnet_events_tx: Sender, pub plugin_manager_commands_tx: Sender, pub config: RpcConfig, } @@ -93,12 +95,14 @@ impl SurfpoolMiddleware { pub fn new( context: Arc>, simnet_commands_tx: &Sender, + simnet_events_tx: &Sender, plugin_manager_commands_tx: &Sender, config: &RpcConfig, ) -> Self { Self { context, simnet_commands_tx: simnet_commands_tx.clone(), + simnet_events_tx: simnet_events_tx.clone(), plugin_manager_commands_tx: plugin_manager_commands_tx.clone(), config: config.clone(), } @@ -123,6 +127,7 @@ impl Middleware> for SurfpoolMiddleware { id: Hash::new_unique(), state: self.context.clone(), simnet_commands_tx: self.simnet_commands_tx.clone(), + simnet_events_tx: self.simnet_events_tx.clone(), plugin_manager_commands_tx: self.plugin_manager_commands_tx.clone(), }); Either::Left(Box::pin(next(request, meta).map(move |res| res))) diff --git a/crates/core/src/rpc/svm_tricks.rs b/crates/core/src/rpc/svm_tricks.rs index f83eda1c..1c473825 100644 --- a/crates/core/src/rpc/svm_tricks.rs +++ b/crates/core/src/rpc/svm_tricks.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::rpc::utils::verify_pubkey; use crate::rpc::State; @@ -5,21 +7,33 @@ use jsonrpc_core::futures::future; use jsonrpc_core::BoxFuture; use jsonrpc_core::{Error, Result}; use jsonrpc_derive::rpc; +use serde::de::Visitor; +use serde::Serialize; +use serde::{Deserialize, Deserializer, Serializer}; +use serde_with::{serde_as, BytesOrString}; use solana_account::Account; +use solana_client::rpc_custom_error::RpcCustomError; use solana_client::rpc_response::RpcResponseContext; use solana_rpc_client_api::response::Response as RpcResponse; use solana_sdk::clock::Epoch; +use solana_sdk::program_option::COption; +use solana_sdk::program_pack::Pack; use solana_sdk::pubkey::Pubkey; +use solana_sdk::system_program; +use spl_associated_token_account::get_associated_token_address_with_program_id; +use spl_token::state::{Account as TokenAccount, AccountState}; +use surfpool_types::SimnetEvent; use super::RunloopContext; +#[serde_as] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AccountUpdate { /// providing this value sets the lamports in the account pub lamports: Option, /// providing this value sets the data held in this account - #[serde(with = "serde_bytes")] + #[serde_as(as = "Option")] pub data: Option>, /// providing this value sets the program that owns this account. If executable, the program that loads this account. pub owner: Option, @@ -74,6 +88,121 @@ impl AccountUpdate { } } +#[serde_as] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenAccountUpdate { + /// providing this value sets the amount of the token in the account data + pub amount: Option, + /// providing this value sets the delegate of the token account + pub delegate: Option, + /// providing this value sets the state of the token account + pub state: Option, + /// providing this value sets the amount authorized to the delegate + pub delegated_amount: Option, + /// providing this value sets the close authority of the token account + pub close_authority: Option, +} + +#[derive(Debug, Clone)] +pub enum SetSomeAccount { + Account(String), + NoAccount, +} + +impl<'de> Deserialize<'de> for SetSomeAccount { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + struct SetSomeAccountVisitor; + + impl<'de> Visitor<'de> for SetSomeAccountVisitor { + type Value = SetSomeAccount; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a Pubkey String or the String 'null'") + } + + fn visit_some(self, deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).map(|v: String| match v.as_str() { + "null" => SetSomeAccount::NoAccount, + _ => SetSomeAccount::Account(v.to_string()), + }) + } + } + + deserializer.deserialize_option(SetSomeAccountVisitor) + } +} + +impl Serialize for SetSomeAccount { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + SetSomeAccount::Account(val) => serializer.serialize_str(&val), + SetSomeAccount::NoAccount => serializer.serialize_str("null"), + } + } +} + +impl TokenAccountUpdate { + /// Apply the update to the account + pub fn apply(self, token_account: &mut TokenAccount) -> Result<()> { + if let Some(amount) = self.amount { + token_account.amount = amount; + } + if let Some(delegate) = self.delegate { + match delegate { + SetSomeAccount::Account(pubkey) => { + token_account.delegate = + COption::Some(verify_pubkey(&pubkey).map_err(|e| { + Error::invalid_params(format!("Invalid delegate: {}", e.message)) + })?); + } + SetSomeAccount::NoAccount => { + token_account.delegate = COption::None; + } + } + } + if let Some(state) = self.state { + token_account.state = match state.as_str() { + "uninitialized" => AccountState::Uninitialized, + "frozen" => AccountState::Frozen, + "initialized" => AccountState::Initialized, + _ => { + return Err(Error::invalid_params(format!( + "Invalid token account state: {}", + state + ))) + } + }; + } + if let Some(delegated_amount) = self.delegated_amount { + token_account.delegated_amount = delegated_amount; + } + if let Some(close_authority) = self.close_authority { + match close_authority { + SetSomeAccount::Account(pubkey) => { + token_account.close_authority = + COption::Some(verify_pubkey(&pubkey).map_err(|e| { + Error::invalid_params(format!("Invalid close authority: {}", e.message)) + })?); + } + SetSomeAccount::NoAccount => { + token_account.close_authority = COption::None; + } + } + } + Ok(()) + } +} + #[rpc] pub trait SvmTricksRpc { type Metadata; @@ -85,6 +214,16 @@ pub trait SvmTricksRpc { pubkey: String, update: AccountUpdate, ) -> BoxFuture>>; + + #[rpc(meta, name = "svm_setTokenAccount")] + fn set_token_account( + &self, + meta: Self::Metadata, + owner: String, + mint: String, + update: TokenAccountUpdate, + token_program: Option, + ) -> BoxFuture>>; } pub fn write_account( @@ -145,9 +284,22 @@ impl SvmTricksRpc for SurfpoolSvmTricksRpc { if let Some(fetched_account) = rpc_client.get_account(&pubkey).await.ok() { fetched_account } else { - return Err(Error::invalid_params(format!( - "cannot mutate account that does not exist unless all account fields are provided" - ))); + let Some(ctx) = &meta else { + return Err(RpcCustomError::NodeUnhealthy { + num_slots_behind: None, + } + .into()); + }; + let _ = ctx.simnet_events_tx.send(SimnetEvent::info( + format!("Account {pubkey} not found, creating a new account from default values"), + )); + Account { + lamports: 0, + owner: system_program::id(), + executable: false, + rent_epoch: 0, + data: vec![], + } } } }; @@ -166,4 +318,112 @@ impl SvmTricksRpc for SurfpoolSvmTricksRpc { }); } } + + fn set_token_account( + &self, + meta: Self::Metadata, + owner_str: String, + mint_str: String, + update: TokenAccountUpdate, + some_token_program_str: Option, + ) -> BoxFuture>> { + let owner = match verify_pubkey(&owner_str) { + Ok(res) => res, + Err(e) => return Box::pin(future::err(e)), + }; + + let mint = match verify_pubkey(&mint_str) { + Ok(res) => res, + Err(e) => return Box::pin(future::err(e)), + }; + + let token_program_id = match some_token_program_str { + Some(token_program_str) => match verify_pubkey(&token_program_str) { + Ok(res) => res, + Err(e) => return Box::pin(future::err(e)), + }, + None => spl_token::id(), + }; + + let associated_token_account = + get_associated_token_address_with_program_id(&owner, &mint, &token_program_id); + + let state_reader = match meta.get_state() { + Ok(res) => res, + Err(e) => return Box::pin(future::err(e.into())), + }; + + let absolute_slot = state_reader.epoch_info.absolute_slot; + let account = state_reader.svm.get_account(&associated_token_account); + let minimum_rent = state_reader + .svm + .minimum_balance_for_rent_exemption(TokenAccount::LEN); + let rpc_client = state_reader.rpc_client.clone(); + drop(state_reader); + + return Box::pin(async move { + let mut token_account = match account { + Some(account) => account, + None => { + if let Some(fetched_account) = + rpc_client.get_account(&associated_token_account).await.ok() + { + fetched_account + } else { + let Some(ctx) = &meta else { + return Err(RpcCustomError::NodeUnhealthy { + num_slots_behind: None, + } + .into()); + }; + let _ = ctx.simnet_events_tx.send(SimnetEvent::info( + format!("Associated token account {associated_token_account} not found, creating a new account from default values"), + )); + let mut data = [0; TokenAccount::LEN]; + let default = TokenAccount { + mint, + owner, + state: AccountState::Initialized, + ..Default::default() + }; + default.pack_into_slice(&mut data); + Account { + lamports: minimum_rent, + owner: token_program_id, + executable: false, + rent_epoch: 0, + data: data.to_vec(), + } + } + } + }; + + let mut token_account_data = match TokenAccount::unpack(&token_account.data) { + Ok(token_account_data) => token_account_data, + Err(e) => { + return Err(Error::invalid_params(format!( + "Failed to unpack token account data: {}", + e + ))) + } + }; + + if let Err(e) = update.apply(&mut token_account_data) { + return Err(e); + }; + + let mut final_account_bytes = [0; TokenAccount::LEN]; + token_account_data.pack_into_slice(&mut final_account_bytes); + token_account.data = final_account_bytes.to_vec(); + return match write_account(meta, associated_token_account, token_account) + .map_err(|e| Error::invalid_params(e)) + { + Ok(_) => Ok(RpcResponse { + context: RpcResponseContext::new(absolute_slot), + value: (), + }), + Err(e) => Err(e), + }; + }); + } } diff --git a/crates/core/src/simnet/mod.rs b/crates/core/src/simnet/mod.rs index 398be32f..0496c79b 100644 --- a/crates/core/src/simnet/mod.rs +++ b/crates/core/src/simnet/mod.rs @@ -526,7 +526,8 @@ fn start_rpc_server_thread( let middleware = SurfpoolMiddleware::new( context, - simnet_commands_tx, + &simnet_commands_tx, + &simnet_events_tx, &plugin_manager_commands_tx, &config.rpc, ); diff --git a/crates/core/src/tests/helpers.rs b/crates/core/src/tests/helpers.rs index 03cc06e8..d13dcf69 100644 --- a/crates/core/src/tests/helpers.rs +++ b/crates/core/src/tests/helpers.rs @@ -36,6 +36,7 @@ pub struct TestSetup { impl TestSetup { pub fn new(rpc: T) -> Self { let (simnet_commands_tx, _rx) = crossbeam_channel::unbounded(); + let (simnet_events_tx, _rx) = crossbeam_channel::unbounded(); let (plugin_manager_commands_tx, _rx) = crossbeam_channel::unbounded(); let mut svm = LiteSVM::new(); @@ -51,6 +52,7 @@ impl TestSetup { TestSetup { context: RunloopContext { simnet_commands_tx, + simnet_events_tx: simnet_events_tx.clone(), plugin_manager_commands_tx, id: Hash::new_unique(), state: Arc::new(RwLock::new(GlobalState {