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