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