1#![allow(clippy::struct_excessive_bools)]
2use super::env_toml::Network;
3use crate::arg_parsing;
4use crate::arg_parsing::ArgParser;
5use crate::commands::build::clients::Error::UpgradeArgsError;
6use crate::commands::build::env_toml;
7use indexmap::IndexMap;
8use regex::Regex;
9use serde_json;
10use shlex::split;
11use std::hash::Hash;
12use std::path::Path;
13use std::process::Command;
14use std::{fmt::Debug, path::PathBuf};
15use stellar_cli::{
16 commands as cli,
17 commands::contract::info::shared::{
18 self as contract_spec, fetch, Args as FetchArgs, Error as FetchError,
19 },
20 commands::NetworkRunnable,
21 print::Print,
22 utils::contract_hash,
23 utils::contract_spec::Spec,
24 CommandParser,
25};
26use stellar_strkey::{self, Contract};
27use stellar_xdr::curr::ScSpecEntry::FunctionV0;
28use stellar_xdr::curr::{Error as xdrError, ScSpecEntry, ScSpecTypeBytesN, ScSpecTypeDef};
29
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
31pub enum ScaffoldEnv {
32 Development,
33 Testing,
34 Staging,
35 Production,
36}
37
38impl std::fmt::Display for ScaffoldEnv {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{}", format!("{self:?}").to_lowercase())
41 }
42}
43
44#[derive(clap::Args, Clone, Debug)]
45pub struct Args {
46 #[arg(env = "STELLAR_SCAFFOLD_ENV", value_enum)]
47 pub env: Option<ScaffoldEnv>,
48 #[arg(skip)]
49 pub workspace_root: Option<std::path::PathBuf>,
50 #[arg(skip)]
52 pub out_dir: Option<std::path::PathBuf>,
53 #[arg(skip)]
54 pub global_args: Option<stellar_cli::commands::global::Args>,
55}
56
57#[derive(thiserror::Error, Debug)]
58pub enum Error {
59 #[error(transparent)]
60 EnvironmentsToml(#[from] env_toml::Error),
61 #[error("⛔ ️invalid network: must either specify a network name or both network_passphrase and rpc_url"
62 )]
63 MalformedNetwork,
64 #[error(transparent)]
65 ParsingNetwork(#[from] cli::network::Error),
66 #[error(transparent)]
67 GeneratingKey(#[from] cli::keys::generate::Error),
68 #[error("⛔ ️can only have one default account; marked as default: {0:?}")]
69 OnlyOneDefaultAccount(Vec<String>),
70 #[error(transparent)]
71 InvalidPublicKey(#[from] cli::keys::public_key::Error),
72 #[error(transparent)]
73 AddressParsing(#[from] stellar_cli::config::address::Error),
74 #[error("⛔ ️you need to provide at least one account, to use as the source account for contract deployment and other operations"
75 )]
76 NeedAtLeastOneAccount,
77 #[error("⛔ ️No contract named {0:?}")]
78 BadContractName(String),
79 #[error("⛔ ️Invalid contract ID: {0:?}")]
80 InvalidContractID(String),
81 #[error("⛔ ️An ID must be set for a contract in production or staging. E.g. <name>.id = C...")]
82 MissingContractID(String),
83 #[error("⛔ ️Unable to parse script: {0:?}")]
84 ScriptParseFailure(String),
85 #[error(transparent)]
86 RpcClient(#[from] soroban_rpc::Error),
87 #[error("⛔ ️Failed to execute subcommand: {0:?}\n{1:?}")]
88 SubCommandExecutionFailure(String, String),
89 #[error(transparent)]
90 ContractInstall(#[from] cli::contract::upload::Error),
91 #[error(transparent)]
92 ContractDeploy(#[from] cli::contract::deploy::wasm::Error),
93 #[error(transparent)]
94 ContractBindings(#[from] cli::contract::bindings::typescript::Error),
95 #[error(transparent)]
96 ContractFetch(#[from] cli::contract::fetch::Error),
97 #[error(transparent)]
98 ConfigLocator(#[from] stellar_cli::config::locator::Error),
99 #[error(transparent)]
100 ConfigNetwork(#[from] stellar_cli::config::network::Error),
101 #[error(transparent)]
102 ContractInvoke(#[from] cli::contract::invoke::Error),
103 #[error(transparent)]
104 ContractInfo(#[from] cli::contract::info::interface::Error),
105 #[error(transparent)]
106 Clap(#[from] clap::Error),
107 #[error(transparent)]
108 WasmHash(#[from] xdrError),
109 #[error(transparent)]
110 Io(#[from] std::io::Error),
111 #[error(transparent)]
112 Json(#[from] serde_json::Error),
113 #[error("⛔ ️Failed to run npm command in {0:?}: {1:?}")]
114 NpmCommandFailure(std::path::PathBuf, String),
115 #[error(transparent)]
116 AccountFund(#[from] cli::keys::fund::Error),
117 #[error("Failed to get upgrade operator: {0:?}")]
118 UpgradeArgsError(arg_parsing::Error),
119 #[error(transparent)]
120 FetchError(#[from] FetchError),
121 #[error(transparent)]
122 SpecError(#[from] stellar_cli::get_spec::contract_spec::Error),
123}
124
125impl Args {
126 fn printer(&self) -> Print {
127 Print::new(self.global_args.as_ref().is_some_and(|args| args.quiet))
128 }
129
130 pub async fn run(&self, package_names: Vec<String>) -> Result<(), Error> {
131 let workspace_root = self
132 .workspace_root
133 .as_ref()
134 .expect("workspace_root must be set before running");
135
136 let Some(current_env) = env_toml::Environment::get(
137 workspace_root,
138 &self.stellar_scaffold_env(ScaffoldEnv::Development),
139 )?
140 else {
141 return Ok(());
142 };
143
144 self.add_network_to_env(¤t_env.network)?;
145 std::fs::create_dir_all(workspace_root.join(".config/stellar"))
147 .map_err(stellar_cli::config::locator::Error::Io)?;
148 self.clone()
149 .handle_accounts(current_env.accounts.as_deref(), ¤t_env.network)
150 .await?;
151 self.clone()
152 .handle_contracts(
153 current_env.contracts.as_ref(),
154 package_names,
155 ¤t_env.network,
156 )
157 .await?;
158
159 Ok(())
160 }
161
162 fn stellar_scaffold_env(&self, default: ScaffoldEnv) -> String {
163 self.env.unwrap_or(default).to_string().to_lowercase()
164 }
165
166 fn add_network_to_env(&self, network: &env_toml::Network) -> Result<(), Error> {
173 let printer = self.printer();
174 match &network {
175 Network {
176 name: Some(name), ..
177 } => {
178 let stellar_cli::config::network::Network {
179 rpc_url,
180 network_passphrase,
181 ..
182 } = (stellar_cli::config::network::Args {
183 network: Some(name.clone()),
184 rpc_url: None,
185 network_passphrase: None,
186 rpc_headers: Vec::new(),
187 })
188 .get(&stellar_cli::config::locator::Args {
189 global: false,
190 config_dir: None,
191 })?;
192 printer.infoln(format!("Using {name} network"));
193 std::env::set_var("STELLAR_RPC_URL", rpc_url);
194 std::env::set_var("STELLAR_NETWORK_PASSPHRASE", network_passphrase);
195 }
196 Network {
197 rpc_url: Some(rpc_url),
198 network_passphrase: Some(passphrase),
199 ..
200 } => {
201 std::env::set_var("STELLAR_RPC_URL", rpc_url);
202 std::env::set_var("STELLAR_NETWORK_PASSPHRASE", passphrase);
203 printer.infoln(format!("Using network at {rpc_url}"));
204 }
205 _ => return Err(Error::MalformedNetwork),
206 }
207
208 Ok(())
209 }
210
211 fn get_network_args(network: &Network) -> stellar_cli::config::network::Args {
212 stellar_cli::config::network::Args {
213 rpc_url: network.rpc_url.clone(),
214 network_passphrase: network.network_passphrase.clone(),
215 network: network.name.clone(),
216 rpc_headers: network.rpc_headers.clone().unwrap_or_default(),
217 }
218 }
219
220 fn get_config_dir(&self) -> PathBuf {
221 self.workspace_root
222 .as_ref()
223 .expect("workspace_root not set")
224 .join(".config")
225 .join("stellar")
226 }
227
228 fn get_config_locator(&self) -> stellar_cli::config::locator::Args {
229 let config_dir = Some(self.get_config_dir());
230 stellar_cli::config::locator::Args {
231 global: false,
232 config_dir,
233 }
234 }
235
236 fn get_contract_alias(
237 &self,
238 name: &str,
239 ) -> Result<Option<Contract>, stellar_cli::config::locator::Error> {
240 let config_dir = self.get_config_locator();
241 let network_passphrase = std::env::var("STELLAR_NETWORK_PASSPHRASE")
242 .expect("No STELLAR_NETWORK_PASSPHRASE environment variable set");
243 config_dir.get_contract_id(name, &network_passphrase)
244 }
245
246 async fn get_contract_hash(
247 &self,
248 contract_id: &Contract,
249 network: &Network,
250 ) -> Result<Option<String>, Error> {
251 let result = cli::contract::fetch::Cmd {
252 contract_id: stellar_cli::config::UnresolvedContract::Resolved(*contract_id),
253 out_file: None,
254 locator: self.get_config_locator(),
255 network: Self::get_network_args(network),
256 }
257 .run_against_rpc_server(self.global_args.as_ref(), None)
258 .await;
259
260 match result {
261 Ok(result) => {
262 let ctrct_hash = contract_hash(&result)?;
263 Ok(Some(hex::encode(ctrct_hash)))
264 }
265 Err(e) => {
266 if e.to_string().contains("Contract not found") {
267 Ok(None)
268 } else {
269 Err(Error::ContractFetch(e))
270 }
271 }
272 }
273 }
274
275 fn save_contract_alias(
276 &self,
277 name: &str,
278 contract_id: &Contract,
279 network: &Network,
280 ) -> Result<(), stellar_cli::config::locator::Error> {
281 let config_dir = self.get_config_locator();
282 let passphrase = network
283 .network_passphrase
284 .clone()
285 .expect("You must set a network passphrase.");
286 config_dir.save_contract_id(&passphrase, contract_id, name)
287 }
288
289 fn create_contract_template(&self, name: &str, contract_id: &str) -> Result<(), Error> {
290 let allow_http = if ["development", "test"]
291 .contains(&self.stellar_scaffold_env(ScaffoldEnv::Production).as_str())
292 {
293 "\n allowHttp: true,"
294 } else {
295 ""
296 };
297 let network = std::env::var("STELLAR_NETWORK_PASSPHRASE")
298 .expect("No STELLAR_NETWORK_PASSPHRASE environment variable set");
299 let template = format!(
300 r"import * as Client from '{name}';
301import {{ rpcUrl }} from './util';
302
303export default new Client.Client({{
304 networkPassphrase: '{network}',
305 contractId: '{contract_id}',
306 rpcUrl,{allow_http}
307 publicKey: undefined,
308}});
309"
310 );
311 let workspace_root = self
312 .workspace_root
313 .as_ref()
314 .expect("workspace_root not set");
315 let path = workspace_root.join(format!("src/contracts/{name}.ts"));
316 std::fs::write(path, template)?;
317 Ok(())
318 }
319
320 async fn generate_contract_bindings(&self, name: &str, contract_id: &str) -> Result<(), Error> {
321 let printer = self.printer();
322 printer.infoln(format!("Binding {name:?} contract"));
323 let workspace_root = self
324 .workspace_root
325 .as_ref()
326 .expect("workspace_root not set");
327 let final_output_dir = workspace_root.join(format!("packages/{name}"));
328
329 let temp_dir = workspace_root.join(format!("target/packages/{name}"));
331 let temp_dir_display = temp_dir.display();
332 cli::contract::bindings::typescript::Cmd::parse_arg_vec(&[
333 "--contract-id",
334 contract_id,
335 "--output-dir",
336 temp_dir.to_str().expect("we do not support non-utf8 paths"),
337 "--config-dir",
338 self.get_config_dir()
339 .to_str()
340 .expect("we do not support non-utf8 paths"),
341 "--overwrite",
342 ])?
343 .run_against_rpc_server(self.global_args.as_ref(), None)
344 .await?;
345
346 printer.infoln(format!("Running 'npm install' in {temp_dir_display:?}"));
348 let output = std::process::Command::new("npm")
349 .current_dir(&temp_dir)
350 .arg("install")
351 .arg("--loglevel=error") .arg("--no-workspaces") .output()?;
354
355 if !output.status.success() {
356 let _ = std::fs::remove_dir_all(&temp_dir);
358 return Err(Error::NpmCommandFailure(
359 temp_dir.clone(),
360 format!(
361 "npm install failed with status: {:?}\nError: {}",
362 output.status.code(),
363 String::from_utf8_lossy(&output.stderr)
364 ),
365 ));
366 }
367 printer.checkln(format!("'npm install' succeeded in {temp_dir_display}"));
368
369 printer.infoln(format!("Running 'npm run build' in {temp_dir_display}"));
370 let output = std::process::Command::new("npm")
371 .current_dir(&temp_dir)
372 .arg("run")
373 .arg("build")
374 .arg("--loglevel=error") .output()?;
376
377 if !output.status.success() {
378 let _ = std::fs::remove_dir_all(&temp_dir);
380 return Err(Error::NpmCommandFailure(
381 temp_dir.clone(),
382 format!(
383 "npm run build failed with status: {:?}\nError: {}",
384 output.status.code(),
385 String::from_utf8_lossy(&output.stderr)
386 ),
387 ));
388 }
389 printer.checkln(format!("'npm run build' succeeded in {temp_dir_display}"));
390
391 if final_output_dir.exists() {
393 for p in ["dist/index.d.ts", "dist/index.js", "src/index.ts"]
394 .iter()
395 .map(Path::new)
396 {
397 std::fs::copy(temp_dir.join(p), final_output_dir.join(p))?;
398 }
399 printer.checkln(format!("Client {name:?} updated successfully"));
400 } else {
401 std::fs::create_dir_all(&final_output_dir)?;
402 std::fs::rename(&temp_dir, &final_output_dir)?;
404 printer.checkln(format!("Client {name:?} created successfully"));
405 let output = std::process::Command::new("npm")
407 .current_dir(&final_output_dir)
408 .arg("install")
409 .arg("--loglevel=error")
410 .output()?;
411
412 if !output.status.success() {
413 return Err(Error::NpmCommandFailure(
414 final_output_dir.clone(),
415 format!(
416 "npm install in final directory failed with status: {:?}\nError: {}",
417 output.status.code(),
418 String::from_utf8_lossy(&output.stderr)
419 ),
420 ));
421 }
422 }
423
424 self.create_contract_template(name, contract_id)?;
425 Ok(())
426 }
427
428 async fn handle_accounts(
429 &self,
430 accounts: Option<&[env_toml::Account]>,
431 network: &Network,
432 ) -> Result<(), Error> {
433 let printer = self.printer();
434 let Some(accounts) = accounts else {
435 return Err(Error::NeedAtLeastOneAccount);
436 };
437
438 let default_account_candidates = accounts
439 .iter()
440 .filter(|&account| account.default)
441 .map(|account| account.name.clone())
442 .collect::<Vec<_>>();
443
444 let default_account = match (default_account_candidates.as_slice(), accounts) {
445 ([], []) => return Err(Error::NeedAtLeastOneAccount),
446 ([], [env_toml::Account { name, .. }, ..]) => name.clone(),
447 ([candidate], _) => candidate.to_string(),
448 _ => return Err(Error::OnlyOneDefaultAccount(default_account_candidates)),
449 };
450 let config = self.get_config_locator();
451
452 for account in accounts {
453 printer.infoln(format!("Creating keys for {:?}", account.name));
454 let args =
456 self.global_args
457 .clone()
458 .unwrap_or_else(|| stellar_cli::commands::global::Args {
459 locator: config.clone(),
460 ..Default::default()
461 });
462
463 let generate_cmd = cli::keys::generate::Cmd {
464 name: account.name.clone().parse()?,
465 fund: true,
466 config_locator: config.clone(),
467 network: Self::get_network_args(network),
468 seed: None,
469 hd_path: None,
470 as_secret: false,
471 secure_store: false,
472 overwrite: false,
473 };
474
475 match generate_cmd.run(&args).await {
476 Err(e) if e.to_string().contains("already exists") => {
477 printer.blankln(e);
478 let rpc_client = soroban_rpc::Client::new(
480 network
481 .rpc_url
482 .as_ref()
483 .expect("network contains the RPC url"),
484 )?;
485
486 let public_key_cmd = cli::keys::public_key::Cmd {
487 name: account.name.parse()?,
488 locator: config.clone(),
489 hd_path: None,
490 };
491 let address = public_key_cmd.public_key().await?;
492
493 if (rpc_client.get_account(&address.to_string()).await).is_err() {
494 printer.infoln("Account not found on chain, funding...");
495 let fund_cmd = cli::keys::fund::Cmd {
496 network: Self::get_network_args(network),
497 address: public_key_cmd,
498 };
499 fund_cmd.run(&args).await?;
500 }
501 }
502 other_result => other_result?,
503 }
504 }
505
506 std::env::set_var("STELLAR_ACCOUNT", &default_account);
507 Ok(())
508 }
509
510 fn maintain_user_ordering(
511 package_names: &[String],
512 contracts: Option<&IndexMap<Box<str>, env_toml::Contract>>,
513 ) -> Vec<String> {
514 contracts.map_or_else(
515 || package_names.to_vec(),
516 |contracts| {
517 let mut reordered: Vec<String> = contracts
518 .keys()
519 .filter_map(|contract_name| {
520 package_names
521 .iter()
522 .find(|&name| name == contract_name.as_ref())
523 .cloned()
524 })
525 .collect();
526
527 reordered.extend(
528 package_names
529 .iter()
530 .filter(|name| !contracts.contains_key(name.as_str()))
531 .cloned(),
532 );
533
534 reordered
535 },
536 )
537 }
538
539 async fn handle_production_contracts(
540 &self,
541 contracts: &IndexMap<Box<str>, env_toml::Contract>,
542 ) -> Result<(), Error> {
543 for (name, contract) in contracts.iter().filter(|(_, settings)| settings.client) {
544 if let Some(id) = &contract.id {
545 if stellar_strkey::Contract::from_string(id).is_err() {
546 return Err(Error::InvalidContractID(id.to_string()));
547 }
548 self.clone()
549 .generate_contract_bindings(name, &id.to_string())
550 .await?;
551 } else {
552 return Err(Error::MissingContractID(name.to_string()));
553 }
554 }
555 Ok(())
556 }
557
558 async fn handle_contracts(
559 &self,
560 contracts: Option<&IndexMap<Box<str>, env_toml::Contract>>,
561 package_names: Vec<String>,
562 network: &Network,
563 ) -> Result<(), Error> {
564 let printer = self.printer();
565 if package_names.is_empty() {
566 return Ok(());
567 }
568
569 let env = self.stellar_scaffold_env(ScaffoldEnv::Production);
570 if env == "production" || env == "staging" {
571 if let Some(contracts) = contracts {
572 self.handle_production_contracts(contracts).await?;
573 }
574 return Ok(());
575 }
576
577 self.validate_contract_names(contracts)?;
578
579 let names = Self::maintain_user_ordering(&package_names, contracts);
580
581 let mut results: Vec<(String, Result<(), String>)> = Vec::new();
582
583 for name in names {
584 let settings = contracts
585 .and_then(|contracts| contracts.get(name.as_str()))
586 .cloned()
587 .unwrap_or_default();
588
589 if !settings.client {
591 continue;
592 }
593
594 match self
595 .process_single_contract(&name, settings, network, &env)
596 .await
597 {
598 Ok(()) => {
599 printer.checkln(format!("Successfully generated client for: {name}"));
600 results.push((name, Ok(())));
601 }
602 Err(e) => {
603 printer.errorln(format!("Failed to generate client for: {name}"));
604 results.push((name, Err(e.to_string())));
605 }
606 }
607 }
608
609 let (successes, failures): (Vec<_>, Vec<_>) =
611 results.into_iter().partition(|(_, result)| result.is_ok());
612
613 printer.infoln("Client Generation Summary:");
615 printer.blankln(format!("Successfully processed: {}", successes.len()));
616 printer.blankln(format!("Failed: {}", failures.len()));
617
618 if !failures.is_empty() {
619 printer.infoln("Failures:");
620 for (name, result) in &failures {
621 if let Err(e) = result {
622 printer.blankln(format!("{name}: {e}"));
623 }
624 }
625 }
626
627 Ok(())
628 }
629
630 fn get_wasm_path(&self, contract_name: &str) -> std::path::PathBuf {
631 if let Some(out_dir) = &self.out_dir {
633 out_dir.join(format!("{contract_name}.wasm"))
634 } else {
635 let workspace_root = self
636 .workspace_root
637 .as_ref()
638 .expect("workspace_root not set");
639 let target_dir = workspace_root.join("target");
640 stellar_build::stellar_wasm_out_file(&target_dir, contract_name)
641 }
642 }
643
644 fn validate_contract_names(
645 &self,
646 contracts: Option<&IndexMap<Box<str>, env_toml::Contract>>,
647 ) -> Result<(), Error> {
648 if let Some(contracts) = contracts {
649 for (name, _) in contracts.iter().filter(|(_, settings)| settings.client) {
650 let wasm_path = self.get_wasm_path(name);
651 if !wasm_path.exists() {
652 return Err(Error::BadContractName(name.to_string()));
653 }
654 }
655 }
656 Ok(())
657 }
658
659 fn get_package_dir(&self, name: &str) -> Result<std::path::PathBuf, Error> {
660 let workspace_root = self
661 .workspace_root
662 .as_ref()
663 .expect("workspace_root must be set before running");
664 let package_dir = workspace_root.join(format!("packages/{name}"));
665 if !package_dir.exists() {
666 return Err(Error::BadContractName(name.to_string()));
667 }
668 Ok(package_dir)
669 }
670
671 async fn process_single_contract(
672 &self,
673 name: &str,
674 settings: env_toml::Contract,
675 network: &Network,
676 env: &str,
677 ) -> Result<(), Error> {
678 let printer = self.printer();
679 let contract_id = if let Some(id) = &settings.id {
681 Contract::from_string(id).map_err(|_| Error::InvalidContractID(id.clone()))?
682 } else {
683 let wasm_path = self.get_wasm_path(name);
684 if !wasm_path.exists() {
685 return Err(Error::BadContractName(name.to_string()));
686 }
687 let new_hash = self.upload_contract_wasm(name, &wasm_path).await?;
688 let mut upgraded_contract = None;
689
690 if let Some(existing_contract_id) = self.get_contract_alias(name)? {
692 let hash = self
693 .get_contract_hash(&existing_contract_id, network)
694 .await?;
695 if let Some(current_hash) = hash {
696 if current_hash == new_hash {
697 printer.checkln(format!("Contract {name:?} is up to date"));
698 if self.get_package_dir(name).is_err() {
700 self.generate_contract_bindings(
701 name,
702 &existing_contract_id.to_string(),
703 )
704 .await?;
705 }
706 return Ok(());
707 }
708 upgraded_contract = self
709 .try_upgrade_contract(
710 name,
711 existing_contract_id,
712 ¤t_hash,
713 &new_hash,
714 network,
715 )
716 .await?;
717 }
718 printer.infoln(format!("Updating contract {name:?}"));
719 }
720
721 let contract_id = if let Some(upgraded) = upgraded_contract {
723 upgraded
724 } else {
725 self.deploy_contract(name, &new_hash, &settings).await?
726 };
727 if let Some(after_deploy) = settings.after_deploy.as_deref() {
729 if env == "development" || env == "testing" {
730 printer.infoln(format!("Running after_deploy script for {name:?}"));
731 self.run_after_deploy_script(name, &contract_id, after_deploy)
732 .await?;
733 }
734 }
735 self.save_contract_alias(name, &contract_id, network)?;
736 contract_id
737 };
738
739 self.generate_contract_bindings(name, &contract_id.to_string())
740 .await?;
741
742 Ok(())
743 }
744
745 async fn upload_contract_wasm(
746 &self,
747 name: &str,
748 wasm_path: &std::path::Path,
749 ) -> Result<String, Error> {
750 let printer = self.printer();
751 printer.infoln(format!("Installing {name:?} wasm bytecode on-chain..."));
752 let hash = cli::contract::upload::Cmd::parse_arg_vec(&[
753 "--wasm",
754 wasm_path
755 .to_str()
756 .expect("we do not support non-utf8 paths"),
757 "--config-dir",
758 self.get_config_dir()
759 .to_str()
760 .expect("we do not support non-utf8 paths"),
761 ])?
762 .run_against_rpc_server(self.global_args.as_ref(), None)
763 .await?
764 .into_result()
765 .expect("no hash returned by 'contract upload'")
766 .to_string();
767 printer.infoln(format!(" ↳ hash: {hash}"));
768 Ok(hash)
769 }
770
771 fn parse_script_line(line: &str) -> Result<(Option<String>, Vec<String>), Error> {
772 let re = Regex::new(r"\$\((.*?)\)").expect("Invalid regex pattern");
773 let (shell, flag) = if cfg!(windows) {
774 ("cmd", "/C")
775 } else {
776 ("sh", "-c")
777 };
778
779 let resolved_line = Self::resolve_line(&re, line, shell, flag)?;
780 let parts = split(&resolved_line)
781 .ok_or_else(|| Error::ScriptParseFailure(resolved_line.to_string()))?;
782
783 let (source_account, command_parts): (Vec<_>, Vec<_>) = parts
784 .iter()
785 .partition(|&part| part.starts_with("STELLAR_ACCOUNT="));
786
787 let source = source_account.first().map(|account| {
788 account
789 .strip_prefix("STELLAR_ACCOUNT=")
790 .unwrap()
791 .to_string()
792 });
793
794 Ok((
795 source,
796 command_parts.iter().map(|s| (*s).to_string()).collect(),
797 ))
798 }
799
800 async fn deploy_contract(
801 &self,
802 name: &str,
803 hash: &str,
804 settings: &env_toml::Contract,
805 ) -> Result<Contract, Error> {
806 let printer = self.printer();
807 let mut deploy_args = vec![
808 "--alias".to_string(),
809 name.to_string(),
810 "--wasm-hash".to_string(),
811 hash.to_string(),
812 "--config-dir".to_string(),
813 self.get_config_dir()
814 .to_str()
815 .expect("we do not support non-utf8 paths")
816 .to_string(),
817 ];
818
819 if let Some(constructor_script) = &settings.constructor_args {
820 let (source_account, mut args) = Self::parse_script_line(constructor_script)?;
821
822 if let Some(account) = source_account {
823 deploy_args.extend_from_slice(&["--source-account".to_string(), account]);
824 }
825
826 deploy_args.push("--".to_string());
827 deploy_args.append(&mut args);
828 }
829
830 printer.infoln(format!("Instantiating {name:?} smart contract"));
831 let deploy_arg_refs: Vec<&str> = deploy_args
832 .iter()
833 .map(std::string::String::as_str)
834 .collect();
835 let contract_id = cli::contract::deploy::wasm::Cmd::parse_arg_vec(&deploy_arg_refs)?
836 .run_against_rpc_server(self.global_args.as_ref(), None)
837 .await?
838 .into_result()
839 .expect("no contract id returned by 'contract deploy'");
840 printer.infoln(format!(" ↳ contract_id: {contract_id}"));
841
842 Ok(contract_id)
843 }
844
845 async fn try_upgrade_contract(
846 &self,
847 name: &str,
848 existing_contract_id: Contract,
849 existing_hash: &str,
850 hash: &str,
851 network: &Network,
852 ) -> Result<Option<Contract>, Error> {
853 let printer = self.printer();
854 let existing_spec = fetch_contract_spec(existing_hash, network).await?;
855 let spec_to_upgrade = fetch_contract_spec(hash, network).await?;
856 let Some(legacy_upgradeable) = Self::is_legacy_upgradeable(existing_spec) else {
857 return Ok(None);
858 };
859
860 if Self::is_legacy_upgradeable(spec_to_upgrade).is_none() {
861 printer.warnln("New WASM is not upgradable. Contract will be redeployed instead of being upgraded.");
862 return Ok(None);
863 }
864
865 printer
866 .infoln("Upgradable contract found, will use 'upgrade' function instead of redeploy");
867
868 let existing_contract_id_str = existing_contract_id.to_string();
869 let mut redeploy_args = vec![
870 "--id",
871 existing_contract_id_str.as_str(),
872 "--",
873 "upgrade",
874 "--new_wasm_hash",
875 hash,
876 ];
877
878 let invoke_cmd = if legacy_upgradeable {
879 let upgrade_operator = ArgParser::get_upgrade_args(name).map_err(UpgradeArgsError)?;
880 redeploy_args.push("--operator");
881 redeploy_args.push(&upgrade_operator);
882 cli::contract::invoke::Cmd::parse_arg_vec(&redeploy_args)
883 } else {
884 cli::contract::invoke::Cmd::parse_arg_vec(&redeploy_args)
885 }?;
886 printer.infoln(format!("Upgrading {name:?} smart contract"));
887 invoke_cmd
888 .run_against_rpc_server(self.global_args.as_ref(), None)
889 .await?
890 .into_result()
891 .expect("no result returned by 'contract invoke'");
892 printer.infoln(format!("Contract upgraded: {existing_contract_id}"));
893
894 Ok(Some(existing_contract_id))
895 }
896
897 fn is_legacy_upgradeable(spec: Vec<ScSpecEntry>) -> Option<bool> {
899 spec.iter()
900 .filter_map(|x| if let FunctionV0(e) = x { Some(e) } else { None })
901 .filter(|x| x.name.to_string() == "upgrade")
902 .find(|x| {
903 x.inputs
904 .iter()
905 .any(|y| matches!(y.type_, ScSpecTypeDef::BytesN(ScSpecTypeBytesN { n: 32 })))
906 })
907 .map(|x| x.inputs.iter().any(|y| y.type_ == ScSpecTypeDef::Address))
908 }
909
910 fn resolve_line(re: &Regex, line: &str, shell: &str, flag: &str) -> Result<String, Error> {
911 let mut result = String::new();
912 let mut last_match = 0;
913 for cap in re.captures_iter(line) {
914 let whole_match = cap.get(0).unwrap();
915 result.push_str(&line[last_match..whole_match.start()]);
916 let cmd = &cap[1];
917 let output = Self::execute_subcommand(shell, flag, cmd)?;
918 result.push_str(&output);
919 last_match = whole_match.end();
920 }
921 result.push_str(&line[last_match..]);
922 Ok(result)
923 }
924
925 fn execute_subcommand(shell: &str, flag: &str, cmd: &str) -> Result<String, Error> {
926 match Command::new(shell).arg(flag).arg(cmd).output() {
927 Ok(output) => {
928 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
929 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
930
931 if output.status.success() {
932 Ok(stdout)
933 } else {
934 Err(Error::SubCommandExecutionFailure(cmd.to_string(), stderr))
935 }
936 }
937 Err(e) => Err(Error::SubCommandExecutionFailure(
938 cmd.to_string(),
939 e.to_string(),
940 )),
941 }
942 }
943
944 async fn run_after_deploy_script(
945 &self,
946 name: &str,
947 contract_id: &Contract,
948 after_deploy_script: &str,
949 ) -> Result<(), Error> {
950 let printer = self.printer();
951 let config_dir_path = self.get_config_dir();
952 let config_dir = config_dir_path.to_str().unwrap();
953 for line in after_deploy_script.lines() {
954 let line = line.trim();
955 if line.is_empty() {
956 continue;
957 }
958
959 let (source_account, command_parts) = Self::parse_script_line(line)?;
960
961 let contract_id_arg = contract_id.to_string();
962 let mut args = vec!["--id", &contract_id_arg, "--config-dir", config_dir];
963 if let Some(account) = source_account.as_ref() {
964 args.extend_from_slice(&["--source-account", account]);
965 }
966 args.extend_from_slice(&["--"]);
967 args.extend(command_parts.iter().map(std::string::String::as_str));
968
969 printer.infoln(format!(
970 " ↳ Executing: stellar contract invoke {}",
971 args.join(" ")
972 ));
973 let result = cli::contract::invoke::Cmd::parse_arg_vec(&args)?
974 .run_against_rpc_server(self.global_args.as_ref(), None)
975 .await?;
976 printer.infoln(format!(" ↳ Result: {result:?}"));
977 }
978 printer.checkln(format!(
979 "After deploy script for {name:?} completed successfully"
980 ));
981 Ok(())
982 }
983}
984
985async fn fetch_contract_spec(
986 wasm_hash: &str,
987 network: &Network,
988) -> Result<Vec<ScSpecEntry>, Error> {
989 let fetched = fetch(
990 &FetchArgs {
991 wasm_hash: Some(wasm_hash.to_string()),
992 network: network.into(),
993 ..Default::default()
994 },
995 &Print::new(true),
997 )
998 .await?;
999
1000 match fetched.contract {
1001 contract_spec::Contract::Wasm { wasm_bytes } => Ok(Spec::new(&wasm_bytes)?.spec),
1002 contract_spec::Contract::StellarAssetContract => unreachable!(),
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use tempfile::TempDir;
1010
1011 #[test]
1012 fn test_get_package_dir() {
1013 let temp_dir = TempDir::new().unwrap();
1014 let package_path = temp_dir.path().join("packages/existing_package");
1015 std::fs::create_dir_all(&package_path).unwrap();
1016 let args = Args {
1017 env: Some(ScaffoldEnv::Development),
1018 workspace_root: Some(temp_dir.path().to_path_buf()),
1019 out_dir: None,
1020 global_args: None,
1021 };
1022 let result = args.get_package_dir("existing_package");
1023 assert!(result.is_ok());
1024 let path = result.unwrap();
1025 assert_eq!(path.file_name().unwrap(), "existing_package");
1026 }
1027
1028 #[test]
1029 fn test_get_package_dir_nonexistent() {
1030 let args = Args {
1031 env: Some(ScaffoldEnv::Development),
1032 workspace_root: Some(std::path::PathBuf::from("tests/nonexistent_workspace")),
1033 out_dir: None,
1034 global_args: None,
1035 };
1036 let result = args.get_package_dir("nonexistent_package");
1037 assert!(result.is_err());
1038 if let Err(Error::BadContractName(name)) = result {
1039 assert_eq!(name, "nonexistent_package");
1040 } else {
1041 panic!("Expected BadContractName error");
1042 }
1043 }
1044}