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("--salt is not supported when deploying multiple contracts")]
171 SaltNotSupported,
172
173 #[error("constructor arguments are not supported when deploying multiple contracts")]
174 ConstructorArgsNotSupported,
175
176 #[error("--build-only is not supported without --wasm or --wasm-hash")]
177 BuildOnlyNotSupported,
178
179 #[error(
180 "--wasm or --wasm-hash is required when not in a Cargo workspace; no Cargo.toml found"
181 )]
182 NotInCargoProject,
183}
184
185impl Cmd {
186 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
187 self.auth_mode.validate_not_enforce()?;
188
189 if self.build_only && self.wasm.is_none() && self.wasm_hash.is_none() {
190 return Err(Error::BuildOnlyNotSupported);
191 }
192
193 let built_contracts = self.resolve_contracts(global_args)?;
194
195 if built_contracts.is_empty() {
198 Self::run_single(self, global_args).await?;
199 } else {
200 if built_contracts.len() > 1 {
201 if self.alias.is_some() {
202 return Err(Error::AliasNotSupported);
203 }
204
205 if self.salt.is_some() {
206 return Err(Error::SaltNotSupported);
207 }
208
209 if !self.slop.is_empty() {
210 return Err(Error::ConstructorArgsNotSupported);
211 }
212 }
213
214 for contract in &built_contracts {
215 let mut cmd = self.clone();
216 cmd.wasm = Some(contract.path.clone());
217
218 if cmd.alias.is_none() && !contract.name.is_empty() {
221 if let Ok(alias) = contract.name.parse::<AliasName>() {
222 cmd.alias = Some(alias);
223 }
224 }
225
226 Self::run_single(&cmd, global_args).await?;
227 }
228 }
229 Ok(())
230 }
231
232 async fn run_single(cmd: &Cmd, global_args: &global::Args) -> Result<(), Error> {
233 let res = cmd
234 .execute(&cmd.config, global_args.quiet, global_args.no_cache)
235 .await?
236 .to_envelope();
237
238 match res {
239 TxnEnvelopeResult::TxnEnvelope(tx) => {
240 println!("{}", tx.to_xdr_base64(Limits::none())?);
241 }
242 TxnEnvelopeResult::Res(contract) => {
243 let network = cmd.config.get_network()?;
244
245 if let Some(alias) = cmd.alias.clone() {
246 if let Some(existing_contract) = cmd
247 .config
248 .locator
249 .get_contract_id(&alias, &network.network_passphrase)?
250 {
251 let print = Print::new(global_args.quiet);
252 print.warnln(format!(
253 "Overwriting existing alias '{alias}' that currently links to contract ID: {existing_contract}"
254 ));
255 }
256
257 cmd.config.locator.save_contract_id(
258 &network.network_passphrase,
259 &contract,
260 &alias,
261 )?;
262 }
263
264 println!("{contract}");
265 }
266 }
267 Ok(())
268 }
269
270 fn resolve_contracts(
271 &self,
272 global_args: &global::Args,
273 ) -> Result<Vec<build::BuiltContract>, Error> {
274 if let Some(wasm) = &self.wasm {
276 return Ok(vec![build::BuiltContract {
277 name: String::new(),
278 path: wasm.clone(),
279 }]);
280 }
281
282 if self.wasm_hash.is_some() {
284 return Ok(vec![]);
285 }
286
287 let build_cmd = build::Cmd {
289 package: self.package.clone(),
290 build_args: self.build_args.clone(),
291 ..build::Cmd::default()
292 };
293 let contracts = build_cmd.run(global_args).map_err(|e| match e {
294 build::Error::Metadata(_) => Error::NotInCargoProject,
295 other => other.into(),
296 })?;
297
298 if contracts.is_empty() {
299 return Err(Error::NoBuildableContracts);
300 }
301
302 Ok(contracts)
303 }
304
305 #[allow(clippy::too_many_lines)]
306 #[allow(unused_variables)]
307 pub async fn execute(
308 &self,
309 config: &config::Args,
310 quiet: bool,
311 no_cache: bool,
312 ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
313 self.auth_mode.validate_not_enforce()?;
314
315 let print = Print::new(quiet);
316 let wasm_hash = if let Some(wasm) = &self.wasm {
317 let is_build = self.build_only;
318 let hash = if is_build {
319 wasm::Args { wasm: wasm.clone() }.hash()?
320 } else {
321 print.infoln("Uploading contract WASM…");
322 upload::Cmd {
323 wasm: Some(wasm.clone()),
324 config: config.clone(),
325 resources: self.resources.clone(),
326 auth_mode: self.auth_mode.clone(),
327 ignore_checks: self.ignore_checks,
328 build_only: is_build,
329 package: None,
330 build_args: build::BuildArgs::default(),
331 }
332 .execute(config, quiet, no_cache)
333 .await?
334 .into_result()
335 .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
336 };
337 hex::encode(hash)
338 } else {
339 self.wasm_hash
340 .as_ref()
341 .ok_or(Error::WasmNotProvided)?
342 .clone()
343 };
344
345 let wasm_hash = Hash(
346 utils::contract_id_from_str(&wasm_hash)
347 .map_err(|e| Error::CannotParseWasmHash {
348 wasm_hash: wasm_hash.clone(),
349 error: e,
350 })?
351 .0,
352 );
353
354 print.infoln(format!("Deploying contract using wasm hash {wasm_hash}").as_str());
355
356 let network = config.get_network()?;
357 let salt: [u8; 32] = match &self.salt {
358 Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
359 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
360 .try_into()
361 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
362 None => rand::thread_rng().gen::<[u8; 32]>(),
363 };
364
365 let client = network.rpc_client()?;
366 let MuxedAccount::Ed25519(bytes) = config.source_account()? else {
367 return Err(Error::OnlyEd25519AccountsAllowed);
368 };
369 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
370 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
371 address: ScAddress::Account(source_account.clone()),
372 salt: Uint256(salt),
373 });
374 let contract_id =
375 get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
376 let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
377 wasm::Args { wasm: wasm.clone() }.read()?
378 } else {
379 if self.build_only {
380 return Err(Error::WasmNotProvided);
381 }
382 get_remote_wasm_from_hash(&client, &wasm_hash).await?
383 };
384 let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
385 let res = soroban_spec_tools::Spec::new(entries.clone().as_slice());
386 let (constructor_params, constructor_signers) =
387 if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
388 if func.inputs.is_empty() {
389 (None, vec![])
390 } else {
391 let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
392 slop.extend_from_slice(&self.slop);
393 let (_, _, invoke_args, signers) = arg_parsing::build_constructor_parameters(
394 &stellar_strkey::Contract(contract_id.0),
395 &slop,
396 &entries,
397 config,
398 )?;
399 (Some(invoke_args), signers)
400 }
401 } else {
402 (None, vec![])
403 };
404
405 client
407 .verify_network_passphrase(Some(&network.network_passphrase))
408 .await?;
409
410 let account_details = client.get_account(&source_account.to_string()).await?;
412 let sequence: i64 = account_details.seq_num.into();
413 let txn = Box::new(build_create_contract_tx(
414 wasm_hash,
415 sequence + 1,
416 config.get_inclusion_fee()?,
417 source_account,
418 contract_id_preimage,
419 constructor_params.as_ref(),
420 )?);
421
422 if self.build_only {
423 print.checkln("Transaction built!");
424 return Ok(TxnResult::Txn(txn));
425 }
426
427 sim_sign_and_send_tx::<Error>(
428 &client,
429 &txn,
430 config,
431 &self.resources,
432 &constructor_signers,
433 self.auth_mode.to_rpc(),
434 quiet,
435 no_cache,
436 )
437 .await?;
438
439 if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
440 print.linkln(url);
441 }
442 print.checkln("Deployed!");
443
444 Ok(TxnResult::Res(contract_id))
445 }
446}
447
448fn build_create_contract_tx(
449 wasm_hash: Hash,
450 sequence: i64,
451 fee: u32,
452 key: AccountId,
453 contract_id_preimage: ContractIdPreimage,
454 constructor_params: Option<&InvokeContractArgs>,
455) -> Result<Transaction, Error> {
456 let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
457 Operation {
458 source_account: None,
459 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
460 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
461 contract_id_preimage,
462 executable: ContractExecutable::Wasm(wasm_hash),
463 constructor_args: args.clone(),
464 }),
465 auth: VecM::default(),
466 }),
467 }
468 } else {
469 Operation {
470 source_account: None,
471 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
472 host_function: HostFunction::CreateContract(CreateContractArgs {
473 contract_id_preimage,
474 executable: ContractExecutable::Wasm(wasm_hash),
475 }),
476 auth: VecM::default(),
477 }),
478 }
479 };
480 let tx = Transaction {
481 source_account: key.into(),
482 fee,
483 seq_num: SequenceNumber(sequence),
484 cond: Preconditions::None,
485 memo: Memo::None,
486 operations: vec![op].try_into()?,
487 ext: TransactionExt::V0,
488 };
489
490 Ok(tx)
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_build_create_contract() {
499 let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
500 .unwrap()
501 .try_into()
502 .unwrap();
503 let salt = [0u8; 32];
504 let key =
505 &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
506 .unwrap();
507 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
508 key.verifying_key().to_bytes(),
509 )));
510
511 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
512 address: ScAddress::Account(source_account.clone()),
513 salt: Uint256(salt),
514 });
515
516 let result = build_create_contract_tx(
517 Hash(hash),
518 300,
519 1,
520 source_account,
521 contract_id_preimage,
522 None,
523 );
524
525 assert!(result.is_ok());
526 }
527}