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