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