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