Skip to main content

soroban_cli/commands/contract/deploy/
wasm.rs

1use std::array::TryFromSliceError;
2use std::ffi::OsString;
3use std::fmt::Debug;
4use std::num::ParseIntError;
5
6use clap::Parser;
7use rand::Rng;
8use soroban_spec_tools::contract as contract_spec;
9
10use crate::config::address::AliasName;
11use crate::resources;
12use crate::tx::sim_sign_and_send_tx;
13use crate::xdr::{
14    AccountId, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress,
15    CreateContractArgs, CreateContractArgsV2, Error as XdrError, Hash, HostFunction,
16    InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo, MuxedAccount, Operation, OperationBody,
17    Preconditions, PublicKey, ScAddress, SequenceNumber, Transaction, TransactionExt, Uint256,
18    VecM, WriteXdr,
19};
20
21use crate::commands::tx::fetch;
22use crate::{
23    commands::{
24        contract::{self, arg_parsing, build, id::wasm::get_contract_id, upload},
25        global,
26        txn_result::{TxnEnvelopeResult, TxnResult},
27        HEADING_TRANSACTION,
28    },
29    config::{self, data, locator, network},
30    print::Print,
31    rpc,
32    utils::{self, rpc::get_remote_wasm_from_hash},
33    wasm,
34};
35
36pub const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
37
38#[derive(Parser, Debug, Clone)]
39#[command(group(
40    clap::ArgGroup::new("wasm_src")
41        .required(false)
42        .args(&["wasm", "wasm_hash"]),
43))]
44#[group(skip)]
45pub struct Cmd {
46    /// WASM file to deploy. When neither --wasm nor --wasm-hash is provided
47    /// inside a Cargo workspace, builds the project automatically. One of
48    /// --wasm or --wasm-hash is required when outside a Cargo workspace.
49    #[arg(long, group = "wasm_src")]
50    pub wasm: Option<std::path::PathBuf>,
51    /// Hash of the already installed/deployed WASM file
52    #[arg(long = "wasm-hash", conflicts_with = "wasm", group = "wasm_src")]
53    pub wasm_hash: Option<String>,
54    /// Custom salt 32-byte salt for the token id
55    #[arg(long)]
56    pub salt: Option<String>,
57    #[command(flatten)]
58    pub config: config::Args,
59    #[arg(long, short = 'i', default_value = "false")]
60    /// Whether to ignore safety checks when deploying contracts
61    pub ignore_checks: bool,
62    /// The alias that will be used to save the contract's id.
63    /// Whenever used, `--alias` will always overwrite the existing contract id
64    /// configuration without asking for confirmation.
65    #[arg(long)]
66    pub alias: Option<AliasName>,
67    #[command(flatten)]
68    pub resources: resources::Args,
69    /// Build the transaction and only write the base64 xdr to stdout
70    #[arg(long, help_heading = HEADING_TRANSACTION)]
71    pub build_only: bool,
72    /// If provided, will be passed to the contract's `__constructor` function with provided arguments for that function as `--arg-name value`
73    #[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")]
74    pub slop: Vec<OsString>,
75    /// Package to build when auto-building without --wasm
76    #[arg(long, help_heading = "Build Options", conflicts_with = "wasm_src")]
77    pub package: Option<String>,
78    #[command(flatten)]
79    pub build_args: build::BuildArgs,
80}
81
82#[derive(thiserror::Error, Debug)]
83pub enum Error {
84    #[error(transparent)]
85    Install(#[from] upload::Error),
86
87    #[error("error parsing int: {0}")]
88    ParseIntError(#[from] ParseIntError),
89
90    #[error("internal conversion error: {0}")]
91    TryFromSliceError(#[from] TryFromSliceError),
92
93    #[error("xdr processing error: {0}")]
94    Xdr(#[from] XdrError),
95
96    #[error("cannot parse salt: {salt}")]
97    CannotParseSalt { salt: String },
98
99    #[error("cannot parse contract ID {contract_id}: {error}")]
100    CannotParseContractId {
101        contract_id: String,
102        error: stellar_strkey::DecodeError,
103    },
104
105    #[error("cannot parse WASM hash {wasm_hash}: {error}")]
106    CannotParseWasmHash {
107        wasm_hash: String,
108        error: stellar_strkey::DecodeError,
109    },
110
111    #[error("Must provide either --wasm or --wasm-hash")]
112    WasmNotProvided,
113
114    #[error(transparent)]
115    Rpc(#[from] rpc::Error),
116
117    #[error(transparent)]
118    Config(#[from] config::Error),
119
120    #[error(transparent)]
121    StrKey(#[from] stellar_strkey::DecodeError),
122
123    #[error(transparent)]
124    Infallible(#[from] std::convert::Infallible),
125
126    #[error(transparent)]
127    WasmId(#[from] contract::id::wasm::Error),
128
129    #[error(transparent)]
130    Data(#[from] data::Error),
131
132    #[error(transparent)]
133    Network(#[from] network::Error),
134
135    #[error(transparent)]
136    Wasm(#[from] wasm::Error),
137
138    #[error(transparent)]
139    Locator(#[from] locator::Error),
140
141    #[error(transparent)]
142    ContractSpec(#[from] contract_spec::Error),
143
144    #[error(transparent)]
145    ArgParse(#[from] arg_parsing::Error),
146
147    #[error("Only ed25519 accounts are allowed")]
148    OnlyEd25519AccountsAllowed,
149
150    #[error(transparent)]
151    Fee(#[from] fetch::fee::Error),
152
153    #[error(transparent)]
154    Fetch(#[from] fetch::Error),
155
156    #[error(transparent)]
157    Build(#[from] build::Error),
158
159    #[error("no buildable contracts found in workspace (no packages with crate-type cdylib)")]
160    NoBuildableContracts,
161
162    #[error("--alias is not supported when deploying multiple contracts; aliases are derived from package names automatically")]
163    AliasNotSupported,
164
165    #[error("--salt is not supported when deploying multiple contracts")]
166    SaltNotSupported,
167
168    #[error("constructor arguments are not supported when deploying multiple contracts")]
169    ConstructorArgsNotSupported,
170
171    #[error("--build-only is not supported without --wasm or --wasm-hash")]
172    BuildOnlyNotSupported,
173
174    #[error(
175        "--wasm or --wasm-hash is required when not in a Cargo workspace; no Cargo.toml found"
176    )]
177    NotInCargoProject,
178}
179
180impl Cmd {
181    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
182        if self.build_only && self.wasm.is_none() && self.wasm_hash.is_none() {
183            return Err(Error::BuildOnlyNotSupported);
184        }
185
186        let built_contracts = self.resolve_contracts(global_args)?;
187
188        // When --wasm-hash is used, no built contracts are returned.
189        // Deploy directly with the hash.
190        if built_contracts.is_empty() {
191            Self::run_single(self, global_args).await?;
192        } else {
193            if built_contracts.len() > 1 {
194                if self.alias.is_some() {
195                    return Err(Error::AliasNotSupported);
196                }
197
198                if self.salt.is_some() {
199                    return Err(Error::SaltNotSupported);
200                }
201
202                if !self.slop.is_empty() {
203                    return Err(Error::ConstructorArgsNotSupported);
204                }
205            }
206
207            for contract in &built_contracts {
208                let mut cmd = self.clone();
209                cmd.wasm = Some(contract.path.clone());
210
211                // When auto-building and no explicit --alias, use the
212                // package name as alias.
213                if cmd.alias.is_none() && !contract.name.is_empty() {
214                    if let Ok(alias) = contract.name.parse::<AliasName>() {
215                        cmd.alias = Some(alias);
216                    }
217                }
218
219                Self::run_single(&cmd, global_args).await?;
220            }
221        }
222        Ok(())
223    }
224
225    async fn run_single(cmd: &Cmd, global_args: &global::Args) -> Result<(), Error> {
226        let res = cmd
227            .execute(&cmd.config, global_args.quiet, global_args.no_cache)
228            .await?
229            .to_envelope();
230
231        match res {
232            TxnEnvelopeResult::TxnEnvelope(tx) => {
233                println!("{}", tx.to_xdr_base64(Limits::none())?);
234            }
235            TxnEnvelopeResult::Res(contract) => {
236                let network = cmd.config.get_network()?;
237
238                if let Some(alias) = cmd.alias.clone() {
239                    if let Some(existing_contract) = cmd
240                        .config
241                        .locator
242                        .get_contract_id(&alias, &network.network_passphrase)?
243                    {
244                        let print = Print::new(global_args.quiet);
245                        print.warnln(format!(
246                            "Overwriting existing alias '{alias}' that currently links to contract ID: {existing_contract}"
247                        ));
248                    }
249
250                    cmd.config.locator.save_contract_id(
251                        &network.network_passphrase,
252                        &contract,
253                        &alias,
254                    )?;
255                }
256
257                println!("{contract}");
258            }
259        }
260        Ok(())
261    }
262
263    fn resolve_contracts(
264        &self,
265        global_args: &global::Args,
266    ) -> Result<Vec<build::BuiltContract>, Error> {
267        // If --wasm is explicitly provided, use it (no package name available)
268        if let Some(wasm) = &self.wasm {
269            return Ok(vec![build::BuiltContract {
270                name: String::new(),
271                path: wasm.clone(),
272            }]);
273        }
274
275        // If --wasm-hash is provided, no WASM file paths needed
276        if self.wasm_hash.is_some() {
277            return Ok(vec![]);
278        }
279
280        // Neither provided: auto-build
281        let build_cmd = build::Cmd {
282            package: self.package.clone(),
283            build_args: self.build_args.clone(),
284            ..build::Cmd::default()
285        };
286        let contracts = build_cmd.run(global_args).map_err(|e| match e {
287            build::Error::Metadata(_) => Error::NotInCargoProject,
288            other => other.into(),
289        })?;
290
291        if contracts.is_empty() {
292            return Err(Error::NoBuildableContracts);
293        }
294
295        Ok(contracts)
296    }
297
298    #[allow(clippy::too_many_lines)]
299    #[allow(unused_variables)]
300    pub async fn execute(
301        &self,
302        config: &config::Args,
303        quiet: bool,
304        no_cache: bool,
305    ) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
306        let print = Print::new(quiet);
307        let wasm_hash = if let Some(wasm) = &self.wasm {
308            let is_build = self.build_only;
309            let hash = if is_build {
310                wasm::Args { wasm: wasm.clone() }.hash()?
311            } else {
312                print.infoln("Uploading contract WASM…");
313                upload::Cmd {
314                    wasm: Some(wasm.clone()),
315                    config: config.clone(),
316                    resources: self.resources.clone(),
317                    ignore_checks: self.ignore_checks,
318                    build_only: is_build,
319                    package: None,
320                    build_args: build::BuildArgs::default(),
321                }
322                .execute(config, quiet, no_cache)
323                .await?
324                .into_result()
325                .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
326            };
327            hex::encode(hash)
328        } else {
329            self.wasm_hash
330                .as_ref()
331                .ok_or(Error::WasmNotProvided)?
332                .clone()
333        };
334
335        let wasm_hash = Hash(
336            utils::contract_id_from_str(&wasm_hash)
337                .map_err(|e| Error::CannotParseWasmHash {
338                    wasm_hash: wasm_hash.clone(),
339                    error: e,
340                })?
341                .0,
342        );
343
344        print.infoln(format!("Deploying contract using wasm hash {wasm_hash}").as_str());
345
346        let network = config.get_network()?;
347        let salt: [u8; 32] = match &self.salt {
348            Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
349                .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
350                .try_into()
351                .map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
352            None => rand::thread_rng().gen::<[u8; 32]>(),
353        };
354
355        let client = network.rpc_client()?;
356        let MuxedAccount::Ed25519(bytes) = config.source_account()? else {
357            return Err(Error::OnlyEd25519AccountsAllowed);
358        };
359        let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
360        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
361            address: ScAddress::Account(source_account.clone()),
362            salt: Uint256(salt),
363        });
364        let contract_id =
365            get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
366        let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
367            wasm::Args { wasm: wasm.clone() }.read()?
368        } else {
369            if self.build_only {
370                return Err(Error::WasmNotProvided);
371            }
372            get_remote_wasm_from_hash(&client, &wasm_hash).await?
373        };
374        let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
375        let res = soroban_spec_tools::Spec::new(entries.clone().as_slice());
376        let constructor_params = if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
377            if func.inputs.is_empty() {
378                None
379            } else {
380                let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
381                slop.extend_from_slice(&self.slop);
382                Some(
383                    arg_parsing::build_constructor_parameters(
384                        &stellar_strkey::Contract(contract_id.0),
385                        &slop,
386                        &entries,
387                        config,
388                    )?
389                    .2,
390                )
391            }
392        } else {
393            None
394        };
395
396        // For network operations, verify the network passphrase
397        client
398            .verify_network_passphrase(Some(&network.network_passphrase))
399            .await?;
400
401        // Get the account sequence number
402        let account_details = client.get_account(&source_account.to_string()).await?;
403        let sequence: i64 = account_details.seq_num.into();
404        let txn = Box::new(build_create_contract_tx(
405            wasm_hash,
406            sequence + 1,
407            config.get_inclusion_fee()?,
408            source_account,
409            contract_id_preimage,
410            constructor_params.as_ref(),
411        )?);
412
413        if self.build_only {
414            print.checkln("Transaction built!");
415            return Ok(TxnResult::Txn(txn));
416        }
417
418        sim_sign_and_send_tx::<Error>(&client, &txn, config, &self.resources, &[], quiet, no_cache)
419            .await?;
420
421        if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
422            print.linkln(url);
423        }
424        print.checkln("Deployed!");
425
426        Ok(TxnResult::Res(contract_id))
427    }
428}
429
430fn build_create_contract_tx(
431    wasm_hash: Hash,
432    sequence: i64,
433    fee: u32,
434    key: AccountId,
435    contract_id_preimage: ContractIdPreimage,
436    constructor_params: Option<&InvokeContractArgs>,
437) -> Result<Transaction, Error> {
438    let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
439        Operation {
440            source_account: None,
441            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
442                host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
443                    contract_id_preimage,
444                    executable: ContractExecutable::Wasm(wasm_hash),
445                    constructor_args: args.clone(),
446                }),
447                auth: VecM::default(),
448            }),
449        }
450    } else {
451        Operation {
452            source_account: None,
453            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
454                host_function: HostFunction::CreateContract(CreateContractArgs {
455                    contract_id_preimage,
456                    executable: ContractExecutable::Wasm(wasm_hash),
457                }),
458                auth: VecM::default(),
459            }),
460        }
461    };
462    let tx = Transaction {
463        source_account: key.into(),
464        fee,
465        seq_num: SequenceNumber(sequence),
466        cond: Preconditions::None,
467        memo: Memo::None,
468        operations: vec![op].try_into()?,
469        ext: TransactionExt::V0,
470    };
471
472    Ok(tx)
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn test_build_create_contract() {
481        let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
482            .unwrap()
483            .try_into()
484            .unwrap();
485        let salt = [0u8; 32];
486        let key =
487            &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
488                .unwrap();
489        let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
490            key.verifying_key().to_bytes(),
491        )));
492
493        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
494            address: ScAddress::Account(source_account.clone()),
495            salt: Uint256(salt),
496        });
497
498        let result = build_create_contract_tx(
499            Hash(hash),
500            300,
501            1,
502            source_account,
503            contract_id_preimage,
504            None,
505        );
506
507        assert!(result.is_ok());
508    }
509}