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