soroban_cli/commands/contract/deploy/
wasm.rs

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