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