1use std::convert::{Infallible, TryInto};
2use std::ffi::OsString;
3use std::num::ParseIntError;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::{fmt::Debug, fs, io};
7
8use clap::{arg, command, Parser, ValueEnum};
9use soroban_rpc::{Client, SimulateHostFunctionResult, SimulateTransactionResponse};
10use soroban_spec::read::FromWasmError;
11
12use super::super::events;
13use super::arg_parsing;
14use crate::assembled::Assembled;
15use crate::log::extract_events;
16use crate::{
17 assembled::simulate_and_assemble_transaction,
18 commands::{
19 contract::arg_parsing::{build_host_function_parameters, output_to_string},
20 global,
21 txn_result::{TxnEnvelopeResult, TxnResult},
22 NetworkRunnable,
23 },
24 config::{self, data, locator, network},
25 get_spec::{self, get_remote_contract_spec},
26 print, rpc,
27 xdr::{
28 self, AccountEntry, AccountEntryExt, AccountId, ContractEvent, ContractEventType,
29 DiagnosticEvent, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo,
30 MuxedAccount, Operation, OperationBody, Preconditions, PublicKey, ScSpecEntry,
31 SequenceNumber, String32, StringM, Thresholds, Transaction, TransactionExt, Uint256, VecM,
32 WriteXdr,
33 },
34 Pwd,
35};
36use soroban_spec_tools::contract;
37
38#[derive(Parser, Debug, Default, Clone)]
39#[allow(clippy::struct_excessive_bools)]
40#[group(skip)]
41pub struct Cmd {
42 #[arg(long = "id", env = "STELLAR_CONTRACT_ID")]
44 pub contract_id: config::UnresolvedContract,
45 #[arg(skip)]
47 pub wasm: Option<std::path::PathBuf>,
48 #[arg(long, env = "STELLAR_INVOKE_VIEW")]
50 pub is_view: bool,
51 #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")]
53 pub slop: Vec<OsString>,
54 #[command(flatten)]
55 pub config: config::Args,
56 #[command(flatten)]
57 pub fee: crate::fee::Args,
58 #[arg(long, value_enum, default_value_t, env = "STELLAR_SEND")]
60 pub send: Send,
61}
62
63impl FromStr for Cmd {
64 type Err = clap::error::Error;
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 use clap::{CommandFactory, FromArgMatches};
68 Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
69 }
70}
71
72impl Pwd for Cmd {
73 fn set_pwd(&mut self, pwd: &Path) {
74 self.config.set_pwd(pwd);
75 }
76}
77
78#[derive(thiserror::Error, Debug)]
79pub enum Error {
80 #[error("cannot add contract to ledger entries: {0}")]
81 CannotAddContractToLedgerEntries(xdr::Error),
82 #[error("reading file {0:?}: {1}")]
83 CannotReadContractFile(PathBuf, io::Error),
84 #[error("committing file {filepath}: {error}")]
85 CannotCommitEventsFile {
86 filepath: std::path::PathBuf,
87 error: events::Error,
88 },
89 #[error("parsing contract spec: {0}")]
90 CannotParseContractSpec(FromWasmError),
91 #[error(transparent)]
92 Xdr(#[from] xdr::Error),
93 #[error("error parsing int: {0}")]
94 ParseIntError(#[from] ParseIntError),
95 #[error(transparent)]
96 Rpc(#[from] rpc::Error),
97 #[error("missing operation result")]
98 MissingOperationResult,
99 #[error("error loading signing key: {0}")]
100 SignatureError(#[from] ed25519_dalek::SignatureError),
101 #[error(transparent)]
102 Config(#[from] config::Error),
103 #[error("unexpected ({length}) simulate transaction result length")]
104 UnexpectedSimulateTransactionResultSize { length: usize },
105 #[error(transparent)]
106 Clap(#[from] clap::Error),
107 #[error(transparent)]
108 Locator(#[from] locator::Error),
109 #[error("Contract Error\n{0}: {1}")]
110 ContractInvoke(String, String),
111 #[error(transparent)]
112 StrKey(#[from] stellar_strkey::DecodeError),
113 #[error(transparent)]
114 ContractSpec(#[from] contract::Error),
115 #[error(transparent)]
116 Io(#[from] std::io::Error),
117 #[error(transparent)]
118 Data(#[from] data::Error),
119 #[error(transparent)]
120 Network(#[from] network::Error),
121 #[error(transparent)]
122 GetSpecError(#[from] get_spec::Error),
123 #[error(transparent)]
124 ArgParsing(#[from] arg_parsing::Error),
125}
126
127impl From<Infallible> for Error {
128 fn from(_: Infallible) -> Self {
129 unreachable!()
130 }
131}
132
133impl Cmd {
134 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
135 let res = self.invoke(global_args).await?.to_envelope();
136 match res {
137 TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
138 TxnEnvelopeResult::Res(output) => {
139 println!("{output}");
140 }
141 }
142 Ok(())
143 }
144
145 pub async fn invoke(&self, global_args: &global::Args) -> Result<TxnResult<String>, Error> {
146 self.run_against_rpc_server(Some(global_args), None).await
147 }
148
149 pub fn read_wasm(&self) -> Result<Option<Vec<u8>>, Error> {
150 Ok(if let Some(wasm) = self.wasm.as_ref() {
151 Some(fs::read(wasm).map_err(|e| Error::CannotReadContractFile(wasm.clone(), e))?)
152 } else {
153 None
154 })
155 }
156
157 pub fn spec_entries(&self) -> Result<Option<Vec<ScSpecEntry>>, Error> {
158 self.read_wasm()?
159 .map(|wasm| {
160 soroban_spec::read::from_wasm(&wasm).map_err(Error::CannotParseContractSpec)
161 })
162 .transpose()
163 }
164
165 fn should_send_tx(&self, sim_res: &SimulateTransactionResponse) -> Result<ShouldSend, Error> {
166 Ok(match self.send {
167 Send::Default => {
168 if self.is_view {
169 ShouldSend::No
170 } else if has_write(sim_res)? || has_published_event(sim_res)? || has_auth(sim_res)?
171 {
172 ShouldSend::Yes
173 } else {
174 ShouldSend::DefaultNo
175 }
176 }
177 Send::No => ShouldSend::No,
178 Send::Yes => ShouldSend::Yes,
179 })
180 }
181
182 async fn simulate(
184 &self,
185 host_function_params: &InvokeContractArgs,
186 account_details: &AccountEntry,
187 rpc_client: &Client,
188 ) -> Result<Assembled, Error> {
189 let sequence: i64 = account_details.seq_num.0;
190 let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) =
191 account_details.account_id.clone();
192
193 let tx = build_invoke_contract_tx(
194 host_function_params.clone(),
195 sequence + 1,
196 self.fee.fee,
197 account_id,
198 )?;
199 Ok(simulate_and_assemble_transaction(rpc_client, &tx).await?)
200 }
201}
202
203#[async_trait::async_trait]
204impl NetworkRunnable for Cmd {
205 type Error = Error;
206 type Result = TxnResult<String>;
207
208 async fn run_against_rpc_server(
209 &self,
210 global_args: Option<&global::Args>,
211 config: Option<&config::Args>,
212 ) -> Result<TxnResult<String>, Error> {
213 let config = config.unwrap_or(&self.config);
214 let print = print::Print::new(global_args.is_some_and(|g| g.quiet));
215 let network = config.get_network()?;
216
217 tracing::trace!(?network);
218
219 let contract_id = self
220 .contract_id
221 .resolve_contract_id(&config.locator, &network.network_passphrase)?;
222
223 let spec_entries = self.spec_entries()?;
224
225 if let Some(spec_entries) = &spec_entries {
226 build_host_function_parameters(&contract_id, &self.slop, spec_entries, config)?;
228 }
229
230 let client = network.rpc_client()?;
231
232 let spec_entries = get_remote_contract_spec(
233 &contract_id.0,
234 &config.locator,
235 &config.network,
236 global_args,
237 Some(config),
238 )
239 .await
240 .map_err(Error::from)?;
241
242 let params =
243 build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config)?;
244
245 let (function, spec, host_function_params, signers) = params;
246
247 let (should_send, cached_simulation) = if self.fee.build_only {
250 (ShouldSend::Yes, None)
251 } else {
252 let assembled = self
253 .simulate(&host_function_params, &default_account_entry(), &client)
254 .await?;
255 let should_send = self.should_send_tx(&assembled.sim_res)?;
256 (should_send, Some(assembled))
257 };
258
259 let account_details = if should_send == ShouldSend::Yes {
260 client
261 .verify_network_passphrase(Some(&network.network_passphrase))
262 .await?;
263
264 client
265 .get_account(&config.source_account().await?.to_string())
266 .await?
267 } else {
268 if should_send == ShouldSend::DefaultNo {
269 print.infoln(
270 "Simulation identified as read-only. Send by rerunning with `--send=yes`.",
271 );
272 }
273
274 let assembled = cached_simulation.expect(
275 "cached_simulation should be available when should_send != Yes and not build_only",
276 );
277 let sim_res = assembled.sim_response();
278 let return_value = sim_res.results()?;
279 let events = sim_res.events()?;
280
281 crate::log::event::all(&events);
282 crate::log::event::contract(&events, &print);
283
284 return Ok(output_to_string(&spec, &return_value[0].xdr, &function)?);
285 };
286
287 let sequence: i64 = account_details.seq_num.into();
288 let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id;
289
290 let tx = Box::new(build_invoke_contract_tx(
291 host_function_params.clone(),
292 sequence + 1,
293 self.fee.fee,
294 account_id,
295 )?);
296
297 if self.fee.build_only {
298 return Ok(TxnResult::Txn(tx));
299 }
300
301 let txn = simulate_and_assemble_transaction(&client, &tx).await?;
302 let assembled = self.fee.apply_to_assembled_txn(txn);
303 let mut txn = Box::new(assembled.transaction().clone());
304 let sim_res = assembled.sim_response();
305
306 if global_args.is_none_or(|a| !a.no_cache) {
307 data::write(sim_res.clone().into(), &network.rpc_uri()?)?;
308 }
309
310 let global::Args { no_cache, .. } = global_args.cloned().unwrap_or_default();
311
312 if let Some(tx) = config.sign_soroban_authorizations(&txn, &signers).await? {
314 txn = Box::new(tx);
315 }
316
317 let res = client
318 .send_transaction_polling(&config.sign(*txn).await?)
319 .await?;
320
321 if !no_cache {
322 data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
323 }
324
325 let return_value = res.return_value()?;
326 let events = extract_events(&res.result_meta.unwrap_or_default());
327
328 crate::log::event::all(&events);
329 crate::log::event::contract(&events, &print);
330
331 Ok(output_to_string(&spec, &return_value, &function)?)
332 }
333}
334
335const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])));
336
337fn default_account_entry() -> AccountEntry {
338 AccountEntry {
339 account_id: DEFAULT_ACCOUNT_ID,
340 balance: 0,
341 seq_num: SequenceNumber(0),
342 num_sub_entries: 0,
343 inflation_dest: None,
344 flags: 0,
345 home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }),
346 thresholds: Thresholds([0; 4]),
347 signers: unsafe { [].try_into().unwrap_unchecked() },
348 ext: AccountEntryExt::V0,
349 }
350}
351
352fn build_invoke_contract_tx(
353 parameters: InvokeContractArgs,
354 sequence: i64,
355 fee: u32,
356 source_account_id: Uint256,
357) -> Result<Transaction, Error> {
358 let op = Operation {
359 source_account: None,
360 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
361 host_function: HostFunction::InvokeContract(parameters),
362 auth: VecM::default(),
363 }),
364 };
365 Ok(Transaction {
366 source_account: MuxedAccount::Ed25519(source_account_id),
367 fee,
368 seq_num: SequenceNumber(sequence),
369 cond: Preconditions::None,
370 memo: Memo::None,
371 operations: vec![op].try_into()?,
372 ext: TransactionExt::V0,
373 })
374}
375
376#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum, Default)]
377pub enum Send {
378 #[default]
381 Default,
382 No,
384 Yes,
386}
387
388#[derive(Debug, PartialEq)]
389enum ShouldSend {
390 DefaultNo,
391 No,
392 Yes,
393}
394
395fn has_write(sim_res: &SimulateTransactionResponse) -> Result<bool, Error> {
396 Ok(!sim_res
397 .transaction_data()?
398 .resources
399 .footprint
400 .read_write
401 .is_empty())
402}
403
404fn has_published_event(sim_res: &SimulateTransactionResponse) -> Result<bool, Error> {
405 Ok(sim_res.events()?.iter().any(
406 |DiagnosticEvent {
407 event: ContractEvent { type_, .. },
408 ..
409 }| matches!(type_, ContractEventType::Contract),
410 ))
411}
412
413fn has_auth(sim_res: &SimulateTransactionResponse) -> Result<bool, Error> {
414 Ok(sim_res
415 .results()?
416 .iter()
417 .any(|SimulateHostFunctionResult { auth, .. }| !auth.is_empty()))
418}