Skip to main content

stellar_registry_cli/commands/
deploy_unnamed.rs

1use std::ffi::OsString;
2
3use clap::Parser;
4use rand::Rng;
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, InvokeContractArgs, ScSpecEntry, ScString, ScVal, Uint256},
13};
14use stellar_registry_build::{named_registry::PrefixedName, registry::Registry};
15
16use crate::commands::global;
17
18use super::deploy::util;
19
20#[derive(Parser, Debug, Clone)]
21pub struct Cmd {
22    /// Name of published wasm to deploy from. Can use prefix if not using verified registry.
23    /// E.g. `unverified/<name>`
24    #[arg(long)]
25    pub wasm_name: PrefixedName,
26
27    /// Arguments for constructor
28    #[arg(last = true, id = "CONSTRUCTOR_ARGS")]
29    pub slop: Vec<OsString>,
30
31    /// Version of the wasm to deploy
32    #[arg(long)]
33    pub version: Option<String>,
34
35    /// Optional salt for deterministic contract address (hex-encoded 32 bytes)
36    #[arg(long)]
37    pub salt: Option<String>,
38
39    /// Deployer account
40    #[arg(long)]
41    pub deployer: Option<UnresolvedMuxedAccount>,
42
43    #[command(flatten)]
44    pub config: global::Args,
45}
46
47#[derive(thiserror::Error, Debug)]
48pub enum Error {
49    #[error(transparent)]
50    Deploy(#[from] super::deploy::Error),
51    #[error(transparent)]
52    Invoke(#[from] invoke::Error),
53    #[error(transparent)]
54    Io(#[from] std::io::Error),
55    #[error(transparent)]
56    Rpc(#[from] rpc::Error),
57    #[error(transparent)]
58    SpecTools(#[from] soroban_spec_tools::Error),
59    #[error(transparent)]
60    Config(#[from] config::Error),
61    #[error(transparent)]
62    ConfigAddress(#[from] config::address::Error),
63    #[error(transparent)]
64    Xdr(#[from] xdr::Error),
65    #[error("Cannot parse contract spec")]
66    CannotParseContractSpec,
67    #[error("Constructor help message: {0}")]
68    ConstructorHelpMessage(String),
69    #[error("{0}")]
70    InvalidReturnValue(String),
71    #[error(transparent)]
72    Registry(#[from] stellar_registry_build::Error),
73}
74
75impl Cmd {
76    pub async fn run(&self) -> Result<(), Error> {
77        match self.invoke().await {
78            Ok(contract_id) => {
79                println!("Contract deployed successfully to {contract_id}");
80                Ok(())
81            }
82            Err(Error::ConstructorHelpMessage(help)) => {
83                println!("Constructor help message:\n{help}");
84                Ok(())
85            }
86            Err(e) => Err(e),
87        }
88    }
89
90    pub async fn hash(&self, registry: &Registry) -> Result<xdr::Hash, Error> {
91        let mut slop = vec!["fetch_hash", "--wasm_name", &self.wasm_name.name];
92        let version = self.version.clone().map(|v| format!("\"{v}\""));
93        if let Some(version) = version.as_deref() {
94            slop.push("--version");
95            slop.push(version);
96        }
97        let res = registry
98            .as_contract()
99            .invoke_with_result(&slop, true)
100            .await?;
101        let res = res.trim_matches('"');
102        Ok(res.parse().unwrap())
103    }
104
105    pub async fn wasm(&self, registry: &Registry) -> Result<Vec<u8>, Error> {
106        Ok(
107            get_remote_wasm_from_hash(&self.config.rpc_client()?, &self.hash(registry).await?)
108                .await?,
109        )
110    }
111
112    pub async fn spec_entries(&self, registry: &Registry) -> Result<Vec<ScSpecEntry>, Error> {
113        Ok(contract_spec::Spec::new(&self.wasm(registry).await?)
114            .map_err(|_| Error::CannotParseContractSpec)?
115            .spec)
116    }
117
118    async fn invoke(&self) -> Result<stellar_strkey::Contract, Error> {
119        let registry = self.wasm_name.registry(&self.config).await?;
120        let client = self.config.rpc_client()?;
121        let key = self.config.key_pair()?;
122        let config = &self.config;
123
124        let contract_address = registry.as_contract().sc_address();
125        let contract_id = &registry.as_contract().id();
126        let spec_entries = self.spec_entries(&registry).await?;
127        let (args, signers) =
128            util::find_args_and_signers(contract_id, self.slop.clone(), &spec_entries).await?;
129
130        let deployer = if let Some(deployer) = &self.deployer {
131            deployer
132                .resolve_muxed_account(&self.config.locator, None)
133                .await?
134        } else {
135            xdr::MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes()))
136        };
137
138        // Build salt argument
139        let salt_arg = ScVal::Bytes(xdr::ScBytes(
140            if let Some(salt) = &self.salt {
141                let bytes: [u8; 32] = hex::decode(salt)
142                    .map_err(|_| Error::InvalidReturnValue("Invalid salt hex".to_string()))?
143                    .try_into()
144                    .map_err(|_| Error::InvalidReturnValue("Salt must be 32 bytes".to_string()))?;
145                bytes
146            } else {
147                rand::rng().random::<[u8; 32]>()
148            }
149            .try_into()
150            .unwrap(),
151        ));
152        let args: [ScVal; 5] = [
153            ScVal::String(ScString(self.wasm_name.name.clone().try_into().unwrap())),
154            self.version.clone().map_or(ScVal::Void, |s| {
155                ScVal::String(ScString(s.try_into().unwrap()))
156            }),
157            args,
158            salt_arg,
159            ScVal::Address(xdr::ScAddress::Account(deployer.account_id())),
160        ];
161        let invoke_contract_args = InvokeContractArgs {
162            contract_address: contract_address.clone(),
163            function_name: "deploy_unnamed".try_into().unwrap(),
164            args: args.try_into().unwrap(),
165        };
166
167        // Get the account sequence number
168        let public_strkey =
169            stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string();
170        let account_details = client.get_account(&public_strkey).await?;
171        let sequence: i64 = account_details.seq_num.into();
172        let tx = util::build_invoke_contract_tx(invoke_contract_args, sequence + 1, 100, &key)?;
173        let assembled = simulate_and_assemble_transaction(&client, &tx, None, None).await?;
174        let mut txn = assembled.transaction().clone();
175        txn = config
176            .sign_soroban_authorizations(&txn, &signers)
177            .await?
178            .unwrap_or(txn);
179        let return_value = client
180            .send_transaction_polling(&config.sign(txn, false).await?)
181            .await?
182            .return_value()?;
183        match return_value {
184            ScVal::Address(xdr::ScAddress::Contract(xdr::ContractId(hash))) => {
185                Ok(stellar_strkey::Contract(hash.0))
186            }
187            _ => Err(Error::InvalidReturnValue(
188                "{return_value:#?} is not a contract address".to_string(),
189            )),
190        }
191    }
192}
193
194#[cfg(feature = "integration-tests")]
195#[cfg(test)]
196mod tests {
197    use stellar_scaffold_test::RegistryTest;
198
199    #[tokio::test]
200    async fn simple() {
201        let registry = RegistryTest::new().await;
202        let v1 = registry.hello_wasm_v1();
203
204        // First publish the contract
205        registry
206            .registry_cli("publish")
207            .arg("--wasm")
208            .arg(v1.to_str().unwrap())
209            .arg("--binver")
210            .arg("0.0.1")
211            .arg("--wasm-name")
212            .arg("hello")
213            .assert()
214            .success();
215
216        // Deploy unnamed
217        registry
218            .registry_cli("deploy-unnamed")
219            .env("RUST_LOGS", "trace")
220            .env("RUSTLOGS", "trace")
221            .arg("--wasm-name")
222            .arg("hello")
223            .arg("--")
224            .arg("--admin=alice")
225            .assert()
226            .success();
227    }
228
229    #[tokio::test]
230    async fn with_version() {
231        let registry = RegistryTest::new().await;
232        let v1 = registry.hello_wasm_v1();
233        let v2 = registry.hello_wasm_v2();
234
235        // Publish v1
236        registry
237            .registry_cli("publish")
238            .arg("--wasm")
239            .arg(v1.to_str().unwrap())
240            .arg("--binver")
241            .arg("0.0.1")
242            .arg("--wasm-name")
243            .arg("hello")
244            .assert()
245            .success();
246
247        // Publish v2
248        registry
249            .registry_cli("publish")
250            .arg("--wasm")
251            .arg(v2.to_str().unwrap())
252            .arg("--binver")
253            .arg("0.0.2")
254            .arg("--wasm-name")
255            .arg("hello")
256            .assert()
257            .success();
258
259        // Deploy unnamed with specific version
260        registry
261            .registry_cli("deploy-unnamed")
262            .arg("--wasm-name")
263            .arg("hello")
264            .arg("--version")
265            .arg("0.0.1")
266            .arg("--")
267            .arg("--admin=alice")
268            .assert()
269            .success();
270    }
271
272    #[tokio::test]
273    async fn unverified() {
274        let registry = RegistryTest::new().await;
275        let v1 = registry.hello_wasm_v1();
276
277        // First publish the contract to unverified registry
278        registry
279            .registry_cli("publish")
280            .arg("--wasm")
281            .arg(v1.to_str().unwrap())
282            .arg("--binver")
283            .arg("0.0.1")
284            .arg("--wasm-name")
285            .arg("unverified/hello")
286            .assert()
287            .success();
288
289        // Deploy unnamed from unverified registry
290        registry
291            .registry_cli("deploy-unnamed")
292            .arg("--wasm-name")
293            .arg("unverified/hello")
294            .arg("--")
295            .arg("--admin=alice")
296            .assert()
297            .success();
298    }
299}