stellar_registry_cli/commands/
deploy_unnamed.rs1use 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 #[arg(long)]
25 pub wasm_name: PrefixedName,
26
27 #[arg(last = true, id = "CONSTRUCTOR_ARGS")]
29 pub slop: Vec<OsString>,
30
31 #[arg(long)]
33 pub version: Option<String>,
34
35 #[arg(long)]
37 pub salt: Option<String>,
38
39 #[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 = ®istry.as_contract().id();
126 let spec_entries = self.spec_entries(®istry).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 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 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 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 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 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 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 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 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 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}