1use crate::commands::contract::deploy::utils::alias_validator;
2use std::array::TryFromSliceError;
3use std::ffi::OsString;
4use std::fmt::Debug;
5use std::num::ParseIntError;
6
7use crate::xdr::{
8 AccountId, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress,
9 CreateContractArgs, CreateContractArgsV2, Error as XdrError, Hash, HostFunction,
10 InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo, MuxedAccount, Operation, OperationBody,
11 Preconditions, PublicKey, ScAddress, SequenceNumber, Transaction, TransactionExt, Uint256,
12 VecM, WriteXdr,
13};
14use clap::{arg, command, Parser};
15use rand::Rng;
16
17use soroban_spec_tools::contract as contract_spec;
18
19use crate::{
20 assembled::simulate_and_assemble_transaction,
21 commands::{
22 contract::{self, arg_parsing, id::wasm::get_contract_id, upload},
23 global,
24 txn_result::{TxnEnvelopeResult, TxnResult},
25 NetworkRunnable, HEADING_RPC,
26 },
27 config::{self, data, locator, network},
28 print::Print,
29 rpc,
30 utils::{self, rpc::get_remote_wasm_from_hash},
31 wasm,
32};
33
34pub const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
35
36#[derive(Parser, Debug, Clone)]
37#[command(group(
38 clap::ArgGroup::new("wasm_src")
39 .required(true)
40 .args(&["wasm", "wasm_hash"]),
41))]
42#[group(skip)]
43pub struct Cmd {
44 #[arg(long, group = "wasm_src")]
46 pub wasm: Option<std::path::PathBuf>,
47 #[arg(long = "wasm-hash", conflicts_with = "wasm", group = "wasm_src")]
49 pub wasm_hash: Option<String>,
50 #[arg(
52 long,
53 help_heading = HEADING_RPC,
54 )]
55 pub salt: Option<String>,
56 #[command(flatten)]
57 pub config: config::Args,
58 #[command(flatten)]
59 pub fee: crate::fee::Args,
60 #[arg(long, short = 'i', default_value = "false")]
61 pub ignore_checks: bool,
63 #[arg(long, value_parser = clap::builder::ValueParser::new(alias_validator))]
67 pub alias: Option<String>,
68 #[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")]
70 pub slop: Vec<OsString>,
71}
72
73#[derive(thiserror::Error, Debug)]
74pub enum Error {
75 #[error(transparent)]
76 Install(#[from] upload::Error),
77 #[error("error parsing int: {0}")]
78 ParseIntError(#[from] ParseIntError),
79 #[error("internal conversion error: {0}")]
80 TryFromSliceError(#[from] TryFromSliceError),
81 #[error("xdr processing error: {0}")]
82 Xdr(#[from] XdrError),
83 #[error("jsonrpc error: {0}")]
84 JsonRpc(#[from] jsonrpsee_core::Error),
85 #[error("cannot parse salt: {salt}")]
86 CannotParseSalt { salt: String },
87 #[error("cannot parse contract ID {contract_id}: {error}")]
88 CannotParseContractId {
89 contract_id: String,
90 error: stellar_strkey::DecodeError,
91 },
92 #[error("cannot parse WASM hash {wasm_hash}: {error}")]
93 CannotParseWasmHash {
94 wasm_hash: String,
95 error: stellar_strkey::DecodeError,
96 },
97 #[error("Must provide either --wasm or --wash-hash")]
98 WasmNotProvided,
99 #[error(transparent)]
100 Rpc(#[from] rpc::Error),
101 #[error(transparent)]
102 Config(#[from] config::Error),
103 #[error(transparent)]
104 StrKey(#[from] stellar_strkey::DecodeError),
105 #[error(transparent)]
106 Infallible(#[from] std::convert::Infallible),
107 #[error(transparent)]
108 WasmId(#[from] contract::id::wasm::Error),
109 #[error(transparent)]
110 Data(#[from] data::Error),
111 #[error(transparent)]
112 Network(#[from] network::Error),
113 #[error(transparent)]
114 Wasm(#[from] wasm::Error),
115 #[error(
116 "alias must be 1-30 chars long, and have only letters, numbers, underscores and dashes"
117 )]
118 InvalidAliasFormat { alias: String },
119 #[error(transparent)]
120 Locator(#[from] locator::Error),
121 #[error(transparent)]
122 ContractSpec(#[from] contract_spec::Error),
123 #[error(transparent)]
124 ArgParse(#[from] arg_parsing::Error),
125 #[error("Only ed25519 accounts are allowed")]
126 OnlyEd25519AccountsAllowed,
127}
128
129impl Cmd {
130 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
131 let res = self
132 .run_against_rpc_server(Some(global_args), None)
133 .await?
134 .to_envelope();
135 match res {
136 TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
137 TxnEnvelopeResult::Res(contract) => {
138 let network = self.config.get_network()?;
139
140 if let Some(alias) = self.alias.clone() {
141 if let Some(existing_contract) = self
142 .config
143 .locator
144 .get_contract_id(&alias, &network.network_passphrase)?
145 {
146 let print = Print::new(global_args.quiet);
147 print.warnln(format!(
148 "Overwriting existing contract id: {existing_contract}"
149 ));
150 }
151
152 self.config.locator.save_contract_id(
153 &network.network_passphrase,
154 &contract,
155 &alias,
156 )?;
157 }
158
159 println!("{contract}");
160 }
161 }
162 Ok(())
163 }
164}
165
166#[async_trait::async_trait]
167impl NetworkRunnable for Cmd {
168 type Error = Error;
169 type Result = TxnResult<stellar_strkey::Contract>;
170
171 #[allow(clippy::too_many_lines)]
172 #[allow(unused_variables)]
173 async fn run_against_rpc_server(
174 &self,
175 global_args: Option<&global::Args>,
176 config: Option<&config::Args>,
177 ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
178 let print = Print::new(global_args.is_some_and(|a| a.quiet));
179 let config = config.unwrap_or(&self.config);
180 let wasm_hash = if let Some(wasm) = &self.wasm {
181 #[cfg(feature = "version_lt_23")]
182 let is_build = self.fee.build_only || self.fee.sim_only;
183 #[cfg(feature = "version_gte_23")]
184 let is_build = self.fee.build_only;
185 let hash = if is_build {
186 wasm::Args { wasm: wasm.clone() }.hash()?
187 } else {
188 upload::Cmd {
189 wasm: wasm::Args { wasm: wasm.clone() },
190 config: config.clone(),
191 fee: self.fee.clone(),
192 ignore_checks: self.ignore_checks,
193 }
194 .run_against_rpc_server(global_args, Some(config))
195 .await?
196 .into_result()
197 .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
198 };
199 hex::encode(hash)
200 } else {
201 self.wasm_hash
202 .as_ref()
203 .ok_or(Error::WasmNotProvided)?
204 .to_string()
205 };
206
207 let wasm_hash = Hash(
208 utils::contract_id_from_str(&wasm_hash)
209 .map_err(|e| Error::CannotParseWasmHash {
210 wasm_hash: wasm_hash.clone(),
211 error: e,
212 })?
213 .0,
214 );
215
216 print.infoln(format!("Using wasm hash {wasm_hash}").as_str());
217
218 let network = config.get_network()?;
219 let salt: [u8; 32] = match &self.salt {
220 Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
221 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
222 .try_into()
223 .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
224 None => rand::thread_rng().gen::<[u8; 32]>(),
225 };
226
227 let client = network.rpc_client()?;
228 client
229 .verify_network_passphrase(Some(&network.network_passphrase))
230 .await?;
231 let MuxedAccount::Ed25519(bytes) = config.source_account().await? else {
232 return Err(Error::OnlyEd25519AccountsAllowed);
233 };
234 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
235 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
236 address: ScAddress::Account(source_account.clone()),
237 salt: Uint256(salt),
238 });
239 let contract_id =
240 get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
241 let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
242 wasm::Args { wasm: wasm.clone() }.read()?
243 } else {
244 get_remote_wasm_from_hash(&client, &wasm_hash).await?
245 };
246 let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
247 let res = soroban_spec_tools::Spec::new(entries.clone());
248 let constructor_params = if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
249 if func.inputs.is_empty() {
250 None
251 } else {
252 let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
253 slop.extend_from_slice(&self.slop);
254 Some(
255 arg_parsing::build_host_function_parameters(
256 &stellar_strkey::Contract(contract_id.0),
257 &slop,
258 &entries,
259 config,
260 )?
261 .2,
262 )
263 }
264 } else {
265 None
266 };
267
268 let account_details = client.get_account(&source_account.to_string()).await?;
270 let sequence: i64 = account_details.seq_num.into();
271 let txn = Box::new(build_create_contract_tx(
272 wasm_hash,
273 sequence + 1,
274 self.fee.fee,
275 source_account,
276 contract_id_preimage,
277 constructor_params.as_ref(),
278 )?);
279
280 if self.fee.build_only {
281 print.checkln("Transaction built!");
282 return Ok(TxnResult::Txn(txn));
283 }
284
285 print.infoln("Simulating deploy transaction…");
286
287 let txn = simulate_and_assemble_transaction(&client, &txn).await?;
288 let txn = Box::new(self.fee.apply_to_assembled_txn(txn).transaction().clone());
289
290 #[cfg(feature = "version_lt_23")]
291 if self.fee.sim_only {
292 print.checkln("Done!");
293 return Ok(TxnResult::Txn(txn));
294 }
295
296 print.log_transaction(&txn, &network, true)?;
297 let signed_txn = &config.sign_with_local_key(*txn).await?;
298 print.globeln("Submitting deploy transaction…");
299
300 let get_txn_resp = client
301 .send_transaction_polling(signed_txn)
302 .await?
303 .try_into()?;
304
305 if global_args.is_none_or(|a| !a.no_cache) {
306 data::write(get_txn_resp, &network.rpc_uri()?)?;
307 }
308
309 if let Some(url) = utils::explorer_url_for_contract(&network, &contract_id) {
310 print.linkln(url);
311 }
312
313 print.checkln("Deployed!");
314
315 Ok(TxnResult::Res(contract_id))
316 }
317}
318
319fn build_create_contract_tx(
320 wasm_hash: Hash,
321 sequence: i64,
322 fee: u32,
323 key: AccountId,
324 contract_id_preimage: ContractIdPreimage,
325 constructor_params: Option<&InvokeContractArgs>,
326) -> Result<Transaction, Error> {
327 let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
328 Operation {
329 source_account: None,
330 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
331 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
332 contract_id_preimage,
333 executable: ContractExecutable::Wasm(wasm_hash),
334 constructor_args: args.clone(),
335 }),
336 auth: VecM::default(),
337 }),
338 }
339 } else {
340 Operation {
341 source_account: None,
342 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
343 host_function: HostFunction::CreateContract(CreateContractArgs {
344 contract_id_preimage,
345 executable: ContractExecutable::Wasm(wasm_hash),
346 }),
347 auth: VecM::default(),
348 }),
349 }
350 };
351 let tx = Transaction {
352 source_account: key.into(),
353 fee,
354 seq_num: SequenceNumber(sequence),
355 cond: Preconditions::None,
356 memo: Memo::None,
357 operations: vec![op].try_into()?,
358 ext: TransactionExt::V0,
359 };
360
361 Ok(tx)
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_build_create_contract() {
370 let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
371 .unwrap()
372 .try_into()
373 .unwrap();
374 let salt = [0u8; 32];
375 let key =
376 &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
377 .unwrap();
378 let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
379 key.verifying_key().to_bytes(),
380 )));
381
382 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
383 address: ScAddress::Account(source_account.clone()),
384 salt: Uint256(salt),
385 });
386
387 let result = build_create_contract_tx(
388 Hash(hash),
389 300,
390 1,
391 source_account,
392 contract_id_preimage,
393 None,
394 );
395
396 assert!(result.is_ok());
397 }
398}