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