stellar_registry_cli/commands/
deploy.rs

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