1use std::array::TryFromSliceError;
2use std::fmt::Debug;
3use std::num::ParseIntError;
4use std::path::{Path, PathBuf};
5
6use crate::xdr::{
7 self, ContractCodeEntryExt, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp,
8 LedgerEntryData, Limits, OperationBody, ReadXdr, ScMetaEntry, ScMetaV0, Transaction,
9 TransactionResult, TransactionResultResult, VecM, WriteXdr,
10};
11use clap::Parser;
12
13use super::{build, restore};
14use crate::commands::tx::fetch;
15use crate::{
16 commands::{
17 global,
18 txn_result::{TxnEnvelopeResult, TxnResult},
19 HEADING_TRANSACTION,
20 },
21 config::{self, data, network},
22 key,
23 print::Print,
24 rpc,
25 tx::{
26 builder::{self, TxExt},
27 sim_sign_and_send_tx,
28 },
29 utils, wasm,
30};
31
32const CONTRACT_META_SDK_KEY: &str = "rssdkver";
33const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";
34
35#[derive(Parser, Debug, Clone)]
36#[group(skip)]
37pub struct Cmd {
38 #[command(flatten)]
39 pub config: config::Args,
40
41 #[command(flatten)]
42 pub resources: crate::resources::Args,
43
44 #[command(flatten)]
45 pub auth_mode: crate::auth_mode::Args,
46
47 #[arg(long)]
50 pub wasm: Option<PathBuf>,
51
52 #[arg(long, short = 'i', default_value = "false")]
53 pub ignore_checks: bool,
55
56 #[arg(long, help_heading = HEADING_TRANSACTION)]
58 pub build_only: bool,
59
60 #[arg(long, help_heading = "Build Options", conflicts_with = "wasm")]
62 pub package: Option<String>,
63 #[command(flatten)]
64 pub build_args: build::BuildArgs,
65}
66
67#[derive(thiserror::Error, Debug)]
68pub enum Error {
69 #[error("error parsing int: {0}")]
70 ParseIntError(#[from] ParseIntError),
71
72 #[error("internal conversion error: {0}")]
73 TryFromSliceError(#[from] TryFromSliceError),
74
75 #[error("xdr processing error: {0}")]
76 Xdr(#[from] XdrError),
77
78 #[error(transparent)]
79 Rpc(#[from] rpc::Error),
80
81 #[error(transparent)]
82 Config(#[from] config::Error),
83
84 #[error(transparent)]
85 Wasm(#[from] wasm::Error),
86
87 #[error("unexpected ({length}) simulate transaction result length")]
88 UnexpectedSimulateTransactionResultSize { length: usize },
89
90 #[error(transparent)]
91 Restore(#[from] restore::Error),
92
93 #[error("cannot parse WASM file {wasm}: {error}")]
94 CannotParseWasm {
95 wasm: std::path::PathBuf,
96 error: wasm::Error,
97 },
98
99 #[error("the deployed smart contract {wasm} was built with Soroban Rust SDK v{version}, a release candidate version not intended for use with the Stellar Public Network. To deploy anyway, use --ignore-checks")]
100 ContractCompiledWithReleaseCandidateSdk {
101 wasm: std::path::PathBuf,
102 version: String,
103 },
104
105 #[error(transparent)]
106 Network(#[from] network::Error),
107
108 #[error(transparent)]
109 Data(#[from] data::Error),
110
111 #[error(transparent)]
112 Builder(#[from] builder::Error),
113
114 #[error(transparent)]
115 Fee(#[from] fetch::fee::Error),
116
117 #[error(transparent)]
118 Fetch(#[from] fetch::Error),
119
120 #[error(transparent)]
121 Build(#[from] build::Error),
122
123 #[error(transparent)]
124 AuthMode(#[from] crate::auth_mode::Error),
125
126 #[error("no buildable contracts found in workspace (no packages with crate-type cdylib)")]
127 NoBuildableContracts,
128
129 #[error("no WASM file specified; use --wasm to provide a contract file")]
130 WasmNotProvided,
131
132 #[error("--build-only is not supported without --wasm")]
133 BuildOnlyNotSupported,
134
135 #[error("--wasm is required when not in a Cargo workspace; no Cargo.toml found")]
136 NotInCargoProject,
137}
138
139impl Cmd {
140 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
141 self.auth_mode.validate_not_enforce()?;
142
143 if self.build_only && self.wasm.is_none() {
144 return Err(Error::BuildOnlyNotSupported);
145 }
146
147 let wasm_paths = self.resolve_wasm_paths(global_args)?;
148
149 for wasm_path in &wasm_paths {
150 let res = self
151 .upload_wasm(
152 wasm_path,
153 &self.config,
154 global_args.quiet,
155 global_args.no_cache,
156 )
157 .await?
158 .to_envelope();
159
160 match res {
161 TxnEnvelopeResult::TxnEnvelope(tx) => {
162 println!("{}", tx.to_xdr_base64(Limits::none())?);
163 }
164 TxnEnvelopeResult::Res(hash) => println!("{}", hex::encode(hash)),
165 }
166 }
167 Ok(())
168 }
169
170 #[allow(clippy::too_many_lines)]
173 #[allow(unused_variables)]
174 pub async fn execute(
175 &self,
176 config: &config::Args,
177 quiet: bool,
178 no_cache: bool,
179 ) -> Result<TxnResult<Hash>, Error> {
180 let wasm_path = self.wasm.clone().ok_or(Error::WasmNotProvided)?;
181 self.upload_wasm(&wasm_path, config, quiet, no_cache).await
182 }
183
184 fn resolve_wasm_paths(&self, global_args: &global::Args) -> Result<Vec<PathBuf>, Error> {
185 if let Some(wasm) = &self.wasm {
186 Ok(vec![wasm.clone()])
187 } else {
188 let build_cmd = build::Cmd {
189 package: self.package.clone(),
190 build_args: self.build_args.clone(),
191 ..build::Cmd::default()
192 };
193 let contracts = build_cmd.run(global_args).map_err(|e| match e {
194 build::Error::Metadata(_) => Error::NotInCargoProject,
195 other => other.into(),
196 })?;
197
198 if contracts.is_empty() {
199 return Err(Error::NoBuildableContracts);
200 }
201
202 Ok(contracts.into_iter().map(|c| c.path).collect())
203 }
204 }
205
206 #[allow(clippy::too_many_lines)]
207 #[allow(unused_variables)]
208 async fn upload_wasm(
209 &self,
210 wasm_path: &Path,
211 config: &config::Args,
212 quiet: bool,
213 no_cache: bool,
214 ) -> Result<TxnResult<Hash>, Error> {
215 self.auth_mode.validate_not_enforce()?;
216
217 let print = Print::new(quiet);
218 let wasm_path = wasm_path.to_path_buf();
219 let wasm_args = wasm::Args {
220 wasm: wasm_path.clone(),
221 };
222 let contract = wasm_args.read()?;
223 let network = config.get_network()?;
224 let client = network.rpc_client()?;
225 client
226 .verify_network_passphrase(Some(&network.network_passphrase))
227 .await?;
228 let wasm_spec = &wasm_args.parse().map_err(|e| Error::CannotParseWasm {
229 wasm: wasm_path.clone(),
230 error: e,
231 })?;
232
233 if let Some(rs_sdk_ver) = get_contract_meta_sdk_version(wasm_spec) {
235 if rs_sdk_ver.contains("rc")
236 && !self.ignore_checks
237 && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
238 {
239 return Err(Error::ContractCompiledWithReleaseCandidateSdk {
240 wasm: wasm_path.clone(),
241 version: rs_sdk_ver,
242 });
243 } else if rs_sdk_ver.contains("rc")
244 && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
245 {
246 tracing::warn!("the deployed smart contract {path} was built with Soroban Rust SDK v{rs_sdk_ver}, a release candidate version not intended for use with the Stellar Public Network", path = wasm_path.display());
247 }
248 }
249
250 let source_account = config.source_account()?;
252
253 let account_details = client
254 .get_account(&source_account.clone().to_string())
255 .await?;
256 let sequence: i64 = account_details.seq_num.into();
257
258 let (tx_without_preflight, hash) = build_install_contract_code_tx(
259 &contract,
260 sequence + 1,
261 config.get_inclusion_fee()?,
262 &source_account,
263 )?;
264
265 if self.build_only {
266 return Ok(TxnResult::Txn(Box::new(tx_without_preflight)));
267 }
268
269 let should_check = true;
270
271 if should_check {
272 let code_key =
273 xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
274 let contract_data = client.get_ledger_entries(&[code_key]).await?;
275
276 if let Some(entries) = contract_data.entries {
281 if let Some(entry_result) = entries.first() {
282 let entry: LedgerEntryData =
283 LedgerEntryData::from_xdr_base64(&entry_result.xdr, Limits::none())?;
284
285 match &entry {
286 LedgerEntryData::ContractCode(code) => {
287 if code.ext.ne(&ContractCodeEntryExt::V0) {
290 print.infoln("Skipping install because wasm already installed");
291 return Ok(TxnResult::Res(hash));
292 }
293 }
294 _ => {
295 tracing::warn!("Entry retrieved should be of type ContractCode");
296 }
297 }
298 }
299 }
300 }
301
302 let txn_resp = sim_sign_and_send_tx::<Error>(
303 &client,
304 &tx_without_preflight,
305 config,
306 &self.resources,
307 &[],
308 self.auth_mode.to_rpc(),
309 quiet,
310 no_cache,
311 )
312 .await?;
313
314 if let Some(TransactionResult {
316 result: TransactionResultResult::TxInternalError,
317 ..
318 }) = txn_resp.result
319 {
320 restore::Cmd {
322 key: key::Args {
323 contract_id: None,
324 key: None,
325 key_xdr: None,
326 wasm: Some(wasm_path.clone()),
327 wasm_hash: None,
328 durability: super::Durability::Persistent,
329 },
330 config: config.clone(),
331 resources: self.resources.clone(),
332 ledgers_to_extend: None,
333 ttl_ledger_only: true,
334 build_only: self.build_only,
335 }
336 .execute(config, quiet, no_cache)
337 .await?;
338 }
339
340 if !no_cache {
341 data::write_spec(&hash.to_string(), &wasm_spec.spec)?;
342 }
343
344 Ok(TxnResult::Res(hash))
345 }
346}
347
348fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) -> Option<String> {
349 let rs_sdk_version_option = if let Some(_meta) = &wasm_spec.meta_base64 {
350 wasm_spec.meta.iter().find(|entry| match entry {
351 ScMetaEntry::ScMetaV0(ScMetaV0 { key, .. }) => {
352 key.to_utf8_string_lossy().contains(CONTRACT_META_SDK_KEY)
353 }
354 })
355 } else {
356 None
357 };
358
359 if let Some(rs_sdk_version_entry) = &rs_sdk_version_option {
360 match rs_sdk_version_entry {
361 ScMetaEntry::ScMetaV0(ScMetaV0 { val, .. }) => {
362 return Some(val.to_utf8_string_lossy());
363 }
364 }
365 }
366
367 None
368}
369
370pub(crate) fn build_install_contract_code_tx(
371 source_code: &[u8],
372 sequence: i64,
373 fee: u32,
374 source: &xdr::MuxedAccount,
375) -> Result<(Transaction, Hash), Error> {
376 let hash = utils::contract_hash(source_code)?;
377
378 let op = xdr::Operation {
379 source_account: None,
380 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
381 host_function: HostFunction::UploadContractWasm(source_code.try_into()?),
382 auth: VecM::default(),
383 }),
384 };
385 let tx = Transaction::new_tx(source.clone(), fee, sequence, op);
386
387 Ok((tx, hash))
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_build_install_contract_code() {
396 let result = build_install_contract_code_tx(
397 b"foo",
398 300,
399 1,
400 &stellar_strkey::ed25519::PublicKey::from_payload(
401 utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
402 .unwrap()
403 .verifying_key()
404 .as_bytes(),
405 )
406 .unwrap()
407 .to_string()
408 .parse()
409 .unwrap(),
410 );
411
412 assert!(result.is_ok());
413 }
414}