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