soroban_cli/commands/contract/
upload.rs

1use 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    /// Whether to ignore safety checks when deploying contracts
42    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        // Check Rust SDK version if using the public network.
122        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        // Get the account sequence number
139        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        // Don't check whether the contract is already installed when the user
154        // has requested to perform simulation only and is hoping to get a
155        // transaction back.
156        #[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            // Skip install if the contract is already installed, and the contract has an extension version that isn't V0.
167            // In protocol 21 extension V1 was added that stores additional information about a contract making execution
168            // of the contract cheaper. So if folks want to reinstall we should let them which is why the install will still
169            // go ahead if the contract has a V0 extension.
170            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                            // Skip reupload if this isn't V0 because V1 extension already
178                            // exists.
179                            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        // Currently internal errors are not returned if the contract code is expired
212        if let Some(TransactionResult {
213            result: TransactionResultResult::TxInternalError,
214            ..
215        }) = txn_resp.result
216        {
217            // Now just need to restore it and don't have to install again
218            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}