Skip to content

Commit 2feb84d

Browse files
feat: add surfpool cloud start command (#81)
Co-authored-by: Ludo Galabru <[email protected]>
1 parent c139d27 commit 2feb84d

File tree

9 files changed

+319
-18
lines changed

9 files changed

+319
-18
lines changed

Cargo.lock

Lines changed: 11 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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace.package]
2-
version = "0.1.15"
2+
version = "0.2.1"
33
edition = "2021"
44
description = "Surfpool is the best place to train before surfing Solana."
55
license = "Apache-2.0"
@@ -69,13 +69,14 @@ spl-associated-token-account = "6.0.0"
6969
# txtx-addon-network-svm-types = { package = "txtx-addon-network-svm-types", path = "../txtx/addons/svm/types" }
7070
# txtx-core = { path = "../txtx/crates/txtx-core" }
7171
# txtx-gql = { path = "../txtx/crates/txtx-gql" }
72+
# txtx-cloud = { path = "../txtx/crates/txtx-cloud" }
7273
# txtx-supervisor-ui = { path = "../txtx/crates/txtx-supervisor-ui", default-features = false, features = ["bin_build"] }
7374
# txtx-cloud = { path = "../txtx/crates/txtx-cloud" }
74-
txtx-addon-kit = { version = "0.2.10", features = ["wasm"] }
75-
txtx-core = { version = "0.2.13" }
75+
txtx-addon-kit = { version = "0.2.11", features = ["wasm"] }
76+
txtx-core = { version = "0.2.14" }
7677
txtx-addon-network-svm = { version = "0.1.18" }
7778
txtx-addon-network-svm-types = { version = "0.1.17" }
7879
txtx-gql = { version = "0.2.6" }
7980
txtx-supervisor-ui = { version = "0.1.4", default-features = false, features = ["crates_build"]}
80-
txtx-cloud = "0.1.1"
81+
txtx-cloud = "0.1.3"
8182
uuid = "1.15.1"

crates/cli/src/cli/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use txtx_cloud::LoginCommand;
1010
use txtx_core::manifest::WorkspaceManifest;
1111
use txtx_gql::kit::helpers::fs::FileLocation;
1212

13-
use crate::runbook::handle_execute_runbook_command;
13+
use crate::{cloud::CloudStartCommand, runbook::handle_execute_runbook_command};
1414

1515
mod simnet;
1616

@@ -31,6 +31,10 @@ pub const DEVNET_RPC_URL: &str = "https://api.devnet.solana.com";
3131
pub const TESTNET_RPC_URL: &str = "https://api.testnet.solana.com";
3232
pub const DEFAULT_ID_SVC_URL: &str = "https://id.txtx.run/v1";
3333
pub const DEFAULT_AUTH_SVC_URL: &str = "https://auth.txtx.run";
34+
pub const DEFAULT_CONSOLE_URL: &str = "https://cloud.txtx.run";
35+
pub const DEFAULT_SVM_GQL_URL: &str = "https://svm-cloud.gql.txtx.run/v1/graphql";
36+
pub const DEFAULT_SVM_CLOUD_API_URL: &str =
37+
"https://3zsafgw57plgwwvv3ddt5mucnu0nioih.lambda-url.us-east-1.on.aws/v1/surfnets";
3438
pub const DEFAULT_RUNBOOK: &str = "deployment";
3539
pub const DEFAULT_AIRDROP_AMOUNT: &str = "10000000000000";
3640
pub const DEFAULT_AIRDROPPED_KEYPAIR_PATH: &str = "~/.config/solana/id.json";
@@ -263,6 +267,9 @@ pub enum CloudCommand {
263267
/// Login to the Txtx Cloud
264268
#[clap(name = "login", bin_name = "login")]
265269
Login(LoginCommand),
270+
/// Start a new Cloud Surfnet instance
271+
#[clap(name = "start", bin_name = "start")]
272+
Start(CloudStartCommand),
266273
}
267274

268275
#[derive(Parser, PartialEq, Clone, Debug)]
@@ -414,5 +421,15 @@ async fn handle_cloud_commands(cmd: CloudCommand) -> Result<(), String> {
414421
)
415422
.await
416423
}
424+
CloudCommand::Start(cmd) => {
425+
cmd.start(
426+
DEFAULT_AUTH_SVC_URL,
427+
DEFAULT_TXTX_PORT,
428+
DEFAULT_ID_SVC_URL,
429+
DEFAULT_SVM_GQL_URL,
430+
DEFAULT_SVM_CLOUD_API_URL,
431+
)
432+
.await
433+
}
417434
}
418435
}

crates/cli/src/cloud/mod.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use std::fmt;
2+
3+
use clap::{builder::PossibleValue, Parser, ValueEnum};
4+
use dialoguer::{console::Style, theme::ColorfulTheme, Input, Select};
5+
use surfpool_types::{BlockProductionMode, CreateNetworkRequest, CreateNetworkResponse};
6+
use txtx_cloud::{auth::AuthConfig, workspace::get_user_workspaces, LoginCommand};
7+
use txtx_gql::kit::{reqwest, uuid::Uuid};
8+
9+
use crate::cli::DEFAULT_RPC_URL;
10+
11+
#[derive(Parser, PartialEq, Clone, Debug)]
12+
pub struct CloudStartCommand {
13+
/// The name of the workspace
14+
#[arg(long = "workspace", short = 'w')]
15+
pub workspace_name: Option<String>,
16+
17+
/// The name of the surfnet that will be created
18+
#[arg(long = "name", short = 'n')]
19+
pub name: Option<String>,
20+
21+
/// A description for the surfnet
22+
#[arg(long = "description", short = 'd')]
23+
pub description: Option<String>,
24+
25+
/// The RPC url to use for the datasource
26+
#[arg(long = "rpc-url", short = 'u', default_value = DEFAULT_RPC_URL)]
27+
pub datasource_rpc_url: String,
28+
29+
/// The block production mode for the surfnet. Options are `clock`, `transaction`, and `manual`.
30+
#[arg(long = "block-production", short = 'b')]
31+
pub block_production_mode: Option<CliBlockProductionMode>,
32+
}
33+
34+
#[derive(Debug, Clone, PartialEq)]
35+
pub struct CliBlockProductionMode(BlockProductionMode);
36+
impl ValueEnum for CliBlockProductionMode {
37+
fn value_variants<'a>() -> &'a [Self] {
38+
&[
39+
CliBlockProductionMode(BlockProductionMode::Clock),
40+
CliBlockProductionMode(BlockProductionMode::Transaction),
41+
CliBlockProductionMode(BlockProductionMode::Manual),
42+
]
43+
}
44+
45+
fn to_possible_value(&self) -> Option<PossibleValue> {
46+
match self.0 {
47+
BlockProductionMode::Clock => Some(PossibleValue::new("clock")),
48+
BlockProductionMode::Transaction => Some(PossibleValue::new("transaction")),
49+
BlockProductionMode::Manual => Some(PossibleValue::new("manual")),
50+
}
51+
}
52+
}
53+
54+
impl CliBlockProductionMode {
55+
fn choices() -> Vec<String> {
56+
vec![
57+
"Produce blocks every 400ms".to_string(),
58+
"Only produce blocks when transactions are received".to_string(),
59+
"Full manual control (via RPC methods / cloud.txtx.run)".to_string(),
60+
]
61+
}
62+
fn from_index(index: usize) -> Result<Self, String> {
63+
let m = match index {
64+
0 => BlockProductionMode::Clock,
65+
1 => BlockProductionMode::Transaction,
66+
2 => BlockProductionMode::Manual,
67+
_ => return Err(format!("invalid block production mode index: {}", index)),
68+
};
69+
Ok(Self(m))
70+
}
71+
}
72+
73+
impl fmt::Display for CliBlockProductionMode {
74+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75+
match self.0 {
76+
BlockProductionMode::Clock => write!(f, "clock"),
77+
BlockProductionMode::Transaction => write!(f, "transaction"),
78+
BlockProductionMode::Manual => write!(f, "manual"),
79+
}
80+
}
81+
}
82+
83+
impl CloudStartCommand {
84+
pub async fn start(
85+
&self,
86+
auth_service_url: &str,
87+
auth_callback_port: &str,
88+
id_service_url: &str,
89+
svm_gql_url: &str,
90+
svm_cloud_api_url: &str,
91+
) -> Result<(), String> {
92+
let auth_config = match AuthConfig::read_from_system_config()
93+
.map_err(|e| format!("failed to authenticate user: {e}"))?
94+
{
95+
Some(auth_config) => auth_config,
96+
None => {
97+
println!(
98+
"{} Not authenticated, you must log in to continue",
99+
yellow!("-")
100+
);
101+
txtx_cloud::login::handle_login_command(
102+
&LoginCommand::default(),
103+
auth_service_url,
104+
auth_callback_port,
105+
id_service_url,
106+
)
107+
.await?;
108+
AuthConfig::read_from_system_config()
109+
.map_err(|e| format!("failed to authenticate user: {e}"))?
110+
.ok_or(
111+
"failed to authenticate user: no auth data found after login".to_string(),
112+
)?
113+
}
114+
};
115+
116+
auth_config
117+
.refresh_session_if_needed(&id_service_url, &auth_config.pat)
118+
.await?;
119+
120+
let theme = ColorfulTheme {
121+
values_style: Style::new().green(),
122+
hint_style: Style::new().cyan(),
123+
..ColorfulTheme::default()
124+
};
125+
126+
let workspaces = get_user_workspaces(&auth_config.access_token, svm_gql_url)
127+
.await
128+
.map_err(|e| format!("failed to get available workspaces: {}", e))?;
129+
130+
let (workspace_names, workspace_ids): (Vec<String>, Vec<Uuid>) = workspaces
131+
.iter()
132+
.map(|workspace| (workspace.name.clone(), workspace.id))
133+
.unzip();
134+
135+
let selected_workspace_idx = Select::with_theme(&theme)
136+
.with_prompt("Select the workspace to create the surfnet in")
137+
.items(&workspace_names)
138+
.default(0)
139+
.interact()
140+
.map_err(|e| format!("unable to get workspace: {e}"))?;
141+
142+
let workspace_id = workspace_ids[selected_workspace_idx];
143+
144+
let name = match &self.name {
145+
Some(name) => name.clone(),
146+
None => {
147+
// Ask for the name of the surfnet
148+
Input::with_theme(&theme)
149+
.with_prompt("Enter the name of the surfnet")
150+
.interact_text()
151+
.map_err(|e| format!("unable to get surfnet name: {e}"))?
152+
}
153+
};
154+
155+
let description = match &self.description {
156+
Some(description) => Some(description.clone()),
157+
None => {
158+
// Ask for the description of the surfnet
159+
let description: String = Input::with_theme(&theme)
160+
.with_prompt("Enter an optional description for the surfnet")
161+
.allow_empty(true)
162+
.interact_text()
163+
.map_err(|e| format!("unable to get surfnet description: {e}"))?;
164+
if description.is_empty() {
165+
None
166+
} else {
167+
Some(description)
168+
}
169+
}
170+
};
171+
172+
let datasource_rpc_url: String = Input::with_theme(&theme)
173+
.with_prompt("Enter the RPC URL for the datasource")
174+
.default(self.datasource_rpc_url.clone())
175+
.interact_text()
176+
.map_err(|e| format!("unable to get datasource RPC URL: {e}"))?;
177+
178+
let block_production_mode = match &self.block_production_mode {
179+
Some(mode) => mode.0.clone(),
180+
None => {
181+
// Ask for the block production mode
182+
183+
let selected_mode = Select::with_theme(&theme)
184+
.with_prompt("Select the block production mode")
185+
.items(&CliBlockProductionMode::choices())
186+
.default(0)
187+
.interact()
188+
.map_err(|e| format!("unable to get block production mode: {e}"))?;
189+
190+
CliBlockProductionMode::from_index(selected_mode)?.0
191+
}
192+
};
193+
194+
println!("{} Spining up your hosted Surfnet...", yellow!("→"));
195+
let request = CreateNetworkRequest::new(
196+
workspace_id,
197+
name,
198+
description,
199+
datasource_rpc_url,
200+
block_production_mode,
201+
);
202+
let client = reqwest::Client::new();
203+
let res = client
204+
.post(svm_cloud_api_url)
205+
.bearer_auth(auth_config.access_token)
206+
.json(&request)
207+
.send()
208+
.await
209+
.map_err(|e| format!("failed to send request to start cloud surfnet: {e}"))?;
210+
211+
if !res.status().is_success() {
212+
let err = res
213+
.text()
214+
.await
215+
.unwrap_or_else(|_| "Unknown error".to_string());
216+
return Err(format!("failed to start cloud surfnet: {}", err));
217+
}
218+
219+
let res = res
220+
.json::<CreateNetworkResponse>()
221+
.await
222+
.map_err(|e| format!("failed to parse response: {e}"))?;
223+
224+
println!(
225+
"\n🌊 Surf is up for network '{}'\n- Dashboard: {}\n- Rpc url: {}\n",
226+
request.name,
227+
green!("https://cloud.txtx.run/networks"),
228+
green!(res.rpc_url)
229+
);
230+
231+
Ok(())
232+
}
233+
}

crates/cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#[macro_use]
22
mod macros;
3+
mod cloud;
34

45
#[macro_use]
56
extern crate hiro_system_kit;

0 commit comments

Comments
 (0)