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 #[allow(unused_variables)]
104 async fn run_against_rpc_server(
105 &self,
106 args: Option<&global::Args>,
107 config: Option<&config::Args>,
108 ) -> Result<TxnResult<Hash>, Error> {
109 let print = Print::new(args.is_some_and(|a| a.quiet));
110 let config = config.unwrap_or(&self.config);
111 let contract = self.wasm.read()?;
112 let network = config.get_network()?;
113 let client = network.rpc_client()?;
114 client
115 .verify_network_passphrase(Some(&network.network_passphrase))
116 .await?;
117 let wasm_spec = &self.wasm.parse().map_err(|e| Error::CannotParseWasm {
118 wasm: self.wasm.wasm.clone(),
119 error: e,
120 })?;
121
122 if let Some(rs_sdk_ver) = get_contract_meta_sdk_version(wasm_spec) {
124 if rs_sdk_ver.contains("rc")
125 && !self.ignore_checks
126 && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
127 {
128 return Err(Error::ContractCompiledWithReleaseCandidateSdk {
129 wasm: self.wasm.wasm.clone(),
130 version: rs_sdk_ver,
131 });
132 } else if rs_sdk_ver.contains("rc")
133 && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
134 {
135 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());
136 }
137 }
138
139 let source_account = config.source_account().await?;
141
142 let account_details = client
143 .get_account(&source_account.clone().to_string())
144 .await?;
145 let sequence: i64 = account_details.seq_num.into();
146
147 let (tx_without_preflight, hash) =
148 build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &source_account)?;
149
150 if self.fee.build_only {
151 return Ok(TxnResult::Txn(Box::new(tx_without_preflight)));
152 }
153
154 #[cfg(feature = "version_lt_23")]
158 let should_check = !self.fee.sim_only;
159 #[cfg(feature = "version_gte_23")]
160 let should_check = true;
161
162 if should_check {
163 let code_key =
164 xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() });
165 let contract_data = client.get_ledger_entries(&[code_key]).await?;
166
167 if let Some(entries) = contract_data.entries {
172 if let Some(entry_result) = entries.first() {
173 let entry: LedgerEntryData =
174 LedgerEntryData::from_xdr_base64(&entry_result.xdr, Limits::none())?;
175
176 match &entry {
177 LedgerEntryData::ContractCode(code) => {
178 if code.ext.ne(&ContractCodeEntryExt::V0) {
181 print.infoln("Skipping install because wasm already installed");
182 return Ok(TxnResult::Res(hash));
183 }
184 }
185 _ => {
186 tracing::warn!("Entry retrieved should be of type ContractCode");
187 }
188 }
189 }
190 }
191 }
192
193 print.infoln("Simulating install transaction…");
194
195 let txn = simulate_and_assemble_transaction(&client, &tx_without_preflight).await?;
196 let txn = Box::new(self.fee.apply_to_assembled_txn(txn).transaction().clone());
197
198 #[cfg(feature = "version_lt_23")]
199 if self.fee.sim_only {
200 return Ok(TxnResult::Txn(txn));
201 }
202
203 let signed_txn = &self.config.sign(*txn).await?;
204
205 print.globeln("Submitting install transaction…");
206 let txn_resp = client.send_transaction_polling(signed_txn).await?;
207
208 if args.is_none_or(|a| !a.no_cache) {
209 data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?;
210 }
211
212 if let Some(TransactionResult {
214 result: TransactionResultResult::TxInternalError,
215 ..
216 }) = txn_resp.result
217 {
218 restore::Cmd {
220 key: key::Args {
221 contract_id: None,
222 key: None,
223 key_xdr: None,
224 wasm: Some(self.wasm.wasm.clone()),
225 wasm_hash: None,
226 durability: super::Durability::Persistent,
227 },
228 config: config.clone(),
229 fee: self.fee.clone(),
230 ledgers_to_extend: None,
231 ttl_ledger_only: true,
232 }
233 .run_against_rpc_server(args, None)
234 .await?;
235 }
236
237 if args.is_none_or(|a| !a.no_cache) {
238 data::write_spec(&hash.to_string(), &wasm_spec.spec)?;
239 }
240
241 Ok(TxnResult::Res(hash))
242 }
243}
244
245fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) -> Option<String> {
246 let rs_sdk_version_option = if let Some(_meta) = &wasm_spec.meta_base64 {
247 wasm_spec.meta.iter().find(|entry| match entry {
248 ScMetaEntry::ScMetaV0(ScMetaV0 { key, .. }) => {
249 key.to_utf8_string_lossy().contains(CONTRACT_META_SDK_KEY)
250 }
251 })
252 } else {
253 None
254 };
255
256 if let Some(rs_sdk_version_entry) = &rs_sdk_version_option {
257 match rs_sdk_version_entry {
258 ScMetaEntry::ScMetaV0(ScMetaV0 { val, .. }) => {
259 return Some(val.to_utf8_string_lossy());
260 }
261 }
262 }
263
264 None
265}
266
267pub(crate) fn build_install_contract_code_tx(
268 source_code: &[u8],
269 sequence: i64,
270 fee: u32,
271 source: &xdr::MuxedAccount,
272) -> Result<(Transaction, Hash), Error> {
273 let hash = utils::contract_hash(source_code)?;
274
275 let op = xdr::Operation {
276 source_account: None,
277 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
278 host_function: HostFunction::UploadContractWasm(source_code.try_into()?),
279 auth: VecM::default(),
280 }),
281 };
282 let tx = Transaction::new_tx(source.clone(), fee, sequence, op);
283
284 Ok((tx, hash))
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_build_install_contract_code() {
293 let result = build_install_contract_code_tx(
294 b"foo",
295 300,
296 1,
297 &stellar_strkey::ed25519::PublicKey::from_payload(
298 utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
299 .unwrap()
300 .verifying_key()
301 .as_bytes(),
302 )
303 .unwrap()
304 .to_string()
305 .parse()
306 .unwrap(),
307 );
308
309 assert!(result.is_ok());
310 }
311}