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