stellar_scaffold_cli/commands/
upgrade.rs1use crate::arg_parsing::ArgParser;
2use crate::commands::build::env_toml::{Account, Contract, Environment, Network};
3use clap::Parser;
4use degit::degit;
5use indexmap::IndexMap;
6use std::fs;
7use std::fs::{create_dir_all, metadata, read_dir, write};
8use std::io;
9use std::path::{Path, PathBuf};
10use stellar_cli::commands::global::Args;
11use toml_edit::{value, DocumentMut, Item, Table};
12
13use crate::{arg_parsing, commands::build, commands::init::FRONTEND_TEMPLATE};
14use stellar_cli::print::Print;
15
16#[derive(Parser, Debug, Clone)]
18pub struct Cmd {
19 #[arg(default_value = ".")]
21 pub workspace_path: PathBuf,
22 #[arg(long)]
24 pub skip_prompt: bool,
25}
26
27#[derive(thiserror::Error, Debug)]
29pub enum Error {
30 #[error("Failed to clone template: {0}")]
31 DegitError(String),
32 #[error(
33 "Workspace path contains invalid UTF-8 characters and cannot be converted to a string"
34 )]
35 InvalidWorkspacePathEncoding,
36 #[error("IO error: {0}")]
37 IoError(#[from] io::Error),
38 #[error("No Cargo.toml found in workspace path")]
39 NoCargoToml,
40 #[error("No contracts/ directory found in workspace path")]
41 NoContractsDirectory,
42 #[error("Invalid package name in Cargo.toml")]
43 InvalidPackageName,
44 #[error("Failed to parse TOML: {0}")]
45 TomlParseError(#[from] toml_edit::TomlError),
46 #[error("Failed to serialize TOML: {0}")]
47 TomlSerializeError(#[from] toml::ser::Error),
48 #[error("Failed to deserialize TOML: {0}")]
49 TomlDeserializeError(#[from] toml::de::Error),
50 #[error(transparent)]
51 BuildError(#[from] build::Error),
52 #[error("Failed to get constructor arguments: {0:?}")]
53 ConstructorArgsError(arg_parsing::Error),
54 #[error("WASM file not found for contract '{0}'. Please build the contract first.")]
55 WasmFileNotFound(String),
56 #[error(transparent)]
57 Clap(#[from] clap::Error),
58 #[error(transparent)]
59 SorobanSpecTools(#[from] soroban_spec_tools::contract::Error),
60 #[error(transparent)]
61 CopyError(#[from] fs_extra::error::Error),
62}
63
64impl Cmd {
65 pub async fn run(
74 &self,
75 global_args: &stellar_cli::commands::global::Args,
76 ) -> Result<(), Error> {
77 let printer = Print::new(global_args.quiet);
78
79 printer.infoln(format!(
80 "Upgrading Soroban workspace to scaffold project in {}",
81 self.workspace_path.display()
82 ));
83
84 self.validate_workspace()?;
86
87 let temp_dir = tempfile::tempdir().map_err(Error::IoError)?;
89 let temp_path = temp_dir.path();
90
91 printer.infoln("Downloading frontend template...");
92 Self::clone_frontend_template(temp_path)?;
93
94 printer.infoln("Copying frontend files...");
95 self.copy_frontend_files(temp_path)?;
96
97 printer.infoln("Setting up environment file...");
98 self.setup_env_file()?;
99
100 printer.infoln("Creating environments.toml...");
101 self.create_environments_toml(global_args).await?;
102
103 printer.checkln(format!(
104 "Workspace successfully upgraded to scaffold project at {}",
105 self.workspace_path.display()
106 ));
107
108 Ok(())
109 }
110
111 fn validate_workspace(&self) -> Result<(), Error> {
112 let cargo_toml = self.workspace_path.join("Cargo.toml");
114 if !cargo_toml.exists() {
115 return Err(Error::NoCargoToml);
116 }
117
118 let contracts_dir = self.workspace_path.join("contracts");
120 if !contracts_dir.exists() {
121 return Err(Error::NoContractsDirectory);
122 }
123
124 Ok(())
125 }
126
127 fn clone_frontend_template(temp_path: &Path) -> Result<(), Error> {
128 let temp_str = temp_path
129 .to_str()
130 .ok_or(Error::InvalidWorkspacePathEncoding)?;
131
132 degit(FRONTEND_TEMPLATE, temp_str);
133
134 if metadata(temp_path).is_err() || read_dir(temp_path)?.next().is_none() {
135 return Err(Error::DegitError(format!(
136 "Failed to clone template into {temp_str}: directory is empty or missing",
137 )));
138 }
139
140 Ok(())
141 }
142
143 fn copy_frontend_files(&self, temp_path: &Path) -> Result<(), Error> {
144 let skip_items = ["contracts", "environments.toml", "Cargo.toml"];
146
147 for entry in read_dir(temp_path)? {
149 let entry = entry?;
150 let item_name = entry.file_name();
151
152 if let Some(name_str) = item_name.to_str() {
154 if skip_items.contains(&name_str) {
155 continue;
156 }
157 }
158
159 let src = entry.path();
160 let dest = self.workspace_path.join(&item_name);
161
162 if dest.exists() {
164 continue;
165 }
166
167 if src.is_dir() {
168 let copy_options = fs_extra::dir::CopyOptions::new()
169 .overwrite(false) .skip_exist(true); fs_extra::dir::copy(&src, &self.workspace_path, ©_options)?;
173 } else {
174 let copy_options = fs_extra::file::CopyOptions::new().overwrite(false); fs_extra::file::copy(&src, &dest, ©_options)?;
177 }
178 }
179
180 let packages_dir = self.workspace_path.join("packages");
182 if !packages_dir.exists() {
183 create_dir_all(&packages_dir)?;
184 }
185
186 Ok(())
187 }
188
189 async fn create_environments_toml(
190 &self,
191 global_args: &stellar_cli::commands::global::Args,
192 ) -> Result<(), Error> {
193 let env_path = self.workspace_path.join("environments.toml");
194
195 if env_path.exists() {
197 return Ok(());
198 }
199
200 let contracts = self.discover_contracts(global_args)?;
202
203 self.build_contracts(global_args).await?;
205
206 let contract_configs = contracts
208 .iter()
209 .map(|contract_name| {
210 let constructor_args = self.get_constructor_args(contract_name)?;
211 Ok((
212 contract_name.clone().into_boxed_str(),
213 Contract {
214 constructor_args,
215 ..Default::default()
216 },
217 ))
218 })
219 .collect::<Result<IndexMap<_, _>, Error>>()?;
220
221 let env_config = Environment {
222 accounts: Some(vec![Account {
223 name: "default".to_string(),
224 default: true,
225 }]),
226 network: Network {
227 name: None,
228 rpc_url: Some("http://localhost:8000/rpc".to_string()),
229 network_passphrase: Some("Standalone Network ; February 2017".to_string()),
230 rpc_headers: None,
231 run_locally: true,
232 },
233 contracts: (!contract_configs.is_empty()).then_some(contract_configs),
234 };
235
236 let mut doc = DocumentMut::new();
237
238 let mut dev_table = Table::new();
240
241 let mut accounts_array = toml_edit::Array::new();
243 accounts_array.push("default");
244 dev_table["accounts"] = Item::Value(accounts_array.into());
245
246 let mut network_table = Table::new();
248 network_table["rpc-url"] = value(env_config.network.rpc_url.as_ref().unwrap());
249 network_table["network-passphrase"] =
250 value(env_config.network.network_passphrase.as_ref().unwrap());
251 network_table["run-locally"] = value(env_config.network.run_locally);
252 dev_table["network"] = Item::Table(network_table);
253
254 let contracts_table = env_config
256 .contracts
257 .as_ref()
258 .map(|contracts| {
259 contracts
260 .iter()
261 .map(|(name, config)| {
262 let mut contract_constructor_args = Table::new();
263 if let Some(args) = &config.constructor_args {
264 contract_constructor_args["constructor_args"] = value(args);
265 }
266 let contract_key = name.replace('-', "_");
268 (contract_key, Item::Table(contract_constructor_args))
269 })
270 .collect::<Table>()
271 })
272 .unwrap_or_default();
273
274 dev_table["contracts"] = Item::Table(contracts_table);
275
276 doc["development"] = Item::Table(dev_table);
277
278 write(&env_path, doc.to_string())?;
279
280 Ok(())
281 }
282
283 fn discover_contracts(&self, global_args: &Args) -> Result<Vec<String>, Error> {
284 let contracts_dir = self.workspace_path.join("contracts");
285 let printer = Print::new(global_args.quiet);
286
287 let contracts = std::fs::read_dir(&contracts_dir)?
288 .map(|entry_res| -> Result<Option<String>, Error> {
289 let entry = entry_res?;
290 let path = entry.path();
291
292 let cargo_toml = path.join("Cargo.toml");
294 if !path.is_dir() || !cargo_toml.exists() {
295 return Ok(None);
296 }
297
298 let mut content = fs::read_to_string(&cargo_toml)?;
299 if !content.contains("cdylib") {
300 return Ok(None);
301 }
302
303 let tv = content.parse::<toml::Value>()?;
305 let name = tv
306 .get("package")
307 .and_then(|p| p.get("name"))
308 .and_then(|n| n.as_str())
309 .ok_or_else(|| Error::InvalidPackageName)?;
310
311 if content.contains("[package.metadata.stellar]") {
313 printer.infoln("Found metadata section [package.metadata.stellar]");
314 } else {
315 content.push_str("\n[package.metadata.stellar]\ncargo_inherit = true\n");
316
317 let res = write(path.join("Cargo.toml"), content);
318 if let Err(e) = res {
319 printer.errorln(format!("Failed to write Cargo.toml file {e}"));
320 }
321 }
322
323 Ok(Some(name.to_string()))
324 })
325 .collect::<Result<Vec<Option<String>>, Error>>()? .into_iter()
327 .flatten()
328 .collect();
329
330 Ok(contracts)
331 }
332
333 async fn build_contracts(
334 &self,
335 global_args: &stellar_cli::commands::global::Args,
336 ) -> Result<(), Error> {
337 let build_cmd = build::Command {
339 build_clients_args: build::clients::Args {
340 env: Some(build::clients::ScaffoldEnv::Development),
341 workspace_root: Some(self.workspace_path.clone()),
342 out_dir: None,
343 global_args: Some(global_args.clone()),
344 },
345 build: stellar_cli::commands::contract::build::Cmd {
346 manifest_path: None,
347 package: None,
348 profile: "release".to_string(),
349 features: None,
350 all_features: false,
351 no_default_features: false,
352 out_dir: None,
353 print_commands_only: false,
354 meta: Vec::new(),
355 },
356 list: false,
357 build_clients: false, };
359
360 build_cmd.run(global_args).await?;
361 Ok(())
362 }
363
364 fn get_constructor_args(&self, contract_name: &str) -> Result<Option<String>, Error> {
365 let target_dir = self.workspace_path.join("target");
367 let wasm_path = stellar_build::stellar_wasm_out_file(&target_dir, contract_name);
368
369 if !wasm_path.exists() {
370 return Err(Error::WasmFileNotFound(contract_name.to_string()));
371 }
372
373 let raw_wasm = fs::read(&wasm_path)?;
375 ArgParser::get_constructor_args(self.skip_prompt, contract_name, &raw_wasm)
376 .map_err(Error::ConstructorArgsError)
377 }
378
379 fn setup_env_file(&self) -> Result<(), Error> {
380 let env_example_path = self.workspace_path.join(".env.example");
381 let env_path = self.workspace_path.join(".env");
382
383 if env_example_path.exists() && !env_path.exists() {
385 let copy_options = fs_extra::file::CopyOptions::new();
386 fs_extra::file::copy(&env_example_path, &env_path, ©_options)?;
387 }
388
389 Ok(())
390 }
391}