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