1use std::array::TryFromSliceError;
2use std::ffi::OsString;
3use std::fmt::Debug;
4use std::num::ParseIntError;
5
6use clap::Parser;
7use rand::Rng;
8use soroban_spec_tools::contract as contract_spec;
9
10use crate::config::address::AliasName;
11use crate::resources;
12use crate::tx::sim_sign_and_send_tx;
13use crate::xdr::{
14 AccountId, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress,
15 CreateContractArgs, CreateContractArgsV2, Error as XdrError, Hash, HostFunction,
16 InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo, MuxedAccount, Operation, OperationBody,
17 Preconditions, PublicKey, ScAddress, SequenceNumber, Transaction, TransactionExt, Uint256,
18 VecM, WriteXdr,
19};
20
21use crate::commands::tx::fetch;
22use crate::utils::XDR_DEPTH_LIMIT;
23use crate::{
24 commands::{
25 contract::{self, arg_parsing, build, id::wasm::get_contract_id, upload},
26 global,
27 txn_result::{TxnEnvelopeResult, TxnResult},
28 HEADING_TRANSACTION,
29 },
30 config::{self, data, locator, network},
31 print::Print,
32 rpc,
33 utils::{self, rpc::get_remote_wasm_from_hash},
34 wasm,
35};
36
37pub const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
38
39#[derive(Parser, Debug, Clone)]
40#[command(group(
41 clap::ArgGroup::new("wasm_src")
42 .required(false)
43 .args(&["wasm", "wasm_hash"]),
44))]
45#[group(skip)]
46pub struct Cmd {
47 #[arg(long, group = "wasm_src")]
51 pub wasm: Option<std::path::PathBuf>,
52 #[arg(long = "wasm-hash", conflicts_with = "wasm", group = "wasm_src")]
54 pub wasm_hash: Option<String>,
55 #[arg(long)]
57 pub salt: Option<String>,
58 #[command(flatten)]
59 pub config: config::Args,
60 #[arg(long, short = 'i', default_value = "false")]
61 pub ignore_checks: bool,
63 #[arg(long)]
67 pub alias: Option<AliasName>,
68 #[command(flatten)]
69 pub resources: resources::Args,
70 #[command(flatten)]
71 pub auth_mode: crate::auth_mode::Args,
72 #[arg(long, help_heading = HEADING_TRANSACTION)]
74 pub build_only: bool,
75 #[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")]
77 pub slop: Vec<OsString>,
78 #[arg(long, help_heading = "Build Options", conflicts_with = "wasm_src")]
80 pub package: Option<String>,
81 #[command(flatten)]
82 pub build_args: build::BuildArgs,
83}
84
85#[derive(thiserror::Error, Debug)]
86pub enum Error {
87 #[error(transparent)]
88 Install(#[from] upload::Error),
89
90 #[error("error parsing int: {0}")]
91 ParseIntError(#[from] ParseIntError),
92
93 #[error("internal conversion error: {0}")]
94 TryFromSliceError(#[from] TryFromSliceError),
95
96 #[error("xdr processing error: {0}")]
97 Xdr(#[from] XdrError),
98
99 #[error("cannot parse salt: {salt}")]
100 CannotParseSalt { salt: String },
101
102 #[error("cannot parse contract ID {contract_id}: {error}")]
103 CannotParseContractId {
104 contract_id: String,
105 error: stellar_strkey::DecodeError,
106 },
107
108 #[error("cannot parse WASM hash {wasm_hash}: {error}")]
109 CannotParseWasmHash {
110 wasm_hash: String,
111 error: stellar_strkey::DecodeError,
112 },
113
114 #[error("Must provide either --wasm or --wasm-hash")]
115 WasmNotProvided,
116
117 #[error(transparent)]
118 Rpc(#[from] rpc::Error),
119
120 #[error(transparent)]
121 Config(#[from] config::Error),
122
123 #[error(transparent)]
124 StrKey(#[from] stellar_strkey::DecodeError),
125
126 #[error(transparent)]
127 Infallible(#[from] std::convert::Infallible),
128
129 #[error(transparent)]
130 WasmId(#[from] contract::id::wasm::Error),
131
132 #[error(transparent)]
133 Data(#[from] data::Error),
134
135 #[error(transparent)]
136 Network(#[from] network::Error),
137
138 #[error(transparent)]
139 Wasm(#[from] wasm::Error),
140
141 #[error(transparent)]
142 Locator(#[from] locator::Error),
143
144 #[error(transparent)]
145 ContractSpec(#[from] contract_spec::Error),
146
147 #[error(transparent)]
148 ArgParse(#[from] arg_parsing::Error),
149
150 #[error("Only ed25519 accounts are allowed")]
151 OnlyEd25519AccountsAllowed,
152
153 #[error(transparent)]
154 Fee(#[from] fetch::fee::Error),
155
156 #[error(transparent)]
157 Fetch(#[from] fetch::Error),
158
159 #[error(transparent)]
160 Build(#[from] build::Error),
161
162 #[error(transparent)]
163 AuthMode(#[from] crate::auth_mode::Error),
164
165 #[error("no buildable contracts found in workspace (no packages with crate-type cdylib)")]
166 NoBuildableContracts,
167
168 #[error("--alias is not supported when deploying multiple contracts; aliases are derived from package names automatically")]
169 AliasNotSupported,
170
171 #[error("workspace package '{0}' resolves to the reserved contract alias '{0}'; rename the package, or deploy it on its own with `--package {0} --alias <name>`")]
172 ReservedPackageAlias(String),
173
174 #[error("--salt is not supported when deploying multiple contracts")]
175 SaltNotSupported,
176
177 #[error("constructor arguments are not supported when deploying multiple contracts")]
178 ConstructorArgsNotSupported,
179
180 #[error("--build-only is not supported without --wasm or --wasm-hash")]
181 BuildOnlyNotSupported,
182
183 #[error(
184 "--wasm or --wasm-hash is required when not in a Cargo workspace; no Cargo.toml found"
185 )]
186 NotInCargoProject,
187}
188
189impl Cmd {
190 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
191 self.auth_mode.validate_not_enforce()?;
192
193 if self.build_only && self.wasm.is_none() && self.wasm_hash.is_none() {
194 return Err(Error::BuildOnlyNotSupported);
195 }
196
197 let built_contracts = self.resolve_contracts(global_args).await?;
198
199 if let Some(name) = reserved_package_alias(self.alias.as_ref(), &built_contracts) {
204 return Err(Error::ReservedPackageAlias(name));
205 }
206
207 if built_contracts.is_empty() {
210 Self::run_single(self, global_args).await?;
211 } else {
212 if built_contracts.len() > 1 {
213 if self.alias.is_some() {
214 return Err(Error::AliasNotSupported);
215 }
216
217 if self.salt.is_some() {
218 return Err(Error::SaltNotSupported);
219 }
220
221 if !self.slop.is_empty() {
222 return Err(Error::ConstructorArgsNotSupported);
223 }
224 }
225
226 for contract in &built_contracts {
227 let mut cmd = self.clone();
228 cmd.wasm = Some(contract.path.clone());
229
230 if cmd.alias.is_none() && !contract.name.is_empty() {
233 if let Ok(alias) = contract.name.parse::<AliasName>() {
234 cmd.alias = Some(alias);
235 }
236 }
237
238 Self::run_single(&cmd, global_args).await?;
239 }
240 }
241 Ok(())
242 }
243
244 async fn run_single(cmd: &Cmd, global_args: &global::Args) -> Result<(), Error> {
245 if let Some(alias) = &cmd.alias {
250 crate::config::alias::validate_reserved_aliases(alias)?;
251 }
252
253 let res = cmd
254 .execute(&cmd.config, global_args.quiet, global_args.no_cache)
255 .await?
256 .to_envelope();
257
258 match res {
259 TxnEnvelopeResult::TxnEnvelope(tx) => {
260 println!("{}", tx.to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?);
261 }
262 TxnEnvelopeResult::Res(contract) => {
263 let network = cmd.config.get_network()?;
264
265 if let Some(alias) = cmd.alias.clone() {
266 if let Some(existing_contract) = cmd
267 .config
268 .locator
269 .get_contract_id(&alias, &network.network_passphrase)?
270 {
271 let print = Print::new(global_args.quiet);
272 print.warnln(format!(
273 "Overwriting existing alias '{alias}' that currently links to contract ID: {existing_contract}"
274 ));
275 }
276
277 cmd.config.locator.save_contract_id(
278 &network.network_passphrase,
279 &contract,
280 &alias,
281 )?;
282 }
283
284 println!("{contract}");
285 }
286 }
287 Ok(())
288 }
289
290 async fn resolve_contracts(
291 &self,
292 global_args: &global::Args,
293 ) -> Result<Vec<build::BuiltContract>, Error> {
294 if let Some(wasm) = &self.wasm {
296 return Ok(vec![build::BuiltContract {
297 name: String::new(),
298 path: wasm.clone(),
299 }]);
300 }
301
302 if self.wasm_hash.is_some() {
304 return Ok(vec![]);
305 }
306
307 let build_cmd = build::Cmd {
309 package: self.package.clone(),
310 build_args: self.build_args.clone(),
311 ..build::Cmd::default()
312 };
313 let contracts = build_cmd.run(global_args).await.map_err(|e| match e {
314 build::Error::Metadata(_) => Error::NotInCargoProject,
315 other => other.into(),
316 })?;
317
318 if contracts.is_empty() {
319 return Err(Error::NoBuildableContracts);
320 }
321
322 Ok(contracts)
323 }
324
325 #[allow(clippy::too_many_lines)]
326 #[allow(unused_variables)]
327 pub async fn execute(
328 &self,
329 config: &config::Args,
330 quiet: bool,
331 no_cache: bool,
332 ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
333 self.auth_mode.validate_not_enforce()?;
334
335 let print = Print::new(quiet);
336 let wasm_hash = if let Some(wasm) = &self.wasm {
337 let is_build = self.build_only;
338 let hash = if is_build {
339 wasm::Args { wasm: wasm.clone() }.hash()?
340 } else {
341 print.infoln("Uploading contract WASM…");
342 upload::Cmd {
343 wasm: Some(wasm.clone()),
344 config: config.clone(),
345 resources: self.resources.clone(),
346 auth_mode: self.auth_mode.clone(),
347 ignore_checks: self.ignore_checks,
348 build_only: is_build,
349 package: None,
350 build_args: build::BuildArgs::default(),
351 }
352 .execute(config, quiet, no_cache)
353 .await?
354 .into_result()
355 .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
356 };
357 hex::encode(hash)
358 } else {
359 self.wasm_hash
360 .as_ref()
361 .ok_or(Error::WasmNotProvided)?
362 .clone()
363 };
364
365 let wasm_hash = Hash(
366 utils::contract_id_from_str(&wasm_hash)
367 .map_err(|e| Error::CannotParseWasmHash {
368 wasm_hash: wasm_hash.clone(),
369 error: e,
370 })?
371 .0,
372 );
373
374 print.infoln(format!("Deploying contract using wasm hash {wasm_hash}").as_str());
375
376 let network = config.get_network()?;
377 let salt: [u8; 32] = match &self.salt {
378 Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
379 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
380 .try_into()
381 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
382 None => rand::thread_rng().gen::<[u8; 32]>(),
383 };
384
385 let client = network.rpc_client()?;
386 let MuxedAccount::Ed25519(bytes) = config.source_account()? else {
387 return Err(Error::OnlyEd25519AccountsAllowed);
388 };
389 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
390 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
391 address: ScAddress::Account(source_account.clone()),
392 salt: Uint256(salt),
393 });
394 let contract_id =
395 get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
396 let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
397 wasm::Args { wasm: wasm.clone() }.read()?
398 } else {
399 if self.build_only {
400 return Err(Error::WasmNotProvided);
401 }
402 get_remote_wasm_from_hash(&client, &wasm_hash).await?
403 };
404 let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
405 let res = soroban_spec_tools::Spec::new(entries.clone().as_slice());
406 let (constructor_params, constructor_signers) =
407 if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
408 if func.inputs.is_empty() {
409 (None, vec![])
410 } else {
411 let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
412 slop.extend_from_slice(&self.slop);
413 let (_, _, invoke_args, signers) = arg_parsing::build_constructor_parameters(
414 &stellar_strkey::Contract(contract_id.0),
415 &slop,
416 &entries,
417 config,
418 )?;
419 (Some(invoke_args), signers)
420 }
421 } else {
422 (None, vec![])
423 };
424
425 client
427 .verify_network_passphrase(Some(&network.network_passphrase))
428 .await?;
429
430 let account_details = client.get_account(&source_account.to_string()).await?;
432 let sequence: i64 = account_details.seq_num.into();
433 let txn = Box::new(build_create_contract_tx(
434 wasm_hash,
435 sequence + 1,
436 config.get_inclusion_fee()?,
437 source_account,
438 contract_id_preimage,
439 constructor_params.as_ref(),
440 )?);
441
442 if self.build_only {
443 print.checkln("Transaction built!");
444 return Ok(TxnResult::Txn(txn));
445 }
446
447 sim_sign_and_send_tx::<Error>(
448 &client,
449 &txn,
450 config,
451 &self.resources,
452 &constructor_signers,
453 self.auth_mode.to_rpc(),
454 quiet,
455 no_cache,
456 )
457 .await?;
458
459 if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
460 print.linkln(url);
461 }
462 print.checkln("Deployed!");
463
464 Ok(TxnResult::Res(contract_id))
465 }
466}
467
468fn reserved_package_alias(
472 explicit_alias: Option<&AliasName>,
473 built_contracts: &[build::BuiltContract],
474) -> Option<String> {
475 if explicit_alias.is_some() {
476 return None;
477 }
478
479 built_contracts.iter().find_map(|contract| {
480 (!contract.name.is_empty() && crate::config::alias::is_reserved(&contract.name))
481 .then(|| contract.name.clone())
482 })
483}
484
485fn build_create_contract_tx(
486 wasm_hash: Hash,
487 sequence: i64,
488 fee: u32,
489 key: AccountId,
490 contract_id_preimage: ContractIdPreimage,
491 constructor_params: Option<&InvokeContractArgs>,
492) -> Result<Transaction, Error> {
493 let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
494 Operation {
495 source_account: None,
496 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
497 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
498 contract_id_preimage,
499 executable: ContractExecutable::Wasm(wasm_hash),
500 constructor_args: args.clone(),
501 }),
502 auth: VecM::default(),
503 }),
504 }
505 } else {
506 Operation {
507 source_account: None,
508 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
509 host_function: HostFunction::CreateContract(CreateContractArgs {
510 contract_id_preimage,
511 executable: ContractExecutable::Wasm(wasm_hash),
512 }),
513 auth: VecM::default(),
514 }),
515 }
516 };
517 let tx = Transaction {
518 source_account: key.into(),
519 fee,
520 seq_num: SequenceNumber(sequence),
521 cond: Preconditions::None,
522 memo: Memo::None,
523 operations: vec![op].try_into()?,
524 ext: TransactionExt::V0,
525 };
526
527 Ok(tx)
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_build_create_contract() {
536 let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
537 .unwrap()
538 .try_into()
539 .unwrap();
540 let salt = [0u8; 32];
541 let key =
542 &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
543 .unwrap();
544 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
545 key.verifying_key().to_bytes(),
546 )));
547
548 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
549 address: ScAddress::Account(source_account.clone()),
550 salt: Uint256(salt),
551 });
552
553 let result = build_create_contract_tx(
554 Hash(hash),
555 300,
556 1,
557 source_account,
558 contract_id_preimage,
559 None,
560 );
561
562 assert!(result.is_ok());
563 }
564
565 fn built(name: &str) -> build::BuiltContract {
566 build::BuiltContract {
567 name: name.to_string(),
568 path: std::path::PathBuf::new(),
569 }
570 }
571
572 #[test]
573 fn reserved_package_alias_flags_reserved_package_before_deploy() {
574 let native = crate::config::alias::NATIVE;
575 let contracts = [built("adapter"), built(native), built("token")];
576
577 assert_eq!(
578 reserved_package_alias(None, &contracts),
579 Some(native.to_string())
580 );
581 }
582
583 #[test]
584 fn reserved_package_alias_ignores_regular_packages() {
585 let contracts = [built("adapter"), built("token")];
586
587 assert_eq!(reserved_package_alias(None, &contracts), None);
588 }
589
590 #[test]
591 fn reserved_package_alias_skipped_with_explicit_alias() {
592 let alias = "my-contract".parse::<AliasName>().unwrap();
595 let contracts = [built(crate::config::alias::NATIVE)];
596
597 assert_eq!(reserved_package_alias(Some(&alias), &contracts), None);
598 }
599}