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