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