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