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 crate::commands::tx::fetch;
18use crate::{
19    assembled::simulate_and_assemble_transaction,
20    commands::{
21        contract::{self, arg_parsing, id::wasm::get_contract_id, upload},
22        global,
23        txn_result::{TxnEnvelopeResult, TxnResult},
24        NetworkRunnable, HEADING_RPC,
25    },
26    config::{self, data, locator, network},
27    print::Print,
28    rpc,
29    utils::{self, rpc::get_remote_wasm_from_hash},
30    wasm,
31};
32use soroban_spec_tools::contract as contract_spec;
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
78    #[error("error parsing int: {0}")]
79    ParseIntError(#[from] ParseIntError),
80
81    #[error("internal conversion error: {0}")]
82    TryFromSliceError(#[from] TryFromSliceError),
83
84    #[error("xdr processing error: {0}")]
85    Xdr(#[from] XdrError),
86
87    #[error("jsonrpc error: {0}")]
88    JsonRpc(#[from] jsonrpsee_core::Error),
89
90    #[error("cannot parse salt: {salt}")]
91    CannotParseSalt { salt: String },
92
93    #[error("cannot parse contract ID {contract_id}: {error}")]
94    CannotParseContractId {
95        contract_id: String,
96        error: stellar_strkey::DecodeError,
97    },
98
99    #[error("cannot parse WASM hash {wasm_hash}: {error}")]
100    CannotParseWasmHash {
101        wasm_hash: String,
102        error: stellar_strkey::DecodeError,
103    },
104
105    #[error("Must provide either --wasm or --wash-hash")]
106    WasmNotProvided,
107
108    #[error(transparent)]
109    Rpc(#[from] rpc::Error),
110
111    #[error(transparent)]
112    Config(#[from] config::Error),
113
114    #[error(transparent)]
115    StrKey(#[from] stellar_strkey::DecodeError),
116
117    #[error(transparent)]
118    Infallible(#[from] std::convert::Infallible),
119
120    #[error(transparent)]
121    WasmId(#[from] contract::id::wasm::Error),
122
123    #[error(transparent)]
124    Data(#[from] data::Error),
125
126    #[error(transparent)]
127    Network(#[from] network::Error),
128
129    #[error(transparent)]
130    Wasm(#[from] wasm::Error),
131
132    #[error(transparent)]
133    Locator(#[from] locator::Error),
134
135    #[error(transparent)]
136    ContractSpec(#[from] contract_spec::Error),
137
138    #[error(transparent)]
139    ArgParse(#[from] arg_parsing::Error),
140
141    #[error("Only ed25519 accounts are allowed")]
142    OnlyEd25519AccountsAllowed,
143
144    #[error(transparent)]
145    Fee(#[from] fetch::fee::Error),
146
147    #[error(transparent)]
148    Fetch(#[from] fetch::Error),
149}
150
151impl Cmd {
152    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
153        let res = self
154            .run_against_rpc_server(Some(global_args), None)
155            .await?
156            .to_envelope();
157        match res {
158            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
159            TxnEnvelopeResult::Res(contract) => {
160                let network = self.config.get_network()?;
161
162                if let Some(alias) = self.alias.clone() {
163                    if let Some(existing_contract) = self
164                        .config
165                        .locator
166                        .get_contract_id(&alias, &network.network_passphrase)?
167                    {
168                        let print = Print::new(global_args.quiet);
169                        print.warnln(format!(
170                            "Overwriting existing alias {alias:?} that currently links to contract ID: {existing_contract}"
171                        ));
172                    }
173
174                    self.config.locator.save_contract_id(
175                        &network.network_passphrase,
176                        &contract,
177                        &alias,
178                    )?;
179                }
180
181                println!("{contract}");
182            }
183        }
184        Ok(())
185    }
186}
187
188#[async_trait::async_trait]
189impl NetworkRunnable for Cmd {
190    type Error = Error;
191    type Result = TxnResult<stellar_strkey::Contract>;
192
193    #[allow(clippy::too_many_lines)]
194    #[allow(unused_variables)]
195    async fn run_against_rpc_server(
196        &self,
197        global_args: Option<&global::Args>,
198        config: Option<&config::Args>,
199    ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
200        let quiet = global_args.is_some_and(|a| a.quiet);
201        let print = Print::new(quiet);
202        let config = config.unwrap_or(&self.config);
203        let wasm_hash = if let Some(wasm) = &self.wasm {
204            let is_build = self.fee.build_only;
205            let hash = if is_build {
206                wasm::Args { wasm: wasm.clone() }.hash()?
207            } else {
208                upload::Cmd {
209                    wasm: wasm::Args { wasm: wasm.clone() },
210                    config: config.clone(),
211                    fee: self.fee.clone(),
212                    ignore_checks: self.ignore_checks,
213                }
214                .run_against_rpc_server(global_args, Some(config))
215                .await?
216                .into_result()
217                .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
218            };
219            hex::encode(hash)
220        } else {
221            self.wasm_hash
222                .as_ref()
223                .ok_or(Error::WasmNotProvided)?
224                .clone()
225        };
226
227        let wasm_hash = Hash(
228            utils::contract_id_from_str(&wasm_hash)
229                .map_err(|e| Error::CannotParseWasmHash {
230                    wasm_hash: wasm_hash.clone(),
231                    error: e,
232                })?
233                .0,
234        );
235
236        print.infoln(format!("Using wasm hash {wasm_hash}").as_str());
237
238        let network = config.get_network()?;
239        let salt: [u8; 32] = match &self.salt {
240            Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
241                .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
242                .try_into()
243                .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
244            None => rand::thread_rng().gen::<[u8; 32]>(),
245        };
246
247        let client = network.rpc_client()?;
248        let MuxedAccount::Ed25519(bytes) = config.source_account().await? else {
249            return Err(Error::OnlyEd25519AccountsAllowed);
250        };
251        let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
252        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
253            address: ScAddress::Account(source_account.clone()),
254            salt: Uint256(salt),
255        });
256        let contract_id =
257            get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
258        let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
259            wasm::Args { wasm: wasm.clone() }.read()?
260        } else {
261            if self.fee.build_only {
262                return Err(Error::WasmNotProvided);
263            }
264            get_remote_wasm_from_hash(&client, &wasm_hash).await?
265        };
266        let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
267        let res = soroban_spec_tools::Spec::new(entries.clone().as_slice());
268        let constructor_params = if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
269            if func.inputs.is_empty() {
270                None
271            } else {
272                let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
273                slop.extend_from_slice(&self.slop);
274                Some(
275                    arg_parsing::build_constructor_parameters(
276                        &stellar_strkey::Contract(contract_id.0),
277                        &slop,
278                        &entries,
279                        config,
280                    )
281                    .await?
282                    .2,
283                )
284            }
285        } else {
286            None
287        };
288
289        // For network operations, verify the network passphrase
290        client
291            .verify_network_passphrase(Some(&network.network_passphrase))
292            .await?;
293
294        // Get the account sequence number
295        let account_details = client.get_account(&source_account.to_string()).await?;
296        let sequence: i64 = account_details.seq_num.into();
297        let txn = Box::new(build_create_contract_tx(
298            wasm_hash,
299            sequence + 1,
300            self.fee.fee,
301            source_account,
302            contract_id_preimage,
303            constructor_params.as_ref(),
304        )?);
305
306        if self.fee.build_only {
307            print.checkln("Transaction built!");
308            return Ok(TxnResult::Txn(txn));
309        }
310
311        print.infoln("Simulating deploy transaction…");
312
313        let assembled =
314            simulate_and_assemble_transaction(&client, &txn, self.fee.resource_config()).await?;
315        let assembled = self.fee.apply_to_assembled_txn(assembled);
316
317        let txn = Box::new(assembled.transaction().clone());
318
319        print.log_transaction(&txn, &network, true)?;
320        let signed_txn = &config.sign(*txn, quiet).await?;
321        print.globeln("Submitting deploy transaction…");
322
323        let get_txn_resp = client.send_transaction_polling(signed_txn).await?;
324
325        self.fee.print_cost_info(&get_txn_resp)?;
326
327        if global_args.is_none_or(|a| !a.no_cache) {
328            data::write(get_txn_resp.clone().try_into()?, &network.rpc_uri()?)?;
329        }
330
331        if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
332            print.linkln(url);
333        }
334
335        print.checkln("Deployed!");
336
337        Ok(TxnResult::Res(contract_id))
338    }
339}
340
341fn build_create_contract_tx(
342    wasm_hash: Hash,
343    sequence: i64,
344    fee: u32,
345    key: AccountId,
346    contract_id_preimage: ContractIdPreimage,
347    constructor_params: Option<&InvokeContractArgs>,
348) -> Result<Transaction, Error> {
349    let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
350        Operation {
351            source_account: None,
352            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
353                host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
354                    contract_id_preimage,
355                    executable: ContractExecutable::Wasm(wasm_hash),
356                    constructor_args: args.clone(),
357                }),
358                auth: VecM::default(),
359            }),
360        }
361    } else {
362        Operation {
363            source_account: None,
364            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
365                host_function: HostFunction::CreateContract(CreateContractArgs {
366                    contract_id_preimage,
367                    executable: ContractExecutable::Wasm(wasm_hash),
368                }),
369                auth: VecM::default(),
370            }),
371        }
372    };
373    let tx = Transaction {
374        source_account: key.into(),
375        fee,
376        seq_num: SequenceNumber(sequence),
377        cond: Preconditions::None,
378        memo: Memo::None,
379        operations: vec![op].try_into()?,
380        ext: TransactionExt::V0,
381    };
382
383    Ok(tx)
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_build_create_contract() {
392        let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
393            .unwrap()
394            .try_into()
395            .unwrap();
396        let salt = [0u8; 32];
397        let key =
398            &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
399                .unwrap();
400        let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
401            key.verifying_key().to_bytes(),
402        )));
403
404        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
405            address: ScAddress::Account(source_account.clone()),
406            salt: Uint256(salt),
407        });
408
409        let result = build_create_contract_tx(
410            Hash(hash),
411            300,
412            1,
413            source_account,
414            contract_id_preimage,
415            None,
416        );
417
418        assert!(result.is_ok());
419    }
420}