Skip to content

Commit c9b633e

Browse files
feat: add surfnet_registerScenario RPC method for scenario registration with account overrides (#407)
Co-authored-by: Ludo Galabru <[email protected]>
1 parent 6b6d294 commit c9b633e

File tree

11 files changed

+625
-53
lines changed

11 files changed

+625
-53
lines changed

Cargo.lock

Lines changed: 15 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace.package]
2-
version = "0.11.2"
2+
version = "0.12.0"
33
edition = "2024"
44
description = "Surfpool is where developers start their Solana journey."
55
license = "Apache-2.0"
@@ -96,6 +96,8 @@ ratatui = { version = "0.29.0", features = [
9696
reqwest = { version = "0.12.23", default-features = false }
9797
rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", rev = "ff71a526156e6c9409c450f71eccd6aced9bc339", package = "rmcp" }
9898
rust-embed = "8.2.0"
99+
schemars = { version = "0.8.22" }
100+
schemars_derive = { version = "0.8.22" }
99101
serde = { version = "1.0.226", default-features = false }
100102
serde_derive = { version = "1.0.226", default-features = false } # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251
101103
serde_json = { version = "1.0.135", default-features = false }
@@ -160,8 +162,8 @@ surfpool-subgraph = { path = "crates/subgraph", default-features = false }
160162
surfpool-types = { path = "crates/types", default-features = false }
161163

162164
txtx-addon-kit = "0.4.10"
163-
txtx-addon-network-svm = { version = "0.3.16" }
164-
txtx-addon-network-svm-types = { version = "0.3.15" }
165+
txtx-addon-network-svm = { version = "0.3.17" }
166+
txtx-addon-network-svm-types = { version = "0.3.16" }
165167
txtx-cloud = { version = "0.1.13", features = [
166168
"clap",
167169
"toml",

crates/cli/src/http/mod.rs

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#![allow(unused_imports, unused_variables)]
22
use std::{
3-
collections::HashMap, error::Error as StdError, sync::RwLock, thread::JoinHandle,
3+
collections::HashMap,
4+
error::Error as StdError,
5+
sync::{Arc, RwLock},
6+
thread::JoinHandle,
47
time::Duration,
58
};
69

@@ -9,7 +12,7 @@ use actix_web::{
912
App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
1013
dev::ServerHandle,
1114
http::header::{self},
12-
middleware,
15+
middleware, post,
1316
web::{self, Data, route},
1417
};
1518
use convert_case::{Case, Casing};
@@ -19,6 +22,7 @@ use juniper_graphql_ws::ConnectionConfig;
1922
use log::{debug, error, info, trace, warn};
2023
#[cfg(feature = "explorer")]
2124
use rust_embed::RustEmbed;
25+
use serde::{Deserialize, Serialize};
2226
use surfpool_core::scenarios::TemplateRegistry;
2327
use surfpool_gql::{
2428
DynamicSchema,
@@ -29,8 +33,8 @@ use surfpool_gql::{
2933
};
3034
use surfpool_studio_ui::serve_studio_static_files;
3135
use surfpool_types::{
32-
DataIndexingCommand, OverrideTemplate, SanitizedConfig, SubgraphCommand, SubgraphEvent,
33-
SurfpoolConfig,
36+
DataIndexingCommand, OverrideTemplate, SanitizedConfig, Scenario, SubgraphCommand,
37+
SubgraphEvent, SurfpoolConfig,
3438
};
3539
use txtx_core::kit::types::types::Value;
3640
use txtx_gql::kit::uuid::Uuid;
@@ -68,6 +72,7 @@ pub async fn start_subgraph_and_explorer_server(
6872

6973
// Initialize template registry and load templates
7074
let template_registry_wrapped = Data::new(RwLock::new(TemplateRegistry::new()));
75+
let loaded_scenarios = Data::new(RwLock::new(LoadedScenarios::new()));
7176

7277
let subgraph_handle = start_subgraph_runloop(
7378
subgraph_events_tx,
@@ -86,12 +91,12 @@ pub async fn start_subgraph_and_explorer_server(
8691
.app_data(config_wrapped.clone())
8792
.app_data(collections_metadata_lookup_wrapped.clone())
8893
.app_data(template_registry_wrapped.clone())
94+
.app_data(loaded_scenarios.clone())
8995
.wrap(
9096
Cors::default()
9197
.allow_any_origin()
92-
.allowed_methods(vec!["POST", "GET", "OPTIONS", "DELETE"])
93-
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
94-
.allowed_header(header::CONTENT_TYPE)
98+
.allow_any_method()
99+
.allow_any_header()
95100
.supports_credentials()
96101
.max_age(3600),
97102
)
@@ -100,6 +105,10 @@ pub async fn start_subgraph_and_explorer_server(
100105
.service(get_config)
101106
.service(get_indexers)
102107
.service(get_scenario_templates)
108+
.service(post_scenarios)
109+
.service(get_scenarios)
110+
.service(delete_scenario)
111+
.service(patch_scenario)
103112
.service(
104113
web::scope("/workspace")
105114
.route("/v1/indexers", web::post().to(post_graphql))
@@ -109,6 +118,7 @@ pub async fn start_subgraph_and_explorer_server(
109118
);
110119

111120
if enable_studio {
121+
app = app.app_data(Arc::new(RwLock::new(LoadedScenarios::new())));
112122
app = app.service(serve_studio_static_files);
113123
}
114124

@@ -191,6 +201,106 @@ async fn get_scenario_templates(
191201
.body(response))
192202
}
193203

204+
#[derive(Debug, Serialize, Deserialize)]
205+
pub struct LoadedScenarios {
206+
pub scenarios: Vec<Scenario>,
207+
}
208+
impl LoadedScenarios {
209+
pub fn new() -> Self {
210+
Self {
211+
scenarios: Vec::new(),
212+
}
213+
}
214+
}
215+
216+
#[post("/v1/scenarios")]
217+
async fn post_scenarios(
218+
req: HttpRequest,
219+
scenario: web::Json<Scenario>,
220+
data: Data<RwLock<LoadedScenarios>>,
221+
) -> Result<HttpResponse, Error> {
222+
let mut loaded_scenarios = data
223+
.write()
224+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to acquire write lock"))?;
225+
let scenario_data = scenario.into_inner();
226+
let scenario_id = scenario_data.id.clone();
227+
loaded_scenarios.scenarios.push(scenario_data);
228+
let response = serde_json::json!({"id": scenario_id});
229+
Ok(HttpResponse::Ok()
230+
.content_type("application/json")
231+
.body(response.to_string()))
232+
}
233+
234+
#[actix_web::get("/v1/scenarios")]
235+
async fn get_scenarios(data: Data<RwLock<LoadedScenarios>>) -> Result<HttpResponse, Error> {
236+
let loaded_scenarios = data
237+
.read()
238+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to acquire read lock"))?;
239+
let response = serde_json::to_string(&loaded_scenarios.scenarios).map_err(|_| {
240+
actix_web::error::ErrorInternalServerError("Failed to serialize loaded scenarios")
241+
})?;
242+
243+
Ok(HttpResponse::Ok()
244+
.content_type("application/json")
245+
.body(response))
246+
}
247+
248+
#[actix_web::delete("/v1/scenarios/{id}")]
249+
async fn delete_scenario(
250+
path: web::Path<String>,
251+
data: Data<RwLock<LoadedScenarios>>,
252+
) -> Result<HttpResponse, Error> {
253+
let scenario_id = path.into_inner();
254+
let mut loaded_scenarios = data
255+
.write()
256+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to acquire write lock"))?;
257+
258+
let initial_len = loaded_scenarios.scenarios.len();
259+
loaded_scenarios.scenarios.retain(|s| s.id != scenario_id);
260+
261+
if loaded_scenarios.scenarios.len() == initial_len {
262+
return Ok(
263+
HttpResponse::NotFound().body(format!("Scenario with id '{}' not found", scenario_id))
264+
);
265+
}
266+
267+
Ok(HttpResponse::Ok().body(format!("Scenario '{}' deleted", scenario_id)))
268+
}
269+
270+
#[actix_web::patch("/v1/scenarios/{id}")]
271+
async fn patch_scenario(
272+
path: web::Path<String>,
273+
scenario: web::Json<Scenario>,
274+
data: Data<RwLock<LoadedScenarios>>,
275+
) -> Result<HttpResponse, Error> {
276+
let scenario_id = path.into_inner();
277+
let mut loaded_scenarios = data
278+
.write()
279+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to acquire write lock"))?;
280+
281+
let scenario_index = loaded_scenarios
282+
.scenarios
283+
.iter()
284+
.position(|s| s.id == scenario_id);
285+
286+
match scenario_index {
287+
Some(index) => {
288+
loaded_scenarios.scenarios[index] = scenario.into_inner();
289+
let response = serde_json::json!({"id": scenario_id});
290+
Ok(HttpResponse::Ok()
291+
.content_type("application/json")
292+
.body(response.to_string()))
293+
}
294+
None => {
295+
loaded_scenarios.scenarios.push(scenario.into_inner());
296+
let response = serde_json::json!({"id": scenario_id});
297+
Ok(HttpResponse::Ok()
298+
.content_type("application/json")
299+
.body(response.to_string()))
300+
}
301+
}
302+
}
303+
194304
#[allow(dead_code)]
195305
#[cfg(not(feature = "explorer"))]
196306
fn handle_embedded_file(_path: &str) -> HttpResponse {

0 commit comments

Comments
 (0)