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 #[arg(long, help_heading = HEADING_TRANSACTION)]
71 pub build_only: bool,
72 #[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")]
74 pub slop: Vec<OsString>,
75 #[arg(long, help_heading = "Build Options", conflicts_with = "wasm_src")]
77 pub package: Option<String>,
78 #[command(flatten)]
79 pub build_args: build::BuildArgs,
80}
81
82#[derive(thiserror::Error, Debug)]
83pub enum Error {
84 #[error(transparent)]
85 Install(#[from] upload::Error),
86
87 #[error("error parsing int: {0}")]
88 ParseIntError(#[from] ParseIntError),
89
90 #[error("internal conversion error: {0}")]
91 TryFromSliceError(#[from] TryFromSliceError),
92
93 #[error("xdr processing error: {0}")]
94 Xdr(#[from] XdrError),
95
96 #[error("cannot parse salt: {salt}")]
97 CannotParseSalt { salt: String },
98
99 #[error("cannot parse contract ID {contract_id}: {error}")]
100 CannotParseContractId {
101 contract_id: String,
102 error: stellar_strkey::DecodeError,
103 },
104
105 #[error("cannot parse WASM hash {wasm_hash}: {error}")]
106 CannotParseWasmHash {
107 wasm_hash: String,
108 error: stellar_strkey::DecodeError,
109 },
110
111 #[error("Must provide either --wasm or --wasm-hash")]
112 WasmNotProvided,
113
114 #[error(transparent)]
115 Rpc(#[from] rpc::Error),
116
117 #[error(transparent)]
118 Config(#[from] config::Error),
119
120 #[error(transparent)]
121 StrKey(#[from] stellar_strkey::DecodeError),
122
123 #[error(transparent)]
124 Infallible(#[from] std::convert::Infallible),
125
126 #[error(transparent)]
127 WasmId(#[from] contract::id::wasm::Error),
128
129 #[error(transparent)]
130 Data(#[from] data::Error),
131
132 #[error(transparent)]
133 Network(#[from] network::Error),
134
135 #[error(transparent)]
136 Wasm(#[from] wasm::Error),
137
138 #[error(transparent)]
139 Locator(#[from] locator::Error),
140
141 #[error(transparent)]
142 ContractSpec(#[from] contract_spec::Error),
143
144 #[error(transparent)]
145 ArgParse(#[from] arg_parsing::Error),
146
147 #[error("Only ed25519 accounts are allowed")]
148 OnlyEd25519AccountsAllowed,
149
150 #[error(transparent)]
151 Fee(#[from] fetch::fee::Error),
152
153 #[error(transparent)]
154 Fetch(#[from] fetch::Error),
155
156 #[error(transparent)]
157 Build(#[from] build::Error),
158
159 #[error("no buildable contracts found in workspace (no packages with crate-type cdylib)")]
160 NoBuildableContracts,
161
162 #[error("--alias is not supported when deploying multiple contracts; aliases are derived from package names automatically")]
163 AliasNotSupported,
164
165 #[error("--salt is not supported when deploying multiple contracts")]
166 SaltNotSupported,
167
168 #[error("constructor arguments are not supported when deploying multiple contracts")]
169 ConstructorArgsNotSupported,
170
171 #[error("--build-only is not supported without --wasm or --wasm-hash")]
172 BuildOnlyNotSupported,
173
174 #[error(
175 "--wasm or --wasm-hash is required when not in a Cargo workspace; no Cargo.toml found"
176 )]
177 NotInCargoProject,
178}
179
180impl Cmd {
181 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
182 if self.build_only && self.wasm.is_none() && self.wasm_hash.is_none() {
183 return Err(Error::BuildOnlyNotSupported);
184 }
185
186 let built_contracts = self.resolve_contracts(global_args)?;
187
188 if built_contracts.is_empty() {
191 Self::run_single(self, global_args).await?;
192 } else {
193 if built_contracts.len() > 1 {
194 if self.alias.is_some() {
195 return Err(Error::AliasNotSupported);
196 }
197
198 if self.salt.is_some() {
199 return Err(Error::SaltNotSupported);
200 }
201
202 if !self.slop.is_empty() {
203 return Err(Error::ConstructorArgsNotSupported);
204 }
205 }
206
207 for contract in &built_contracts {
208 let mut cmd = self.clone();
209 cmd.wasm = Some(contract.path.clone());
210
211 if cmd.alias.is_none() && !contract.name.is_empty() {
214 if let Ok(alias) = contract.name.parse::<AliasName>() {
215 cmd.alias = Some(alias);
216 }
217 }
218
219 Self::run_single(&cmd, global_args).await?;
220 }
221 }
222 Ok(())
223 }
224
225 async fn run_single(cmd: &Cmd, global_args: &global::Args) -> Result<(), Error> {
226 let res = cmd
227 .execute(&cmd.config, global_args.quiet, global_args.no_cache)
228 .await?
229 .to_envelope();
230
231 match res {
232 TxnEnvelopeResult::TxnEnvelope(tx) => {
233 println!("{}", tx.to_xdr_base64(Limits::none())?);
234 }
235 TxnEnvelopeResult::Res(contract) => {
236 let network = cmd.config.get_network()?;
237
238 if let Some(alias) = cmd.alias.clone() {
239 if let Some(existing_contract) = cmd
240 .config
241 .locator
242 .get_contract_id(&alias, &network.network_passphrase)?
243 {
244 let print = Print::new(global_args.quiet);
245 print.warnln(format!(
246 "Overwriting existing alias '{alias}' that currently links to contract ID: {existing_contract}"
247 ));
248 }
249
250 cmd.config.locator.save_contract_id(
251 &network.network_passphrase,
252 &contract,
253 &alias,
254 )?;
255 }
256
257 println!("{contract}");
258 }
259 }
260 Ok(())
261 }
262
263 fn resolve_contracts(
264 &self,
265 global_args: &global::Args,
266 ) -> Result<Vec<build::BuiltContract>, Error> {
267 if let Some(wasm) = &self.wasm {
269 return Ok(vec![build::BuiltContract {
270 name: String::new(),
271 path: wasm.clone(),
272 }]);
273 }
274
275 if self.wasm_hash.is_some() {
277 return Ok(vec![]);
278 }
279
280 let build_cmd = build::Cmd {
282 package: self.package.clone(),
283 build_args: self.build_args.clone(),
284 ..build::Cmd::default()
285 };
286 let contracts = build_cmd.run(global_args).map_err(|e| match e {
287 build::Error::Metadata(_) => Error::NotInCargoProject,
288 other => other.into(),
289 })?;
290
291 if contracts.is_empty() {
292 return Err(Error::NoBuildableContracts);
293 }
294
295 Ok(contracts)
296 }
297
298 #[allow(clippy::too_many_lines)]
299 #[allow(unused_variables)]
300 pub async fn execute(
301 &self,
302 config: &config::Args,
303 quiet: bool,
304 no_cache: bool,
305 ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
306 let print = Print::new(quiet);
307 let wasm_hash = if let Some(wasm) = &self.wasm {
308 let is_build = self.build_only;
309 let hash = if is_build {
310 wasm::Args { wasm: wasm.clone() }.hash()?
311 } else {
312 print.infoln("Uploading contract WASM…");
313 upload::Cmd {
314 wasm: Some(wasm.clone()),
315 config: config.clone(),
316 resources: self.resources.clone(),
317 ignore_checks: self.ignore_checks,
318 build_only: is_build,
319 package: None,
320 build_args: build::BuildArgs::default(),
321 }
322 .execute(config, quiet, no_cache)
323 .await?
324 .into_result()
325 .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
326 };
327 hex::encode(hash)
328 } else {
329 self.wasm_hash
330 .as_ref()
331 .ok_or(Error::WasmNotProvided)?
332 .clone()
333 };
334
335 let wasm_hash = Hash(
336 utils::contract_id_from_str(&wasm_hash)
337 .map_err(|e| Error::CannotParseWasmHash {
338 wasm_hash: wasm_hash.clone(),
339 error: e,
340 })?
341 .0,
342 );
343
344 print.infoln(format!("Deploying contract using wasm hash {wasm_hash}").as_str());
345
346 let network = config.get_network()?;
347 let salt: [u8; 32] = match &self.salt {
348 Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
349 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
350 .try_into()
351 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
352 None => rand::thread_rng().gen::<[u8; 32]>(),
353 };
354
355 let client = network.rpc_client()?;
356 let MuxedAccount::Ed25519(bytes) = config.source_account()? else {
357 return Err(Error::OnlyEd25519AccountsAllowed);
358 };
359 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
360 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
361 address: ScAddress::Account(source_account.clone()),
362 salt: Uint256(salt),
363 });
364 let contract_id =
365 get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
366 let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
367 wasm::Args { wasm: wasm.clone() }.read()?
368 } else {
369 if self.build_only {
370 return Err(Error::WasmNotProvided);
371 }
372 get_remote_wasm_from_hash(&client, &wasm_hash).await?
373 };
374 let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
375 let res = soroban_spec_tools::Spec::new(entries.clone().as_slice());
376 let constructor_params = if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
377 if func.inputs.is_empty() {
378 None
379 } else {
380 let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
381 slop.extend_from_slice(&self.slop);
382 Some(
383 arg_parsing::build_constructor_parameters(
384 &stellar_strkey::Contract(contract_id.0),
385 &slop,
386 &entries,
387 config,
388 )?
389 .2,
390 )
391 }
392 } else {
393 None
394 };
395
396 client
398 .verify_network_passphrase(Some(&network.network_passphrase))
399 .await?;
400
401 let account_details = client.get_account(&source_account.to_string()).await?;
403 let sequence: i64 = account_details.seq_num.into();
404 let txn = Box::new(build_create_contract_tx(
405 wasm_hash,
406 sequence + 1,
407 config.get_inclusion_fee()?,
408 source_account,
409 contract_id_preimage,
410 constructor_params.as_ref(),
411 )?);
412
413 if self.build_only {
414 print.checkln("Transaction built!");
415 return Ok(TxnResult::Txn(txn));
416 }
417
418 sim_sign_and_send_tx::<Error>(&client, &txn, config, &self.resources, &[], quiet, no_cache)
419 .await?;
420
421 if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
422 print.linkln(url);
423 }
424 print.checkln("Deployed!");
425
426 Ok(TxnResult::Res(contract_id))
427 }
428}
429
430fn build_create_contract_tx(
431 wasm_hash: Hash,
432 sequence: i64,
433 fee: u32,
434 key: AccountId,
435 contract_id_preimage: ContractIdPreimage,
436 constructor_params: Option<&InvokeContractArgs>,
437) -> Result<Transaction, Error> {
438 let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
439 Operation {
440 source_account: None,
441 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
442 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
443 contract_id_preimage,
444 executable: ContractExecutable::Wasm(wasm_hash),
445 constructor_args: args.clone(),
446 }),
447 auth: VecM::default(),
448 }),
449 }
450 } else {
451 Operation {
452 source_account: None,
453 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
454 host_function: HostFunction::CreateContract(CreateContractArgs {
455 contract_id_preimage,
456 executable: ContractExecutable::Wasm(wasm_hash),
457 }),
458 auth: VecM::default(),
459 }),
460 }
461 };
462 let tx = Transaction {
463 source_account: key.into(),
464 fee,
465 seq_num: SequenceNumber(sequence),
466 cond: Preconditions::None,
467 memo: Memo::None,
468 operations: vec![op].try_into()?,
469 ext: TransactionExt::V0,
470 };
471
472 Ok(tx)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_build_create_contract() {
481 let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
482 .unwrap()
483 .try_into()
484 .unwrap();
485 let salt = [0u8; 32];
486 let key =
487 &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
488 .unwrap();
489 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
490 key.verifying_key().to_bytes(),
491 )));
492
493 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
494 address: ScAddress::Account(source_account.clone()),
495 salt: Uint256(salt),
496 });
497
498 let result = build_create_contract_tx(
499 Hash(hash),
500 300,
501 1,
502 source_account,
503 contract_id_preimage,
504 None,
505 );
506
507 assert!(result.is_ok());
508 }
509}