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