soroban_cli/commands/contract/
upload.rs1use std::array::TryFromSliceError;
2use std::fmt::Debug;
3use std::num::ParseIntError;
4
5use crate::xdr::{
6 self, ContractCodeEntryExt, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp,
7 LedgerEntryData, Limits, OperationBody, ReadXdr, ScMetaEntry, ScMetaV0, Transaction,
8 TransactionResult, TransactionResultResult, VecM, WriteXdr,
9};
10use clap::Parser;
11
12use super::restore;
13use crate::commands::tx::fetch;
14use crate::{
15 assembled::simulate_and_assemble_transaction,
16 commands::{
17 global,
18 txn_result::{TxnEnvelopeResult, TxnResult},
19 },
20 config::{self, data, network},
21 key,
22 print::Print,
23 rpc,
24 tx::builder::{self, TxExt},
25 utils, wasm,
26};
27
28const CONTRACT_META_SDK_KEY: &str = "rssdkver";
29const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";
30
31#[derive(Parser, Debug, Clone)]
32#[group(skip)]
33pub struct Cmd {
34 #[command(flatten)]
35 pub config: config::Args,
36
37 #[command(flatten)]
38 pub resources: crate::resources::Args,
39
40 #[command(flatten)]
41 pub wasm: wasm::Args,
42
43 #[arg(long, short = 'i', default_value = "false")]
44 pub ignore_checks: bool,
46
47 #[arg(long)]
49 pub build_only: bool,
50}
51
52#[derive(thiserror::Error, Debug)]
53pub enum Error {
54 #[error("error parsing int: {0}")]
55 ParseIntError(#[from] ParseIntError),
56
57 #[error("internal conversion error: {0}")]
58 TryFromSliceError(#[from] TryFromSliceError),
59
60 #[error("xdr processing error: {0}")]
61 Xdr(#[from] XdrError),
62
63 #[error("jsonrpc error: {0}")]
64 JsonRpc(#[from] jsonrpsee_core::Error),
65
66 #[error(transparent)]
67 Rpc(#[from] rpc::Error),
68
69 #[error(transparent)]
70 Config(#[from] config::Error),
71
72 #[error(transparent)]
73 Wasm(#[from] wasm::Error),
74
75 #[error("unexpected ({length}) simulate transaction result length")]
76 UnexpectedSimulateTransactionResultSize { length: usize },
77
78 #[error(transparent)]
79 Restore(#[from] restore::Error),
80
81 #[error("cannot parse WASM file {wasm}: {error}")]
82 CannotParseWasm {
83 wasm: std::path::PathBuf,
84 error: wasm::Error,
85 },
86
87 #[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")]
88 ContractCompiledWithReleaseCandidateSdk {
89 wasm: std::path::PathBuf,
90 version: String,
91 },
92
93 #[error(transparent)]
94 Network(#[from] network::Error),
95
96 #[error(transparent)]
97 Data(#[from] data::Error),
98
99 #[error(transparent)]
100 Builder(#[from] builder::Error),
101
102 #[error(transparent)]
103 Fee(#[from] fetch::fee::Error),
104
105 #[error(transparent)]
106 Fetch(#[from] fetch::Error),
107}
108
109impl Cmd {
110 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
111 let res = self
112 .execute(&self.config, global_args.quiet, global_args.no_cache)
113 .await?
114 .to_envelope();
115 match res {
116 TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
117 TxnEnvelopeResult::Res(hash) => println!("{}", hex::encode(hash)),
118 }
119 Ok(())
120 }
121
122 #[allow(clippy::too_many_lines)]
123 #[allow(unused_variables)]
124 pub async fn execute(
125 &self,
126 config: &config::Args,
127 quiet: bool,
128 no_cache: bool,
129 ) -> Result<TxnResult<Hash>, Error> {
130 let print = Print::new(quiet);
131 let contract = self.wasm.read()?;
132 let network = config.get_network()?;
133 let client = network.rpc_client()?;
134 client
135 .verify_network_passphrase(Some(&network.network_passphrase))
136 .await?;
137 let wasm_spec = &self.wasm.parse().map_err(|e| Error::CannotParseWasm {
138 wasm: self.wasm.wasm.clone(),
139 error: e,
140 })?;
141
142 if let Some(rs_sdk_ver) = get_contract_meta_sdk_version(wasm_spec) {
144 if rs_sdk_ver.contains("rc")
145 && !self.ignore_checks
146 && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
147 {
148 return Err(Error::ContractCompiledWithReleaseCandidateSdk {
149 wasm: self.wasm.wasm.clone(),
150 version: rs_sdk_ver,
151 });
152 } else if rs_sdk_ver.contains("rc")
153 && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
154 {
155 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 = self.wasm.wasm.display());
156 }
157 }
158
159 let source_account = config.source_account().await?;
161
162 let account_details = client
163 .get_account(&source_account.clone().to_string())
164 .await?;
165 let sequence: i64 = account_details.seq_num.into();
166
167 let (tx_without_preflight, hash) = build_install_contract_code_tx(
168 &contract,
169 sequence + 1,
170 config.get_inclusion_fee()?,
171 &source_account,
172 )?;
173
174 if self.build_only {
175 return Ok(TxnResult::Txn(Box::new(tx_without_preflight)));
176 }
177
178 let should_check = true;
179
180 if should_check {
181 let code_key =
182 xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
183 let contract_data = client.get_ledger_entries(&[code_key]).await?;
184
185 if let Some(entries) = contract_data.entries {
190 if let Some(entry_result) = entries.first() {
191 let entry: LedgerEntryData =
192 LedgerEntryData::from_xdr_base64(&entry_result.xdr, Limits::none())?;
193
194 match &entry {
195 LedgerEntryData::ContractCode(code) => {
196 if code.ext.ne(&ContractCodeEntryExt::V0) {
199 print.infoln("Skipping install because wasm already installed");
200 return Ok(TxnResult::Res(hash));
201 }
202 }
203 _ => {
204 tracing::warn!("Entry retrieved should be of type ContractCode");
205 }
206 }
207 }
208 }
209 }
210
211 print.infoln("Simulating install transaction…");
212
213 let assembled = simulate_and_assemble_transaction(
214 &client,
215 &tx_without_preflight,
216 self.resources.resource_config(),
217 self.resources.resource_fee,
218 )
219 .await?;
220 let assembled = self.resources.apply_to_assembled_txn(assembled);
221 let txn = Box::new(assembled.transaction().clone());
222 let signed_txn = &self.config.sign(*txn, quiet).await?;
223
224 print.globeln("Submitting install transaction…");
225 let txn_resp = client.send_transaction_polling(signed_txn).await?;
226 self.resources.print_cost_info(&txn_resp)?;
227
228 if !no_cache {
229 data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?;
230 }
231
232 if let Some(TransactionResult {
234 result: TransactionResultResult::TxInternalError,
235 ..
236 }) = txn_resp.result
237 {
238 restore::Cmd {
240 key: key::Args {
241 contract_id: None,
242 key: None,
243 key_xdr: None,
244 wasm: Some(self.wasm.wasm.clone()),
245 wasm_hash: None,
246 durability: super::Durability::Persistent,
247 },
248 config: config.clone(),
249 resources: self.resources.clone(),
250 ledgers_to_extend: None,
251 ttl_ledger_only: true,
252 build_only: self.build_only,
253 }
254 .execute(config, quiet, no_cache)
255 .await?;
256 }
257
258 if !no_cache {
259 data::write_spec(&hash.to_string(), &wasm_spec.spec)?;
260 }
261
262 Ok(TxnResult::Res(hash))
263 }
264}
265
266fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) -> Option<String> {
267 let rs_sdk_version_option = if let Some(_meta) = &wasm_spec.meta_base64 {
268 wasm_spec.meta.iter().find(|entry| match entry {
269 ScMetaEntry::ScMetaV0(ScMetaV0 { key, .. }) => {
270 key.to_utf8_string_lossy().contains(CONTRACT_META_SDK_KEY)
271 }
272 })
273 } else {
274 None
275 };
276
277 if let Some(rs_sdk_version_entry) = &rs_sdk_version_option {
278 match rs_sdk_version_entry {
279 ScMetaEntry::ScMetaV0(ScMetaV0 { val, .. }) => {
280 return Some(val.to_utf8_string_lossy());
281 }
282 }
283 }
284
285 None
286}
287
288pub(crate) fn build_install_contract_code_tx(
289 source_code: &[u8],
290 sequence: i64,
291 fee: u32,
292 source: &xdr::MuxedAccount,
293) -> Result<(Transaction, Hash), Error> {
294 let hash = utils::contract_hash(source_code)?;
295
296 let op = xdr::Operation {
297 source_account: None,
298 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
299 host_function: HostFunction::UploadContractWasm(source_code.try_into()?),
300 auth: VecM::default(),
301 }),
302 };
303 let tx = Transaction::new_tx(source.clone(), fee, sequence, op);
304
305 Ok((tx, hash))
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_build_install_contract_code() {
314 let result = build_install_contract_code_tx(
315 b"foo",
316 300,
317 1,
318 &stellar_strkey::ed25519::PublicKey::from_payload(
319 utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
320 .unwrap()
321 .verifying_key()
322 .as_bytes(),
323 )
324 .unwrap()
325 .to_string()
326 .parse()
327 .unwrap(),
328 );
329
330 assert!(result.is_ok());
331 }
332}