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