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