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