Skip to main content

stellar_registry_cli/commands/
deploy.rs

1#![allow(dead_code)]
2use std::{ffi::OsString, path::PathBuf};
3
4use clap::Parser;
5use soroban_rpc as rpc;
6pub use soroban_spec_tools::contract as contract_spec;
7use stellar_cli::{
8    assembled::simulate_and_assemble_transaction,
9    commands::contract::invoke,
10    config::{self, UnresolvedMuxedAccount},
11    utils::rpc::get_remote_wasm_from_hash,
12    xdr::{self, AccountId, InvokeContractArgs, ScSpecEntry, ScString, ScVal, Uint256},
13};
14use stellar_registry_build::{named_registry::PrefixedName, registry::Registry};
15
16use crate::commands::global;
17
18pub mod util;
19
20#[derive(Parser, Debug, Clone)]
21pub struct Cmd {
22    /// Name of contract to be deployed. Can use prefix of not using verified registry.
23    /// E.g. `unverified/<name>`
24    #[arg(long, visible_alias = "deploy-as")]
25    pub contract_name: PrefixedName,
26    /// Name of published contract to deploy from. Can use prefix of not using verified registry.
27    /// E.g. `unverified/<name>`
28    #[arg(long)]
29    pub wasm_name: PrefixedName,
30    /// Arguments for constructor
31    #[arg(last = true, id = "CONSTRUCTOR_ARGS")]
32    pub slop: Vec<OsString>,
33    /// Version of the wasm to deploy
34    #[arg(long)]
35    pub version: Option<String>,
36    /// Optional deployer, by default is registry contract itself
37    #[arg(long)]
38    pub deployer: Option<UnresolvedMuxedAccount>,
39    #[command(flatten)]
40    pub config: global::Args,
41}
42
43#[derive(thiserror::Error, Debug)]
44pub enum Error {
45    #[error(transparent)]
46    Invoke(#[from] invoke::Error),
47    #[error(transparent)]
48    Io(#[from] std::io::Error),
49    #[error(transparent)]
50    Install(#[from] super::create_alias::Error),
51    #[error(transparent)]
52    Rpc(#[from] rpc::Error),
53    #[error(transparent)]
54    SpecTools(#[from] soroban_spec_tools::Error),
55    #[error(transparent)]
56    Config(#[from] config::Error),
57    #[error(transparent)]
58    ConfigAddress(#[from] config::address::Error),
59    #[error(transparent)]
60    Xdr(#[from] xdr::Error),
61    #[error("Cannot parse contract spec")]
62    CannotParseContractSpec,
63    #[error("argument count ({current}) surpasses maximum allowed count ({maximum})")]
64    MaxNumberOfArgumentsReached { current: usize, maximum: usize },
65    #[error("function {0} was not found in the contract")]
66    FunctionNotFoundInContractSpec(String),
67    #[error("parsing argument {arg}: {error}")]
68    CannotParseArg {
69        arg: String,
70        error: stellar_cli::commands::contract::arg_parsing::Error,
71    },
72    #[error("function name {0} is too long")]
73    FunctionNameTooLong(String),
74    #[error("Missing file argument {0:#?}")]
75    MissingFileArg(PathBuf),
76    #[error("Missing argument {0}")]
77    MissingArgument(String),
78    #[error("Constructor help message: {0}")]
79    ConstructorHelpMessage(String),
80    #[error("{0}")]
81    InvalidReturnValue(String),
82    #[error(transparent)]
83    Registry(#[from] stellar_registry_build::Error),
84}
85
86impl Cmd {
87    pub async fn run(&self) -> Result<(), Error> {
88        match self.invoke().await {
89            Ok(contract_id) => {
90                println!(
91                    "Contract {} deployed successfully to {contract_id}",
92                    self.contract_name.name
93                );
94                Ok(())
95            }
96            Err(Error::ConstructorHelpMessage(help)) => {
97                println!("Constructor help message:\n{help}");
98                Ok(())
99            }
100            Err(e) => Err(e),
101        }
102    }
103
104    pub async fn hash(&self, registry: &Registry) -> Result<xdr::Hash, Error> {
105        let res = registry
106            .as_contract()
107            .invoke_with_result(&["fetch_hash", "--wasm_name", &self.wasm_name.name], true)
108            .await?;
109        let res = res.trim_matches('"');
110        Ok(res.parse().unwrap())
111    }
112
113    pub async fn wasm(&self, registry: &Registry) -> Result<Vec<u8>, Error> {
114        Ok(
115            get_remote_wasm_from_hash(&self.config.rpc_client()?, &self.hash(registry).await?)
116                .await?,
117        )
118    }
119
120    pub async fn spec_entries(&self, registry: &Registry) -> Result<Vec<ScSpecEntry>, Error> {
121        Ok(contract_spec::Spec::new(&self.wasm(registry).await?)
122            .map_err(|_| Error::CannotParseContractSpec)?
123            .spec)
124    }
125
126    async fn invoke(&self) -> Result<stellar_strkey::Contract, Error> {
127        let registry = self.contract_name.registry(&self.config).await?;
128        let client = self.config.rpc_client()?;
129        let key = self.config.key_pair()?;
130        let config = &self.config;
131
132        let contract_address = registry.as_contract().sc_address();
133        let contract_id = &registry.as_contract().id();
134        let spec_entries = self.spec_entries(&registry).await?;
135        let (args, signers) =
136            util::find_args_and_signers(contract_id, self.slop.clone(), &spec_entries).await?;
137        let deployer = if let Some(deployer) = &self.deployer {
138            Some(
139                deployer
140                    .resolve_muxed_account(&self.config.locator, None)
141                    .await?,
142            )
143        } else {
144            None
145        };
146        let invoke_contract_args = InvokeContractArgs {
147            contract_address: contract_address.clone(),
148            function_name: "deploy".try_into().unwrap(),
149            args: [
150                ScVal::String(ScString(self.wasm_name.name.clone().try_into().unwrap())),
151                self.version.clone().map_or(ScVal::Void, |s| {
152                    ScVal::String(ScString(s.try_into().unwrap()))
153                }),
154                ScVal::String(ScString(
155                    self.contract_name.name.clone().try_into().unwrap(),
156                )),
157                ScVal::Address(xdr::ScAddress::Account(AccountId(
158                    xdr::PublicKey::PublicKeyTypeEd25519(Uint256(key.verifying_key().to_bytes())),
159                ))),
160                args,
161                deployer.map_or(ScVal::Void, |muxed_account| {
162                    ScVal::Address(xdr::ScAddress::Account(muxed_account.account_id()))
163                }),
164            ]
165            .try_into()
166            .unwrap(),
167        };
168
169        // Get the account sequence number
170        let public_strkey =
171            stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string();
172        let account_details = client.get_account(&public_strkey).await?;
173        let sequence: i64 = account_details.seq_num.into();
174        let tx = util::build_invoke_contract_tx(invoke_contract_args, sequence + 1, 100, &key)?;
175        let assembled = simulate_and_assemble_transaction(&client, &tx, None, None).await?;
176        let mut txn = assembled.transaction().clone();
177        txn = config
178            .sign_soroban_authorizations(&txn, &signers)
179            .await?
180            .unwrap_or(txn);
181        let return_value = client
182            .send_transaction_polling(&config.sign(txn, false).await?)
183            .await?
184            .return_value()?;
185        match return_value {
186            ScVal::Address(xdr::ScAddress::Contract(xdr::ContractId(hash))) => {
187                Ok(stellar_strkey::Contract(hash.0))
188            }
189            _ => Err(Error::InvalidReturnValue(
190                "{return_value:#?} is not a contract address".to_string(),
191            )),
192        }
193    }
194}