Skip to main content

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::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    },
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
37    #[command(flatten)]
38    pub resources: crate::resources::Args,
39
40    #[command(flatten)]
41    pub wasm: wasm::Args,
42
43    #[arg(long, short = 'i', default_value = "false")]
44    /// Whether to ignore safety checks when deploying contracts
45    pub ignore_checks: bool,
46
47    /// Build the transaction and only write the base64 xdr to stdout
48    #[arg(long)]
49    pub build_only: bool,
50}
51
52#[derive(thiserror::Error, Debug)]
53pub enum Error {
54    #[error("error parsing int: {0}")]
55    ParseIntError(#[from] ParseIntError),
56
57    #[error("internal conversion error: {0}")]
58    TryFromSliceError(#[from] TryFromSliceError),
59
60    #[error("xdr processing error: {0}")]
61    Xdr(#[from] XdrError),
62
63    #[error("jsonrpc error: {0}")]
64    JsonRpc(#[from] jsonrpsee_core::Error),
65
66    #[error(transparent)]
67    Rpc(#[from] rpc::Error),
68
69    #[error(transparent)]
70    Config(#[from] config::Error),
71
72    #[error(transparent)]
73    Wasm(#[from] wasm::Error),
74
75    #[error("unexpected ({length}) simulate transaction result length")]
76    UnexpectedSimulateTransactionResultSize { length: usize },
77
78    #[error(transparent)]
79    Restore(#[from] restore::Error),
80
81    #[error("cannot parse WASM file {wasm}: {error}")]
82    CannotParseWasm {
83        wasm: std::path::PathBuf,
84        error: wasm::Error,
85    },
86
87    #[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")]
88    ContractCompiledWithReleaseCandidateSdk {
89        wasm: std::path::PathBuf,
90        version: String,
91    },
92
93    #[error(transparent)]
94    Network(#[from] network::Error),
95
96    #[error(transparent)]
97    Data(#[from] data::Error),
98
99    #[error(transparent)]
100    Builder(#[from] builder::Error),
101
102    #[error(transparent)]
103    Fee(#[from] fetch::fee::Error),
104
105    #[error(transparent)]
106    Fetch(#[from] fetch::Error),
107}
108
109impl Cmd {
110    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
111        let res = self
112            .execute(&self.config, global_args.quiet, global_args.no_cache)
113            .await?
114            .to_envelope();
115        match res {
116            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
117            TxnEnvelopeResult::Res(hash) => println!("{}", hex::encode(hash)),
118        }
119        Ok(())
120    }
121
122    #[allow(clippy::too_many_lines)]
123    #[allow(unused_variables)]
124    pub async fn execute(
125        &self,
126        config: &config::Args,
127        quiet: bool,
128        no_cache: bool,
129    ) -> Result<TxnResult<Hash>, Error> {
130        let print = Print::new(quiet);
131        let contract = self.wasm.read()?;
132        let network = config.get_network()?;
133        let client = network.rpc_client()?;
134        client
135            .verify_network_passphrase(Some(&network.network_passphrase))
136            .await?;
137        let wasm_spec = &self.wasm.parse().map_err(|e| Error::CannotParseWasm {
138            wasm: self.wasm.wasm.clone(),
139            error: e,
140        })?;
141
142        // Check Rust SDK version if using the public network.
143        if let Some(rs_sdk_ver) = get_contract_meta_sdk_version(wasm_spec) {
144            if rs_sdk_ver.contains("rc")
145                && !self.ignore_checks
146                && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
147            {
148                return Err(Error::ContractCompiledWithReleaseCandidateSdk {
149                    wasm: self.wasm.wasm.clone(),
150                    version: rs_sdk_ver,
151                });
152            } else if rs_sdk_ver.contains("rc")
153                && network.network_passphrase == PUBLIC_NETWORK_PASSPHRASE
154            {
155                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());
156            }
157        }
158
159        // Get the account sequence number
160        let source_account = config.source_account().await?;
161
162        let account_details = client
163            .get_account(&source_account.clone().to_string())
164            .await?;
165        let sequence: i64 = account_details.seq_num.into();
166
167        let (tx_without_preflight, hash) = build_install_contract_code_tx(
168            &contract,
169            sequence + 1,
170            config.get_inclusion_fee()?,
171            &source_account,
172        )?;
173
174        if self.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.resources.resource_config(),
217            self.resources.resource_fee,
218        )
219        .await?;
220        let assembled = self.resources.apply_to_assembled_txn(assembled);
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.resources.print_cost_info(&txn_resp)?;
227
228        if !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                resources: self.resources.clone(),
250                ledgers_to_extend: None,
251                ttl_ledger_only: true,
252                build_only: self.build_only,
253            }
254            .execute(config, quiet, no_cache)
255            .await?;
256        }
257
258        if !no_cache {
259            data::write_spec(&hash.to_string(), &wasm_spec.spec)?;
260        }
261
262        Ok(TxnResult::Res(hash))
263    }
264}
265
266fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) -> Option<String> {
267    let rs_sdk_version_option = if let Some(_meta) = &wasm_spec.meta_base64 {
268        wasm_spec.meta.iter().find(|entry| match entry {
269            ScMetaEntry::ScMetaV0(ScMetaV0 { key, .. }) => {
270                key.to_utf8_string_lossy().contains(CONTRACT_META_SDK_KEY)
271            }
272        })
273    } else {
274        None
275    };
276
277    if let Some(rs_sdk_version_entry) = &rs_sdk_version_option {
278        match rs_sdk_version_entry {
279            ScMetaEntry::ScMetaV0(ScMetaV0 { val, .. }) => {
280                return Some(val.to_utf8_string_lossy());
281            }
282        }
283    }
284
285    None
286}
287
288pub(crate) fn build_install_contract_code_tx(
289    source_code: &[u8],
290    sequence: i64,
291    fee: u32,
292    source: &xdr::MuxedAccount,
293) -> Result<(Transaction, Hash), Error> {
294    let hash = utils::contract_hash(source_code)?;
295
296    let op = xdr::Operation {
297        source_account: None,
298        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
299            host_function: HostFunction::UploadContractWasm(source_code.try_into()?),
300            auth: VecM::default(),
301        }),
302    };
303    let tx = Transaction::new_tx(source.clone(), fee, sequence, op);
304
305    Ok((tx, hash))
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_build_install_contract_code() {
314        let result = build_install_contract_code_tx(
315            b"foo",
316            300,
317            1,
318            &stellar_strkey::ed25519::PublicKey::from_payload(
319                utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
320                    .unwrap()
321                    .verifying_key()
322                    .as_bytes(),
323            )
324            .unwrap()
325            .to_string()
326            .parse()
327            .unwrap(),
328        );
329
330        assert!(result.is_ok());
331    }
332}