stellar_registry_cli/commands/
deploy.rs

1#![allow(dead_code)]
2use std::{ffi::OsString, path::PathBuf};
3
4use clap::Parser;
5use ed25519_dalek::SigningKey;
6
7use soroban_sdk::xdr::{
8    self, AccountId, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo, MuxedAccount,
9    Operation, OperationBody, Preconditions, ScSpecEntry, ScString, ScVal, SequenceNumber,
10    Transaction, TransactionExt, Uint256, VecM,
11};
12use stellar_cli::{
13    assembled::simulate_and_assemble_transaction,
14    commands::contract::{arg_parsing, invoke},
15    config, fee,
16    utils::rpc::get_remote_wasm_from_hash,
17};
18
19use soroban_rpc as rpc;
20pub use soroban_spec_tools::contract as contract_spec;
21
22use crate::contract::NetworkContract;
23
24#[derive(Parser, Debug, Clone)]
25pub struct Cmd {
26    /// Name of contract to be deployed
27    #[arg(long, visible_alias = "deploy-as")]
28    pub contract_name: String,
29    /// Name of published contract to deploy from
30    #[arg(long)]
31    pub wasm_name: String,
32    /// Arguments for constructor
33    #[arg(last = true, id = "CONSTRUCTOR_ARGS")]
34    pub slop: Vec<OsString>,
35
36    /// Version of the wasm to deploy
37    #[arg(long)]
38    pub version: Option<String>,
39
40    #[command(flatten)]
41    pub config: config::Args,
42    #[command(flatten)]
43    pub fee: fee::Args,
44}
45
46#[derive(thiserror::Error, Debug)]
47pub enum Error {
48    #[error(transparent)]
49    Invoke(#[from] invoke::Error),
50    #[error(transparent)]
51    Io(#[from] std::io::Error),
52    #[error(transparent)]
53    StellarBuild(#[from] stellar_build::Error),
54    #[error(transparent)]
55    Install(#[from] super::install::Error),
56    #[error(transparent)]
57    Rpc(#[from] rpc::Error),
58    #[error(transparent)]
59    SpecTools(#[from] soroban_spec_tools::Error),
60    #[error(transparent)]
61    Config(#[from] config::Error),
62    #[error(transparent)]
63    Xdr(#[from] xdr::Error),
64    #[error("Cannot parse contract spec")]
65    CannotParseContractSpec,
66    #[error("argument count ({current}) surpasses maximum allowed count ({maximum})")]
67    MaxNumberOfArgumentsReached { current: usize, maximum: usize },
68    #[error("function {0} was not found in the contract")]
69    FunctionNotFoundInContractSpec(String),
70    #[error("parsing argument {arg}: {error}")]
71    CannotParseArg {
72        arg: String,
73        error: stellar_cli::commands::contract::arg_parsing::Error,
74    },
75    #[error("function name {0} is too long")]
76    FunctionNameTooLong(String),
77    #[error("Missing file argument {0:#?}")]
78    MissingFileArg(PathBuf),
79    #[error("Missing argument {0}")]
80    MissingArgument(String),
81}
82
83impl Cmd {
84    pub async fn run(&self) -> Result<(), Error> {
85        self.invoke().await?;
86        Ok(())
87    }
88
89    pub async fn hash(&self) -> Result<xdr::Hash, Error> {
90        let res = self
91            .config
92            .invoke_registry(
93                &["fetch_hash", "--wasm_name", &self.wasm_name],
94                Some(&self.fee),
95                true,
96            )
97            .await?;
98        let res = res.trim_matches('"');
99        Ok(res.parse().unwrap())
100    }
101
102    pub async fn wasm(&self) -> Result<Vec<u8>, Error> {
103        Ok(get_remote_wasm_from_hash(&self.config.rpc_client()?, &self.hash().await?).await?)
104    }
105
106    pub async fn spec_entries(&self) -> Result<Vec<ScSpecEntry>, Error> {
107        Ok(contract_spec::Spec::new(&self.wasm().await?)
108            .map_err(|_| Error::CannotParseContractSpec)?
109            .spec)
110    }
111
112    async fn invoke(&self) -> Result<(), Error> {
113        let client = self.config.rpc_client()?;
114        let key = self.config.key_pair()?;
115        let config = &self.config;
116
117        // Get the account sequence number
118        let public_strkey =
119            stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string();
120
121        let contract_address = self.config.contract_sc_address()?;
122        let contract_id = &self.config.contract_id()?;
123        let spec_entries = self.spec_entries().await?;
124
125        let (args, signers) = if self.slop.is_empty() {
126            (ScVal::Void, vec![])
127        } else {
128            let res = arg_parsing::build_host_function_parameters(
129                contract_id,
130                &self.slop,
131                &spec_entries,
132                &config::Args::default(),
133            );
134            match res {
135                Ok((_, _, host_function_params, signers)) => {
136                    if host_function_params.function_name.len() > 64 {
137                        return Err(Error::FunctionNameTooLong(
138                            host_function_params.function_name.to_string(),
139                        ));
140                    }
141                    let args = ScVal::Vec(Some(
142                        vec![
143                            ScVal::Symbol(host_function_params.function_name),
144                            ScVal::Vec(Some(host_function_params.args.into())),
145                        ]
146                        .try_into()
147                        .unwrap(),
148                    ));
149                    (args, signers)
150                }
151                Err(arg_parsing::Error::HelpMessage(help)) => {
152                    println!("{help}");
153                    return Ok(());
154                }
155                Err(e) => {
156                    return Err(Error::CannotParseArg {
157                        arg: self
158                            .slop
159                            .iter()
160                            .map(|s| s.to_string_lossy())
161                            .collect::<Vec<_>>()
162                            .join(" "),
163                        error: e,
164                    });
165                }
166            }
167        };
168
169        let invoke_contract_args = InvokeContractArgs {
170            contract_address: contract_address.clone(),
171            function_name: "deploy".try_into().unwrap(),
172            args: [
173                ScVal::String(ScString(self.wasm_name.clone().try_into().unwrap())),
174                self.version.clone().map_or(ScVal::Void, |s| {
175                    ScVal::String(ScString(s.try_into().unwrap()))
176                }),
177                ScVal::String(ScString(self.contract_name.clone().try_into().unwrap())),
178                ScVal::Address(xdr::ScAddress::Account(AccountId(
179                    xdr::PublicKey::PublicKeyTypeEd25519(Uint256(key.verifying_key().to_bytes())),
180                ))),
181                args,
182            ]
183            .try_into()
184            .unwrap(),
185        };
186
187        let account_details = client.get_account(&public_strkey).await?;
188        let sequence: i64 = account_details.seq_num.into();
189        let tx = build_invoke_contract_tx(invoke_contract_args, sequence + 1, self.fee.fee, &key)?;
190        let assembled = simulate_and_assemble_transaction(&client, &tx).await?;
191        let mut txn = assembled.transaction().clone();
192        txn = config
193            .sign_soroban_authorizations(&txn, &signers)
194            .await?
195            .unwrap_or(txn);
196        let res = client
197            .send_transaction_polling(&config.sign_with_local_key(txn).await?)
198            .await?;
199
200        let return_value = res.return_value()?;
201        println!("{return_value:#?}");
202        Ok(())
203    }
204}
205
206fn build_invoke_contract_tx(
207    parameters: InvokeContractArgs,
208    sequence: i64,
209    fee: u32,
210    key: &SigningKey,
211) -> Result<Transaction, Error> {
212    let op = Operation {
213        source_account: None,
214        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
215            host_function: HostFunction::InvokeContract(parameters),
216            auth: VecM::default(),
217        }),
218    };
219    Ok(Transaction {
220        source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())),
221        fee,
222        seq_num: SequenceNumber(sequence),
223        cond: Preconditions::None,
224        memo: Memo::None,
225        operations: vec![op].try_into()?,
226        ext: TransactionExt::V0,
227    })
228}