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