soroban_cli/commands/contract/deploy/
wasm.rs

1use crate::commands::contract::deploy::utils::alias_validator;
2use std::array::TryFromSliceError;
3use std::ffi::OsString;
4use std::fmt::Debug;
5use std::num::ParseIntError;
6
7use crate::xdr::{
8    AccountId, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress,
9    CreateContractArgs, CreateContractArgsV2, Error as XdrError, Hash, HostFunction,
10    InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo, MuxedAccount, Operation, OperationBody,
11    Preconditions, PublicKey, ScAddress, SequenceNumber, Transaction, TransactionExt, Uint256,
12    VecM, WriteXdr,
13};
14use clap::{arg, command, Parser};
15use rand::Rng;
16
17use soroban_spec_tools::contract as contract_spec;
18
19use crate::{
20    assembled::simulate_and_assemble_transaction,
21    commands::{
22        contract::{self, arg_parsing, id::wasm::get_contract_id, upload},
23        global,
24        txn_result::{TxnEnvelopeResult, TxnResult},
25        NetworkRunnable, HEADING_RPC,
26    },
27    config::{self, data, locator, network},
28    print::Print,
29    rpc,
30    utils::{self, rpc::get_remote_wasm_from_hash},
31    wasm,
32};
33
34pub const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
35
36#[derive(Parser, Debug, Clone)]
37#[command(group(
38    clap::ArgGroup::new("wasm_src")
39        .required(true)
40        .args(&["wasm", "wasm_hash"]),
41))]
42#[group(skip)]
43pub struct Cmd {
44    /// WASM file to deploy
45    #[arg(long, group = "wasm_src")]
46    pub wasm: Option<std::path::PathBuf>,
47    /// Hash of the already installed/deployed WASM file
48    #[arg(long = "wasm-hash", conflicts_with = "wasm", group = "wasm_src")]
49    pub wasm_hash: Option<String>,
50    /// Custom salt 32-byte salt for the token id
51    #[arg(
52        long,
53        help_heading = HEADING_RPC,
54    )]
55    pub salt: Option<String>,
56    #[command(flatten)]
57    pub config: config::Args,
58    #[command(flatten)]
59    pub fee: crate::fee::Args,
60    #[arg(long, short = 'i', default_value = "false")]
61    /// Whether to ignore safety checks when deploying contracts
62    pub ignore_checks: bool,
63    /// The alias that will be used to save the contract's id.
64    /// Whenever used, `--alias` will always overwrite the existing contract id
65    /// configuration without asking for confirmation.
66    #[arg(long, value_parser = clap::builder::ValueParser::new(alias_validator))]
67    pub alias: Option<String>,
68    /// If provided, will be passed to the contract's `__constructor` function with provided arguments for that function as `--arg-name value`
69    #[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")]
70    pub slop: Vec<OsString>,
71}
72
73#[derive(thiserror::Error, Debug)]
74pub enum Error {
75    #[error(transparent)]
76    Install(#[from] upload::Error),
77    #[error("error parsing int: {0}")]
78    ParseIntError(#[from] ParseIntError),
79    #[error("internal conversion error: {0}")]
80    TryFromSliceError(#[from] TryFromSliceError),
81    #[error("xdr processing error: {0}")]
82    Xdr(#[from] XdrError),
83    #[error("jsonrpc error: {0}")]
84    JsonRpc(#[from] jsonrpsee_core::Error),
85    #[error("cannot parse salt: {salt}")]
86    CannotParseSalt { salt: String },
87    #[error("cannot parse contract ID {contract_id}: {error}")]
88    CannotParseContractId {
89        contract_id: String,
90        error: stellar_strkey::DecodeError,
91    },
92    #[error("cannot parse WASM hash {wasm_hash}: {error}")]
93    CannotParseWasmHash {
94        wasm_hash: String,
95        error: stellar_strkey::DecodeError,
96    },
97    #[error("Must provide either --wasm or --wash-hash")]
98    WasmNotProvided,
99    #[error(transparent)]
100    Rpc(#[from] rpc::Error),
101    #[error(transparent)]
102    Config(#[from] config::Error),
103    #[error(transparent)]
104    StrKey(#[from] stellar_strkey::DecodeError),
105    #[error(transparent)]
106    Infallible(#[from] std::convert::Infallible),
107    #[error(transparent)]
108    WasmId(#[from] contract::id::wasm::Error),
109    #[error(transparent)]
110    Data(#[from] data::Error),
111    #[error(transparent)]
112    Network(#[from] network::Error),
113    #[error(transparent)]
114    Wasm(#[from] wasm::Error),
115    #[error(
116        "alias must be 1-30 chars long, and have only letters, numbers, underscores and dashes"
117    )]
118    InvalidAliasFormat { alias: String },
119    #[error(transparent)]
120    Locator(#[from] locator::Error),
121    #[error(transparent)]
122    ContractSpec(#[from] contract_spec::Error),
123    #[error(transparent)]
124    ArgParse(#[from] arg_parsing::Error),
125    #[error("Only ed25519 accounts are allowed")]
126    OnlyEd25519AccountsAllowed,
127}
128
129impl Cmd {
130    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
131        let res = self
132            .run_against_rpc_server(Some(global_args), None)
133            .await?
134            .to_envelope();
135        match res {
136            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
137            TxnEnvelopeResult::Res(contract) => {
138                let network = self.config.get_network()?;
139
140                if let Some(alias) = self.alias.clone() {
141                    if let Some(existing_contract) = self
142                        .config
143                        .locator
144                        .get_contract_id(&alias, &network.network_passphrase)?
145                    {
146                        let print = Print::new(global_args.quiet);
147                        print.warnln(format!(
148                            "Overwriting existing contract id: {existing_contract}"
149                        ));
150                    }
151
152                    self.config.locator.save_contract_id(
153                        &network.network_passphrase,
154                        &contract,
155                        &alias,
156                    )?;
157                }
158
159                println!("{contract}");
160            }
161        }
162        Ok(())
163    }
164}
165
166#[async_trait::async_trait]
167impl NetworkRunnable for Cmd {
168    type Error = Error;
169    type Result = TxnResult<stellar_strkey::Contract>;
170
171    #[allow(clippy::too_many_lines)]
172    #[allow(unused_variables)]
173    async fn run_against_rpc_server(
174        &self,
175        global_args: Option<&global::Args>,
176        config: Option<&config::Args>,
177    ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
178        let print = Print::new(global_args.is_some_and(|a| a.quiet));
179        let config = config.unwrap_or(&self.config);
180        let wasm_hash = if let Some(wasm) = &self.wasm {
181            #[cfg(feature = "version_lt_23")]
182            let is_build = self.fee.build_only || self.fee.sim_only;
183            #[cfg(feature = "version_gte_23")]
184            let is_build = self.fee.build_only;
185            let hash = if is_build {
186                wasm::Args { wasm: wasm.clone() }.hash()?
187            } else {
188                upload::Cmd {
189                    wasm: wasm::Args { wasm: wasm.clone() },
190                    config: config.clone(),
191                    fee: self.fee.clone(),
192                    ignore_checks: self.ignore_checks,
193                }
194                .run_against_rpc_server(global_args, Some(config))
195                .await?
196                .into_result()
197                .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
198            };
199            hex::encode(hash)
200        } else {
201            self.wasm_hash
202                .as_ref()
203                .ok_or(Error::WasmNotProvided)?
204                .to_string()
205        };
206
207        let wasm_hash = Hash(
208            utils::contract_id_from_str(&wasm_hash)
209                .map_err(|e| Error::CannotParseWasmHash {
210                    wasm_hash: wasm_hash.clone(),
211                    error: e,
212                })?
213                .0,
214        );
215
216        print.infoln(format!("Using wasm hash {wasm_hash}").as_str());
217
218        let network = config.get_network()?;
219        let salt: [u8; 32] = match &self.salt {
220            Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
221                .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
222                .try_into()
223                .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
224            None => rand::thread_rng().gen::<[u8; 32]>(),
225        };
226
227        let client = network.rpc_client()?;
228        client
229            .verify_network_passphrase(Some(&network.network_passphrase))
230            .await?;
231        let MuxedAccount::Ed25519(bytes) = config.source_account().await? else {
232            return Err(Error::OnlyEd25519AccountsAllowed);
233        };
234        let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
235        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
236            address: ScAddress::Account(source_account.clone()),
237            salt: Uint256(salt),
238        });
239        let contract_id =
240            get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
241        let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
242            wasm::Args { wasm: wasm.clone() }.read()?
243        } else {
244            get_remote_wasm_from_hash(&client, &wasm_hash).await?
245        };
246        let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
247        let res = soroban_spec_tools::Spec::new(entries.clone());
248        let constructor_params = if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
249            if func.inputs.is_empty() {
250                None
251            } else {
252                let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
253                slop.extend_from_slice(&self.slop);
254                Some(
255                    arg_parsing::build_host_function_parameters(
256                        &stellar_strkey::Contract(contract_id.0),
257                        &slop,
258                        &entries,
259                        config,
260                    )?
261                    .2,
262                )
263            }
264        } else {
265            None
266        };
267
268        // Get the account sequence number
269        let account_details = client.get_account(&source_account.to_string()).await?;
270        let sequence: i64 = account_details.seq_num.into();
271        let txn = Box::new(build_create_contract_tx(
272            wasm_hash,
273            sequence + 1,
274            self.fee.fee,
275            source_account,
276            contract_id_preimage,
277            constructor_params.as_ref(),
278        )?);
279
280        if self.fee.build_only {
281            print.checkln("Transaction built!");
282            return Ok(TxnResult::Txn(txn));
283        }
284
285        print.infoln("Simulating deploy transaction…");
286
287        let txn = simulate_and_assemble_transaction(&client, &txn).await?;
288        let txn = Box::new(self.fee.apply_to_assembled_txn(txn).transaction().clone());
289
290        #[cfg(feature = "version_lt_23")]
291        if self.fee.sim_only {
292            print.checkln("Done!");
293            return Ok(TxnResult::Txn(txn));
294        }
295
296        print.log_transaction(&txn, &network, true)?;
297        let signed_txn = &config.sign_with_local_key(*txn).await?;
298        print.globeln("Submitting deploy transaction…");
299
300        let get_txn_resp = client
301            .send_transaction_polling(signed_txn)
302            .await?
303            .try_into()?;
304
305        if global_args.is_none_or(|a| !a.no_cache) {
306            data::write(get_txn_resp, &network.rpc_uri()?)?;
307        }
308
309        if let Some(url) = utils::explorer_url_for_contract(&network, &contract_id) {
310            print.linkln(url);
311        }
312
313        print.checkln("Deployed!");
314
315        Ok(TxnResult::Res(contract_id))
316    }
317}
318
319fn build_create_contract_tx(
320    wasm_hash: Hash,
321    sequence: i64,
322    fee: u32,
323    key: AccountId,
324    contract_id_preimage: ContractIdPreimage,
325    constructor_params: Option<&InvokeContractArgs>,
326) -> Result<Transaction, Error> {
327    let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
328        Operation {
329            source_account: None,
330            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
331                host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
332                    contract_id_preimage,
333                    executable: ContractExecutable::Wasm(wasm_hash),
334                    constructor_args: args.clone(),
335                }),
336                auth: VecM::default(),
337            }),
338        }
339    } else {
340        Operation {
341            source_account: None,
342            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
343                host_function: HostFunction::CreateContract(CreateContractArgs {
344                    contract_id_preimage,
345                    executable: ContractExecutable::Wasm(wasm_hash),
346                }),
347                auth: VecM::default(),
348            }),
349        }
350    };
351    let tx = Transaction {
352        source_account: key.into(),
353        fee,
354        seq_num: SequenceNumber(sequence),
355        cond: Preconditions::None,
356        memo: Memo::None,
357        operations: vec![op].try_into()?,
358        ext: TransactionExt::V0,
359    };
360
361    Ok(tx)
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_build_create_contract() {
370        let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
371            .unwrap()
372            .try_into()
373            .unwrap();
374        let salt = [0u8; 32];
375        let key =
376            &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
377                .unwrap();
378        let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
379            key.verifying_key().to_bytes(),
380        )));
381
382        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
383            address: ScAddress::Account(source_account.clone()),
384            salt: Uint256(salt),
385        });
386
387        let result = build_create_contract_tx(
388            Hash(hash),
389            300,
390            1,
391            source_account,
392            contract_id_preimage,
393            None,
394        );
395
396        assert!(result.is_ok());
397    }
398}